Async Service
rsbinder supports async/await with the Tokio runtime, making it
straightforward to build non-blocking Binder services. The tokio feature is enabled by
default in rsbinder, so no extra feature flags are required for most projects. This chapter
explains how to implement async Binder services, how they differ from their synchronous
counterparts, and the patterns you will encounter when working with them.
If you have not yet read the Hello, World! chapter, it is recommended to do so first -- the async concepts here build on the synchronous service and client covered there.
Async is transport-agnostic
Async lives in the AIDL-generated stubs, not in the transport. The compiler
emits the async trait (IMyServiceAsyncService), the async proxy
(into_async::<Tokio>()), and the new_async_binder constructor once, and
the very same async impl runs unchanged over kernel binder or RPC — including
through the transport-agnostic service facade.
The reason is that the facade and the transports only ever move a transport-agnostic
SIBinder around (register a binder under a name, look one up). Whether that binder is
sync- or async-backed is decided entirely by which constructor produced it
(new_binder vs new_async_binder) — a fact the transport never sees. So async needs
no transport-specific code: pick the constructor, pick the transport, and the two
compose.
This chapter starts with the kernel-binder setup, then shows the RPC and unified-facade variants — which differ only in the bootstrap and the runtime flavor.
Under the hood — it is a thread bridge, not a reactor. rsbinder async is not epoll-driven non-blocking I/O. On the client a transact runs on
tokio::task::spawn_blocking; on the server each call is wrapped inrt.block_on(..)and driven from a blocking worker thread (the binder thread pool for kernel, theserveworker for RPC). This is the same bridge the kernelbinder_tokiopath uses. You get async/await ergonomics and real concurrency, backed by blocking worker threads — which is sufficient for typical service workloads. A true async I/O reactor is deliberately out of scope.
Sync vs Async at a Glance
The following table summarizes the key differences between a synchronous and an asynchronous Binder service in rsbinder.
| Aspect | Sync | Async |
|---|---|---|
| Trait name | IMyService | IMyServiceAsyncService |
| Method signature | fn method(&self) -> Result<T> | async fn method(&self) -> Result<T> |
| Service creation | BnXxx::new_binder(impl) | BnXxx::new_async_binder(impl, rt()) |
| Remote call (client) | service.Method() | service.clone().into_async::<Tokio>().Method().await |
| Main loop | ProcessState::join_thread_pool() | std::future::pending().await |
| Runtime | Not needed | Tokio runtime required |
The AIDL compiler generates both the sync trait (IMyService) and the async trait
(IMyServiceAsyncService) from the same .aidl file. You choose which one to implement
depending on whether your service needs async capabilities.
The trait, proxy, and constructor rows above are identical across transports — only the
"Main loop" and "Runtime" rows are the kernel-binder values shown here. Over RPC the main
loop is rpc::Host::serve() (or serve_background()) and the runtime must be
multi-threaded; see Async Over RPC and the Unified Facade
below.
Setting Up the Tokio Runtime (kernel binder)
An async Binder service must run inside a Tokio runtime. The standard pattern for kernel binder is:
- Initialize the Binder process state and thread pool (same as sync).
- Build a Tokio runtime.
- Inside the runtime, create and register async services.
- Yield to the runtime with
std::future::pending().await.
Here is the full setup, based on
tests/src/bin/test_service_async.rs:
use rsbinder::*; fn rt() -> TokioRuntime<tokio::runtime::Handle> { TokioRuntime(tokio::runtime::Handle::current()) } fn main() -> std::result::Result<(), Box<dyn std::error::Error>> { // Initialize Binder -- same as the sync case. ProcessState::init_default()?; ProcessState::start_thread_pool(); // Build a single-threaded Tokio runtime. let runtime = tokio::runtime::Builder::new_current_thread() .enable_all() .build() .unwrap(); runtime.block_on(async { // Create and register the async service (see next section). let service = BnMyService::new_async_binder( MyAsyncService::default(), rt(), ); hub::add_service("com.example.myservice", service.as_binder()) .expect("Could not register service"); // Yield to the runtime. This keeps the process alive and // drives the Tokio event loop on the current thread. std::future::pending().await }) }
ProcessState::init_default()returnsResult<&'static ProcessState, …>, so amainthat calls it should also returnResult(or call.expect()/.unwrap()for the very first line of a quick demo).
There are several things to note here:
rt()is a small helper that wraps the current Tokio runtime handle in aTokioRuntime. Every call tonew_async_binderrequires aTokioRuntimeso it knows where to spawn async work.new_current_thread()creates a single-threaded Tokio runtime. This is the recommended choice for Binder services because the Binder thread pool already provides its own threads.std::future::pending().awaitis a future that never resolves. It keeps theblock_oncall (and therefore the process) alive indefinitely, which is the async equivalent of the synchronousProcessState::join_thread_pool().
Async Over RPC and the Unified Facade
Because async lives in the generated stubs and the service
facade only moves an SIBinder, the same async service
runs over RPC with no new code. You register the result of new_async_binder through the
generic Registry, and the client converts the looked-up proxy with into_async:
#![allow(unused)] fn main() { use async_trait::async_trait; use rsbinder::service::{rpc, Registry, Broker}; use rsbinder::*; struct IHelloService; impl Interface for IHelloService {} #[async_trait] impl IHelloAsyncService for IHelloService { async fn echo(&self, echo: &str) -> rsbinder::status::Result<String> { Ok(echo.to_owned()) } } /// Registration is written once, generic over the transport — identical to the /// sync facade example in "Cross-Transport Services". Only the binder /// constructor differs (`new_async_binder` instead of `new_binder`). fn register<R: Registry>( reg: &R, rt: TokioRuntime<tokio::runtime::Handle>, ) -> rsbinder::Result<()> { let binder = BnHello::new_async_binder(IHelloService {}, rt).as_binder(); reg.add_service("hello", binder) } }
The server needs a multi-threaded runtime
This is the one substantive difference from the kernel setup. An RPC async server is driven
by a blocking serve worker that calls rt.block_on(handler). That block_on runs on a
thread that is not the runtime's own, so the runtime must be a multi-threaded one — a
current_thread runtime cannot be driven from an outside thread while it is also being kept
alive elsewhere. (Kernel binder gets away with current_thread only because the binder
thread pool, not a serve loop, supplies the worker threads.)
fn main() -> std::result::Result<(), Box<dyn std::error::Error>> { // RPC async servers require a MULTI-THREAD runtime — the blocking serve // worker calls `rt.block_on(handler)` from a non-runtime thread. let runtime = tokio::runtime::Builder::new_multi_thread() .worker_threads(2) .enable_all() .build()?; let handle = runtime.handle().clone(); let host = rpc::Host::unix("/tmp/hello.sock")?; register(&host, TokioRuntime(handle))?; println!("serving over RPC at /tmp/hello.sock"); host.serve()?; // blocks, driving this one socket Ok(()) }
Use rpc::Host::serve_background() instead of serve()
if you want the socket driven on a background thread while main does other work.
The client converts the looked-up proxy
The client side is the ordinary facade lookup followed by into_async:
#![allow(unused)] fn main() { let runtime = tokio::runtime::Builder::new_multi_thread().enable_all().build()?; let broker = rpc::Broker::unix("/tmp/hello.sock")?; let hello = broker.get_interface::<dyn IHello>("hello")?; // sync Strong<dyn IHello> runtime.block_on(async { let hello = hello.into_async::<Tokio>(); println!("{}", hello.echo("hi").await?); Ok::<_, Box<dyn std::error::Error>>(()) })?; }
The blocking bootstrap —
Broker::unix,get_interface— is done before entering the runtime; only the transacts themselves are.awaited. The async-over-RPC bridge shown here (new_async_binder+into_async::<Tokio>over a real Unix-socket session, multi-thread runtime) is verified end-to-end bytests/tests/rpc_async.rs— including many in-flight calls on one shared session under genuine async concurrency. That test drives the bridge through the lower-levelRpcSessionAPI directly rather than therpc::Host/Brokerfacade, but the facade is a thin wrapper over exactly that path.
Switching transports
To run the exact same async service over kernel binder instead, swap only the host
construction and the runtime flavor — the register helper and the service impl are
unchanged:
#![allow(unused)] fn main() { let host = kernel::Host::new()?; // instead of rpc::Host::unix(..) register(&host, rt())?; host.serve()?; // joins the process-wide binder pool }
Be aware that the two transports' trust boundaries differ: see
Security & Authorization — @EnforcePermission denies over RPC, and
get_calling_uid() is the kernel-vouched peer uid on Unix RPC (fail-closed on uid-less
transports). The facade makes the transport swap easy; it does not make the security models
identical.
Implementing an Async Service
An async service struct implements the generated IMyServiceAsyncService trait using the
#[async_trait] attribute macro. Compare this with the sync version side by side.
Sync service
#![allow(unused)] fn main() { use rsbinder::*; #[derive(Default)] struct MyService; impl Interface for MyService {} impl IMyService::IMyService for MyService { fn echo(&self, input: &str) -> rsbinder::status::Result<String> { Ok(input.to_owned()) } fn RepeatInt(&self, token: i32) -> rsbinder::status::Result<i32> { Ok(token) } } }
Async service
#![allow(unused)] fn main() { use async_trait::async_trait; use rsbinder::*; #[derive(Default)] struct MyAsyncService; impl Interface for MyAsyncService {} #[async_trait] impl IMyService::IMyServiceAsyncService for MyAsyncService { async fn echo(&self, input: &str) -> rsbinder::status::Result<String> { // You can use .await on async operations here. Ok(input.to_owned()) } async fn RepeatInt(&self, token: i32) -> rsbinder::status::Result<i32> { Ok(token) } } }
The differences are minimal:
- Add
use async_trait::async_trait;and annotate theimplblock with#[async_trait]. - Implement
IMyServiceAsyncServiceinstead ofIMyService. - Prefix each method with
async. - When creating the binder, use
BnXxx::new_async_binder(impl, rt())instead ofBnXxx::new_binder(impl).
Inside async methods you can .await any future -- call other async services, perform async
I/O, use tokio::time::sleep, and so on.
Calling Other Services Asynchronously
When an async service needs to call another Binder service, the proxy it receives is
synchronous by default. Use into_async::<Tokio>() to convert it into an async proxy so
you can .await the result.
Async version
Based on tests/src/bin/test_service_async.rs:
#![allow(unused)] fn main() { async fn VerifyName( &self, service: &rsbinder::Strong<dyn INamedCallback::INamedCallback>, name: &str, ) -> rsbinder::status::Result<bool> { service .clone() .into_async::<Tokio>() .GetName() .await .map(|found_name| found_name == name) } }
Sync version for comparison
From tests/src/bin/test_service.rs:
#![allow(unused)] fn main() { fn VerifyName( &self, service: &rsbinder::Strong<dyn INamedCallback::INamedCallback>, name: &str, ) -> std::result::Result<bool, rsbinder::Status> { service.GetName().map(|found_name| found_name == name) } }
The key difference is:
- Sync: Call
service.GetName()directly. - Async: Clone the service reference, convert with
.into_async::<Tokio>(), call.GetName(), and.awaitthe result.
The .clone() is necessary because into_async consumes the Strong reference. This is a
cheap reference-count increment, not a deep copy.
Creating Nested Async Binders
An async service can create and return new async binders, for instance when implementing a
factory-style method. Pass the same rt() helper:
#![allow(unused)] fn main() { async fn GetOtherTestService( &self, name: &str, ) -> rsbinder::status::Result<rsbinder::Strong<dyn INamedCallback::INamedCallback>> { let mut service_map = self.service_map.lock().unwrap(); let other_service = service_map.entry(name.into()).or_insert_with(|| { let named_callback = NamedCallback(name.into()); INamedCallback::BnNamedCallback::new_async_binder(named_callback, rt()) }); Ok(other_service.to_owned()) } }
This pattern is taken directly from the test suite. Note that new_async_binder can be
called from any async context as long as a TokioRuntime is provided.
Registering Multiple Async Services
A single process can host multiple async services. Register each one before yielding to the runtime:
#![allow(unused)] fn main() { runtime.block_on(async { let service = BnTestService::new_async_binder( TestService::default(), rt(), ); hub::add_service(service_name, service.as_binder()) .expect("Could not register service"); let versioned_service = BnFooInterface::new_async_binder( FooInterface, rt(), ); hub::add_service(versioned_service_name, versioned_service.as_binder()) .expect("Could not register service"); let nested_service = INestedService::BnNestedService::new_async_binder( NestedService, rt(), ); hub::add_service(nested_service_name, nested_service.as_binder()) .expect("Could not register service"); // All services are now registered. Yield to the runtime. std::future::pending().await }) }
All services share the same Tokio runtime and the same Binder thread pool. Incoming Binder transactions are dispatched to the correct service automatically.
Advanced: BoxFuture Pattern for Macros
When using declarative macros (macro_rules!) to reduce boilerplate in an #[async_trait]
impl block, there is a subtlety: async_trait transforms async fn into functions returning
a pinned boxed future, but it does not apply this transformation to functions produced by
macro expansion. The workaround is to return a BoxFuture manually.
This is the pattern used in the rsbinder test suite:
#![allow(unused)] fn main() { type BoxFuture<'a, T> = std::pin::Pin<Box<dyn std::future::Future<Output = T> + Send + 'a>>; macro_rules! impl_repeat { ($repeat_name:ident, $type:ty) => { fn $repeat_name<'a, 'b>( &'a self, token: $type, ) -> BoxFuture<'b, rsbinder::status::Result<$type>> where 'a: 'b, Self: 'b, { Box::pin(async move { Ok(token) }) } }; } macro_rules! impl_reverse { ($reverse_name:ident, $type:ty) => { fn $reverse_name<'a, 'b, 'c, 'd>( &'a self, input: &'b [$type], repeated: &'c mut Vec<$type>, ) -> BoxFuture<'d, rsbinder::status::Result<Vec<$type>>> where 'a: 'd, 'b: 'd, 'c: 'd, Self: 'd, { Box::pin(async move { repeated.clear(); repeated.extend_from_slice(input); Ok(input.iter().rev().cloned().collect()) }) } }; } }
These macros can then be used inside the #[async_trait] impl block alongside regular async fn methods:
#![allow(unused)] fn main() { #[async_trait] impl ITestService::ITestServiceAsyncService for TestService { impl_repeat! {RepeatInt, i32} impl_reverse! {ReverseInt, i32} async fn RepeatString(&self, input: &str) -> rsbinder::status::Result<String> { Ok(input.into()) } // ... other methods } }
The lifetime annotations ('a: 'b, Self: 'b) are necessary to satisfy the borrow checker
when the future captures &self and method arguments. This is an advanced pattern -- you
will only need it when combining declarative macros with async trait implementations.
Compare with the sync macro version, which is simpler because no future is involved:
#![allow(unused)] fn main() { macro_rules! impl_repeat { ($repeat_name:ident, $type:ty) => { fn $repeat_name( &self, token: $type, ) -> std::result::Result<$type, rsbinder::Status> { Ok(token) } }; } }
Tips and Best Practices
-
Use
#[async_trait]from theasync-traitcrate for all async trait implementations. This is required because Rust does not yet have native async trait support in all contexts that rsbinder needs. -
into_async::<Tokio>()converts a synchronous proxy into an async proxy. Always use this when calling another Binder service from an async context, rather than making blocking calls that could stall the Tokio runtime. -
std::future::pending().awaitis the idiomatic way to keep an async service process alive. Unlike the sync approach whereProcessState::join_thread_pool()blocks the main thread, the async approach yields to Tokio so the runtime can drive spawned tasks. -
The
rt()helper should capture the current runtime handle. Define it as a function that returnsTokioRuntime(tokio::runtime::Handle::current())and call it from within the Tokio runtime context. -
Both sync and async services can coexist in the same process. You can register some services with
new_binderand others withnew_async_binder. They share the same Binder thread pool. -
Prefer
new_current_thread()for the Tokio runtime builder. The Binder thread pool handles multi-threaded transaction dispatching already, so a multi-threaded Tokio runtime is typically unnecessary. -
Avoid blocking the Tokio runtime. If your async service method must perform a CPU-intensive or blocking operation, use
tokio::task::spawn_blockingto move that work off the async executor thread.
Summary
Async services in rsbinder follow the same structure as sync services, with a few additional steps:
- Build a Tokio runtime and run your service setup inside
runtime.block_on(async { ... }). - Define an
rt()helper that wrapstokio::runtime::Handle::current(). - Implement
IMyServiceAsyncServicewith#[async_trait]instead ofIMyService. - Create binders with
BnXxx::new_async_binder(impl, rt()). - Use
into_async::<Tokio>()when calling other Binder services from async code. - Keep the process alive with
std::future::pending().await(kernel binder) orrpc::Host::serve()/serve_background()(RPC).
Because async lives in the generated stubs, the same async service runs over either
transport — register the new_async_binder result through the service
facade's Registry and look it up through Broker. The only
RPC-specific requirement is a multi-threaded runtime, because the blocking serve worker
calls rt.block_on(handler).
For complete working examples, see tests/src/bin/test_service_async.rs (kernel binder) and
tests/tests/rpc_async.rs (RPC) in the rsbinder repository.