4

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?

vijoc
  • 683
  • 8
  • 17
Alex Zhukovskiy
  • 9,565
  • 11
  • 75
  • 151
  • I would strongly recommend templates instead of macros for `Result`. A simple templated `struct`, with a type alias would work just fine. It's also not necessary to `typedef struct` in c++. `struct` are implicitly proper type names. – François Andrieux Jan 24 '18 at 19:02
  • @FrançoisAndrieux IIRC templates cannot be used in `extern C` interfaces. See https://stackoverflow.com/questions/4877705/why-cant-templates-be-within-extern-c-blocks – Alex Zhukovskiy Jan 24 '18 at 19:03
  • I hadn't noticed it was `extern C`. – François Andrieux Jan 24 '18 at 19:04
  • 2
    The documentation on [`CString::from_raw`](https://doc.rust-lang.org/stable/std/ffi/struct.CString.html#method.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`"_. This question is then narrowed down to returning an owning string from C into Rust code. – E_net4 Jan 24 '18 at 19:07
  • @E_net4 when I saw OpenCV examples from documentation ([link](https://docs.opencv.org/2.4/modules/core/doc/intro.html#error-handling)) I didn't see they free `e.what()` so probably it's a static string which can be safely converted into `CStr`. But i'm not sure. However, I see your point, good catch. – Alex Zhukovskiy Jan 24 '18 at 19:10
  • 2
    In C++, exceptions and their contents are already freed automatically. The `what` member function usually either returns a static string or returns a pointer to a string that was previously built and kept as a member of the exception. In OpenCV, it seems to be the latter. I'm afraid that it is not safe to return that pointer in particular. – E_net4 Jan 24 '18 at 19:16
  • 1
    Are `try`/`catch` allowed in `extern "C"` sections since `try` and `catch` are not in the C language? – Thomas Matthews Jan 24 '18 at 19:28
  • @ThomasMatthews I think it's ok since it isn't used in public API. – Alex Zhukovskiy Jan 24 '18 at 19:29
  • 3
    Can you check whether that `return ResultDouble{value: cv::someOpenCvMethod(), message: nullptr};` compiles? I don't think naming the arguments like that is valid C++. – E_net4 Jan 24 '18 at 19:31
  • @E_net4 i'm sure it does and I actually get the error message on rust side: `running 1 test OpenCV Error: Assertion failed (A.size == arrays[i0]->size) in init, file C:\Users\Alex\Documents\foo\opencv\modules\core\src\matrix.cpp, line 4845 error: test failed, to rerun pass '--test test_imgproc' `. I think it's works because of gcc, see [link](https://stackoverflow.com/a/14101575/2559709) – Alex Zhukovskiy Jan 24 '18 at 19:35
  • 2
    @AlexZhukovskiy: That's not C initializer syntax. Designated initializers would look like `{.value = ..., .message = ...}`. – Nicol Bolas Jan 24 '18 at 20:05

1 Answers1

7
  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())
        }
    }
}
E_net4
  • 27,810
  • 13
  • 101
  • 139
  • If you want C-strings, I suggest you use C-like code instead. The entire catch clause can be rewritten: `return ResultDouble{0, std::strdup(e.what())};`, then you have to rewrite the body of `free_result` to `std::free(res->message);`. – Matthieu M. Jan 24 '18 at 20:15
  • The `str_slice` you create on the Rust side is never used... I think it's a left-over. Or that there is a mix-up as `s` is not a `String`. – Matthieu M. Jan 24 '18 at 20:17
  • @MatthiewM I deliberately avoided `strdup` because it is not ISO C (although it should work in practice, all right). As for the `str_slice `, yep, I meant to return it instead of `s`. That's fixed. – E_net4 Jan 24 '18 at 20:29
  • @E_net4 nice one. However, your `exercise to the reader` has been already solved in the question :) – Alex Zhukovskiy Jan 24 '18 at 21:37
  • @AlexZhukovskiy Well, yet another reason to omit the solution here for brevity. :) – E_net4 Jan 24 '18 at 21:39