被FFI做局了(2) —— mut & unsafe#
在rust中写ffi有两种大方向:手动绑定以及自动绑定。 如果万幸对接的是modern Cpp,那还可以用诸如cxx之类的库方便快速且安全的生成。 但是如果不幸对接的是C、 stdc++ < 11,特别是头文件不多的,用bindgen可能还没手写快。
涉及到互相暴露接口的(rust需要调C,C也需要调rust),还是只能手动添加必要的绑定。
基本接口#
由于我们需要封装rust代码到c++侧使用,所以大多数接口是在rust代码中extern的。
例如我们要暴露一个client以及其方法:
/// rust lib
struct Client;
impl Client {
fn new() -> Self {
todo!()
}
fn foo(&self) {
todo!()
}
}
我们就需要实现对应的extern方法,通过传递裸指针的方式。
叽里咕噜写了一大堆,还是自动生成binding舒服啊。
struct ExportClient {
client: Client,
}
#[no_mangle]
unsafe extern "C" fn client_new() -> *mut ExportClient {
let c = ExportClient {
client: Client::new(),
};
Box::into_raw(Box::new(c))
}
#[no_mangle]
unsafe extern "C" fn client_delete(ptr: *mut ExportClient) {
if !ptr.is_null() {
let _ = unsafe { Box::from_raw(ptr) };
}
}
#[no_mangle]
unsafe extern "C" fn client_foo(ptr: *mut ExportClient) {
// 我们简单假定ptr非空,所以我们不再检查
unsafe { (*ptr).client.foo() };
}
与之对应的C++侧需要一些绑定,以便调用到rust的函数。我们不希望rust实现公开,只希望提供一个lib和一个header便可以使用。因此我们在头文件中需要隐藏绑定的细节。
// sdk.h
namespace sdk {
class Client {
public:
Client(const Client&) = delete;
static Client build();
void foo();
private:
struct ClientImpl;
Client(ClientImpl* d_ptr);
ClientImpl* d_ptr_;
}
} // namespace sdk
//sdk.cpp
// 我们在源文件中进行ffi绑定
namespace sdk_rust {
extern "C" {
// 透明类型
struct ExportClient;
ExportClient* client_new();
void client_delete(ExportClient* ptr);
void client_foo(ExportClient* ptr);
} // extern "C"
} // namespace sdk_rust
// 并且在源文件中把绑定细节封装好
namespace sdk {
// 在源文件中实现头文件中的透明类型
struct Client::ClientImpl {
sdk_rust::ExportClient* client;
~ClientImpl() { client_delete(this->client); }
}
// 后面解释私有构造函数的原因
Client::Client(ClientImpl* d_ptr): d_ptr_(d_ptr) {}
Client Client::build() {
sdk_rust::ExportClient* client = sdk_rust::client_new();
ClientImpl* d_ptr = new ClientImpl();
d_ptr->client = client;
return Client(d_ptr);
}
Client::~Client() {
delete this->d_ptr;
}
void Client::foo() {
if (this->d_ptr_ != nullptr) {
return sdk_rust::foo(this->d_ptr_->client);
}
}
}
诶我++西佳佳怎么这么坏,绑定几个函数能写出这么多恶心玩意。
一般new client都不太可能不返回错误,但是想把rust的result跨过ffi还是有点强人所难了,风格也不太一致。为了搞定错误,我们还是决定返回int错误码以及通过出参返回错误信息。
// sdk.h
namespace sdk {
class Client {
public:
// 我们通过可变引用出参来得到错误
// 因此我们没有使用构造函数
static Client build(int32_t& err_code, std::string& err);
}
}
```cpp
```cpp
// sdk.cpp
namespace sdk_rust {
extern "C" {
// 我们通过在cpp侧导出一个std::string的包装函数来修改std::string
void append_str(void* ptr, const char* str) {
std::string* s = static_cast<std::string*> ptr;
s->append(str);
}
// 通过ffi传递一个void*远比一个std::string&容易
ExportClient* client_new(int32_t&, void*);
}
}
namespace sdk {
Client Client::build(int32_t& err_code, std::string& err) {
sdk_rust::ExportClient* client = sdk_rust::client_new(err_code, static_cast<void*>(&err));
if (client == nullptr) {
return Client(nullptr);
}
ClientImpl* d_ptr = new ClientImpl();
d_ptr->client = client;
return Client(d_ptr);
}
}
在rust侧代码加上相应的result逻辑
unsafe extern "C" {
// 添加对c++暴露接口的绑定
fn append_str(ptr: *mut c_void, string: *const c_char);
}
// 用safe包一下
pub fn push_str(ptr: *mut c_void, string: impl ToString) {
fn inner(ptr: *mut c_void, string: String) {
match CString::new(string) {
Ok(v) => unsafe { append_str(ptr, v.as_ptr()) },
Err(_) unsafe { append_str(ptr, c"internal".as_ptr()) },
}
}
inner(ptr, string.to_string())
}
// 加上result逻辑
impl Client {
fn new() -> Result<(),(i32, String)> {
todo!()
}
}
// 起码从 int32_t& 转换到 &mut i32 还是不需要额外操作的
#[no_mangle]
unsafe extern "C" fn client_new(err_code: &mut i32, err: *mut void) -> *mut ExportClient {
match Client::new() {
Ok(client) => Box::into_raw(Box::new(ExportClient { client })),
Err((code, msg)) => {
*err_code = code;
push_str(err, msg);
null_mut()
}
}
}
废了老大劲终于绑定好了,总算可以用了。跳过编译的部分,直接看代码中怎么调用:
// main.cpp
int main() {
int32_t code = 0;
std::String err;
sdk::Client client = sdk::Client::build(code, err);
if (code != 0) {
printf("%s\n", err.c_str());
return code;
}
client.foo();
}
就为了能在cpp代码中调一个函数,洋洋洒洒写了几百行绑定,cpp怎么这么坏。
这一小段代码就埋了个大雷,在下一篇会讲
