Am I calling the C function correctly?

I want to use libc::statfs64 to get the f_type value of a file path.

I wrote the following code and it appears to be working.
However, I'm not familiar with calling C functions, so some questions remain, as you can see from the comments in the code.

Can anyone give me some advice?

#[cfg(not(unix))]
compile_error!("This program is for Unix-like systems.");

use std::{ffi::CString, io, mem::MaybeUninit, os::unix::prelude::OsStrExt, path::Path};

use libc::{c_char, statfs64};

fn main() -> io::Result<()> {
    // path for debugging
    let path: &Path = Path::new("/tmp");

    let stat: statfs64 = unsafe {
        // Q1. Is MaybeUninit suitable for allocating memory for structs passed to libc functions?
        let mut uninit = MaybeUninit::<statfs64>::uninit();

        // Q2. I want to get `*const c_char` from `&Path` to pass it to `libc::statfs64`.
        //     Is this code correct (on Unix)?
        let path_str = CString::new(path.as_os_str().as_bytes())?;
        let path_ptr: *const c_char = path_str.as_ptr();

        let status = libc::statfs64(path_ptr, uninit.as_mut_ptr());

        if status == 0 {
            // Q3. Is `assume_init()` the proper way to get a value from MaybeUninit,
            //     which holds a struct initialized by a C function?
            uninit.assume_init()
        } else {
            // Q4. Do I need to drop or free the `uninit` variable before returning `Err`?
            //     If the answer is yes, how should I do it?
            uninit.assume_init_drop();

            return Err(io::Error::last_os_error());
        }
    };

    println!("path = {}", path.display());
    println!("f_type = {}", stat.f_type);

    // Q5. Do I need to drop or free the `stat` variable here?
    //libc::free(...);

    Ok(())
}

You've done everything correctly except Q4. When libc::statfs64() fails you don't generally want to run the destructor for uninit because there is no guarantee that it was initialized. Indeed, you can probably assume it wasn't initialized because the function initializing it failed.

You'll most probably get away with this because statfs64 is a Copy type and doesn't have a destructor, but for something more complicated (e.g. maybe you've added a Drop impl to some sort of vector that frees it automatically) then you'd be reading uninitialized memory.

3 Likes

yes, it's a perfectly valid use case for MaybeUninit when calling foreign function with an "output" pointer. but you should convert it the underlying type as soon as you validated the foreign function's result, i.e. by calling MaybeUninit::assume_init()

yes, CStr::as_ptr() is the documented way to get a valid ffi pointer (i.e. C style string). to quote the documentation for OsString from std:

OsString and OsStr bridge this gap by simultaneously representing Rust and platform-native string values, and in particular allowing a Rust string to be converted into an “OS” string with no cost if possible. A consequence of this is that OsString instances are not NUL terminated; in order to pass to e.g., Unix system call, you should create a CStr.

yes, assume_init() consumes the MaybeUnint<T> and returns a T.

no, if the initialization failed, the memory stays in "uninitialized" state and you should do nothing. specifically, DO NOT call assume_init_drop() on uninitialized memory, it's UB.

no, DO NOT call libc::free() unless it's a pointer returned by libc::malloc().

4 Likes

Thanks guys, the questions were cleared up. :smiling_face_with_three_hearts:

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.