Services
Services turn Rust impl blocks into class-like objects with constructors, methods, and automatic cleanup. They are the primary mechanism for exposing stateful, object-oriented APIs across the FFI boundary.
In forward interop, a service is defined by marking a struct with #[ffi(service)] and annotating its impl block with #[ffi].
#[ffi(service)]
pub struct ServiceBasic {}
#[ffi]
impl ServiceBasic {
pub fn create() -> ffi::Result<Self, Error> {
ffi::Ok(Self {})
}
}
Any method returning Self or Result<Self, E> is treated as a constructor. A service can have multiple constructors with different names and parameter lists.
Methods take either &self, &mut self, or—in the case of async services—this: Async<Self> (see below). Methods taking &mut self must honor Rust's aliasing guarantees. These methods must not be called in parallel from two different threads.
Service methods can return ffi::Result<T, Error> to propagate errors across the FFI boundary. The error type must be an #[ffi] enum. In C#, error results are translated into typed exceptions that can be caught with standard try/catch.
Async Services
For asynchronous methods, the service derives AsyncRuntime and includes a runtime field. Async methods take Async<Self> instead of &self, which provides access to the service and its thread-local runtime context.
#[ffi(service)]
#[derive(AsyncRuntime)]
pub struct ServiceAsyncBasic {
runtime: Tokio,
}
#[ffi]
impl ServiceAsyncBasic {
pub fn create() -> ffi::Result<Self, Error> {
result_to_ffi(|| {
let runtime = Tokio::new();
Ok(Self { runtime })
})
}
pub async fn call(_: Async<Self>) -> ffi::Result<(), Error> {
ffi::Ok(())
}
}Inventory Registration
Services must be registered in the RustInventory with the service!() macro:
pub fn inventory() -> RustInventory {
RustInventory::new()
.register(service!(ServiceBasic))
.register(service!(ServiceMultipleCtors))
.register(service!(ServiceAsyncBasic))
.validate()
}