| Crates.io | ffi_rpc |
| lib.rs | ffi_rpc |
| version | 0.6.0 |
| created_at | 2024-10-31 08:55:02.034173+00 |
| updated_at | 2025-03-13 03:47:11.822164+00 |
| description | Use FFI with RPC. |
| homepage | |
| repository | https://github.com/MXWXZ/ffi_rpc |
| max_upload_size | |
| id | 1429820 |
| size | 24,007 |
Use FFI with RPC! The ABI is stable, any serializable type can be safely transferred through the FFI boundary.
It has been quite a long time that the Rust does not have a stable ABI. Developing a plugin system is a challenging work since we have meet several problems such as Segmentation fault, Bus error and unexpected behaviors across the FFI boundary with libloading. The bugs exist only at runtime, depending on a lot of variables (OS type, rustc version, etc.). It is a nightmare to debug these errors.
Luckily, we have abi_stable crate with can provide a working stable ABI for us. However, it would be tricky and complex to introduce customized types to the interface. Thus, we have this crate to transfer any serializable type across the FFI boundary.
Assume you have three projects:
.dylib/.so/.dll library that defines the interface.ffi_rpc to [dependencies] in Cargo.toml.lib.rs:
use ffi_rpc::{
abi_stable, async_trait, bincode,
ffi_rpc_macro::{self, plugin_api},
};
#[plugin_api(Client)]
pub trait ClientApi {
async fn add(a: i32, b: i32) -> i32;
}
How to split one interface into multiple traits: example.
abi_stable = "0.11" and ffi_rpc to [dependencies] in Cargo.toml.lib.rs:
use ffi_rpc::{
abi_stable::prefix_type::PrefixTypeTrait,
async_ffi, async_trait, bincode,
ffi_rpc_macro::{plugin_impl_call, plugin_impl_instance, plugin_impl_root, plugin_impl_trait},
registry::Registry,
};
#[plugin_impl_instance(|| Api{})]
#[plugin_impl_root]
#[plugin_impl_call(client_interface::ClientApi)] // must use full path
struct Api;
#[plugin_impl_trait]
impl client_interface::ClientApi for Api { // must use full path
async fn add(&self, _: &Registry, a: i32, b: i32) -> i32 {
a + b
}
}
How to implement multiple interfaces: example.
How to invoke other clients: example.
let mut r = Registry::default();
let lib = client_interface::Client::new(
format!("./target/debug/{}client{}", DLL_PREFIX, DLL_SUFFIX).as_ref(),
&mut r,
"client",
).unwrap();
let ret = lib.add(&r, &1, &2).await;
How to mock a client: example.
Customize _ffi_call to route to different implementations manually.
#[sabi_extern_fn]
pub fn _ffi_call(
func: RString, // function to call `Trait::Method`.
reg: &Registry, // registry.
param: RVec<u8>, // function params.
) -> BorrowingFfiFuture<'_, RVec<u8>> {
BorrowingFfiFuture::new(async move {
if func.as_str().starts_with("crate::mod::Trait1::") {
return crate::mod::Trait1Impl::parse_crate_mod_Trait1(func, reg, param).await;
}
if func.as_str().starts_with("crate::mod::Trait2::") {
return crate::mod::Trait2Impl::parse_crate_mod_Trait2(func, reg, param).await;
}
panic!("Function is not defined in the library");
})
}