10 releases (4 breaking)

0.5.1 Nov 27, 2024
0.5.0 Nov 22, 2024
0.4.2 Nov 21, 2024
0.3.2 Nov 12, 2024
0.1.0 Oct 31, 2024

#1699 in Network programming

Download history 319/week @ 2024-10-28 146/week @ 2024-11-04 117/week @ 2024-11-11 611/week @ 2024-11-18 266/week @ 2024-11-25 16/week @ 2024-12-02 99/week @ 2024-12-09

996 downloads per month
Used in 4 crates

GPL-3.0 license

10KB
83 lines

ffi_rpc

crates.io docs.rs license

Use FFI with RPC! The ABI is stable, any serializable type can be safely transferred through the FFI boundary.

Why this crate

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.

Limitations

  1. Generic is not supported.
  2. Panic on incompatible API/Library, please manage your API version by yourself.

Quick start

Assume you have three projects:

  • server: typically the binary which will load dynamic libraries and use the FFI functions.
  • client(plugin): the .dylib/.so/.dll library that defines the interface.
  • client_interface: the interface that will be shared between server and client.

client_interface

  1. Add ffi_rpc to [dependencies] in Cargo.toml.
  2. In 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.

client

  1. Add abi_stable = "0.11" and ffi_rpc to [dependencies] in Cargo.toml.
  2. In lib.rs:
    use client_interface::ClientApi;
    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(ClientApi)]
    struct Api;
    
    #[plugin_impl_trait]
    impl ClientApi for Api {
        async fn add(&self, _: &Registry, a: i32, b: i32) -> i32 {
            a + b
        }
    }
    

How to implement multiple interfaces: example.

How to invoke other clients: example.

server

  1. Init the registry:
    let mut r = Registry::default();
    
  2. Init all clients:
    let lib = client_interface::Client::new(
        format!("./target/debug/{}client{}", DLL_PREFIX, DLL_SUFFIX).as_ref(),
        &mut r,
        "client",
    ).unwrap();
    
  3. Invoke methods:
    let ret = lib.add(&r, &1, &2).await;
    

How to mock a client: example.

Black magic

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("Trait1::") {
            return Trait1Impl::parse_trait1(func, reg, param).await;
        }
        if func.as_str().starts_with("Trait2::") {
            return Trait2Impl::parse_trait2(func, reg, param).await;
        }
        panic!("Function is not defined in the library");
    })
}

Dependencies

~4–9.5MB
~104K SLoC