I'm writing a Rust library which is a wrapper over a C++ library.
Here is the C++ side:
#define Result(type,name) typedef struct { type value; const char* message; } name
extern "C"
{
Result(double, ResultDouble);
ResultDouble myFunc()
{
try
{
return ResultDouble{value: cv::someOpenCvMethod(), message: nullptr};
}
catch( cv::Exception& e )
{
const char* err_msg = e.what();
return ResultDouble{value: 0, message: err_msg};
}
}
}
and corresponding Rust side:
#[repr(C)]
struct CResult<T> {
value: T,
message: *mut c_char,
}
extern "C" {
fn myFunc() -> CResult<c_double>;
}
pub fn my_func() -> Result<f64, Cow<'static, str>> {
let result = unsafe { myFunc() };
if result.message.is_null() {
Ok(result.value)
} else {
unsafe {
let str = std::ffi::CString::from_raw(result.message);
let err = match str.into_string() {
Ok(message) => message.into(),
_ => "Unknown error".into(),
};
Err(err)
}
}
}
I have two questions here:
*const char
on C++ side but *mut c_char
on Rust one? I need it because CString::from_raw
requires mutable reference.CStr
instead? If yes, how should I manage its lifetime? Should I free this memory or maybe it has static lifetime?Generally I just want to map a C++ exception which occurs in FFI call to Rust Result<T,E>
What is the idiomatic way to do it?
- Is it ok that I use *const char on C++ side but *mut c_char on Rust one? I need it because CString::from_raw requires mutable reference.
The documentation on CString::from_raw
already answers the first part of the question:
"This should only ever be called with a pointer that was earlier obtained by calling into_raw on a
CString
".
Attempting to use a pointer to a string which was not created by CString
is inappropriate here, and will eat your laundry.
- Should I use CStr instead? If yes, how should I manage its lifetime? Should I free this memory or maybe it has static lifetime?
If the returned C-style string is guaranteed to have a static lifetime (as in, it has static duration), then you could create a &'static CStr
from it and return that. However, this is not the case: cv::Exception
contains multiple members, some of which are owning string objects. Once the program leaves the scope of myFunc
, the caught exception object e
is destroyed, and so, anything that came from what()
is invalidated.
const char* err_msg = e.what();
return ResultDouble{0, err_msg}; // oops! a dangling pointer is returned
While it is possible to transfer values across the FFI boundary, the responsibility of ownership should always stay at the source of that value. In other words, if the C++ code is creating exceptions and we want to provide that information to Rust code, then it's the C++ code that must retain that value and free it in the end. I took the liberty of choosing one possible approach below.
By following this question on duplicating C strings, we can reimplement myFunc
to store the string in a dynamically allocated array:
#include <cstring>
ResultDouble myFunc()
{
try
{
return ResultDouble{value: cv::someOpenCvMethod(), message: nullptr};
}
catch( cv::Exception& e )
{
const char* err_msg = e.what();
auto len = std::strlen(err_msg);
auto retained_err = new char[len + 1];
std::strcpy(retained_err, err_msg);
return ResultDouble{value: 0, message: retained_err};
}
}
This makes it so that we are returning a pointer to valid memory. Then, a new public function will have to be exposed to free the result:
// in extern "C"
void free_result(ResultDouble* res) {
delete[] res->message;
}
In Rust-land, we'll retain a copy of the same string with the approach described in this question. Once that is done, we no longer need the contents of result
, and so it can be freed with an FFI function call to free_result
. Handling the outcome of to_str()
without unwrap
is left as an exercise to the reader.
extern "C" {
fn myFunc() -> CResult<c_double>;
fn free_result(res: *mut CResult<c_double>);
}
pub fn my_func() -> Result<f64, String> {
let result = unsafe {
myFunc()
};
if result.message.is_null() {
Ok(result.value)
} else {
unsafe {
let s = std::ffi::CStr::from_ptr(result.message);
let str_slice: &str = c_str.to_str().unwrap();
free_result(&mut result);
Err(str_slice.to_owned())
}
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With