跳过正文
Background Image
  1. Posts/

被FFI做局了(2)

·1279 字·3 分钟·
目录

被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怎么这么坏。

这一小段代码就埋了个大雷,在下一篇会讲