Service Patterns
This chapter covers the common patterns for implementing Binder services in rsbinder. Whether you are building a simple single-method service or a complex multi-service process, the patterns described here will help you structure your code effectively.
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::status::Result<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. ProcessState::init_default(); // Start additional threads for handling concurrent Binder transactions. // Optional but recommended for services that handle multiple clients. 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. hub::add_service("com.example.myservice", service.as_binder())?; // 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 additional threads so the process can handle multiple concurrent transactions. Without this call, only one thread handles all incoming requests.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.as_binder())?; // Register a versioned interface service. let versioned_service = BnFooInterface::new_binder(FooInterface); hub::add_service(versioned_service_name, versioned_service.as_binder())?; // Register a nested service. let nested_service = INestedService::BnNestedService::new_binder(NestedService); hub::add_service(nested_service_name, nested_service.as_binder())?; // 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.as_binder())?; // 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}").unwrap(); } Ok(()) } } }
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 (mut read_file, write_file) = build_pipe(); let args = vec!["dump".to_owned(), "MyService".to_owned()]; service.as_binder().as_proxy().unwrap().dump(write_file, &args)?; let mut buf = String::new(); read_file.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 service = get_service(); assert_eq!(service.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::get_interface returns a Strong<dyn IMyService> on success. let service: rsbinder::Strong<dyn IMyService::IMyService> = hub::get_interface("com.example.myservice")?; // Call service methods through the proxy. let result = service.echo("hello")?; println!("Got: {result}"); Ok(()) }
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::status::Result<()> { println!("Service registered: {name}"); Ok(()) } } let callback = hub::BnServiceCallback::new_binder(MyServiceCallback); hub::register_for_notifications(SERVICE_NAME, &callback)?; }
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.
Tips and Best Practices
ProcessState::init_default()must be called before any Binder operations. Failing to do so will result in a panic.start_thread_pool()is optional but recommended. Without it, only a single thread handles all Binder transactions, which can become a bottleneck under load.- Each process needs only one
ProcessState::init_default()call. Multiple calls are safe but unnecessary. join_thread_pool()blocks the calling thread. Place it at the end ofmain()after all setup is complete.dump()is optional but highly useful for debugging. It provides a standardized way to inspect service state from outside the process.- Use
MutexorRwLockfor mutable service state. All AIDL methods receive&self, so interior mutability is required for state changes. - Service names should follow reverse-domain naming. For example,
com.example.myserviceormy.hello. This prevents name collisions when multiple services are registered. - Error handling: Return
rsbinder::Statuserrors from service methods to communicate failures to clients. UseStatus::new_service_specific_error()for application-level errors that clients can inspect programmatically.