Skip to content

Commit 3cb5fb0

Browse files
Close original standard descriptors and keep C++ streams attached
Signed-off-by: Perfloop Agent <agent@perfloop.ai> Signed-off-by: Tomás Senart <ts@perfloop.ai>
1 parent 337827d commit 3cb5fb0

22 files changed

Lines changed: 537 additions & 113 deletions

‎docs/src/runtime/io.md‎

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -42,14 +42,20 @@ through the `CFile`; the unsafe variant forwards to `libc::fread`.
4242
## C++ Streams
4343

4444
In the refcount model `cin`, `cout`, and `cerr` are translated as
45-
`Ptr<std::fs::File>` values over duplicates of the standard descriptors,
46-
returned by the `cin`, `cout`, and `cerr` functions. `Ptr<T>` implements
47-
`write_fmt` and `write_all` whenever `T: Write`, forwarding to the pointee
48-
through `with_mut`, so `cout << x` becomes `write!(cout(), "{}", x)` and a raw
49-
byte range is written with `cout().write_all(..)`. In the unsafe model
50-
`cin_unsafe`, `cout_unsafe`, and `cerr_unsafe` return raw pointers to
51-
thread-local `std::fs::File` values. C++ streams do not map fully onto
52-
`std::fs::File`, so this translation may change in the future.
45+
`Ptr<CppStream>` values returned by the `cin`, `cout`, and `cerr` functions.
46+
`CppStream` uses the original standard descriptor number without owning or
47+
duplicating it. Its unbuffered reads and writes return an I/O error if the
48+
descriptor is closed, even when the stream is first used after the close. File
49+
streams use the same type, wrapping an owned `std::fs::File`; cloning these
50+
streams still duplicates the owned file handle.
51+
52+
`Ptr<T>` implements `write_fmt` and `write_all` whenever `T: Write`, forwarding
53+
to the pointee through `with_mut`, so `cout << x` becomes
54+
`write!(cout(), "{}", x)` and a raw byte range is written with
55+
`cout().write_all(..)`. In the unsafe model `cin_unsafe`, `cout_unsafe`, and
56+
`cerr_unsafe` return raw pointers to thread-local `CppStream` values. The
57+
translation does not yet model the full C++ iostream state, including sticky
58+
error flags, and insertion expressions discard the returned I/O error.
5359

5460
## Formatting
5561

@@ -94,8 +100,11 @@ The registry follows the descriptor's life. When a rule opens a file,
94100
raw number. When a rule performs I/O on that number, `FdRegistry::with_fd` looks
95101
the entry up and lends it out as a `BorrowedFd` for the duration of the call.
96102
When the program calls `close`, `FdRegistry::close` removes the entry, which
97-
closes the descriptor. The registry starts out holding the standard descriptors
98-
0, 1, and 2.
103+
closes the descriptor. The registry starts out borrowing the live standard
104+
descriptors 0, 1, and 2 from Rust's standard stream handles, without duplicating
105+
them. An explicit `close` releases the original descriptor, but dropping the
106+
thread-local registry does not close these borrowed standard descriptors. A file
107+
opened at one of these numbers is owned and closed normally.
99108

100109
In the `fstat` rule, the descriptor argument goes through `with_fd`:
101110

@@ -127,4 +136,8 @@ FdRegistry::with_fds(&wanted, |borrowed: &[BorrowedFd<'_>]| {
127136

128137
Using a descriptor that was never opened, or using it after it was closed, is a
129138
bug in the original program. The registry turns such a use into a panic (with a
130-
message prefixed `ub:`) so the bug surfaces instead of going unnoticed.
139+
message prefixed `ub:`) so the bug surfaces instead of going unnoticed. This
140+
policy also applies when another thread has already closed a standard descriptor
141+
before this thread initializes its registry. The table remains thread-local: it
142+
does not track descriptor ownership or closure across threads, and does not
143+
provide native C's `EBADF` behavior for unregistered numbers.

‎libcc2rs/src/fd.rs‎

Lines changed: 57 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,51 @@
44
use std::cell::RefCell;
55
use std::os::fd::{AsFd, AsRawFd, BorrowedFd, OwnedFd};
66

7+
enum Fd {
8+
Owned(OwnedFd),
9+
Stdin(std::io::Stdin),
10+
Stdout(std::io::Stdout),
11+
Stderr(std::io::Stderr),
12+
}
13+
14+
impl AsRawFd for Fd {
15+
fn as_raw_fd(&self) -> i32 {
16+
match self {
17+
Self::Owned(fd) => fd.as_raw_fd(),
18+
Self::Stdin(fd) => fd.as_raw_fd(),
19+
Self::Stdout(fd) => fd.as_raw_fd(),
20+
Self::Stderr(fd) => fd.as_raw_fd(),
21+
}
22+
}
23+
}
24+
25+
impl AsFd for Fd {
26+
fn as_fd(&self) -> BorrowedFd<'_> {
27+
match self {
28+
Self::Owned(fd) => fd.as_fd(),
29+
Self::Stdin(fd) => fd.as_fd(),
30+
Self::Stdout(fd) => fd.as_fd(),
31+
Self::Stderr(fd) => fd.as_fd(),
32+
}
33+
}
34+
}
35+
736
pub struct FdRegistry {
8-
fds: Vec<Option<OwnedFd>>,
37+
fds: Vec<Option<Fd>>,
938
}
1039

1140
thread_local! {
1241
static FD_REGISTRY: RefCell<FdRegistry> = RefCell::new(FdRegistry {
13-
fds: vec![
14-
std::io::stdin().as_fd().try_clone_to_owned().ok(),
15-
std::io::stdout().as_fd().try_clone_to_owned().ok(),
16-
std::io::stderr().as_fd().try_clone_to_owned().ok(),
17-
],
42+
// Borrow the original standard descriptors, without closing them when
43+
// this thread exits. Only an explicit close releases these descriptors.
44+
fds: [
45+
Fd::Stdin(std::io::stdin()),
46+
Fd::Stdout(std::io::stdout()),
47+
Fd::Stderr(std::io::stderr()),
48+
].into_iter().map(|fd| {
49+
// Check the number without borrowing a potentially closed descriptor.
50+
(unsafe { libc::fcntl(fd.as_raw_fd(), libc::F_GETFD) } >= 0).then_some(fd)
51+
}).collect(),
1852
});
1953
}
2054

@@ -27,8 +61,13 @@ impl FdRegistry {
2761
if fds.len() <= idx {
2862
fds.resize_with(idx + 1, || None);
2963
}
30-
assert!(fds[idx].is_none(), "ub: fd registry collision on fd {raw}");
31-
fds[idx] = Some(fd);
64+
// The first open may reuse an initially closed standard descriptor
65+
// before the registry is initialized. Replace its borrowed entry.
66+
assert!(
67+
!matches!(fds[idx], Some(Fd::Owned(_))),
68+
"ub: fd registry collision on fd {raw}"
69+
);
70+
fds[idx] = Some(Fd::Owned(fd));
3271
});
3372
raw
3473
}
@@ -57,13 +96,22 @@ impl FdRegistry {
5796
}
5897

5998
pub fn close(fd: i32) -> i32 {
60-
FD_REGISTRY.with(|r| {
99+
let entry = FD_REGISTRY.with(|r| {
61100
r.borrow_mut()
62101
.fds
63102
.get_mut(fd as usize)
64103
.and_then(|slot| slot.take())
65104
.unwrap_or_else(|| panic!("ub: bad fd {fd}"))
66105
});
106+
match entry {
107+
Fd::Owned(fd) => drop(fd),
108+
fd => {
109+
if let Err(e) = nix::unistd::close(fd.as_raw_fd()) {
110+
crate::cpp2rust_errno().write(e as i32);
111+
return -1;
112+
}
113+
}
114+
}
67115
0
68116
}
69117
}

‎libcc2rs/src/io.rs‎

Lines changed: 113 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -3,49 +3,92 @@
33

44
use crate::{AnyPtr, AsPointer, CFile, Ptr, Value};
55
use std::cell::{RefCell, UnsafeCell};
6-
use std::os::fd::{AsFd, FromRawFd, IntoRawFd};
6+
use std::io::{self, Read, Write};
77
use std::rc::Rc;
88

9+
enum StreamHandle {
10+
File(std::fs::File),
11+
Standard(i32),
12+
}
13+
14+
pub struct CppStream(StreamHandle);
15+
16+
impl From<std::fs::File> for CppStream {
17+
fn from(file: std::fs::File) -> Self {
18+
Self(StreamHandle::File(file))
19+
}
20+
}
21+
22+
impl CppStream {
23+
fn standard(fd: i32) -> Self {
24+
Self(StreamHandle::Standard(fd))
25+
}
26+
27+
pub fn try_clone(&self) -> io::Result<Self> {
28+
match &self.0 {
29+
StreamHandle::File(file) => file.try_clone().map(Self::from),
30+
StreamHandle::Standard(fd) => Ok(Self::standard(*fd)),
31+
}
32+
}
33+
}
34+
35+
impl Read for &CppStream {
36+
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
37+
match &self.0 {
38+
StreamHandle::File(file) => (&*file).read(buf),
39+
StreamHandle::Standard(fd) => {
40+
// A standard descriptor may have been closed. Do not create an
41+
// owning File or a BorrowedFd, which requires an open descriptor.
42+
let n = unsafe { libc::read(*fd, buf.as_mut_ptr().cast(), buf.len()) };
43+
if n < 0 {
44+
Err(io::Error::last_os_error())
45+
} else {
46+
Ok(n as usize)
47+
}
48+
}
49+
}
50+
}
51+
}
52+
53+
impl Read for CppStream {
54+
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
55+
(&*self).read(buf)
56+
}
57+
}
58+
59+
impl Write for CppStream {
60+
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
61+
match &mut self.0 {
62+
StreamHandle::File(file) => file.write(buf),
63+
StreamHandle::Standard(fd) => {
64+
// The slice is valid for the duration of this unbuffered write.
65+
let n = unsafe { libc::write(*fd, buf.as_ptr().cast(), buf.len()) };
66+
if n < 0 {
67+
Err(io::Error::last_os_error())
68+
} else {
69+
Ok(n as usize)
70+
}
71+
}
72+
}
73+
}
74+
75+
fn flush(&mut self) -> io::Result<()> {
76+
match &mut self.0 {
77+
StreamHandle::File(file) => file.flush(),
78+
StreamHandle::Standard(_) => Ok(()),
79+
}
80+
}
81+
}
82+
83+
impl crate::ByteRepr for CppStream {}
84+
985
thread_local! {
10-
static SAFE_STDIN: Value<std::fs::File> = Rc::new(RefCell::new(std::fs::File::from(
11-
std::io::stdin().as_fd().try_clone_to_owned().unwrap(),
12-
)));
13-
static SAFE_STDOUT: Value<std::fs::File> = Rc::new(RefCell::new(std::fs::File::from(
14-
std::io::stdout().as_fd().try_clone_to_owned().unwrap(),
15-
)));
16-
static SAFE_STDERR: Value<std::fs::File> = Rc::new(RefCell::new(std::fs::File::from(
17-
std::io::stderr().as_fd().try_clone_to_owned().unwrap(),
18-
)));
19-
static UNSAFE_STDIN: UnsafeCell<std::fs::File> = unsafe {
20-
UnsafeCell::new(
21-
std::fs::File::from_raw_fd(
22-
std::io::stdin()
23-
.as_fd()
24-
.try_clone_to_owned()
25-
.unwrap()
26-
.into_raw_fd(),
27-
))
28-
};
29-
static UNSAFE_STDOUT: UnsafeCell<std::fs::File> = unsafe {
30-
UnsafeCell::new(
31-
std::fs::File::from_raw_fd(
32-
std::io::stdout()
33-
.as_fd()
34-
.try_clone_to_owned()
35-
.unwrap()
36-
.into_raw_fd(),
37-
))
38-
};
39-
static UNSAFE_STDERR: UnsafeCell<std::fs::File> = unsafe {
40-
UnsafeCell::new(
41-
std::fs::File::from_raw_fd(
42-
std::io::stderr()
43-
.as_fd()
44-
.try_clone_to_owned()
45-
.unwrap()
46-
.into_raw_fd(),
47-
))
48-
};
86+
static SAFE_STDIN: Value<CppStream> = Rc::new(RefCell::new(CppStream::standard(0)));
87+
static SAFE_STDOUT: Value<CppStream> = Rc::new(RefCell::new(CppStream::standard(1)));
88+
static SAFE_STDERR: Value<CppStream> = Rc::new(RefCell::new(CppStream::standard(2)));
89+
static UNSAFE_STDIN: UnsafeCell<CppStream> = UnsafeCell::new(CppStream::standard(0));
90+
static UNSAFE_STDOUT: UnsafeCell<CppStream> = UnsafeCell::new(CppStream::standard(1));
91+
static UNSAFE_STDERR: UnsafeCell<CppStream> = UnsafeCell::new(CppStream::standard(2));
4992
}
5093

5194
thread_local! {
@@ -66,39 +109,39 @@ pub fn c_stderr() -> Ptr<CFile> {
66109
C_STDERR.with(AsPointer::as_pointer)
67110
}
68111

69-
pub fn cin() -> Ptr<std::fs::File> {
112+
pub fn cin() -> Ptr<CppStream> {
70113
SAFE_STDIN.with(AsPointer::as_pointer)
71114
}
72115

73-
pub fn cout() -> Ptr<std::fs::File> {
116+
pub fn cout() -> Ptr<CppStream> {
74117
SAFE_STDOUT.with(AsPointer::as_pointer)
75118
}
76119

77-
pub fn cerr() -> Ptr<std::fs::File> {
120+
pub fn cerr() -> Ptr<CppStream> {
78121
SAFE_STDERR.with(AsPointer::as_pointer)
79122
}
80123

81124
/// # Safety
82125
///
83126
/// The caller must ensure that the returned pointer is not used after the
84127
/// thread finishes.
85-
pub unsafe fn cin_unsafe() -> *mut std::fs::File {
128+
pub unsafe fn cin_unsafe() -> *mut CppStream {
86129
UNSAFE_STDIN.with(UnsafeCell::get)
87130
}
88131

89132
/// # Safety
90133
///
91134
/// The caller must ensure that the returned pointer is not used after the
92135
/// thread finishes.
93-
pub unsafe fn cout_unsafe() -> *mut std::fs::File {
136+
pub unsafe fn cout_unsafe() -> *mut CppStream {
94137
UNSAFE_STDOUT.with(UnsafeCell::get)
95138
}
96139

97140
/// # Safety
98141
///
99142
/// The caller must ensure that the returned pointer is not used after the
100143
/// thread finishes.
101-
pub unsafe fn cerr_unsafe() -> *mut std::fs::File {
144+
pub unsafe fn cerr_unsafe() -> *mut CppStream {
102145
UNSAFE_STDERR.with(UnsafeCell::get)
103146
}
104147

@@ -189,3 +232,27 @@ pub unsafe fn fread_unsafe(
189232
) -> usize {
190233
unsafe { libc::fread(a0, a1, a2, a3) }
191234
}
235+
236+
#[cfg(test)]
237+
mod tests {
238+
use super::*;
239+
240+
#[test]
241+
fn file_stream_io() {
242+
let (read, write) = nix::unistd::pipe().unwrap();
243+
let mut input = CppStream::from(std::fs::File::from(read));
244+
let output = CppStream::from(std::fs::File::from(write));
245+
let mut copy = output.try_clone().unwrap();
246+
drop(output);
247+
copy.write_all(&[0, 127, 255]).unwrap();
248+
copy.flush().unwrap();
249+
drop(copy);
250+
251+
let mut first = [9];
252+
input.read_exact(&mut first).unwrap();
253+
assert_eq!(first, [0]);
254+
let mut rest = Vec::new();
255+
(&input).read_to_end(&mut rest).unwrap();
256+
assert_eq!(rest, [127, 255]);
257+
}
258+
}

‎rules/algorithm/tgt_refcount.rs‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,11 @@ fn f12<T1: CppInputIterator<T2>, T2: Clone + ByteRepr>(a0: T1, a1: T1, a2: T2) {
119119
}
120120
}
121121

122-
fn f13<T1: CppInputIterator<i8>>(a0: T1, a1: T1, a2: &mut ::std::fs::File) -> ::std::fs::File {
122+
fn f13<T1: CppInputIterator<i8>>(
123+
a0: T1,
124+
a1: T1,
125+
a2: &mut libcc2rs::CppStream,
126+
) -> libcc2rs::CppStream {
123127
let mut __it = a0;
124128
let __last = a1;
125129
let __out = &mut *a2;

‎rules/algorithm/tgt_unsafe.rs‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -127,8 +127,8 @@ unsafe fn f12<T1: UnsafeCppInputIterator<T2>, T2: Clone>(a0: T1, a1: T1, a2: T2)
127127
unsafe fn f13<T1: UnsafeCppInputIterator<libc::c_char>>(
128128
a0: T1,
129129
a1: T1,
130-
a2: &mut ::std::fs::File,
131-
) -> ::std::fs::File {
130+
a2: &mut libcc2rs::CppStream,
131+
) -> libcc2rs::CppStream {
132132
let mut __it = a0;
133133
let __last = a1;
134134
let __out = &mut *a2;

0 commit comments

Comments
 (0)