I'm writing a Rust wrapper for a (mostly C-style) C++ plug-in SDK. The plug-in host is a graphical desktop application that runs an event loop. The plug-in is regularly called as part of that event loop. Whenever this happens, the plug-in has control and can call arbitrary host functions.
One C function which I want to wrap returns a raw pointer. Right after that function returns, the pointer is guaranteed to be a valid C string, so it is safe to dereference it. However, after the plug-in callback returns (thus giving back control to the host), the pointer can become stale. How can I write an ergonomic function wrapper for this which will not result in undefined behavior at some point, e.g. when the consumer tries to access the string in the next event loop cycle?
I've thought about the following approaches:
1. Return an owned string
I could immediately dereference the pointer and copy the content into an owned CString
:
pub fn get_string_from_host() -> CString {
let ptr: *const c_char = unsafe { ffi.get_string() };
unsafe { CStr::from_ptr(ptr).to_owned() }
}
This is presumptuous — maybe the consumer of my wrapper is not interested in getting an owned string because they just want to make a comparison (that's even the primary use case I would say). Copying the string would be a total waste then.
2. Return the raw pointer
pub fn get_string_from_host() -> *const c_char {
unsafe { ffi.get_string() }
}
This just shifts the problem to the consumer.
3. Return a CStr
reference (unsafe method)
pub unsafe fn get_string_from_host<'a>() -> &'a CStr {
let ptr: *const c_char = ffi.get_string();
CStr::from_ptr(ptr)
}
This is unsafe because the lifetime of the reference is not accurate. Accessing the reference at a later point in time can result in undefined behavior. Another way of shifting the problem to the consumer.
4. Take a closure instead of returning something
pub fn with_string_from_host<T>(f: impl Fn(&CStr) -> T) -> T {
let ptr: *const c_char = unsafe { ffi.get_string() };
f(unsafe { CStr::from_ptr(ptr) })
}
pub fn consuming_function() {
let length = with_string_from_host(|s| s.to_bytes().len());
}
This works but really needs getting used to.
None of these solutions are really satisfying.
Is there a way to make sure a return value is used "immediately", meaning that it is not stored anywhere or never escapes the caller's scope?
This sounds like a job for references/lifetimes, but I'm not aware of any lifetime annotation which means something like "valid just in the current stackframe". If there would be, I would use that (just for illustration):
pub fn get_string_from_host() -> &'??? CStr {
let ptr: *const c_char = unsafe { ffi.get_string() };
unsafe { CStr::from_ptr(ptr) }
}
pub fn consuming_function() {
// For example, this shouldn't be possible in this case
let prolonged: &'static CStr = get_string_from_host();
// But this should
let owned = get_string_from_host().to_owned();
}
unsafe
, which is very good for API ergonomics. – helgoboss