This isn't a request for help, I figured out the problem already. But I wanted to share this interesting bug I had in my own code, which was tricky to find (for me).
Try to guess why the following program only prints
(no text supplied)
instead of adding Try to find out why this is not printed! to the output.
The code can be fixed by removing or adding a single character.
use std::ffi::CString;
use std::os::raw::c_char;
use libc;
fn do_the_call(text: Option<CString>) {
unsafe {
libc::puts(match text {
None => "(no text supplied)\0".as_ptr() as *const c_char,
Some(s) => s.as_ptr(),
});
}
}
fn print_opt_string(text: Option<String>) {
// convert `Option<String>` to `Option<CString>`
let c_text: Option<CString> = text.map(|s| {
CString::new(s).unwrap_or_else(|_| {
CString::new("(invalid text supplied)").unwrap()
})
});
// do the call:
do_the_call(c_text)
}
fn main() {
print_opt_string(None); // this prints "(no text supplied)"
print_opt_string(Some("Try to find out why this is not printed!".to_string()));
}
This is why I recommend that char_p::Ref<'_> be used in extern function declarations rather than a lifetime-unchecked *const c_char parameter. It gives the answer to your problem right away:
If you don't want to go all in with rewriting many C APIs with safer-ffi's lifetime-checked types, you can also, at the very least, try to use something less error-prone than the .as_ptr() methods:
fn do_the_call(text: Option<CString>) {
unsafe {
match text {
None => libc::puts("(no text supplied)\0".as_ptr() as *const c_char),
Some(s) => s.with_ptr(|ptr| libc::puts(ptr)),
};
}
}
(the idea being that within the scope of the callback, you know that ptr is usable, which may very well not be true once you're outside that scope (so it's not enforced —nothing can be as fool-proof as true lifetime-infected types—, but at least it's visually obvious when you misuse the API).
So this can be seen as a CString/as_ptr() issue, but that's not my takeaway. This seems like a much more fundamental problem.
Basically, if I'm reading this correctly, the issue is that:
To call FFI functions (which I suspect is the most common use case for unsafe in systems programmig, at least), you need an unsafe block.
It's super-trivial to have the unsafe block involve code (expressions fed into the function) which goes beyond the underlying FFI call you're making, thereby introducing bugs.
So a bigger picture solution is to have better ways of doing unsafe calls that minimize the scope of unsafe code as much as possible. For example, imagine:
unsafe_call(libc::puts, match text {
None => "(no text supplied)\0".as_ptr() as *const c_char,
Some(s) => s.as_ptr(),
});
Ohhh I didn't read the spoiler closely enough, it's only caught by compiler because it's using a different API, not because it's out of the unsafe block.
The real issue is the borrow checker can't check lifetime of the raw pointer returned by as_ptr(), but this is a classic case of use-after-free which would be caught by such check.