Skip to content

Commit 161d3bf

Browse files
Close named standard descriptors and preserve stream identity
Signed-off-by: Perfloop Agent <agent@perfloop.ai> Signed-off-by: Tomás Senart <ts@perfloop.ai>
1 parent e2b679d commit 161d3bf

22 files changed

Lines changed: 657 additions & 141 deletions

‎docs/src/runtime/io.md‎

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -42,14 +42,28 @@ 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`.
50+
51+
The stream object is distinct from its I/O handle. It is not cloneable:
52+
references, stream buffers, and iterators refer to the same stream object rather
53+
than duplicating its file handle. In the refcount model these are
54+
`Ptr<CppStream>` values; in the unsafe model they are raw pointers. Reads and
55+
writes borrow the stream mutably, leaving a place on the object for future
56+
formatting state, such as floating-point precision and string padding,
57+
independently of the handle. Iterators do not keep a file stream alive after its
58+
owner is destroyed.
59+
60+
`Ptr<T>` implements `write_fmt` and `write_all` whenever `T: Write`, forwarding
61+
to the pointee through `with_mut`, so `cout << x` becomes
62+
`write!(cout(), "{}", x)` and a raw byte range is written with
63+
`cout().write_all(..)`. In the unsafe model `cin_unsafe`, `cout_unsafe`, and
64+
`cerr_unsafe` return raw pointers to thread-local `CppStream` values. The
65+
translation does not yet model the full C++ iostream state, including sticky
66+
error flags, and insertion expressions discard the returned I/O error.
5367

5468
## Formatting
5569

@@ -94,8 +108,11 @@ The registry follows the descriptor's life. When a rule opens a file,
94108
raw number. When a rule performs I/O on that number, `FdRegistry::with_fd` looks
95109
the entry up and lends it out as a `BorrowedFd` for the duration of the call.
96110
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.
111+
closes the descriptor. The registry starts out borrowing the live standard
112+
descriptors 0, 1, and 2 from Rust's standard stream handles, without duplicating
113+
them. An explicit `close` releases the original descriptor, but dropping the
114+
thread-local registry does not close these borrowed standard descriptors. A file
115+
opened at one of these numbers is owned and closed normally.
99116

100117
In the `fstat` rule, the descriptor argument goes through `with_fd`:
101118

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

128145
Using a descriptor that was never opened, or using it after it was closed, is a
129146
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.
147+
message prefixed `ub:`) so the bug surfaces instead of going unnoticed. This
148+
policy also applies when another thread has already closed a standard descriptor
149+
before this thread initializes its registry. The table remains thread-local: it
150+
does not track descriptor ownership or closure across threads, and does not
151+
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: 109 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -3,49 +3,88 @@
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+
// The stream object is separate from its I/O handle so formatting and error
15+
// state can live here. References and iterators must refer to this same object,
16+
// not clone its handle into another stream.
17+
pub struct CppStream {
18+
handle: StreamHandle,
19+
}
20+
21+
impl From<std::fs::File> for CppStream {
22+
fn from(file: std::fs::File) -> Self {
23+
Self {
24+
handle: StreamHandle::File(file),
25+
}
26+
}
27+
}
28+
29+
impl CppStream {
30+
fn standard(fd: i32) -> Self {
31+
Self {
32+
handle: StreamHandle::Standard(fd),
33+
}
34+
}
35+
}
36+
37+
impl Read for CppStream {
38+
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
39+
match &mut self.handle {
40+
StreamHandle::File(file) => file.read(buf),
41+
StreamHandle::Standard(fd) => {
42+
// A standard descriptor may have been closed. Do not create an
43+
// owning File or a BorrowedFd, which requires an open descriptor.
44+
let n = unsafe { libc::read(*fd, buf.as_mut_ptr().cast(), buf.len()) };
45+
if n < 0 {
46+
Err(io::Error::last_os_error())
47+
} else {
48+
Ok(n as usize)
49+
}
50+
}
51+
}
52+
}
53+
}
54+
55+
impl Write for CppStream {
56+
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
57+
match &mut self.handle {
58+
StreamHandle::File(file) => file.write(buf),
59+
StreamHandle::Standard(fd) => {
60+
// The slice is valid for the duration of this unbuffered write.
61+
let n = unsafe { libc::write(*fd, buf.as_ptr().cast(), buf.len()) };
62+
if n < 0 {
63+
Err(io::Error::last_os_error())
64+
} else {
65+
Ok(n as usize)
66+
}
67+
}
68+
}
69+
}
70+
71+
fn flush(&mut self) -> io::Result<()> {
72+
match &mut self.handle {
73+
StreamHandle::File(file) => file.flush(),
74+
StreamHandle::Standard(_) => Ok(()),
75+
}
76+
}
77+
}
78+
79+
impl crate::ByteRepr for CppStream {}
80+
981
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-
};
82+
static SAFE_STDIN: Value<CppStream> = Rc::new(RefCell::new(CppStream::standard(0)));
83+
static SAFE_STDOUT: Value<CppStream> = Rc::new(RefCell::new(CppStream::standard(1)));
84+
static SAFE_STDERR: Value<CppStream> = Rc::new(RefCell::new(CppStream::standard(2)));
85+
static UNSAFE_STDIN: UnsafeCell<CppStream> = UnsafeCell::new(CppStream::standard(0));
86+
static UNSAFE_STDOUT: UnsafeCell<CppStream> = UnsafeCell::new(CppStream::standard(1));
87+
static UNSAFE_STDERR: UnsafeCell<CppStream> = UnsafeCell::new(CppStream::standard(2));
4988
}
5089

5190
thread_local! {
@@ -66,39 +105,39 @@ pub fn c_stderr() -> Ptr<CFile> {
66105
C_STDERR.with(AsPointer::as_pointer)
67106
}
68107

69-
pub fn cin() -> Ptr<std::fs::File> {
108+
pub fn cin() -> Ptr<CppStream> {
70109
SAFE_STDIN.with(AsPointer::as_pointer)
71110
}
72111

73-
pub fn cout() -> Ptr<std::fs::File> {
112+
pub fn cout() -> Ptr<CppStream> {
74113
SAFE_STDOUT.with(AsPointer::as_pointer)
75114
}
76115

77-
pub fn cerr() -> Ptr<std::fs::File> {
116+
pub fn cerr() -> Ptr<CppStream> {
78117
SAFE_STDERR.with(AsPointer::as_pointer)
79118
}
80119

81120
/// # Safety
82121
///
83122
/// The caller must ensure that the returned pointer is not used after the
84123
/// thread finishes.
85-
pub unsafe fn cin_unsafe() -> *mut std::fs::File {
124+
pub unsafe fn cin_unsafe() -> *mut CppStream {
86125
UNSAFE_STDIN.with(UnsafeCell::get)
87126
}
88127

89128
/// # Safety
90129
///
91130
/// The caller must ensure that the returned pointer is not used after the
92131
/// thread finishes.
93-
pub unsafe fn cout_unsafe() -> *mut std::fs::File {
132+
pub unsafe fn cout_unsafe() -> *mut CppStream {
94133
UNSAFE_STDOUT.with(UnsafeCell::get)
95134
}
96135

97136
/// # Safety
98137
///
99138
/// The caller must ensure that the returned pointer is not used after the
100139
/// thread finishes.
101-
pub unsafe fn cerr_unsafe() -> *mut std::fs::File {
140+
pub unsafe fn cerr_unsafe() -> *mut CppStream {
102141
UNSAFE_STDERR.with(UnsafeCell::get)
103142
}
104143

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

‎rules/algorithm/tgt_refcount.rs‎

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -107,16 +107,19 @@ fn f12<T1: CppInputIterator<T2>, T2: Clone + ByteRepr>(a0: T1, a1: T1, a2: T2) {
107107
}
108108
}
109109

110-
fn f13<T1: CppInputIterator<i8>>(a0: T1, a1: T1, a2: &mut ::std::fs::File) -> ::std::fs::File {
110+
fn f13<T1: CppInputIterator<i8>>(
111+
a0: T1,
112+
a1: T1,
113+
a2: Ptr<libcc2rs::CppStream>,
114+
) -> Ptr<libcc2rs::CppStream> {
111115
let mut __it = a0;
112-
let __out = &mut *a2;
113116
let mut __bytes: Vec<u8> = Vec::new();
114117
while __it != a1 {
115118
__bytes.push(__it.iter_deref().read() as u8);
116119
__it.iter_inc();
117120
}
118-
__out.write_all(&__bytes);
119-
__out.try_clone().unwrap()
121+
a2.write_all(&__bytes);
122+
a2
120123
}
121124

122125
fn f14<T1: Clone + ByteRepr, T2>(a0: Ptr<T1>, a1: Ptr<T1>, a2: T2)

0 commit comments

Comments
 (0)