For educational purpose I try to access the FILE
struct in Rust:
unsafe {
let passwd = libc::fopen("/etc/passwd".to_ptr(), &('r' as libc::c_char));
let fp = &mut *(passwd as *mut MY_FILE);
println!("flags={}, file={}", fp._flags, fp._file);
}
the MY_FILE
struct I obtained by running bindgen on stdio.h (I'm on OS X):
bindgen /usr/include/stdio.h
Somehow _flags
is always 8
for files open in write mode (4 in read mode), so this flags seems off (I tested with a C
code to verify that it indeed is not 4 or 8). The file pointer however seems to be right. What could cause this? Am I extracting the binding from the wrong header file? Is there something I need to add to the #[repr(C,)]
attribute?
Here is the full code including the struct.
This is a follow up question from an earlier question
First, your implementation of ToPtr
invites unsound code. Reproduced here:
// code in italics is wrong
impl ToPtr for str {
fn to_ptr(&self) -> *const i8 {
CString::new(self).unwrap().as_ptr()
}
}
This allocates a new CString
and returns a pointer to its contents, but the CString
is dropped when to_ptr
returns, so this is a dangling pointer. Any dereference of this pointer is undefined behavior. The documentation has a big warning about this, but it's still a very common mistake.
One correct way to make a *const c_char
from a string literal is b"string here\0".as_ptr() as *const c_char
. The string is null terminated and there is no dangling pointer because string literals live for the entire program. If you have a non-constant string to be converted, you must keep the CString
alive while it is being used, like this:
let s = "foo";
let cs = CString::new(s).unwrap(); // don't call .as_ptr(), so the CString stays alive
unsafe { some_c_function(cs.as_ptr()); }
// CString is dropped here, after we're done with it
Aside: an editor "suggested" (I'm new to Stack Overflow, but it seems more polite to comment rather than try to rewrite my answer) that the above code could be written like this:
let s = "foo";
unsafe {
// due to temporary drop rules, the CString will be dropped at the end of the statement (the `;`)
some_c_function(CString::new(s).unwrap().as_ptr());
}
While this is technically correct (the best kind of correct), the "temporary drop rules" involved are subtle -- this works because as_ptr
takes a reference to the CString
(actually a &CStr, because the compiler changes the method chain to CString::new(s).unwrap().deref().as_ptr()) instead of consuming it, and because we have only one C function to call. I don't like to rely on anything that's subtle or non-obvious when writing unsafe code.
With that out of the way, I fixed that unsoundness in your code (your calls all use string literals so I just used my first strategy above). I get this output on OSX:
0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0
0, 0, 8, 1, 0, 0, 0, 0, 0, 0, 0
0, 0, 8, 2, 0, 0, 0, 0, 0, 0, 0
0, 0, 4, 3, 0, 0, 0, 0, 0, 0, 0
So, this matches your results, right? I also wrote the following C program:
#include <stdio.h>
#include <unistd.h>
int main() {
struct __sFILE *fp1 = fdopen(STDIN_FILENO, "r");
struct __sFILE *fp2 = fdopen(STDOUT_FILENO, "w");
struct __sFILE *fp3 = fdopen(STDERR_FILENO, "w");
struct __sFILE *passwd = fopen("/etc/passwd", "r");
printf("%i %i %i %i\n", fp1->_flags, fp2->_flags, fp3->_flags, passwd->_flags);
}
And got the output:
4 8 8 4
Which seems to vindicate the Rust results. There is a comment at the top of /usr/include/stdio.h
that says:
/*
* The following always hold:
*
* if (_flags&(__SLBF|__SWR)) == (__SLBF|__SWR),
* _lbfsize is -_bf._size, else _lbfsize is 0
* if _flags&__SRD, _w is 0
* if _flags&__SWR, _r is 0
*/
And looking up those constants:
#define __SLBF 0x0001 /* line buffered */
#define __SRD 0x0004 /* OK to read */
#define __SWR 0x0008 /* OK to write */
This seems to match the output we get: 4 for file opened in read mode, 8 for write. So what is the problem here?