Service Patterns
How to structure a service, from a single method to a process hosting several.
This chapter uses the low-level lifecycle —
ProcessState, the thread pool,hub::add_service— because that is what the pieces actually are.rsbinder::serve(Uri::kernel())?.add(name, binder)?.run()?performs exactly these steps and is the shorter way to write them; reach past it when you need control aUridoes not express. See Cross-Transport Services.
Basic Service Structure
Every Binder service in rsbinder requires three pieces:
- A struct that holds service state.
- An
impl Interfaceblock for the struct, optionally providing adump()method. - An
impl IYourServiceblock for the struct, implementing the AIDL-defined methods.
#![allow(unused)] fn main() { use rsbinder::*; // 1. Define a struct to hold service state. // Use #[derive(Default)] when you want to construct with ::default(). #[derive(Default)] struct MyService { // Add fields here to maintain service state. // Use Mutex<T> or RwLock<T> for fields that need interior mutability, // since method receivers are &self (shared references). } // 2. Implement the Interface trait (required for all services). // The dump() method is optional but recommended for debugging. impl Interface for MyService { fn dump(&self, writer: &mut dyn std::io::Write, args: &[String]) -> Result<()> { for arg in args { writeln!(writer, "{arg}").unwrap(); } Ok(()) } } // 3. Implement your AIDL-generated interface trait. impl IMyService::IMyService for MyService { fn echo(&self, input: &str) -> rsbinder::BinderResult<String> { Ok(input.to_owned()) } } }
Note that all AIDL method implementations receive &self, not &mut self. If your
service needs mutable state, wrap the relevant fields in std::sync::Mutex or
std::sync::RwLock. The test suite demonstrates this pattern with a HashMap
protected by a Mutex:
#![allow(unused)] fn main() { #[derive(Default)] struct TestService { service_map: Mutex<HashMap<String, rsbinder::Strong<dyn INamedCallback::INamedCallback>>>, } }
Service Registration and Main Loop
A service binary follows a consistent lifecycle: initialize the process state, start the thread pool, create and register services, then block on the thread pool. Here is the standard pattern based on the project's test service implementation:
fn main() -> std::result::Result<(), Box<dyn std::error::Error>> { // Initialize ProcessState. This opens the Binder device and configures // the process for Binder IPC. Must be called before any Binder operations. // `init_default()` returns `Result<&'static ProcessState, ...>`; use `?` // to surface device-open or initialization failures. ProcessState::init_default()?; // Start the binder thread pool and enable kernel-driven thread // spawning, so concurrent transactions are handled in parallel. ProcessState::start_thread_pool(); // Create a Binder object from your service implementation. // BnMyService is the server-side stub generated by the AIDL compiler. let service = BnMyService::new_binder(MyService::default()); // Register the service with the service manager (hub). // The first argument is the service name used by clients to find it. // `add_service` accepts anything convertible into `SIBinder`, so the // typed `Strong<dyn IMyService>` goes in directly — no `.as_binder()`. hub::add_service("com.example.myservice", &service)?; // Block the main thread and process incoming Binder transactions. // This call does not return under normal operation. Ok(ProcessState::join_thread_pool()?) }
Each function in this lifecycle serves a specific purpose:
ProcessState::init_default()opens the Binder device (typically/dev/binderfs/binder) and sets up process-wide state for Binder communication.ProcessState::start_thread_pool()spawns a binder worker thread and enables kernel-driven spawning of further workers. Without this call, transactions are served only by threads that explicitly calljoin_thread_pool(), so the process is effectively single-threaded.BnMyService::new_binder()wraps your implementation struct in a Binder-compatible object. TheBnprefix stands for "Binder native" (the server-side stub).hub::add_service()registers your service with the service manager so that clients can discover it by name.ProcessState::join_thread_pool()makes the calling thread join the Binder thread pool, blocking it to process transactions indefinitely.
Multiple Services in One Process
A single process can host multiple Binder services. The test suite registers four
distinct services in one binary. Each service gets its own struct, its own Interface
and AIDL trait implementations, and its own registration call:
fn main() -> std::result::Result<(), Box<dyn std::error::Error>> { ProcessState::init_default()?; ProcessState::start_thread_pool(); // Register the primary test service. let service = BnTestService::new_binder(TestService::default()); hub::add_service(test_service_name, &service)?; // Register a versioned interface service. let versioned_service = BnFooInterface::new_binder(FooInterface); hub::add_service(versioned_service_name, versioned_service)?; // Register a nested service. let nested_service = INestedService::BnNestedService::new_binder(NestedService); hub::add_service(nested_service_name, nested_service)?; // Register a fixed-size array service. let fixed_size_array_service = IRepeatFixedSizeArray::BnRepeatFixedSizeArray::new_binder(FixedSizeArrayService); hub::add_service(fixed_size_array_service_name, fixed_size_array_service)?; // All services share the same thread pool and process state. Ok(ProcessState::join_thread_pool()?) }
All services in a process share the same ProcessState and thread pool. You only need
to call ProcessState::init_default() and ProcessState::start_thread_pool() once,
regardless of how many services you register.
Implementing dump()
The dump() method is part of the Interface trait and provides a way to inspect
service state at runtime. It is optional -- if you do not override it, the default
implementation does nothing. However, implementing it is valuable for debugging
and diagnostics.
The method receives a writer and a list of string arguments:
#![allow(unused)] fn main() { impl Interface for MyService { fn dump(&self, writer: &mut dyn std::io::Write, args: &[String]) -> Result<()> { for arg in args { writeln!(writer, "{arg}")?; } Ok(()) } } }
An Err from dump() is the reply status the caller receives. A write to a
pipe whose reader has exited fails with EPIPE, which ? converts to
StatusCode::DeadObject; rsbinder sends a handler's DeadObject as
FailedTransaction, because an AOSP BpBinder that receives DEAD_OBJECT
marks the proxy dead and fails every later call on it. The same substitution
applies to every method a service implements.
On the client side, you can invoke dump() on a remote service through its proxy.
The output is written to a file descriptor (typically a pipe):
#![allow(unused)] fn main() { let (read_end, write_end) = rsbinder::ParcelFileDescriptor::pipe()?; let args = vec!["dump".to_owned(), "MyService".to_owned()]; service.as_binder().as_proxy().unwrap().dump(write_end, &args)?; let mut buf = String::new(); (&read_end).read_to_string(&mut buf)?; // buf now contains the dump output }
Testing Service Liveness with ping_binder()
The ping_binder() method tests whether a service is reachable and responsive.
It sends a lightweight ping transaction and returns Ok(()) on success:
#![allow(unused)] fn main() { let hello: Strong<dyn IHello> = rsbinder::connect(Uri::kernel().with_service("my.hello"))?; assert_eq!(hello.as_binder().ping_binder(), Ok(())); }
This is useful for health checks and for verifying that a service is still alive before making more expensive calls.
Default Implementation Pattern
rsbinder supports a default implementation pattern that provides fallback behavior when a method is not implemented by the remote service. This is especially useful for forward compatibility: a client compiled against a newer AIDL interface can still communicate with an older service that does not implement all methods.
To set up a default implementation:
- Define a struct that implements the
Defaulttrait variant of your interface. - Wrap it in an
Arcand register it withsetDefaultImpl. - When the remote service returns
StatusCode::UnknownTransactionfor a method, the default implementation is called instead.
#![allow(unused)] fn main() { // Define a default implementation for methods the server may not support. struct MyDefaultImpl; impl rsbinder::Interface for MyDefaultImpl {} impl IMyServiceDefault for MyDefaultImpl { fn UnimplementedMethod(&self, arg: i32) -> std::result::Result<i32, Status> { // Provide fallback logic. Ok(arg * 2) } } // Register the default implementation globally for this interface. let di: IMyServiceDefaultRef = Arc::new(MyDefaultImpl); <BpMyService as IMyService::IMyService>::setDefaultImpl(di); // When the remote service does not implement UnimplementedMethod, // the default implementation is used transparently. let result = service.UnimplementedMethod(100); assert_eq!(result, Ok(200)); }
Note that setDefaultImpl is a static method on the proxy type (BpMyService). Once
registered, the default implementation applies to all proxies of that interface within
the process.
Client-Side Patterns
Getting a Service Proxy
Clients obtain a typed proxy to a remote service through the hub module:
fn main() -> std::result::Result<(), Box<dyn std::error::Error>> { // ProcessState must be initialized before any Binder operations. ProcessState::init_default()?; // Obtain a strongly-typed proxy for the service. // hub::wait_for_interface blocks until the service is registered and // returns a Strong<dyn IMyService> on success. Use hub::check_interface // instead for a non-blocking probe that fails immediately with // NameNotFound when the service is absent. let service: rsbinder::Strong<dyn IMyService::IMyService> = hub::wait_for_interface("com.example.myservice")?; // Call service methods through the proxy. let result = service.echo("hello")?; println!("Got: {result}"); Ok(()) }
A purely-outbound client like this one needs no binder thread pool. As soon
as the client receives inbound calls — a callback object it passed to the
service, a service-registration notification, or a death recipient — it must
also call ProcessState::start_thread_pool() (see below).
Listing Available Services
You can discover all registered services through the service manager:
#![allow(unused)] fn main() { for name in hub::list_services(hub::DUMP_FLAG_PRIORITY_DEFAULT) { println!("{name}"); } }
Service Notifications
Clients can register a callback to be notified when a service is registered:
#![allow(unused)] fn main() { struct MyServiceCallback; impl Interface for MyServiceCallback {} impl hub::IServiceCallback for MyServiceCallback { fn onRegistration(&self, name: &str, _service: &SIBinder) -> rsbinder::BinderResult<()> { println!("Service registered: {name}"); Ok(()) } } let callback = hub::BnServiceCallback::new_binder(MyServiceCallback); hub::register_for_notifications(SERVICE_NAME, &callback)?; }
onRegistration is an inbound transaction, so it needs the thread pool
running in the client.
Death Recipients
Clients can monitor whether a remote service process is still alive by registering
a death recipient. The binder_died callback is invoked if the service process
terminates:
#![allow(unused)] fn main() { struct MyDeathRecipient; impl DeathRecipient for MyDeathRecipient { fn binder_died(&self, _who: &WIBinder) { println!("The remote service has died."); } } let recipient = Arc::new(MyDeathRecipient); service.as_binder().link_to_death( Arc::downgrade(&(recipient as Arc<dyn DeathRecipient>)) )?; }
To stop receiving notifications, call unlink_to_death with the same weak reference.
binder_died arrives as an inbound binder command, so it too needs the thread
pool running in the client.
Cancelling a Long Operation
Binder has no per-call deadline and no way to abort a transaction in flight,
so a long call is stopped the way the Android framework stops one
(android.os.ICancellationSignal): the service creates a signal, returns
its transport binder to the caller, and the caller sends a oneway cancel()
to that binder. Only the service's own loop can act on it.
#![allow(unused)] fn main() { use rsbinder::cancel::{CancellationSignal, ICancellationSignal}; fn start_export(&self, path: &str) -> rsbinder::BinderResult<Strong<dyn ICancellationSignal>> { let signal = CancellationSignal::new(); let transport = signal.create_transport(); let token = signal.token(); // the work keeps this std::thread::spawn(move || { for chunk in 0..10_000 { if token.is_canceled() { break; } // ... one chunk of work } }); Ok(transport) } }
The caller cancels through the binder it got back. A typed proxy calls
transport.cancel()? directly. A method that returns a bare IBinder hands
the caller an SIBinder, which cancel_remote takes without compiling the
interface a second time:
#![allow(unused)] fn main() { rsbinder::cancel::cancel_remote(&transport)?; }
Points worth knowing before you build on it:
- The service decides what cancellation means. This carries the
notification only; returning a service-specific error saying "cancelled" is
the service's call (AOSP likewise throws
OperationCanceledExceptionfrom the service). - Delivery is best-effort and unordered against your other calls. The
transport is a different binder object than the service, and the kernel does
not order a
onewayto one against a twoway to another: a caller that cancels and then immediately asks the service what it saw can be answered first. Poll, or report through the result of the operation itself. - Only this direction. A caller creating the signal and the service polling it would make every poll a transaction back to the caller — and on the RPC stack it would need the client to have opened incoming connections.
- Declaring it in
.aidl: returningandroid.os.ICancellationSignalmeans your crate compiles its own copy of that interface, which is a different Rust type fromrsbinder::cancel::ICancellationSignal(the wire is the same). Returning a bareIBinderand cancelling it withcancel_remoteavoids the second copy. async:signal.token().canceled().await(thetokiofeature) waits instead of polling.
Tips
- Anything that receives a call needs
start_thread_pool()— not only services. A client holding a service-notification callback, a callback object it passed to a service, or a death recipient is receiving inbound transactions, and without a thread pool (or a thread parked injoin_thread_pool()) those callbacks andbinder_diedsimply never fire. Only a purely outbound client can skip it. ProcessState::init_default()comes before any binder call and panics if it does not — but only once per process; further calls are harmless no-ops.- Name services reverse-domain,
com.example.myservice, so two crates in one process cannot collide. - Mutable state needs
MutexorRwLock. Every AIDL method takes&self.