通过调用接收它的方法使用的 FFI 对象的固定引用

Pinned reference to FFI object consumed by calling methods that receive it

我正在编写一个外部函数接口 (ffi) 以将预先存在的 C++ 库的 API 暴露给我正在编写的一些新 Rust 代码。为此,我正在使用 Rust cxx 模块。

我 运行 遇到了一些与 Pin 相关的问题(我不得不承认我没有完全掌握这个话题)。

我的 C++ 模块有一个 API 从拥有这些包含对象的主对象公开指向某些包含对象的指针。这是一个人为的例子:

// test.hpp
#include <string>
#include <memory>

class Row {
    std::string row_data;
public:
    void set_value(const std::string& new_value) {
        this->row_data = new_value;
    }
};

class Table {
    Row row;
public:
    Table() : row() {};
    Row* get_row() {
        return &this->row;
    }

};

inline std::unique_ptr<Table> make_table() {
    return std::make_unique<Table>();
}

想法是您创建一个 Table 对象,然后允许您获取指向它的指针 Row 以便您可以操作它。

我创建 Rust FFI 的尝试如下所示:

// main.rs
use std::pin::Pin;

use cxx::let_cxx_string;


#[cxx::bridge]
mod ffi {

    unsafe extern "C++" {
        include!("src/test.hpp");

        type Table;

        pub fn make_table() -> UniquePtr<Table>;

        fn get_row(self: Pin<&mut Table>) -> *mut Row;

        type Row;

        pub fn set_value(self: Pin<&mut Row>, value: &CxxString);
    }
}

impl ffi::Table {
    pub fn get_row_ref<'a>(self: Pin<&'a mut ffi::Table>) -> Pin<&'a mut ffi::Row> {
        unsafe { Pin::new_unchecked(&mut *self.get_row()) }
    }
}

fn main() {

    let mut table = ffi::make_table();

    let row = table.pin_mut().get_row_ref();

    let_cxx_string!(hello="hello world");
    
    row.set_value(&hello);
    
    let_cxx_string!(hello2="bye world");
    row.set_value(&hello2);


}

注意:

我有两个问题:

  1. 我在这里打电话给Pin::new_unchecked可以吗?文档暗示它不是:

    calling Pin::new_unchecked on an &'a mut T is unsafe because while you are able to pin it for the given lifetime 'a, you have no control over whether it is kept pinned once 'a ends

    如果那不安全,我该如何进行?

  2. 此程序编译失败,出现以下错误:

    error[E0382]: use of moved value: `row`
      --> src/main.rs:41:2
       |
    34 |     let row = table.pin_mut().get_row_ref();
       |         --- move occurs because `row` has type `Pin<&mut Row>`, which does not implement the `Copy` trait
    ...
    38 |     row.set_value(&hello);
       |     --- value moved here
    ...
    41 |     row.set_value(&hello2);
       |     ^^^ value used here after move
    
    

    第一次调用 set_value 消耗了固定引用,之后就不能了 再次使用。 &mut T 不是 Copy,所以 Pin<&mut Row> 也不是 Copy

    如何设置 API 以便对 Row 的引用可用于多个 连续的方法调用(在 cxx 建立的约束范围内)?

想要尝试的人:

# Cargo.toml
[dependencies]
cxx = "1.0.52"

[build-dependencies]
cxx-build = "1.0"
// build.rs
fn main() {
    cxx_build::bridge("src/main.rs")
        .flag("-std=c++17")
        .include(".")
        .compile("test");
}
  1. Is it sound for me to call Pin::new_unchecked here?

是的,很好听。在这种情况下,我们知道 Row 已固定,因为:

  1. Table 已固定;
  2. Row 内联存储在 Table 中;
  3. C++ 的移动语义本质上意味着每个 C++ 对象无论如何都是“固定”的。
  1. This program fails to compile with the following error:

当您在普通可变引用 (&mut T) 上调用方法时,编译器会隐式执行 重新借用 以避免移动可变引用,因为 &mut T 不是 Copy。不幸的是,这个编译器“魔法”没有扩展到 Pin<&mut T>(也不是 Copy),所以我们必须明确地重新借用。

最简单的再借方法是使用Pin::as_mut()。这个用例甚至在文档中被调用:

This method is useful when doing multiple calls to functions that consume the pinned type.

fn main() {
    let mut table = ffi::make_table();

    let mut row = table.pin_mut().get_row_ref();

    let_cxx_string!(hello="hello world");
    row.as_mut().set_value(&hello);

    let_cxx_string!(hello2="bye world");
    row.as_mut().set_value(&hello2);
}

在最后一次使用 row 时使用 as_mut() 并不是绝对必要的,但始终如一地应用它可能会更清楚。当使用优化编译时,这个函数可能是一个 noop(对于 Pin<&mut T>)。

How do I set up the API so that the reference to Row can be used for multiple successive method calls (within the constraints established by cxx)?

如果您想隐藏 as_mut(),您可以添加一个接受 &mut Pin<&mut ffi::Row> 并执行 as_mut() 调用的方法。 (请注意,as_mut() 是在 &mut Pin<P> 上定义的,因此编译器将插入外部 &mut 的重新借用。)是的,这意味着现在有两个间接级别。

impl ffi::Row {
    pub fn set_value2(self: &mut Pin<&mut ffi::Row>, value: &cxx::CxxString) {
        self.as_mut().set_value(value)
    }
}

fn main() {
    let mut table = ffi::make_table();

    let mut row = table.pin_mut().get_row_ref();

    let_cxx_string!(hello="hello world");
    row.set_value2(&hello);

    let_cxx_string!(hello2="bye world");
    row.set_value2(&hello2);
}