【问题标题】:How do you work with a C++ function that returns a shared_ptr<T> when calling it from Rust over FFI?当通过 FFI 从 Rust 调用它时,如何使用返回 shared_ptr<T> 的 C++ 函数?
【发布时间】:2018-11-14 11:25:50
【问题描述】:

C++

shared_ptr<Foo> create_foo();

生锈

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

Bindgen 将 shared_ptr 变成不透明的 blob。

我不能只取原始指针,因为 C++ 代码不知道我有一个对 Foo 的引用,并且可能会调用它的解构函数。

【问题讨论】:

  • 您是否有可能以 C++ 或 Rust 拥有对象并管理其生命周期的方式设计代码?你可以在它被销毁时触发一个回调,告诉“其他”语言停止持有对它的引用。
  • 你也可以返回一个new shared_ptr&lt;Foo&gt;(...)...有点像有时从Rust方面完成的双重Box技巧。
  • 只是为了澄清这是一个现有的 c++ 库,我无法控制指针的创建方式。我只是从一个函数中得到一个 shared_ptr
  • 所以你希望能够访问指针内部类型?
  • @Stargateur 是的,但我不希望它在我使用它时是免费的,如果我只使用原始指针,我不能保证。

标签: c++ rust shared-ptr ffi


【解决方案1】:

std::shared_ptr 是 C++ 类和非平凡类型,不能按原样从库中导出 - 您需要在目标语言中定义它以符合 C++ 中的定义。要使用 FFI,您需要为您的库函数提供一个简单的 C ABI(C++ ABI 不稳定,可能会在编译器版本之间发生变化(Rust 的 ABI 也可能如此)),我怀疑与 std::shared_ptr 相关的所有函数都是这样的,所以还有一个障碍。

我建议从你的库中返回一个原始 C 指针,并在 Rust 中拥有它。

即使在 C++ 中,要加载 C++ 库,您也需要提供 C-ABI 函数(通过 extern C)来访问您的类型的指针,然后在 C++ 中按照您的需要使用它。

所以,有几点:

  1. 从一个没有名称修改的函数返回一个原始 C 指针,以便我们知道它的名称并可以链接到它:

    extern "C" Foo* create_foo();
    
  2. 添加一个知道如何正确释放对象的删除器:

    extern "C" void delete_foo(Foo *);
    
  3. 让库用户 (Rust) 决定如何拥有它,例如,通过 boxing 值并通过 std::sync::Arc 将其与原子引用计数器一起使用(就像 std::shared_ptr 所做的那样):

    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. 让 C++ 端忘记拥有这个指针 - 如果它是从库外部使用的,并且你给它一个原始指针,你就不能依赖它。

如果您对库源没有任何访问权限,则不能对它做任何事情:std::shared_ptr 对象永远不会释放指针,我们也不能让它不删除指针。

【讨论】:

  • “C++ abi 不稳定” -> “未指定”
  • 这意味着我猜它还没有稳定下来:)
  • 这是有区别的,因为没有稳定意味着(恕我直言),即将稳定并且有一个标准。未指定意味着根本没有规范。
  • 感谢您的建议。如果我返回一个原始指针,我如何确保 c++ 不会释放指针? Foo * create_foo() { shared_ptr&lt;Foo&gt; smart_p = lib::create_foo(); return smart_p.get(); } 这里 smart_p 的析构函数将在函数结束时被调用,如果它是最后一个,它将释放指针
  • TL;DR:你如何在 FFI 中使用 C++ shared_ptr你不会
【解决方案2】:

我不能只获取原始指针,因为 C++ 代码不知道我有一个对 Foo 的引用,并且可能将其称为解构函数。

是的,不是的。用你的实际例子。 C++ 会将shared_ptr 的所有权授予调用create_foo 的人,因此C++ 知道仍然存在拥有该指针的东西。

您需要添加一个get 函数,该函数将为您获取值而不会丢失指针的所有权,如下所示:

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) {
    }
    */
}

另外,shared_ptr&lt;Foo&gt; 不是有效的 C,所以我不知道 bindgen 和 C++ 是否接受这个(可能是一个警告),但你的代码中已经存在。

在 Rust 方面,您可以这样做:

// 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);
    }
}

当然,这只是一个例子,其中没有什么是真正安全的。由于我无法测试,如果我写了一些不起作用的东西,请告诉我。 bindgen 应该可以完成这项工作。

【讨论】:

  • 认为 std::shared_ptr&lt;Foo&gt; &amp;&amp;create_foo() 是错误的,完全没有意义。
  • @E_net4 是的,我只是创建了一些可以在 Rust 中编译的东西,但实际上这个结构需要匹配实际的结构 std::shared_ptr 如果不是,Rust 无法正确移动它。
  • @hellow 我也不确定,因为我们告诉 rust 使用 C 语义,我想它不会,但在这种情况下destroy_foo(std::shared_ptr&lt;Foo&gt; &amp;&amp;foo) 也没有意义。因为 rust 会复制值?这是工作吗?我真的不知道。
  • 在你的例子中,如果我们将 shared_ptr 包装在一个结构中并使其不透明,那么我们可以保持所有权并仍然获得原始 ptr 例如:struct SmartPtr {smart_ptr: opaque_blob[u64; 2],raw_ptr: * mut Foo, }
  • @Tom 将原始指针与 shared_ptr 分开是个坏主意,在我的示例中我会这样做,但我建议您创建一个隐藏 shared_ptr 原始指针的结构。
【解决方案3】:

你可以返回一个指向动态分配的std::shared_ptr的指针

在 C++ 方面:

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;
}

在 Rust 方面:

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

如果您未能调用rust_delete_foo,那么动态分配的指针将会泄漏,因此该对象将永远不会被释放。

然后,当您想使用该对象时,您必须编写接受 void* 的包装函数,进行转换并调用适当的函数。

【讨论】:

    猜你喜欢
    • 2018-01-14
    • 1970-01-01
    • 2019-06-15
    • 1970-01-01
    • 2021-10-31
    • 1970-01-01
    • 2017-07-18
    • 2019-12-28
    • 2015-02-07
    相关资源
    最近更新 更多