Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Map C++ exceptions to Result

Tags:

c++

opencv

rust

ffi

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:

  1. 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.
  2. Should I use 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?

like image 380
Alex Zhukovskiy Avatar asked Mar 08 '23 02:03

Alex Zhukovskiy


1 Answers

  1. 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.

  1. 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())
        }
    }
}
like image 198
E_net4 stands with Ukraine Avatar answered Mar 17 '23 05:03

E_net4 stands with Ukraine