Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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.

like image 863
Tom Avatar asked Nov 14 '18 11:11

Tom


People also ask

How does rust call C functions?

Rust can link to/call C functions via its FFI, but not C++ functions. While I don't know why you can't call C++ functions, it is probably because C++ functions are complicated. You can just define C linkage on any C++ function, making it available from C and thus also Rust. extern "C" is your friend here.

What is the purpose of the shared_ptr <> template?

std::shared_ptr is a smart pointer that retains shared ownership of an object through a pointer. Several shared_ptr objects may own the same object.

What happens when shared_ptr goes out of scope?

All the instances point to the same object, and share access to one "control block" that increments and decrements the reference count whenever a new shared_ptr is added, goes out of scope, or is reset. When the reference count reaches zero, the control block deletes the memory resource and itself.


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

like image 113
Victor Polevoy Avatar answered Sep 28 '22 05:09

Victor Polevoy


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.

like image 30
Stargateur Avatar answered Sep 28 '22 07:09

Stargateur