How do you work with a C++ function that returns a shared_ptr<T> when calling it from Rust over FFI?

Viewed 1183

C++

shared_ptr<Foo> create_foo();

Rust

extern "C" {
    pub fn create_foo() -> ???;
}

Bindgen turns a shared_ptr into an opaque blob.

I can't just take the raw pointer because then the C++ code is unaware that I have a reference to Foo and might call its deconstructor.

3 Answers

std::shared_ptr is a C++ class and a non-trivial type that can not be exported as is from a library — you need its definition in your target language to conform to the one in C++. To use FFI you need to provide your library functions with a simple C ABI (the C++ ABI is not stable and may change between compiler versions (as might Rust's ABI)) and I doubt that all of functions related to std::shared_ptr are such, so there is one more obstacle for that.

I'd suggest to return a raw C-pointer from your library instead and own it in Rust.

Even in C++, to load a C++ library you provide C-ABI functions (via extern C) to gain access to a pointer of your type and then you use it in C++ as how as you want.

So, a few points:

  1. Return a raw C pointer from a function without name mangling so that we know its name and can link to it:

    extern "C" Foo* create_foo();
    
  2. Add a deleter which knows how to properly deallocate the object:

    extern "C" void delete_foo(Foo *);
    
  3. Let the library user (Rust) decide how to own it, for example, by boxing the value and using it with atomic reference counter via std::sync::Arc (as std::shared_ptr does):

    extern "C" {
        fn create_foo() -> *mut Foo;
        fn delete_foo(p: *mut Foo);
    }
    
    struct MyFoo {
        raw: *mut Foo,
    }
    
    impl MyFoo {
        fn new() -> MyFoo {
            unsafe { MyFoo { raw: create_foo() } }
        }
    }
    
    impl Drop for MyFoo {
        fn drop(&mut self) {
            unsafe {
                delete_foo(self.raw);
            }
        }
    }
    
    fn main() {
        use std::sync::Arc;
    
        let value = Arc::new(MyFoo::new());
        let another_value = value.clone();
        println!("Shared counter: {}", Arc::strong_count(&value));
    }
    
  4. Let the C++ side forget about owning this pointer - you can't rely on it if it is used from outside the library and you give a raw pointer to it.

If you don't have any access to the library sources, can't do anything with it: the std::shared_ptr object will not release the pointer ever and we can't make it not to delete the pointer.

I can't just take the raw pointer because then the C++ code is unaware that I have a reference to Foo and might calls it's deconstructor.

Yes and no. With your actual example. C++ will give ownership of the shared_ptr to the one who called create_foo, so C++ knows that there is still something that owns the pointer.

You need to add a get function that will get the value for you without losing ownership of the pointer, something like this:

extern "C" {
    std::shared_ptr<Foo> create_foo() {
        // do the thing
    }
    /* or maybe this
    std::shared_ptr<Foo> &&create_foo() {
        // do the thing
    }
    */

    Foo *get_foo(std::shared_ptr<Foo> &foo) {
        foo.get();
    }

    void destroy_foo(std::shared_ptr<Foo> foo) {
    }
    /* or maybe this
    void destroy_foo(std::shared_ptr<Foo> &&foo) {
    }
    */
}

Also shared_ptr<Foo> is not valid C, so I don't know if bindgen and C++ with accept this (probably a warning) but that is already present in your code.

On the Rust side, you could do this:

// must be generated by bindgen and this might create a lot of problems
// this need to be the same struct as the shared_ptr on the C++ side.
// if even one octet is not correct you will run into bugs
// BE SURE that bindgen don't implement Copy for this
struct shared_ptr<T>; 

struct Foo(i32);

extern "C" {
    fn create_foo() -> shared_ptr<Foo>;

    fn get_foo(foo: &shared_ptr<Foo>) -> *mut Foo;

    fn destroy_foo(foo: shared_ptr<Foo>);
}

fn main() {
    unsafe {
        let my_shared_foo = create_foo();
        let foo = get_foo(&my_shared_foo);
        (*foo).0;
        destroy_foo(my_shared_foo);
    }
}

Of course this is just an example, and nothing is really safe in any of this. And as I can't test, please let me know if I wrote something that doesn't work. bindgen should do the job.

You can return a pointer to a dynamically allocated std::shared_ptr

In the C++ side:

shared_ptr<Foo> create_foo();
extern "C" void *rust_create_foo()
{
    shared_ptr<Foo> foo = create_foo();
    return static_cast<void*>(new shared_ptr<Foo>(foo));        
}
extern "C" void rust_delete_foo(void *ptr)
{
    shared_ptr<Foo> *foo = static_cast<shared_ptr<Foo>*>(ptr);
    delete foo;
}

And in the Rust side:

extern "C" {
    pub fn rust_create_foo() -> *const c_void;
    pub fn rust_delete_foo(foo: *const c_void);
}

If you fail to call rust_delete_foo then the dynamically allocated pointer will leak and so the object will never be deallocated.

Then, when you want to use the object, you will have to write wrapper functions that take that void*, do the cast and call the appropriate function.

Related