Passing an owned Rust string, cleaning up and UB

I'm trying to transfer a Rust string &str across an FFI boundary and think that I am going about it completely backwards. As a first approach, I've decided to create a CString from the provided string, box this into a Box<CStr>, and get a pointer to the underlying C-string, as well as "forget" the box by

let string_box = c_string.into_boxed_c_str();
let ptr: *const i8 = string_box.as_ptr();

let _ = Box::into_raw(string_box);

This is all well and good for passing the string. Now, I'm concerned with freeing the box after reading the string across the FFI. I can't just transmute the pointer I've passed back into a *mut CStr seeing as how CStr is dynamically-sized (hence behind wide pointers). I don't really want to resort to using implementation details, so my questions are:

  1. What is the correct way to pass said strings and not leak the memory? Is it to provide a pointer to a Vec<u8>?
  2. If I could transmute said pointer, am I allowed to free the memory from said transmuted pointer? I understand that the *const i8 I obtained has a different provenance than the pointer from Box::from_raw, though these pointers will have the same integer representation. Will the compiler agree with me, or is this UB?

With such an unsafe C-FFI boundary, you must first and foremost decide who is going to own that string data. Will it be given back by the C code or shall ownership be transferred to the C code?

That's not entirely accurate. Assuming you want the other side to free your CStr for you:

#[repr(C)]
struct CStrFFI {
    ptr: *const i8,
    free: extern "C" fn(*const i8),
}

fn main() {
    use std::ffi::CStr;
    let cstr: Box<CStr> = c"cstr".into();

    let ptr = Box::leak(cstr).as_ptr();
    extern "C" fn free(ptr: *const i8) {
        // SAFETY: before - Box<CStr> -> &'static mut CStr -> *const i8
        // now - *const i8 -> &CStr -> *const CStr -> *mut CStr -> Box<CStr>;
        unsafe {
            let cstr = CStr::from_ptr(ptr) as *const CStr as *mut CStr;
            let alloc = Box::from_raw(cstr);
            let _ = drop(alloc);
        }
    }

    let cstr_ffi = CStrFFI { ptr, free };
    // pass your `CStrFFI` across your boundary,
    // and let the other side call `free` manually
    // once the `CStr` in question is no longer required
}

Currently CStr is a transparent wrapper around [u8], which makes it a fat pointer. Technically you could recreate *mut [u8] by counting strlen and cast that into *mut CStr that works with Box::from_raw. However, comments in libstd warn against that. Libstd deliberately doesn't commit to representation of CStr, because there's a hope it can someday be simplified to a thin pointer that is ABI-compatible with char* directly.

This should be safe:

Box::from_raw(CStr::from_ptr(charptr) as *mut CStr)

CStr::from_ptr handles conversion from thin to fat pointer, but gives you a temporary non-owning reference. Another cast lets you transmute that into a boxed one.

Thanks! This is more or less what I wished to do. I was unsure whether the compiler would like it or not, as is the case for transmuting shared references to exclusive ones.

I think I have a gap in my mental model of how pointer casts should work, because the conversion &CStr -> *mut CStr feels like it would lead to UB. Is this specific instance legal because the const pointer producing the shared reference shares the same integer address with the pointer that has mutable provenance?

Tangentially, miri here warns of precisely this "mismatch" of provenance; is there a way to satisfy miri in this case (possibly through some form of annotation)?

You'd probably want to convert &CStr -> *const CStr --to_raw_parts--> (*const (), <CStr as Pointee>::Metadata), then throw out that first pointer and reconstruct fat pointer with full provenance using from_raw_parts.

Note that you only need CStr if you neeed C-style strings (NUL-terminated). If your C code can accept a pointer+length encoded in UTF-8, you can just pass the original &str (but as two arguments or as one #[repr(C)] struct MyStr(*const u8, usize);, not as &str).

This is great! I was able to finally replicate it, though I'm not super stoked about the fact that it relies on the ptr_metadata unstable feature.

This is already satisfactory, though I'd like to ask a final question following this before finalizing my implementation:

The documentation page for Pointee states that

For the str type, metadata is the length in bytes as usize

is this also the case for CStr; i.e., can I use the extra provenance information I pass along (which is a usize as well) as the length information? This intuitively feels correct, though I am unsure whether this is actually the case or not, since it's difficult to guess what parts of the ABI is stable or subject to change.

Thanks!

Best I've been able to produce about this is as follows:

  1. CStr is internally represented as a [c_char],
  2. The call to metadata is resolved by calling the intrinsic function ptr_metadata, which is reduced to an UnOp::PtrMetadata as specified in the documentation,
  3. The PtrMetadata variant of UnOp resolves types based on their Pointee implementation?, and in this specific case, <CStr as Pointee>::Metadata == usize because of point 1, though this is also "subject to change" again by point 1.

It seems the best course of action then is to simply take this for granted and react accordingly if and when this changes.

No, you should consider computing CStr's length O(N), because it will be switched to that one day. If you need the length frequently, carry it with you (CString is not affected).

CStr today has limited usefulness. It's mostly useful only to convert from-FFI C string into Rust string and to be sure the C strings your API accepts from Rust code and sends to C code (via *const c_char, not CStr) are valid.