【CXX】5.2 extern “C++“
#[cxx::bridge]
mod ffi {
extern "C++" {
include!("path/to/header.h");
include!("path/to/another.h");
// 这里声明暴露给Rust的C++类型和函数
}
}
extern "C++"部分是CXX桥接模块中用于声明暴露给Rust的C++类型和函数签名的部分,并指定包含相应C++声明的头文件路径。
一个桥接模块可以包含零个或多个extern "C++"块。
不透明的C++类型
在C++中定义的类型,可以通过间接方式暴露给Rust。
extern "C++" {
type MyType;
type MyOtherType;
}
例如,前面案例(BlobstoreClient)作为一个不透明的C++类型实现。BlobstoreClient在C++中创建,并通过UniquePtr返回给Rust。
可变性:与extern "Rust"类型和共享类型不同,extern "C++"类型不允许通过普通的可变引用&mut MyType跨FFI桥接传递。为了支持可变性,桥接必须使用Pin<&mut MyType>。这是为了防止像mem::swap这样的操作交换两个可变引用的内容,因为Rust没有关于底层对象大小的信息,也无法调用适当的C++移动构造函数。
线程安全性:请注意,CXX不会对你的extern "C++"类型的线程安全性做出任何假设。换句话说,CXX为你生成的Rust绑定MyType等不会自动实现Send和Sync。如果你确定你的C++类型满足Send和/或Sync的要求,并需要在Rust中利用这一点,你必须自己提供unsafe的标记trait实现。
/// C++实现的MyType是线程安全的。
unsafe impl Send for ffi::MyType {}
unsafe impl Sync for ffi::MyType {}
在这样做时要小心,因为如果你来自Rust背景,C++中的线程安全性可能非常难以评估。例如,教程中的BlobstoreClient类型并不是线程安全的,尽管它的实现中只做了一些完全无害的事情。并发调用tag成员函数会触发blobs映射上的数据竞争。
函数和成员函数
这部分主要遵循与extern "Rust"函数和方法相同的原则。特别是,任何带有self参数的签名都被解释为C++的非静态成员函数,并作为方法暴露给Rust。
程序员不需要保证他们输入的签名是准确的;这是不合理的。CXX会执行静态断言,确保签名与C++中声明的完全一致。相反,程序员只需要负责那些C++静态信息无法精确捕获的内容,即那些在C++代码中最多只能通过注释表示的内容(静态断言无法理解的内容):即C++函数从Rust调用是否安全。
安全性:extern "C++"块负责决定是否将每个签名暴露为安全调用或不安全调用。如果extern块包含至少一个安全调用的签名,则必须将其写为unsafe extern块,这表示块内容中做出了未经检查的安全性声明。
#[cxx::bridge]
mod ffi {
unsafe extern "C++" {
fn f(); // 安全调用
}
extern "C++" {
unsafe fn g(); // 不安全调用
}
}
生命周期
持有借用数据的C++类型可以通过带有泛型生命周期参数的extern类型在Rust中自然描述。例如,对于以下一对类型:
// header.h
class Resource;
class TypeContainingBorrow {
TypeContainingBorrow(const Resource &res) : res(res) {}
const Resource &res;
};
std::shared_ptr<TypeContainingBorrow> create(const Resource &res);
我们希望将其暴露给Rust为:
#[cxx::bridge]
mod ffi {
unsafe extern "C++" {
type Resource;
type TypeContainingBorrow<'a>;
fn create<'a>(res: &'a Resource) -> SharedPtr<TypeContainingBorrow<'a>>;
// 或者使用生命周期省略:
fn create(res: &Resource) -> SharedPtr<TypeContainingBorrow>;
}
}
重用现有的绑定类型
extern "C++"类型支持一种语法,用于声明当前桥接模块外部已经存在正确的C++类型的Rust绑定。这避免了生成一个新的绑定,Rust的类型系统会认为该绑定与第一个绑定不可互换。
#[cxx::bridge(namespace = "path::to")]
mod ffi {
extern "C++" {
type MyType = crate::existing::MyType;
}
extern "Rust" {
fn f(x: &MyType) -> usize;
}
}
在这种情况下,CXX不会为C++的::path::to::MyType生成一个新的Rust类型ffi::MyType,而是会重用已经存在的crate::existing::MyType绑定来表达f的签名以及桥接模块中MyType的任何其他用途。
CXX通过生成基于crate::existing::MyType的ExternType实现的静态断言,安全地验证crate::existing::MyType实际上是正确的C++类型::path::to::MyType的绑定。ExternType是一个trait,CXX会为它生成的绑定自动实现,但也可以手动实现,如下所述。
ExternType服务于以下两个相关的用例。
安全地在多个桥接中统一外部类型的出现
在以下代码片段中,两个不同文件(可能是不同的crate)中的#[cxx::bridge]调用都包含涉及相同C++类型example::Demo的函数签名。如果两者都只包含type Demo;,那么两个宏扩展都会生成各自独立的Rust类型Demo,因此编译器不允许我们将file1::ffi::create_demo返回的Demo作为file2::ffi::take_ref_demo接受的Demo参数传递。相反,其中一个Demo被定义为另一个的外部类型别名,使它们在Rust中成为相同的类型。
// file1.rs
#[cxx::bridge(namespace = "example")]
pub mod ffi {
unsafe extern "C++" {
type Demo;
fn create_demo() -> UniquePtr<Demo>;
}
}
// file2.rs
#[cxx::bridge(namespace = "example")]
pub mod ffi {
unsafe extern "C++" {
type Demo = crate::file1::ffi::Demo;
fn take_ref_demo(demo: &Demo);
}
}
与bindgen生成或手写的不安全绑定集成
手写的ExternType实现使得可以将bindgen生成的数据结构插入为CXX生成的C++类型的定义。
通过编写unsafe ExternType实现,程序员断言type id中给出的C++命名空间和类型名称引用了一个与实现中的Self类型等价的C++类型。
mod folly_sys; // bindgen生成的绑定
use cxx::{type_id, ExternType};
unsafe impl ExternType for folly_sys::StringPiece {
type Id = type_id!("folly::StringPiece");
type Kind = cxx::kind::Opaque;
}
#[cxx::bridge(namespace = "folly")]
pub mod ffi {
unsafe extern "C++" {
include!("rust_cxx_bindings.h");
type StringPiece = crate::folly_sys::StringPiece;
fn print_string_piece(s: &StringPiece);
}
}
// 现在如果我们构造一个StringPiece或通过bindgen生成的签名获得一个,
// 我们能够将其传递给ffi::print_string_piece。
ExternType::Id关联类型编码了类型的C++命名空间和类型名称的类型级表示。它将始终使用cxx crate中暴露的type_id!宏定义。
ExternType::Kind关联类型将始终是cxx::kind::Opaque或cxx::kind::Trivial,用于标识C++类型是否可以通过Rust的移动语义安全地重定位。只有在C++类型的移动构造函数是平凡的且没有析构函数时,才能在Rust中按值持有和传递它。在CXX中,这些类型称为Trivial的extern "C++"类型,而具有非平凡移动行为或析构函数的类型必须被视为Opaque,并且只能通过引用或UniquePtr等间接方式在Rust中处理。
如果你认为你的C++类型确实可以在Rust中按值持有和移动,你可以指定:
type Kind = cxx::kind::Trivial;
这将使你能够按值将其传递给C++函数,按值返回它,并将其包含在你声明给cxx::bridge的结构体中。你关于C++类型平凡性的声明将通过生成的C++绑定中的static_assert进行检查。
显式的shim trait实现
这是一个较为小众的功能,但在需要时非常重要。
CXX对C++的std::unique_ptr和std::vector的支持建立在一组内部trait实现上,这些实现将UniquePtr和CxxVector的Rust API连接到C++编译器执行的模板实例化。
当在多个桥接模块中重用绑定类型时(如前一节所述),你可能会发现你的代码需要一些CXX没有决定生成的trait实现。
#[cxx::bridge]
mod ffi1 {
extern "C++" {
include!("path/to/header.h");
type A;
type B;
// 正常:CXX看到UniquePtr<B>使用了同一桥接中定义的B类型,
// 并自动生成与std::unique_ptr<B>对应的模板实例化。
fn get_b() -> UniquePtr<B>;
}
}
#[cxx::bridge]
mod ffi2 {
extern "C++" {
type A = crate::ffi1::A;
// Rust trait错误:CXX处理此模块时无法看到上游库是否已经
// 生成了与std::unique_ptr<A>对应的模板实例化,因此它不会
// 在此处生成它们。如果上游库没有任何涉及UniquePtr<A>的签名,
// 则需要在其中一个模块中显式请求模板实例化。
fn get_a() -> UniquePtr<A>;
}
}
你可以通过在定义A但不包含任何UniquePtr使用的桥接模块中编写impl UniquePtr {}来请求在Rust crate层次结构中的特定位置进行特定的模板实例化。
#[cxx::bridge]
mod ffi1 {
extern "C++" {
include!("path/to/header.h");
type A;
type B;
fn get_b() -> UniquePtr<B>;
}
impl UniquePtr<A> {} // 显式实例化
}