Concerns for FFI

enums are not FFI-safe

What to look for: enums appearing in signatures of extern fns.

Summary: It is undefined behavior for an enum in rust to carry an invalid value. Therefore, do not make it possible for C code to supply the value of an enum type.

Incorrect:

# fn main() {}
#[repr(u16)]
pub enum Mode {
    Read = 0,
    Write = 1,
}

#[allow(unused)]
extern "C" fn rust_from_c(mode: Mode) {
    // ...
}

Also incorrect:

# #[repr(u16)]
# pub enum Mode {
#     Read = 0,
#     Write = 1,
# }
#
extern "C" {
    fn c_from_rust(mode: *mut Mode);
}

fn main() {
    let mut mode = Mode::Read;
    unsafe { c_from_rust(&mut mode); }
}

CString::from_raw

Things to look for: Any usage of CString::{into_raw, from_raw}.

Summary: As documented, CString::from_raw recomputes the length by scanning for a null byte. What it doesn't (currently) mention is that this length must match the original length.

I think you'll be hard pressed to find any C API function that mutates a char * without changing its length!

Incorrect

extern crate libc;

use std::ffi::{CString, CStr};

fn main() {
    let ptr = CString::new("Hello, world!").unwrap().into_raw();
    let delim = CString::new(" ").unwrap();
    
    let first_word_ptr = unsafe { libc::strtok(ptr, delim.as_ptr()) };
    
    assert_eq!(
        unsafe { CStr::from_ptr(first_word_ptr) },
        &CString::new("Hello,").unwrap()[..],
    );
    
    drop(unsafe { CString::from_raw(ptr) });
}

This is incorrect because strtok inserts a NUL byte after the comma in "Hello, world!", causing the CString to have a different length once it is reconstructed. As a result, when the CString is freed, it will pass the wrong size to the allocator.

The fix is to never use these methods. If a C API needs to modify a string, use a Vec<u8> buffer instead.

Correct

extern crate libc;

use std::ffi::{CString, CStr};
use libc::c_char;

fn main() {
    let mut buf = CString::new("Hello, world!").unwrap().into_bytes_with_nul();
    let delim = CString::new(" ").unwrap();

    let first_word_ptr = unsafe {
        libc::strtok(buf.as_mut_ptr() as *mut c_char, delim.as_ptr())
    };

    assert_eq!(
        unsafe { CStr::from_ptr(first_word_ptr) },
        &CString::new("Hello,").unwrap()[..],
    );
}

Also: Store a CString to a local before calling as_ptr()

Just as an aside, there's another footgun here. If I had written:

Incorrect:

# use std::ffi::CString;
# fn main() {
let delim = CString::new(" ").unwrap().as_ptr();
# let _ = delim;
# }

the buffer would have been freed immediately.