Overview
Welcome to rsbinder!
rsbinder is a Rust library and toolset that enables you to utilize Binder IPC on Linux and Android OS. It provides pure Rust implementations that make Binder IPC available across both platforms.
Binder IPC is an object-oriented IPC (Inter-Process Communication) mechanism that Google added to the Linux kernel for Android. Android uses Binder IPC for all process communication, and it has been part of the mainline Linux kernel since version 4.17, making it available on all modern Linux systems.
However, since it is rarely used outside of Android, it is disabled by default in most Linux distributions.
Crates
rsbinder offers the following crates:
rsbinder: Core library crate for implementing binder service/client functionality.rsbinder-aidl: AIDL-to-Rust code generator for rsbinder.rsbinder-tools: CLI tools, including a Binder Service Manager (rsb_hub) for Linux.tests: Port of Android's binder test cases for client/server testing.example-hello: Example service/client implementation using rsbinder.
Key Features of Binder IPC
- Object-oriented: Binder IPC provides a clean and intuitive object-oriented API for inter-process communication.
- Efficient: Binder IPC is designed for high performance and low overhead with efficient data serialization.
- Secure: Binder IPC provides strong security features to prevent unauthorized access and tampering.
- Versatile: Binder IPC can be used for a variety of purposes, including remote procedure calls, data sharing, and event notification.
- Cross-platform: Works on both Android and Linux environments.
- Async/Sync Support: Supports both synchronous and asynchronous programming models with optional tokio runtime integration.
Core Components
- Parcel: Data serialization and deserialization for IPC transactions
- Binder Objects: Strong and weak references for cross-process communication
- AIDL Compiler: Generates Rust code from Android Interface Definition Language files
- Service Manager: Centralized service discovery and registration (rsb_hub for Linux)
- Thread Pool: Efficient handling of concurrent IPC transactions
- Death Notification: Service lifecycle management and cleanup
Resources
- API Documentation: docs.rs/rsbinder
- Repository: github.com/hiking90/rsbinder
Before using Binder IPC on Linux, you must enable the feature in the kernel. Please refer to Enable binder for Linux for detailed instructions. For Android, see Android Development.
Architecture
---
title: Binder IPC Architecture
---
flowchart BT
AIDL
G[[Generated Rust Code
for Service and Client]]
S(Your Binder Service)
C(Binder Client)
H(HUB
Service Manager)
AIDL-->|rsbinder-aidl compiler|G;
G-.->|Include|S;
G-.->|Include|C;
S-.->|Register Service|H;
C-.->|Query Service|H;
C<-->|Communication|S;
How It Works
- You define your service interface in an AIDL file
- The rsbinder-aidl compiler generates Rust code (traits, proxies, and stubs)
- Your Service implements the generated trait and registers itself with the HUB (service manager)
- A Client queries the HUB to discover the service, then communicates with it through the generated proxy
Description of each component of the diagram
-
AIDL (Android Interface Definition Language)
- The Android Interface Definition Language (AIDL) is a tool that lets users abstract away IPC. Given an interface (specified in a .aidl file), the rsbinder-aidl compiler constructs Rust bindings so that this interface can be used across processes, regardless of the runtime or bitness.
- The compiler generates both synchronous and asynchronous Rust code with full type safety.
- https://source.android.com/docs/core/architecture/aidl
-
Generated Rust Code
- rsbinder-aidl generates trait definitions, proxy implementations (Bp*), and native service stubs (Bn*).
- Includes Parcelable implementations for data serialization/deserialization.
- Supports both sync and async programming models with async-trait integration.
- Features automatic memory management and error handling.
-
Your Binder Service
- Implement the generated trait interface to create your service logic.
- Use BnServiceName::new_binder() to create a binder service instance.
- Register your service with the HUB using hub::add_service().
- Join the thread pool to handle incoming client requests.
- Supports both native services and async services with runtime integration.
-
Binder Client
- Use hub::get_interface() to obtain a strongly-typed proxy to the service.
- The generated proxy code handles all IPC marshalling/unmarshalling automatically.
- Supports death notifications for service lifecycle management.
- Can register service callbacks for service availability notifications.
- Full type safety with compile-time interface validation.
-
HUB (Service Manager)
- In rsbinder, the service manager is referred to as HUB. The
hubmodule in the rsbinder API provides a unified interface (hub::add_service(),hub::get_interface(), etc.) that works on both platforms. - On Linux: rsbinder provides rsb_hub, a standalone service manager that you run as a separate process. You must start
rsb_hubbefore registering or discovering services. - On Android: The system already provides its own service manager (
servicemanager). rsbinder connects to it automatically — no need to runrsb_hub. - Handles service registration, discovery, and lifecycle management.
- Provides APIs for listing services, checking service status, and notifications.
- In rsbinder, the service manager is referred to as HUB. The
Getting Started
Welcome to rsbinder! This guide will help you get started with Binder IPC development using Rust.
Learning Path
If you are new to Binder IPC, we recommend following this learning path:
-
Overview and Architecture - Start here to understand Binder IPC fundamentals
- Learn about the core concepts and components
- Understand the relationship between services and clients
- See how AIDL generates Rust code
-
Installation - Set up your development environment
- Install required dependencies
- Set up binder devices and service manager
- Configure your Rust project
-
Hello World - Build your first Binder service
- Create a simple echo service
- Learn AIDL basics
- Understand service registration and client communication
-
AIDL Guide - Dive deeper into AIDL language features:
- Data Types - How AIDL types map to Rust types
- Parcelable - Custom data structures for IPC
- Enum and Union - Enum and union type support
- Annotations - Code generation annotations
-
Service Development - Build production-quality services:
- Service Patterns - Advanced service patterns and best practices
- Async Service - Non-blocking services with tokio
- Callbacks and Interfaces - Bidirectional communication
- ParcelFileDescriptor - File descriptor passing
- Error Handling - Error types and handling strategies
- Service Manager (HUB) - Service registration and discovery
-
Platform-specific Setup - Choose your target platform:
- Linux Setup - For Linux development
- Android Development - For Android integration
Platform Requirements
rsbinder requires a Linux kernel with Binder IPC support. It runs on:
- Linux: Requires kernel 4.17+ with binderfs enabled (disabled by default in most distributions)
- Android: Binder is available natively; no kernel modification needed
Note: macOS and Windows are not supported as runtime environments. You can use macOS for cross-compiling to Android targets, but running Binder services requires Linux.
Quick Start Checklist
Before diving into development, ensure you have:
- Rust 1.85+ installed
- Linux kernel with binder support enabled (or an Android device/emulator)
-
Created binder device using
rsb_device(Linux only) -
Service manager (
rsb_hub) running (Linux only) - Basic understanding of AIDL syntax (covered in the Hello World tutorial)
Key Concepts to Understand
- Services: Server-side implementations that provide functionality
- Clients: Applications that consume services through proxies
- AIDL: Interface definition language for describing service contracts
- Service Manager: Central registry for service discovery
- Parcels: Serialization format for data exchange
- Binder Objects: References that enable cross-process communication
Common Development Workflow
- Define your service interface in an
.aidlfile - Use
rsbinder-aidlto generate Rust code - Implement your service logic
- Register the service with the service manager
- Create clients that discover and use your service
Ready to start? Head to the Overview section to learn the fundamentals!
Installation
Prerequisites
Rust Version Requirements
rsbinder requires Rust 1.85 or later. Ensure you have the latest stable Rust toolchain:
$ rustup update stable
$ rustc --version # Should be 1.85+
Enable binder for Linux
Please refer to Enable binder for Linux for detailed instructions on setting it up.
Create binder device file for Linux
After the binder configuration of the Linux kernel is complete, a binder device file must be created.
Method 1: Install from crates.io
Install rsbinder-tools and run to create a binder device:
$ cargo install rsbinder-tools
$ sudo rsb_device binder
Method 2: Build from source
If you prefer to build from source:
$ git clone https://github.com/hiking90/rsbinder.git
$ cd rsbinder
$ cargo build --release
$ sudo target/release/rsb_device binder
The rsb_device tool will:
- Create
/dev/binderfsdirectory if it doesn't exist - Mount binderfs filesystem
- Create the specified binder device
- Set appropriate permissions (0666) for user access
Run a service manager for Linux
If rsbinder-tools is already installed, the rsb_hub executable is also installed. Run it as follows:
$ rsb_hub
Alternatively, if building from source:
$ cargo run --bin rsb_hub
The service manager (rsb_hub) provides:
- Service registration and discovery
- Service lifecycle management
- Priority-based service access
Using a custom binder device path
By default, rsb_hub uses /dev/binderfs/binder. If you created a binder device with a different name, use the --device (-d) option:
$ rsb_hub --device custom_binder
# or
$ rsb_hub -d custom_binder
In your Rust code, use ProcessState::init() instead of ProcessState::init_default() to specify the matching path:
#![allow(unused)] fn main() { // Use a custom binder device path ProcessState::init("/dev/binderfs/custom_binder", 0); }
Important: The service manager, service, and client must all use the same binder device path.
Dependencies for rsbinder projects
Add the following configuration to your Cargo.toml file:
[dependencies]
rsbinder = "0.5"
async-trait = "0.1"
env_logger = "0.11" # Optional: for logging
[build-dependencies]
rsbinder-aidl = "0.5"
Feature Flags
rsbinder supports various feature flags for different use cases:
[dependencies]
rsbinder = "0.5" # Default: includes tokio async runtime
# or
rsbinder = { version = "0.5", features = ["async"] } # Async trait support without tokio — use your own async runtime
# or
rsbinder = { version = "0.5", default-features = false } # Synchronous only (no async support)
Available features:
tokio(default): Full tokio async runtime support (includesasyncfeature)async: Async trait support without tokio runtime — use this when integrating with a different async runtimeandroid_*: Android version compatibility flags (see Android Development)
Crate Purposes:
- rsbinder: Core library providing Binder IPC functionality, including kernel communication, data serialization/deserialization, thread pool management, and service lifecycle.
- async-trait: Required for async interface implementations generated by rsbinder-aidl.
- rsbinder-aidl: AIDL-to-Rust code generator, used in build.rs for compile-time code generation.
- env_logger: Optional but recommended for debugging and development logging.
Hello World!
This tutorial will guide you through creating a simple Binder service that echoes a string back to the client, and a client program that uses the service.
Create a new Rust project
Create a library project for the common library used by both the client and service:
$ cargo new --lib hello
Modify Cargo.toml
In the hello project's Cargo.toml, add the following dependencies:
[package]
name = "hello"
version = "0.1.0"
publish = false
edition = "2021"
[dependencies]
rsbinder = "0.5"
async-trait = "0.1"
env_logger = "0.11"
[build-dependencies]
rsbinder-aidl = "0.5"
Add rsbinder and async-trait to [dependencies], and add rsbinder-aidl to [build-dependencies].
Create an AIDL File
Create an aidl folder in the project's top directory to manage AIDL files:
$ mkdir -p aidl/hello
$ touch aidl/hello/IHello.aidl
The reason for creating an additional hello folder is to create a namespace for the hello package.
Create the aidl/hello/IHello.aidl file with the following contents:
package hello;
// Defining the IHello Interface
interface IHello {
// Defining the echo() Function.
// The function takes a single parameter of type String and returns a value of type String.
String echo(in String hello);
}
For more information on AIDL syntax, refer to the Android AIDL documentation.
Create the build.rs
Create a build.rs file in the project root (next to Cargo.toml) to compile the AIDL file and generate Rust code:
use std::path::PathBuf; fn main() { rsbinder_aidl::Builder::new() .source(PathBuf::from("aidl/hello/IHello.aidl")) .output(PathBuf::from("hello.rs")) .generate() .unwrap(); }
This uses rsbinder-aidl to specify the AIDL source file (IHello.aidl) and the generated Rust file name (hello.rs), and then generates the code during the build process.
Important: The
build.rsfile must be placed in the project root directory, not insidesrc/. If placed in the wrong location, you will get a compile error:environment variable OUT_DIR not defined at compile time. Cargo only recognizesbuild.rsat the project root.
Create a common library for Client and Service
For the Client and Service, create a library that includes the Rust code generated from AIDL.
Create src/lib.rs and add the following content.
#![allow(unused)] fn main() { // Include the code hello.rs generated from AIDL. include!(concat!(env!("OUT_DIR"), "/hello.rs")); // Set up to use the APIs provided in the code generated for Client and Service. pub use crate::hello::IHello::*; // Define the name of the service to be registered in the HUB(service manager). pub const SERVICE_NAME: &str = "my.hello"; }
Create a service
Create the src/bin/ directory and add the service file. Cargo automatically recognizes .rs files under src/bin/ as binary targets, so no [[bin]] section is needed in Cargo.toml.
Let's configure the src/bin/hello_service.rs file as follows.
use env_logger::Env; use rsbinder::*; use hello::*; // Define a struct that implements the IHello interface. struct IHelloService; // Implement the IHello interface for the IHelloService. impl Interface for IHelloService { // Reimplement the dump method. This is optional. fn dump(&self, writer: &mut dyn std::io::Write, _args: &[String]) -> Result<()> { writeln!(writer, "Dump IHelloService")?; Ok(()) } } // Implement the IHello interface for the IHelloService. impl IHello for IHelloService { // Implement the echo method. fn echo(&self, echo: &str) -> rsbinder::status::Result<String> { Ok(echo.to_owned()) } } fn main() -> std::result::Result<(), Box<dyn std::error::Error>> { env_logger::Builder::from_env(Env::default().default_filter_or("warn")).init(); // Initialize ProcessState with the default binder path and the default max threads. println!("Initializing ProcessState..."); ProcessState::init_default(); // Start the thread pool. // This is optional. If you don't call this, only one thread will be created to handle the binder transactions. println!("Starting thread pool..."); ProcessState::start_thread_pool(); // Create a binder service. println!("Creating service..."); let service = BnHello::new_binder(IHelloService{}); // Add the service to binder service manager. println!("Adding service to hub..."); hub::add_service(SERVICE_NAME, service.as_binder())?; // Join the thread pool. // This is a blocking call. It will return when the thread pool is terminated. Ok(ProcessState::join_thread_pool()?) }
Create a client
Create the src/bin/hello_client.rs file and configure it as follows.
#![allow(non_snake_case)] use env_logger::Env; use rsbinder::*; use hello::*; use hub::{BnServiceCallback, IServiceCallback}; use std::sync::Arc; struct MyServiceCallback {} impl Interface for MyServiceCallback {} impl IServiceCallback for MyServiceCallback { fn onRegistration(&self, name: &str, _service: &SIBinder) -> rsbinder::status::Result<()> { println!("MyServiceCallback: {name}"); Ok(()) } } struct MyDeathRecipient {} impl DeathRecipient for MyDeathRecipient { fn binder_died(&self, _who: &WIBinder) { println!("MyDeathRecipient"); } } fn main() -> std::result::Result<(), Box<dyn std::error::Error>> { env_logger::Builder::from_env(Env::default().default_filter_or("warn")).init(); // Initialize ProcessState with the default binder path and the default max threads. ProcessState::init_default(); println!("list services:"); // This is an example of how to use service manager. for name in hub::list_services(hub::DUMP_FLAG_PRIORITY_DEFAULT) { println!("{name}"); } let service_callback = BnServiceCallback::new_binder(MyServiceCallback {}); hub::register_for_notifications(SERVICE_NAME, &service_callback)?; // Create a Hello proxy from binder service manager. let hello: rsbinder::Strong<dyn IHello> = hub::get_interface(SERVICE_NAME) .unwrap_or_else(|_| panic!("Can't find {SERVICE_NAME}")); let recipient = Arc::new(MyDeathRecipient {}); hello .as_binder() .link_to_death(Arc::downgrade(&(recipient as Arc<dyn DeathRecipient>)))?; // Call echo method of Hello proxy. let echo = hello.echo("Hello World!")?; println!("Result: {echo}"); Ok(ProcessState::join_thread_pool()?) }
Project folder and file structure
.
├── Cargo.toml
├── aidl
│ └── hello
│ └── IHello.aidl
├── build.rs
└── src
├── bin
│ ├── hello_client.rs
│ └── hello_service.rs
└── lib.rs
Run Hello Service and Client
Before running the service and client, make sure you have the service manager running:
# In terminal 1: Start the service manager
$ rsb_hub
Now you can run the service and client:
# In terminal 2: Run the service
$ cargo run --bin hello_service
# In terminal 3: Run the client
$ cargo run --bin hello_client
Expected Output
hello_service output:
Initializing ProcessState...
Starting thread pool...
Creating service...
Adding service to hub...
hello_client output:
list services:
my.hello
manager
MyServiceCallback: my.hello
Result: Hello World!
The client demonstrates several advanced features:
- Service Discovery: Lists all available services
- Service Callbacks: Registers for service availability notifications
- Death Recipients: Monitors service lifecycle for cleanup
- Type-safe Proxies: Uses strongly-typed interface for service calls
Troubleshooting
If you encounter issues:
- "ProcessState is not initialized!" -
ProcessState::init_default()(orProcessState::init()) must be called inmain()before using any other rsbinder APIs - "environment variable OUT_DIR not defined" -
build.rsmust be placed in the project root directory (next toCargo.toml), not insidesrc/ - "Can't find my.hello" - Ensure the service is running and registered
- Permission errors - Check that binder device has correct permissions (0666)
- Service manager not found - Verify
rsb_hubis running - Build errors - Ensure all dependencies are correctly specified in Cargo.toml
Next Steps
Congratulations! You've successfully created your first Binder service and client. Here are some next steps to explore:
Caller Identity and Access Control
Inside a service method, you can identify the calling process using CallingContext:
#![allow(unused)] fn main() { use rsbinder::thread_state::CallingContext; fn echo(&self, echo: &str) -> rsbinder::status::Result<String> { let caller = CallingContext::default(); let caller_uid = caller.uid; let caller_pid = caller.pid; let caller_sid = caller.sid; // Optional SELinux context // Enforce your own access control policy if caller_uid != expected_uid { return Err(rsbinder::Status::from(rsbinder::StatusCode::PermissionDenied)); } Ok(echo.to_owned()) } }
This is especially useful since rsbinder does not enforce any access control policy by itself — it is up to each service to validate callers.
Error Handling
Services can return service-specific errors to clients using Status::new_service_specific_error:
#![allow(unused)] fn main() { fn echo(&self, echo: &str) -> rsbinder::status::Result<String> { if echo.is_empty() { return Err(rsbinder::Status::new_service_specific_error(-1, None)); } Ok(echo.to_owned()) } }
On the client side, these errors can be inspected through the Status type to distinguish between transport errors and application-level errors.
AIDL Annotations
Generated types from AIDL do not derive Clone by default, because some AIDL types contain non-cloneable fields such as ParcelFileDescriptor (which wraps OwnedFd) or ParcelableHolder (which contains a Mutex).
You can opt-in to Clone (and other traits) for specific types using the @RustDerive annotation in your AIDL file:
@RustDerive(Clone=true, PartialEq=true)
parcelable MyData {
int id;
String name;
}
@RustDerive is supported for parcelable and union types. This follows the same convention as Android's AIDL Rust backend. The annotation will only compile successfully if all fields in the type actually implement the requested traits.
Explore More Features
- AIDL Data Types: Learn how AIDL types map to Rust, including primitives, arrays, strings, and nullable types
- Parcelable: Define custom data structures that can be sent across Binder IPC
- Enum and Union: Use enum and union types in your AIDL interfaces
- Annotations: Control code generation with
@RustDerive,@Backing,@nullable, and more - Service Patterns: Advanced service patterns including
dump(), default implementations, and multi-service processes - Async Service: Use async/await with tokio runtime for non-blocking services
- Callbacks and Interfaces: Implement bidirectional communication and death recipients
- ParcelFileDescriptor: Pass file descriptors across process boundaries
- Error Handling: Service-specific errors, status codes, and exception handling
- Service Manager (HUB): Registration, lookup, notifications, and debug info
- API Reference: See the full API documentation at docs.rs/rsbinder
Run the Test Suite
The rsbinder project includes a comprehensive test suite ported from Android:
# Terminal 1: Start service manager
$ cargo run --bin rsb_hub
# Terminal 2: Start test service
$ cargo run --bin test_service
# Terminal 3: Run tests
$ cargo test -p tests test_client::
Study Real Examples
Check out the rsbinder repository for more complex examples:
- example-hello: The complete example from this tutorial (note: the package name is
example-hello, so import paths useexample_hello::*instead ofhello::*) - tests: Comprehensive test cases showing various IPC scenarios
- rsbinder-tools: Real-world service manager implementation
AIDL Data Types
AIDL (Android Interface Definition Language) defines the interface contract between a Binder service and its clients. When you write an .aidl file, rsbinder-aidl generates Rust code that maps each AIDL type to the corresponding Rust type. Understanding these mappings is essential for implementing services and calling them correctly from client code.
This chapter covers the supported AIDL data types, how they map to Rust, and common patterns you will encounter when working with rsbinder.
Primitive Types
The following table shows how AIDL primitive types map to Rust types. Input parameters (in) are passed by value or by reference, while output parameters (out) are passed as mutable references so the service can write results back to the caller.
| AIDL Type | Rust Type (in) | Rust Type (out) | Notes |
|---|---|---|---|
| boolean | bool | &mut bool | |
| byte | i8 | &mut i8 | Single values use i8; array Reverse uses u8 |
| char | u16 | &mut u16 | UTF-16 code unit |
| int | i32 | &mut i32 | |
| long | i64 | &mut i64 | |
| float | f32 | &mut f32 | |
| double | f64 | &mut f64 | |
| String | &str | &mut String | |
| @utf8InCpp String | &str | &mut String | Same mapping in rsbinder |
| T[] | &[T] | &mut Vec<T> | |
| @nullable T | Option<&T> | &mut Option<T> | |
| IBinder | &SIBinder | ||
| ParcelFileDescriptor | &ParcelFileDescriptor |
Here is an AIDL interface that exercises the primitive types:
interface IDataService {
boolean RepeatBoolean(boolean token);
byte RepeatByte(byte token);
int RepeatInt(int token);
long RepeatLong(long token);
float RepeatFloat(float token);
double RepeatDouble(double token);
}
The generated Rust trait expects the following signatures. A service implementation simply returns each value back to the caller:
#![allow(unused)] fn main() { impl IDataService for MyService { fn RepeatBoolean(&self, token: bool) -> rsbinder::status::Result<bool> { Ok(token) } fn RepeatByte(&self, token: i8) -> rsbinder::status::Result<i8> { Ok(token) } fn RepeatInt(&self, token: i32) -> rsbinder::status::Result<i32> { Ok(token) } // ... similar for other types } }
Each method returns rsbinder::status::Result<T>, which allows the service to return either a value or a Status error to the client.
String Types
AIDL String maps to &str for input parameters and String for return values. This follows Rust's standard convention of borrowing for inputs and returning owned data for outputs.
The @utf8InCpp annotation exists in Android AIDL to distinguish between UTF-16 and UTF-8 string encodings in the C++ backend. In Android's C++ Binder, strings are UTF-16 by default and @utf8InCpp switches them to std::string (UTF-8). In rsbinder, this annotation has no effect because Rust strings are always UTF-8. Both String and @utf8InCpp String produce the same Rust type mapping.
A simple service method that echoes a string back to the caller looks like this:
#![allow(unused)] fn main() { fn RepeatString(&self, input: &str) -> rsbinder::status::Result<String> { Ok(input.into()) } }
Note the use of .into() to convert the borrowed &str into an owned String for the return value. You can also use input.to_string() or input.to_owned() -- all three are equivalent here.
Arrays and the Reverse Pattern
A common pattern in AIDL test interfaces is the "Reverse" method. The method receives an input array, copies it into an out parameter called repeated, and returns the reversed array. This exercises both input and output array handling in a single call.
The AIDL definition looks like this:
int[] ReverseInt(in int[] input, out int[] repeated);
In the generated Rust trait, the in parameter becomes a slice reference (&[i32]) and the out parameter becomes a mutable reference to a Vec (&mut Vec<i32>). The return value is also a Vec:
#![allow(unused)] fn main() { fn ReverseInt(&self, input: &[i32], repeated: &mut Vec<i32>) -> rsbinder::status::Result<Vec<i32>> { repeated.clear(); repeated.extend_from_slice(input); Ok(input.iter().rev().cloned().collect()) } }
On the client side, you pass the input array and a mutable Vec to receive the repeated copy. After the call returns, both the repeated vector and the return value are populated:
#![allow(unused)] fn main() { let input = vec![1, 2, 3]; let mut repeated = vec![]; let reversed = service.ReverseInt(&input, &mut repeated)?; assert_eq!(repeated, vec![1, 2, 3]); assert_eq!(reversed, vec![3, 2, 1]); }
This pattern applies to all array types, including boolean[], byte[], long[], float[], double[], String[], and arrays of parcelable types. The Reverse pattern is particularly useful in testing because it validates that data survives a round trip through Binder serialization and deserialization in both directions.
Nullable Types
The @nullable annotation indicates that a parameter or return value may be absent. In Rust, this maps naturally to Option<T>.
For input parameters, a nullable array becomes Option<&[T]>. For return values, it becomes Option<Vec<T>>. This allows both the client and service to represent the absence of a value without resorting to sentinel values or empty collections.
AIDL definition:
@nullable int[] RepeatNullableIntArray(@nullable in int[] input);
Rust service implementation:
#![allow(unused)] fn main() { fn RepeatNullableIntArray(&self, input: Option<&[i32]>) -> rsbinder::status::Result<Option<Vec<i32>>> { Ok(input.map(<[i32]>::to_vec)) } }
Client usage:
#![allow(unused)] fn main() { let result = service.RepeatNullableIntArray(Some(&[1, 2, 3])); assert_eq!(result, Ok(Some(vec![1, 2, 3]))); let result = service.RepeatNullableIntArray(None); assert_eq!(result, Ok(None)); }
When None is passed, the Binder transaction sends a null marker and the service receives None. When a value is present, it is serialized and deserialized normally.
The @nullable annotation can also be applied to String, IBinder, and parcelable types. Without @nullable, these types must always be present -- passing a null value will result in a transaction error.
Parameter Direction: in, out, and inout
AIDL parameters have a direction tag that controls how data flows between client and service. This affects both the wire format (what data is serialized into the Binder transaction) and the generated Rust method signatures.
in (default)
Data flows from the client to the service. This is the default direction and does not need to be specified explicitly (though you can write it for clarity). In Rust, in parameters are passed by value for primitives or by reference for complex types like arrays and strings.
void Process(in int[] data); // explicit 'in'
void Process(int[] data); // same as above, 'in' is the default
For primitive types like int and boolean, the in direction simply means the value is copied into the Binder transaction. For complex types like arrays, a slice reference (&[T]) is used so the data is serialized without requiring the caller to give up ownership.
out
Data flows from the service back to the client. The client provides a mutable container and the service fills it with data. In Rust, out parameters are passed as &mut references. The initial contents of the container are not sent to the service -- only the service's written data is transmitted back.
void GetData(out int[] result);
In Rust, this generates a &mut Vec<i32> parameter. The caller should provide an empty or pre-allocated vector; the service is responsible for populating it.
inout
Data flows in both directions. The client sends initial data to the service, the service may modify it, and the modified data is sent back. In Rust, inout parameters are also passed as &mut references, but unlike out parameters, the initial value is serialized and sent to the service.
void Transform(inout int[] data);
Use inout when the service needs to read the existing value and modify it in place. Prefer in or out when data only needs to flow in one direction, as this avoids unnecessary serialization overhead.
Note: Primitive types (
boolean,byte,char,int,long,float,double) do not require a direction tag. Direction tags are only meaningful for non-primitive types such as arrays, strings, and parcelable types.
Tips
Here are a few practical details to keep in mind when working with AIDL data types in rsbinder:
-
The
bytetype has a subtle difference between single values and arrays. A singlebyteparameter maps toi8(signed), but when used in theReverseBytepattern, array elements useu8(unsigned). This matches Android's Binder behavior where byte arrays are treated as unsigned. -
Rust strings are always UTF-8, so
@utf8InCpphas no special behavior. In Android's C++ backend, this annotation switches betweenString16(UTF-16) andstd::string(UTF-8). Since Rust'sStringtype is inherently UTF-8, bothStringand@utf8InCpp Stringproduce identical code. -
Arrays in AIDL map to slices for input and
Vecfor output. Input arrays use&[T], which is efficient because no allocation is needed on the caller side. Output arrays and return values useVec<T>, giving the service ownership of the returned data. -
Nullable types use
Option. This is idiomatic Rust and avoids the null pointer pitfalls found in C++ and Java Binder implementations. Always check forNoneon the client side when calling methods that return nullable types. -
Direction tags affect performance. An
inoutparameter requires serialization in both directions. If you only need data to flow one way, useinoroutto reduce the amount of data copied over the Binder transaction. -
Return values are always
Result. Every AIDL method in rsbinder returnsrsbinder::status::Result<T>, allowing services to report errors usingStatuscodes. Even void methods returnrsbinder::status::Result<()>. -
charis UTF-16, not UTF-8. The AIDLchartype maps to Rust'su16, representing a single UTF-16 code unit. This is not the same as Rust's nativechartype, which is a Unicode scalar value. Be mindful of this difference when working with character data.
For more information on AIDL syntax and features, refer to the Android AIDL documentation.
AIDL Data Types
AIDL (Android Interface Definition Language) defines the interface contract between a Binder service and its clients. When you write an .aidl file, rsbinder-aidl generates Rust code that maps each AIDL type to the corresponding Rust type. Understanding these mappings is essential for implementing services and calling them correctly from client code.
This chapter covers the supported AIDL data types, how they map to Rust, and common patterns you will encounter when working with rsbinder.
Primitive Types
The following table shows how AIDL primitive types map to Rust types. Input parameters (in) are passed by value or by reference, while output parameters (out) are passed as mutable references so the service can write results back to the caller.
| AIDL Type | Rust Type (in) | Rust Type (out) | Notes |
|---|---|---|---|
| boolean | bool | &mut bool | |
| byte | i8 | &mut i8 | Single values use i8; array Reverse uses u8 |
| char | u16 | &mut u16 | UTF-16 code unit |
| int | i32 | &mut i32 | |
| long | i64 | &mut i64 | |
| float | f32 | &mut f32 | |
| double | f64 | &mut f64 | |
| String | &str | &mut String | |
| @utf8InCpp String | &str | &mut String | Same mapping in rsbinder |
| T[] | &[T] | &mut Vec<T> | |
| @nullable T | Option<&T> | &mut Option<T> | |
| IBinder | &SIBinder | ||
| ParcelFileDescriptor | &ParcelFileDescriptor |
Here is an AIDL interface that exercises the primitive types:
interface IDataService {
boolean RepeatBoolean(boolean token);
byte RepeatByte(byte token);
int RepeatInt(int token);
long RepeatLong(long token);
float RepeatFloat(float token);
double RepeatDouble(double token);
}
The generated Rust trait expects the following signatures. A service implementation simply returns each value back to the caller:
#![allow(unused)] fn main() { impl IDataService for MyService { fn RepeatBoolean(&self, token: bool) -> rsbinder::status::Result<bool> { Ok(token) } fn RepeatByte(&self, token: i8) -> rsbinder::status::Result<i8> { Ok(token) } fn RepeatInt(&self, token: i32) -> rsbinder::status::Result<i32> { Ok(token) } // ... similar for other types } }
Each method returns rsbinder::status::Result<T>, which allows the service to return either a value or a Status error to the client.
String Types
AIDL String maps to &str for input parameters and String for return values. This follows Rust's standard convention of borrowing for inputs and returning owned data for outputs.
The @utf8InCpp annotation exists in Android AIDL to distinguish between UTF-16 and UTF-8 string encodings in the C++ backend. In Android's C++ Binder, strings are UTF-16 by default and @utf8InCpp switches them to std::string (UTF-8). In rsbinder, this annotation has no effect because Rust strings are always UTF-8. Both String and @utf8InCpp String produce the same Rust type mapping.
A simple service method that echoes a string back to the caller looks like this:
#![allow(unused)] fn main() { fn RepeatString(&self, input: &str) -> rsbinder::status::Result<String> { Ok(input.into()) } }
Note the use of .into() to convert the borrowed &str into an owned String for the return value. You can also use input.to_string() or input.to_owned() -- all three are equivalent here.
Arrays and the Reverse Pattern
A common pattern in AIDL test interfaces is the "Reverse" method. The method receives an input array, copies it into an out parameter called repeated, and returns the reversed array. This exercises both input and output array handling in a single call.
The AIDL definition looks like this:
int[] ReverseInt(in int[] input, out int[] repeated);
In the generated Rust trait, the in parameter becomes a slice reference (&[i32]) and the out parameter becomes a mutable reference to a Vec (&mut Vec<i32>). The return value is also a Vec:
#![allow(unused)] fn main() { fn ReverseInt(&self, input: &[i32], repeated: &mut Vec<i32>) -> rsbinder::status::Result<Vec<i32>> { repeated.clear(); repeated.extend_from_slice(input); Ok(input.iter().rev().cloned().collect()) } }
On the client side, you pass the input array and a mutable Vec to receive the repeated copy. After the call returns, both the repeated vector and the return value are populated:
#![allow(unused)] fn main() { let input = vec![1, 2, 3]; let mut repeated = vec![]; let reversed = service.ReverseInt(&input, &mut repeated)?; assert_eq!(repeated, vec![1, 2, 3]); assert_eq!(reversed, vec![3, 2, 1]); }
This pattern applies to all array types, including boolean[], byte[], long[], float[], double[], String[], and arrays of parcelable types. The Reverse pattern is particularly useful in testing because it validates that data survives a round trip through Binder serialization and deserialization in both directions.
Nullable Types
The @nullable annotation indicates that a parameter or return value may be absent. In Rust, this maps naturally to Option<T>.
For input parameters, a nullable array becomes Option<&[T]>. For return values, it becomes Option<Vec<T>>. This allows both the client and service to represent the absence of a value without resorting to sentinel values or empty collections.
AIDL definition:
@nullable int[] RepeatNullableIntArray(@nullable in int[] input);
Rust service implementation:
#![allow(unused)] fn main() { fn RepeatNullableIntArray(&self, input: Option<&[i32]>) -> rsbinder::status::Result<Option<Vec<i32>>> { Ok(input.map(<[i32]>::to_vec)) } }
Client usage:
#![allow(unused)] fn main() { let result = service.RepeatNullableIntArray(Some(&[1, 2, 3])); assert_eq!(result, Ok(Some(vec![1, 2, 3]))); let result = service.RepeatNullableIntArray(None); assert_eq!(result, Ok(None)); }
When None is passed, the Binder transaction sends a null marker and the service receives None. When a value is present, it is serialized and deserialized normally.
The @nullable annotation can also be applied to String, IBinder, and parcelable types. Without @nullable, these types must always be present -- passing a null value will result in a transaction error.
Parameter Direction: in, out, and inout
AIDL parameters have a direction tag that controls how data flows between client and service. This affects both the wire format (what data is serialized into the Binder transaction) and the generated Rust method signatures.
in (default)
Data flows from the client to the service. This is the default direction and does not need to be specified explicitly (though you can write it for clarity). In Rust, in parameters are passed by value for primitives or by reference for complex types like arrays and strings.
void Process(in int[] data); // explicit 'in'
void Process(int[] data); // same as above, 'in' is the default
For primitive types like int and boolean, the in direction simply means the value is copied into the Binder transaction. For complex types like arrays, a slice reference (&[T]) is used so the data is serialized without requiring the caller to give up ownership.
out
Data flows from the service back to the client. The client provides a mutable container and the service fills it with data. In Rust, out parameters are passed as &mut references. The initial contents of the container are not sent to the service -- only the service's written data is transmitted back.
void GetData(out int[] result);
In Rust, this generates a &mut Vec<i32> parameter. The caller should provide an empty or pre-allocated vector; the service is responsible for populating it.
inout
Data flows in both directions. The client sends initial data to the service, the service may modify it, and the modified data is sent back. In Rust, inout parameters are also passed as &mut references, but unlike out parameters, the initial value is serialized and sent to the service.
void Transform(inout int[] data);
Use inout when the service needs to read the existing value and modify it in place. Prefer in or out when data only needs to flow in one direction, as this avoids unnecessary serialization overhead.
Note: Primitive types (
boolean,byte,char,int,long,float,double) do not require a direction tag. Direction tags are only meaningful for non-primitive types such as arrays, strings, and parcelable types.
Tips
Here are a few practical details to keep in mind when working with AIDL data types in rsbinder:
-
The
bytetype has a subtle difference between single values and arrays. A singlebyteparameter maps toi8(signed), but when used in theReverseBytepattern, array elements useu8(unsigned). This matches Android's Binder behavior where byte arrays are treated as unsigned. -
Rust strings are always UTF-8, so
@utf8InCpphas no special behavior. In Android's C++ backend, this annotation switches betweenString16(UTF-16) andstd::string(UTF-8). Since Rust'sStringtype is inherently UTF-8, bothStringand@utf8InCpp Stringproduce identical code. -
Arrays in AIDL map to slices for input and
Vecfor output. Input arrays use&[T], which is efficient because no allocation is needed on the caller side. Output arrays and return values useVec<T>, giving the service ownership of the returned data. -
Nullable types use
Option. This is idiomatic Rust and avoids the null pointer pitfalls found in C++ and Java Binder implementations. Always check forNoneon the client side when calling methods that return nullable types. -
Direction tags affect performance. An
inoutparameter requires serialization in both directions. If you only need data to flow one way, useinoroutto reduce the amount of data copied over the Binder transaction. -
Return values are always
Result. Every AIDL method in rsbinder returnsrsbinder::status::Result<T>, allowing services to report errors usingStatuscodes. Even void methods returnrsbinder::status::Result<()>. -
charis UTF-16, not UTF-8. The AIDLchartype maps to Rust'su16, representing a single UTF-16 code unit. This is not the same as Rust's nativechartype, which is a Unicode scalar value. Be mindful of this difference when working with character data.
For more information on AIDL syntax and features, refer to the Android AIDL documentation.
Parcelable
Parcelable types are user-defined data structures that can be serialized and sent across Binder IPC boundaries. They are defined in AIDL .aidl files, and the rsbinder-aidl code generator automatically produces Rust structs from them. Parcelable types are the primary way to pass structured data between a Binder service and its clients.
Unlike primitive types (such as int, String, or boolean), which AIDL handles natively, parcelable types let you group related fields into a single, coherent structure. This is essential for any non-trivial service interface.
Basic Parcelable Definition
A parcelable is declared in its own .aidl file using the parcelable keyword. Here is a simple example:
package com.example;
@RustDerive(Clone=true, PartialEq=true)
parcelable UserProfile {
int id;
String name = "Unknown";
int age = 0;
}
When this AIDL file is processed by rsbinder-aidl, it generates a Rust struct that you can use directly in your service and client code. Several things to note about the definition above:
@RustDerive(Clone=true, PartialEq=true)instructs the code generator to add#[derive(Clone, PartialEq)]to the generated Rust struct. By default, generated types do not deriveClone, because some AIDL types contain non-cloneable fields (such asParcelFileDescriptororParcelableHolder). You must opt in explicitly for each type.- Default values (
"Unknown"forname,0forage) are applied in the generatedDefaulttrait implementation. Fields without explicit defaults use Rust's default for their type (e.g.,0for integers, empty string forString). - The generated struct can be used directly as a parameter or return type in service interface methods.
You can then use the generated struct in Rust:
#![allow(unused)] fn main() { use com::example::UserProfile::UserProfile; let profile = UserProfile { id: 1, name: "Alice".into(), age: 30, }; let default_profile = UserProfile::default(); assert_eq!(default_profile.name, "Unknown"); assert_eq!(default_profile.age, 0); }
Constants in Parcelable
Parcelable types can define constants, including numeric values and bit flags. These become associated constants on the generated Rust struct. This pattern is commonly used for configuration values and flag fields.
@RustDerive(Clone=true, PartialEq=true)
parcelable Config {
const int MAX_RETRIES = 5;
const int BIT_VERBOSE = 0x1;
const int BIT_DEBUG = 0x4;
int retryCount = MAX_RETRIES;
int flags = 0;
String label = "default";
}
In Rust, the constants are accessed as associated constants on the struct:
#![allow(unused)] fn main() { use config::Config; let mut cfg = Config::default(); assert_eq!(cfg.retryCount, 5); cfg.flags = Config::BIT_VERBOSE | Config::BIT_DEBUG; assert_eq!(cfg.flags, 0x5); }
This pattern mirrors how the Android test suite defines and uses bit flags within StructuredParcelable, where constants like BIT0, BIT1, and BIT2 are defined alongside the fields that use them.
Using Parcelable in Services
Parcelable types are passed to and from service methods as regular parameters. A common pattern is to pass a mutable reference to a parcelable so that the service can fill in or modify its fields. This is based on the FillOutStructuredParcelable pattern used in the rsbinder test suite.
Service implementation:
#![allow(unused)] fn main() { fn FillOutStructuredParcelable( &self, parcelable: &mut StructuredParcelable, ) -> rsbinder::status::Result<()> { parcelable.shouldBeJerry = "Jerry".into(); parcelable.shouldContainThreeFs = vec![parcelable.f, parcelable.f, parcelable.f]; parcelable.shouldSetBit0AndBit2 = StructuredParcelable::BIT0 | StructuredParcelable::BIT2; Ok(()) } }
Client side:
#![allow(unused)] fn main() { let mut parcelable = StructuredParcelable { f: 17, shouldSetBit0AndBit2: 0, ..Default::default() }; service.FillOutStructuredParcelable(&mut parcelable)?; assert_eq!(parcelable.shouldBeJerry, "Jerry"); assert_eq!(parcelable.shouldContainThreeFs, vec![17, 17, 17]); assert_eq!( parcelable.shouldSetBit0AndBit2, StructuredParcelable::BIT0 | StructuredParcelable::BIT2 ); }
The service receives the parcelable by mutable reference, reads existing field values, and populates the remaining fields before returning. The client can then inspect the modified parcelable.
Nullable Parcelable
The @nullable annotation allows a parcelable parameter or return type to be None. In the generated Rust code, nullable parcelable types are represented as Option<T>.
AIDL declaration:
@nullable Empty RepeatNullableParcelable(@nullable in Empty input);
Service implementation:
#![allow(unused)] fn main() { fn RepeatNullableParcelable( &self, input: Option<&Empty>, ) -> rsbinder::status::Result<Option<Empty>> { Ok(input.cloned()) } }
When the client passes None, the service receives None and can return None. When a value is provided, standard Option methods like cloned(), map(), and as_ref() work as expected. Note that cloned() requires the parcelable type to derive Clone via @RustDerive(Clone=true).
Recursive Structures
AIDL supports self-referential parcelable types using the @nullable(heap=true) annotation. This is necessary because a struct that directly contains itself would have infinite size. The heap=true attribute causes the field to be wrapped in Box<T> in the generated Rust code, which places the recursive field on the heap and gives the type a known size at compile time.
AIDL definition (from RecursiveList.aidl in the test suite):
parcelable RecursiveList {
int value;
@nullable(heap=true) RecursiveList next;
}
This generates a Rust struct where next has the type Option<Box<RecursiveList>>. The @nullable part makes it Option, and heap=true makes it Box-wrapped. Together they enable a linked-list pattern.
Rust usage (based on the test_reverse_recursive_list test):
#![allow(unused)] fn main() { // Build a linked list: [9, 8, 7, ..., 0] let mut head = None; for n in 0..10 { let node = RecursiveList { value: n, next: head, }; head = Some(Box::new(node)); } // Send to service for reversal let result = service.ReverseList(head.as_ref().unwrap())?; // Traverse the reversed list: [0, 1, ..., 9] let mut current: Option<&RecursiveList> = result.as_ref(); for n in 0..10 { assert_eq!(current.map(|inner| inner.value), Some(n)); current = current.unwrap().next.as_ref().map(|n| n.as_ref()); } assert!(current.is_none()); }
Without heap=true, the compiler would reject the type definition because RecursiveList would need to contain itself directly, leading to an infinite-size type. The Box indirection solves this by storing the nested value behind a pointer.
ExtendableParcelable and ParcelableHolder
ExtendableParcelable is a pattern that uses ParcelableHolder to support type-safe, extensible data. A ParcelableHolder field can hold any parcelable type, allowing you to extend a parcelable without changing its base definition. This is useful for versioned interfaces where new fields may be added in the future.
AIDL definitions:
parcelable ExtendableParcelable {
int a;
@utf8InCpp String b;
ParcelableHolder ext;
long c;
ParcelableHolder ext2;
}
parcelable MyExt {
int a;
@utf8InCpp String b;
}
Setting an extension (based on the test_repeat_extendable_parcelable test):
#![allow(unused)] fn main() { use std::sync::Arc; let ext = Arc::new(MyExt { a: 42, b: "EXT".into(), }); let mut ep = ExtendableParcelable { a: 1, b: "a".into(), c: 42, ..Default::default() }; ep.ext.set_parcelable(Arc::clone(&ext)) .expect("error setting parcelable"); }
Sending through a service and retrieving the extension:
#![allow(unused)] fn main() { let mut ep2 = ExtendableParcelable::default(); service.RepeatExtendableParcelable(&ep, &mut ep2)?; assert_eq!(ep2.a, ep.a); assert_eq!(ep2.b, ep.b); assert_eq!(ep2.c, ep.c); let ret_ext = ep2.ext.get_parcelable::<MyExt>() .expect("error getting parcelable"); assert!(ret_ext.is_some()); let ret_ext = ret_ext.unwrap(); assert_eq!(ret_ext.a, 42); assert_eq!(ret_ext.b, "EXT"); }
Key points about ParcelableHolder:
- Type erasure: The
ParcelableHolderstores the extension in a type-erased manner. You must specify the concrete type when callingget_parcelable::<T>(). - Arc wrapping: Extensions are set using
Arc<T>, which allows shared ownership of the extension data. - Multiple holders: A single parcelable can have multiple
ParcelableHolderfields (as shown withextandext2above), each holding a different extension type. - Versioning: This mechanism is particularly useful for forward compatibility. Older code that does not know about newer extension types can still deserialize the base parcelable and pass the
ParcelableHolderthrough without losing data.
Tips
Here are some practical guidelines when working with parcelable types in rsbinder:
-
Always use
@RustDerive(Clone=true)if you need to clone parcelable values. This is required for patterns likeinput.cloned()with nullable parameters. Only add it when all fields in the parcelable actually implementClone. -
Use
@RustDerive(PartialEq=true)when you need to compare parcelable instances in assertions or business logic. As withClone, all fields must implementPartialEq. -
@nullable(heap=true)is required for recursive types. Without it, the compiler will reject the type due to infinite size. Use this annotation on any self-referential field. -
Default values in AIDL translate to Rust's
Defaulttrait. When you writeint count = 5;in AIDL, callingMyParcelable::default()in Rust will produce a struct withcountset to5. -
Use
..Default::default()for partial initialization. When constructing a parcelable where you only need to set a few fields, use Rust's struct update syntax to fill the rest with defaults:#![allow(unused)] fn main() { let ep = ExtendableParcelable { a: 1, b: "hello".into(), ..Default::default() }; } -
ParcelableHolder extensions are type-erased. Always use
get_parcelable::<T>()with the correct concrete type to extract the extension. If the wrong type is specified, the deserialization will fail. -
Place each parcelable in its own
.aidlfile. Following the AIDL convention, each parcelable type should be defined in a separate file whose name matches the type name (e.g.,UserProfile.aidlforparcelable UserProfile). -
Constants are scoped to the parcelable. When you define
const int MAX_VALUE = 100;inside a parcelable, access it in Rust asMyParcelable::MAX_VALUE. This keeps related constants close to the data they describe.
Enum and Union
AIDL supports two powerful type constructs beyond simple interfaces and parcelables: enums and unions. Enums provide named integer constants with type safety, while unions represent a value that can be one of several different types. Both are fully supported by rsbinder's AIDL compiler and map naturally to Rust constructs.
This chapter covers how to define enums and unions in AIDL, how they translate to Rust code, and how to use them in practice.
Enum Types
AIDL enums are backed by a specific integer type, declared using the @Backing annotation. Unlike Rust's native enums, AIDL enums map to newtype structs wrapping the backing integer. This design preserves wire compatibility and allows values outside the defined set, which is important for forward compatibility in IPC.
Defining Enums in AIDL
The @Backing(type=...) annotation is required and specifies the underlying integer type. Here are examples for each supported backing type:
Byte-backed enum:
@Backing(type="byte")
enum ByteEnum {
FOO = 1,
BAR = 2,
BAZ,
}
Int-backed enum:
@Backing(type="int")
enum IntEnum {
FOO = 1000,
BAR = 2000,
BAZ,
}
Long-backed enum:
@Backing(type="long")
enum LongEnum {
FOO = 100000000000,
BAR = 200000000000,
BAZ,
}
When a value is omitted (as with BAZ above), it is automatically assigned the previous value plus one. So BAZ would be 3, 2001, and 200000000001 respectively.
Backing Type Mapping
The AIDL backing type determines the Rust integer type used inside the generated newtype struct:
| AIDL Backing Type | Rust Type |
|---|---|
byte | i8 |
int | i32 |
long | i64 |
Using Enums in Rust
Enum values are accessed as associated constants on the generated struct. The generated type implements Default, Debug, PartialEq, Eq, and serialization traits automatically.
#![allow(unused)] fn main() { // Access enum values as associated constants let e = ByteEnum::FOO; let result = service.RepeatByteEnum(e)?; assert_eq!(result, ByteEnum::FOO); }
Enums work naturally with arrays and vectors:
#![allow(unused)] fn main() { // Enums can be used in arrays let input = [ByteEnum::FOO, ByteEnum::BAR, ByteEnum::BAZ]; let mut repeated = vec![]; let reversed = service.ReverseByteEnum(&input, &mut repeated)?; }
Each generated enum type provides an enum_values() method that returns a slice of all defined values, which is useful for iteration and validation:
#![allow(unused)] fn main() { // enum_values() returns all defined values let all_values = ByteEnum::enum_values(); }
Enums in Service Interfaces
Enums are commonly used as parameters and return types in AIDL interfaces:
interface ITestService {
ByteEnum RepeatByteEnum(ByteEnum token);
IntEnum RepeatIntEnum(IntEnum token);
LongEnum RepeatLongEnum(LongEnum token);
ByteEnum[] ReverseByteEnum(in ByteEnum[] input, out ByteEnum[] repeated);
}
The generated Rust trait methods use the enum types directly, providing compile-time type safety across the IPC boundary.
Union Types
AIDL unions represent a tagged value that holds exactly one of several possible fields at a time. They map to Rust enum types, which are a natural fit since Rust enums are sum types with variants.
Defining Unions in AIDL
Here is a union definition from the rsbinder test suite:
@RustDerive(Clone=true, PartialEq=true)
union Union {
int[] ns = {};
int n;
int m;
@utf8InCpp String s;
@nullable IBinder ibinder;
@utf8InCpp List<String> ss;
ByteEnum be;
const @utf8InCpp String S1 = "a string constant in union";
}
Key points about union definitions:
- The first field is the default. When a union is default-constructed, it takes the value of the first field. In this example, the default is
nsinitialized to an empty array{}. - Fields can have different types, including primitives, strings, arrays, other AIDL types, and even binder references.
- Constants can be defined inside unions, independent of the union's variants.
@RustDeriveis recommended so the generated Rust type supportsCloneandPartialEq.
Using Unions in Rust
The AIDL union generates a Rust enum. Because AIDL types are organized into modules, the union type and its enum variants live inside a module named after the union. Variants are accessed as Union::Union::VariantName(...):
#![allow(unused)] fn main() { // Default value is the first field assert_eq!(Union::Union::default(), Union::Union::Ns(vec![])); // Creating union variants let u1 = Union::Union::N(42); let u2 = Union::Union::S("hello".into()); let u3 = Union::Union::Be(ByteEnum::FOO); }
Constants defined inside a union are accessed directly on the module, not through a variant:
#![allow(unused)] fn main() { // Constants defined in the union let s = Union::S1; // "a string constant in union" // Using a constant as a union variant value let u = Union::Union::S(Union::S1.to_string()); }
Union Tags
Each union has an associated Tag enum that identifies which variant is currently active. Tags are useful when you need to inspect or communicate which field a union holds without extracting the value itself.
#![allow(unused)] fn main() { let result = service.GetUnionTags(&[ Union::Union::N(0), Union::Union::Ns(vec![]), ])?; assert_eq!(result, vec![Union::Tag::n, Union::Tag::ns]); }
Tags can also be used in match expressions when implementing service logic. Here is an example from the test service implementation:
#![allow(unused)] fn main() { fn GetUnionTags( &self, input: &[Union::Union], ) -> Result<Vec<Union::Tag>> { Ok(input.iter().map(|u| match u { Union::Union::Ns(_) => Union::Tag::ns, Union::Union::N(_) => Union::Tag::n, Union::Union::M(_) => Union::Tag::m, Union::Union::S(_) => Union::Tag::s, Union::Union::Ibinder(_) => Union::Tag::ibinder, Union::Union::Ss(_) => Union::Tag::ss, Union::Union::Be(_) => Union::Tag::be, }).collect()) } }
Unions Containing Enums (EnumUnion)
Unions can contain enum types as fields and specify default values using enum constants:
@RustDerive(Clone=true, PartialEq=true)
union EnumUnion {
IntEnum intEnum = IntEnum.FOO;
LongEnum longEnum;
/** @deprecated do not use this */
int deprecatedField;
}
In this example, the default value is the first field (intEnum) initialized to IntEnum.FOO. In Rust:
#![allow(unused)] fn main() { assert_eq!(EnumUnion::default(), EnumUnion::IntEnum(IntEnum::FOO)); }
Note that the @deprecated Javadoc annotation on deprecatedField will generate a #[deprecated] attribute in the Rust code, so using that variant will produce a compiler warning.
Nested Unions
Unions can also be nested inside other unions:
union UnionInUnion {
EnumUnion first;
int second;
}
This allows building complex tagged-value hierarchies that are fully type-safe on the Rust side.
Tips and Best Practices
- Always specify
@Backingfor enums. The AIDL compiler requires it, and the choice of backing type affects both the wire format and the Rust integer type. - The union default is always the first field. Order your fields accordingly, placing the most common or natural default first.
- Use
@RustDerive(Clone=true, PartialEq=true)on unions so they can be compared and cloned in Rust. Without this, you cannot use==or.clone()on union values. - Union constants are module-level, not variants. Access them as
Union::S1, not through any variant. - Enum
enum_values()returns all defined constants, which is useful for exhaustive testing or validation loops. - Forward compatibility: Because AIDL enums are backed by integers, a service may receive values not defined in the current enum. Design your code to handle unknown values gracefully.
- Tag enums use lowercase field names (e.g.,
Union::Tag::ns, notUnion::Tag::Ns), matching the original AIDL field names.
Further Reading
- Android AIDL documentation -- the upstream reference for AIDL syntax and semantics
- AIDL annotation reference -- details on
@Backing,@RustDerive, and other annotations - The rsbinder test suite at
tests/aidl/andtests/src/test_client.rscontains comprehensive examples of enum and union usage
AIDL Annotations
AIDL annotations modify how the code generator produces Rust code from .aidl files. They control everything from trait derivation and backing types to nullability and interface stability. This chapter covers the annotations relevant to the Rust backend in rsbinder, with examples showing how each annotation affects the generated code.
If you are new to AIDL data types, read the AIDL Data Types chapter first. Annotations build on those type mappings by adding metadata that changes how types are generated, serialized, or constrained.
@RustDerive
The @RustDerive annotation tells the code generator to add Rust derive attributes to parcelable and union types. Without this annotation, generated types receive only the minimum set of derives needed for serialization.
@RustDerive(Clone=true, PartialEq=true)
parcelable Point {
int x;
int y;
}
This generates a Rust struct with both Clone and PartialEq derived:
#![allow(unused)] fn main() { #[derive(Debug, Clone, PartialEq)] pub struct Point { pub x: i32, pub y: i32, } }
Available Derives
| Derive | Description |
|---|---|
Clone | Enables cloning of the type |
PartialEq | Enables equality comparison |
Copy | Enables bitwise copy (fixed-size types only) |
Why Clone Is Not Derived by Default
Generated types do not derive Clone by default. This is intentional because some AIDL types contain fields that cannot be cloned:
ParcelFileDescriptorwraps anOwnedFd, which represents sole ownership of a file descriptor. Cloning it would require duplicating the file descriptor at the OS level, which is not a simple bitwise copy.ParcelableHoldercontains aMutex, which cannot be cloned.
If your parcelable contains only primitive fields and cloneable types, add @RustDerive(Clone=true) explicitly. If the type contains a ParcelFileDescriptor or ParcelableHolder field, attempting to derive Clone will produce a compile error.
Copy Derivation for Fixed-Size Types
For parcelables that contain only primitive fields, you can derive Copy in addition to Clone. This is typically combined with the @FixedSize annotation:
@RustDerive(Clone=true, Copy=true, PartialEq=true)
@FixedSize
parcelable IntParcelable {
int value;
}
The Copy derive is only valid when all fields are Copy types. Using it on a parcelable that contains String, arrays, or other heap-allocated types will result in a compile error.
@Backing
The @Backing annotation specifies the underlying integer type for an AIDL enum. This controls both the wire format and the generated Rust type.
@Backing(type="byte")
enum Priority {
LOW = 0,
MEDIUM = 1,
HIGH = 2,
}
The generated Rust code uses a newtype pattern with the corresponding integer type:
#![allow(unused)] fn main() { pub mod Priority { #![allow(non_upper_case_globals)] pub type Priority = i8; pub const LOW: Priority = 0; pub const MEDIUM: Priority = 1; pub const HIGH: Priority = 2; } }
Supported Backing Types
| AIDL Backing | Rust Type | Size |
|---|---|---|
"byte" | i8 | 1 byte |
"int" | i32 | 4 bytes |
"long" | i64 | 8 bytes |
If no @Backing annotation is specified, the default backing type is "byte".
Choose the smallest backing type that fits your range of values. Enum values are serialized to the Binder transaction as their backing integer type, so a smaller backing type produces a more compact wire format.
@nullable
The @nullable annotation marks a type as optional, indicating that the value may be absent. In Rust, this maps to Option<T>.
Basic Usage
Apply @nullable to method parameters, return values, or struct fields:
interface IUserService {
@nullable String getName();
void setValues(@nullable in int[] values);
}
The generated Rust signatures use Option:
#![allow(unused)] fn main() { fn getName(&self) -> rsbinder::status::Result<Option<String>>; fn setValues(&self, values: Option<&[i32]>) -> rsbinder::status::Result<()>; }
When None is passed over a Binder transaction, a null marker is written to the parcel. The receiving side deserializes it as None without allocating any data.
Heap-Allocated Nullable: @nullable(heap=true)
For recursive or self-referential types, use @nullable(heap=true) to wrap the field in a Box<T>:
parcelable RecursiveList {
int value;
@nullable(heap=true) RecursiveList next;
}
This generates:
#![allow(unused)] fn main() { pub struct RecursiveList { pub value: i32, pub next: Option<Box<RecursiveList>>, } }
The Box indirection is necessary because without it, RecursiveList would contain itself directly, making the type infinitely large. The heap=true parameter places the inner value on the heap, giving the struct a finite, known size at compile time.
Use @nullable(heap=true) only when you need recursive structures. For non-recursive optional fields, plain @nullable is sufficient and avoids the extra heap allocation.
@utf8InCpp
The @utf8InCpp annotation exists in Android AIDL to specify UTF-8 encoding for strings in the C++ backend, where the default encoding is UTF-16. In rsbinder, this annotation has no effect because Rust strings are always UTF-8.
interface ITextService {
@utf8InCpp String getData();
@utf8InCpp List<String> getNames();
}
Both String and @utf8InCpp String produce identical Rust type mappings:
| Direction | Rust Type |
|---|---|
Input (in) | &str |
| Output / Return | String |
You may encounter this annotation in AIDL files that were originally written for Android's C++ backend. It is safe to keep or remove it when targeting rsbinder; the generated Rust code is identical either way.
@Descriptor
The @Descriptor annotation overrides the interface descriptor string that identifies an interface on the Binder wire protocol. This is useful when renaming an interface while maintaining backward compatibility with existing clients or services.
Every Binder interface has a descriptor string derived from its fully qualified name (e.g., android.aidl.tests.IOldName). When you rename an interface, the descriptor changes, breaking compatibility. The @Descriptor annotation lets you decouple the source name from the wire descriptor.
Consider an interface that was originally named IOldName:
// IOldName.aidl
interface IOldName {
String RealName();
}
You can create a new interface INewName that uses the same descriptor:
// INewName.aidl
@Descriptor(value="android.aidl.tests.IOldName")
interface INewName {
String RealName();
}
Because both interfaces share the same descriptor, they are interchangeable at the Binder level:
#![allow(unused)] fn main() { // A service registered as IOldName can be used as INewName let new_from_old = old_service .as_binder() .into_interface::<dyn INewName::INewName>(); assert!(new_from_old.is_ok()); }
This is particularly useful during interface migrations where you want to rename types in your codebase without requiring all clients and services to update simultaneously.
@VintfStability
The @VintfStability annotation marks a type or interface as part of the Vendor Interface (VINTF). VINTF-stable types are subject to stricter compatibility rules to ensure that vendor and system partitions can be updated independently.
@VintfStability
parcelable VintfData {
int value;
}
Stability Rules
VINTF-stable types enforce the following constraints:
- A VINTF-stable parcelable can only contain fields whose types are also VINTF-stable.
- A VINTF-stable interface can only use VINTF-stable types in its method signatures.
- Attempting to embed a non-VINTF type inside a VINTF-stable type will produce a
StatusCode::BadValueerror at runtime.
These rules exist to guarantee that the serialization format of VINTF types remains stable across system updates. On Android, this is critical for maintaining compatibility between the framework and vendor HAL implementations.
On Linux, the @VintfStability annotation is recognized by the code generator but the stability enforcement depends on how the service manager is configured.
@FixedSize
The @FixedSize annotation indicates that a parcelable has a fixed serialization size, meaning its wire format is always the same number of bytes regardless of the field values.
@FixedSize
parcelable FixedPoint {
int x;
int y;
}
Constraints
Fixed-size parcelables can only contain:
- Primitive types (
boolean,byte,char,int,long,float,double) - Other
@FixedSizeparcelables - Enums with a
@Backingannotation
They cannot contain:
Stringor@utf8InCpp String- Arrays (
T[]) ParcelFileDescriptorIBinder- Any variable-length type
Relationship with @RustDerive(Copy=true)
The @FixedSize annotation is a prerequisite for deriving Copy in Rust, because only types with a fixed memory layout can be safely copied with a bitwise copy:
@RustDerive(Clone=true, Copy=true, PartialEq=true)
@FixedSize
parcelable Coordinate {
double latitude;
double longitude;
}
Without @FixedSize, adding Copy to the derive list may compile but violates the intended semantics. Always pair Copy with @FixedSize to make the intent explicit.
Summary
The following table provides a quick reference for all annotations covered in this chapter.
| Annotation | Applies To | Rust Effect |
|---|---|---|
@RustDerive | parcelable, union | Adds derive attributes (Clone, Copy, PartialEq) |
@Backing | enum | Sets the backing integer type (i8, i32, i64) |
@nullable | field, param, return | Maps to Option<T> |
@nullable(heap=true) | field | Maps to Option<Box<T>> for recursive types |
@utf8InCpp | String | No effect in Rust (strings are always UTF-8) |
@Descriptor | interface | Overrides the wire descriptor string |
@VintfStability | parcelable, interface | Enforces VINTF stability rules |
@FixedSize | parcelable | Restricts fields to fixed-size types, enables Copy |
When writing AIDL files for rsbinder, the most commonly used annotations are @RustDerive (for ergonomic Rust types), @Backing (for enums), and @nullable (for optional values). The remaining annotations are important for interoperability with Android or for specific use cases like recursive types and interface migration.
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.
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.
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.
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.
Setting Up the Tokio Runtime
An async Binder service must run inside a Tokio runtime. The standard pattern 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() { // 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 }) }
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().
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.
For a complete working example, see tests/src/bin/test_service_async.rs in the rsbinder
repository.
Callbacks and Interfaces
Binder IPC is not limited to one-way requests from client to service. Through callback interfaces, a client can pass a Binder object to a service, and the service can call methods on that object. This enables bidirectional communication across process boundaries without requiring the client to register itself as a separate service.
This chapter covers how to define callback interfaces in AIDL, implement them in Rust, manage collections of callbacks, pass raw IBinder objects, work with nested interface types, and monitor remote service lifecycle with death recipients.
Defining a Callback Interface
A callback interface is a regular AIDL interface. The only difference is in how it is used: instead of being registered with the service manager, it is created by one process and passed to another through a method call.
Here is a minimal callback interface from the rsbinder test suite (INamedCallback.aidl):
package android.aidl.tests;
interface INamedCallback {
String GetName();
}
This interface defines a single method that returns a string. A service can accept objects implementing this interface, store them, and call GetName later -- regardless of whether the callback lives in the same process or a different one.
Implementing a Callback
Implementing a callback follows the same pattern as implementing any Binder service. Define a struct, implement the rsbinder::Interface trait, and implement the generated AIDL trait:
#![allow(unused)] fn main() { struct NamedCallback(String); impl rsbinder::Interface for NamedCallback {} impl INamedCallback::INamedCallback for NamedCallback { fn GetName(&self) -> std::result::Result<String, Status> { Ok(self.0.clone()) } } }
This implementation is identical in structure to a top-level service -- the only difference is that this object will be passed to another service rather than registered in the service manager.
Service-Side Callback Management
A service that works with callbacks typically needs to create, store, and invoke them. The following pattern uses a HashMap to cache callbacks by name:
#![allow(unused)] fn main() { #[derive(Default)] struct TestService { service_map: Mutex<HashMap<String, rsbinder::Strong<dyn INamedCallback::INamedCallback>>>, } impl Interface for TestService {} impl ITestService::ITestService for TestService { fn GetOtherTestService( &self, name: &str, ) -> std::result::Result<rsbinder::Strong<dyn INamedCallback::INamedCallback>, rsbinder::Status> { 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_binder(named_callback) }); Ok(other_service.to_owned()) } // ... } }
Key points:
BnNamedCallback::new_binder()wraps the struct in a Binder node so it can cross process boundaries. TheBnprefix stands for "Binder native" (server-side stub).Strong<dyn INamedCallback::INamedCallback>is a strong reference to a Binder object, equivalent to Android'ssp<INamedCallback>.Mutex<HashMap<...>>protects the map because Binder calls can arrive on different threads.
Accepting and Invoking Callbacks
A service can also accept callbacks from the client and invoke methods on them. The VerifyName method below receives a callback and calls its GetName method:
#![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) } }
When the client and service are in different processes, calling service.GetName() triggers a Binder transaction back to the client process. This is completely transparent to the service code -- the proxy handles all the marshalling.
Client-Side Usage
From the client side, working with callbacks is straightforward. You request a callback from the service, call methods on it, and pass it back to the service for verification:
#![allow(unused)] fn main() { let service = get_test_service(); // Request a callback from the service let got = service .GetOtherTestService("Smythe") .expect("error calling GetOtherTestService"); // Call a method on the callback assert_eq!(got.GetName().as_ref().map(String::as_ref), Ok("Smythe")); // Pass the callback back to the service for verification assert_eq!(service.VerifyName(&got, "Smythe"), Ok(true)); }
Even though the NamedCallback object was created inside the service process, the client can call GetName() on it through Binder IPC. The generated proxy (BpNamedCallback) handles serialization and deserialization automatically.
Callback Arrays
Services can return and accept arrays of callback interfaces. The GetInterfaceArray method creates a callback for each name in the input and returns them as a Vec. On the client side:
#![allow(unused)] fn main() { let names = vec!["Fizz".into(), "Buzz".into()]; let service = get_test_service(); let got = service .GetInterfaceArray(&names) .expect("error calling GetInterfaceArray"); // Each callback has the correct name assert_eq!( got.iter() .map(|s| s.GetName()) .collect::<std::result::Result<Vec<_>, _>>(), Ok(names.clone()) ); // Verify all names in a single call assert_eq!( service.VerifyNamesWithInterfaceArray(&got, &names), Ok(true) ); }
Nullable Arrays
Callback arrays can also be nullable, where both the array itself and individual elements may be absent:
#![allow(unused)] fn main() { let names = vec![Some("Fizz".into()), None, Some("Buzz".into())]; let got = service .GetNullableInterfaceArray(Some(&names)) .expect("error calling GetNullableInterfaceArray"); }
In this case, the service returns Option<Vec<Option<Strong<dyn INamedCallback::INamedCallback>>>> -- an optional array where each element is itself optional. The None entries in the input produce None entries in the output.
Passing Raw IBinder Objects
You can also pass raw IBinder objects through Binder transactions without committing to a specific interface type. The AIDL definitions use the IBinder type directly:
void TakesAnIBinder(in IBinder input);
void TakesANullableIBinder(in @nullable IBinder input);
void TakesAnIBinderList(in List<IBinder> input);
void TakesANullableIBinderList(in @nullable List<IBinder> input);
In Rust, IBinder maps to SIBinder (a strong Binder reference). You can obtain an SIBinder from any typed interface using the as_binder() method:
#![allow(unused)] fn main() { let service = get_test_service(); // Pass the service's own binder reference let result = service.TakesAnIBinder(&service.as_binder()); assert!(result.is_ok()); // Pass a list of binder references let result = service.TakesAnIBinderList(&[service.as_binder()]); assert!(result.is_ok()); // Nullable binder -- pass None let result = service.TakesANullableIBinder(None); assert!(result.is_ok()); // Nullable list with mixed Some/None entries let result = service.TakesANullableIBinderList( Some(&[Some(service.as_binder()), None]) ); assert!(result.is_ok()); }
Nested Interfaces
AIDL allows you to define interfaces, parcelables, and enums nested inside another interface. This is useful when a callback type is logically scoped to a single service. Here is the INestedService definition from the test suite:
interface INestedService {
@RustDerive(PartialEq=true)
parcelable Result {
ParcelableWithNested.Status status = ParcelableWithNested.Status.OK;
}
Result flipStatus(in ParcelableWithNested p);
interface ICallback {
void done(ParcelableWithNested.Status status);
}
void flipStatusWithCallback(ParcelableWithNested.Status status, ICallback cb);
}
Implementing a Nested Callback
In the generated Rust code, nested types are accessed through the parent module's namespace:
#![allow(unused)] fn main() { #[derive(Debug, Default)] struct Callback { received: Arc<Mutex<Option<ParcelableWithNested::Status::Status>>>, } impl Interface for Callback {} impl INestedService::ICallback::ICallback for Callback { fn done( &self, st: ParcelableWithNested::Status::Status, ) -> std::result::Result<(), Status> { *self.received.lock().unwrap() = Some(st); Ok(()) } } }
Using a Nested Callback
To create and pass a nested callback to the service:
#![allow(unused)] fn main() { let service: rsbinder::Strong<dyn INestedService::INestedService> = hub::get_interface( <INestedService::BpNestedService as INestedService::INestedService>::descriptor(), ) .expect("did not get binder service"); let received = Arc::new(Mutex::new(None)); // Create the callback binder let cb = INestedService::ICallback::BnCallback::new_binder(Callback { received: Arc::clone(&received), }); // Pass NOT_OK to the service; it should flip it to OK via the callback let ret = service.flipStatusWithCallback( ParcelableWithNested::Status::Status::NOT_OK, &cb, ); assert_eq!(ret, Ok(())); // Verify the callback was invoked with the flipped status let received = received.lock().unwrap(); assert_eq!(*received, Some(ParcelableWithNested::Status::Status::OK)); }
The key detail is the fully-qualified path for the nested callback's Binder node: INestedService::ICallback::BnCallback. This follows the Rust module hierarchy generated from the AIDL nesting structure.
Service-Side Nested Callback Handling
On the service side, the nested callback is received as a typed strong reference and can be invoked directly:
#![allow(unused)] fn main() { impl INestedService::INestedService for NestedService { fn flipStatusWithCallback( &self, st: ParcelableWithNested::Status::Status, cb: &rsbinder::Strong<dyn INestedService::ICallback::ICallback>, ) -> std::result::Result<(), Status> { if st == ParcelableWithNested::Status::Status::OK { cb.done(ParcelableWithNested::Status::Status::NOT_OK) } else { cb.done(ParcelableWithNested::Status::Status::OK) } } } }
The service flips the status and calls done on the callback. If the callback lives in a different process, this triggers a Binder transaction back to the caller.
Death Recipients
When a client holds a reference to a remote Binder object, it may need to know if the remote process dies. In rsbinder, you implement the DeathRecipient trait and register it with a Binder reference.
Implementing a Death Recipient
#![allow(unused)] fn main() { use rsbinder::*; use std::sync::{Arc, Mutex}; use std::fs::File; use std::io::Write; struct MyDeathRecipient { write_file: Mutex<File>, } impl DeathRecipient for MyDeathRecipient { fn binder_died(&self, _who: &WIBinder) { let mut writer = self.write_file.lock().unwrap(); writer.write_all(b"binder_died\n").unwrap(); } } }
The binder_died method is called when the remote process hosting the Binder object terminates. The _who parameter is a WIBinder (weak Binder reference) identifying which Binder object died.
Registering and Unregistering
Death recipients are registered using link_to_death and unregistered using unlink_to_death. Both methods take a Weak<dyn DeathRecipient> reference:
#![allow(unused)] fn main() { let recipient = Arc::new(MyDeathRecipient { write_file: Mutex::new(write_file), }); // Register for death notification service .as_binder() .link_to_death(Arc::downgrade( &(recipient.clone() as Arc<dyn DeathRecipient>), )) .unwrap(); // Unregister when no longer needed service .as_binder() .unlink_to_death(Arc::downgrade( &(recipient.clone() as Arc<dyn DeathRecipient>), )) .unwrap(); }
The cast recipient.clone() as Arc<dyn DeathRecipient> is necessary to convert from the concrete type to the trait object before calling Arc::downgrade. The weak reference ensures that the death recipient does not keep the Binder object alive -- if all strong references are dropped, the Binder object can be cleaned up normally.
Note that death notifications only work for remote Binder objects. Calling link_to_death on a local Binder object (one in the same process) will return an error because there is no remote process to monitor.
Tips
Here are key points to keep in mind when working with callbacks and interfaces in rsbinder:
-
Callbacks are full Binder objects. They cross process boundaries transparently. A callback created in the client process can be invoked by the service process through a standard Binder transaction.
-
Use
BnXxx::new_binder()to create callback objects. TheBn(Binder native) wrapper converts your Rust struct into a Binder node that can be sent through Binder transactions. The correspondingBp(Binder proxy) is used automatically on the receiving side. -
Use
Mutexto protect shared state. Binder method calls can arrive on any thread in the thread pool. Any mutable state in your callback or service struct must be protected byMutex,RwLock, or another synchronization primitive. -
Nested types use fully-qualified Rust paths. A callback
ICallbacknested insideINestedServiceis accessed asINestedService::ICallback::ICallbackfor the trait andINestedService::ICallback::BnCallbackfor the Binder node constructor. -
Death recipients use
Weakreferences. Thelink_to_deathAPI takesWeak<dyn DeathRecipient>to avoid preventing cleanup of the death recipient itself. Keep a strongArcreference alive for as long as you want to receive notifications. -
as_binder()converts typed interfaces to rawSIBinder. This is useful when you need to pass a Binder reference to a method that acceptsIBinder, or when you need to call Binder-level methods likelink_to_deathorping_binder. -
Callback equality works through Binder identity. Two
Strong<dyn T>references are equal if they point to the same Binder object. This allows you to compare callbacks received from different sources to determine if they refer to the same underlying implementation.
ParcelFileDescriptor
Binder IPC typically transfers structured data -- integers, strings, parcelables -- but
sometimes you need to pass a file descriptor from one process to another. The
ParcelFileDescriptor type makes this possible by wrapping an OwnedFd so that it
can be serialized into a Binder Parcel, sent across process boundaries, and
deserialized on the other side.
Common use cases include sending pipe endpoints to a service so it can stream data
back, sharing access to an open file or socket, and implementing dump() for
diagnostic output.
Creating a ParcelFileDescriptor
ParcelFileDescriptor::new() accepts any type that implements Into<OwnedFd>,
including std::fs::File, OwnedFd, and the file descriptors returned by
rustix::pipe::pipe().
#![allow(unused)] fn main() { use std::fs::File; // From an existing File let file = File::open("/dev/null").unwrap(); let pfd = rsbinder::ParcelFileDescriptor::new(file); // From a pipe created with rustix let (reader, writer) = rustix::pipe::pipe().unwrap(); let read_pfd = rsbinder::ParcelFileDescriptor::new(reader); let write_pfd = rsbinder::ParcelFileDescriptor::new(writer); }
Sending a File Descriptor to a Service
A typical pattern is to create a pipe, wrap one end in a ParcelFileDescriptor,
send it to a service method, and then read from or write to the other end locally.
The following example is based on the test_parcel_file_descriptor test in the
rsbinder test suite:
#![allow(unused)] fn main() { use std::io::{Read, Write}; let (mut read_file, write_file) = build_pipe(); let write_pfd = rsbinder::ParcelFileDescriptor::new(write_file); // Send the write end to the service; it returns a (duplicated) copy let result_pfd = service.RepeatParcelFileDescriptor(&write_pfd)?; // Write through the returned file descriptor file_from_pfd(&result_pfd).write_all(b"Hello")?; // Read from the original pipe's read end let mut buf = [0u8; 5]; read_file.read_exact(&mut buf)?; assert_eq!(&buf, b"Hello"); }
Because the service duplicates the descriptor before returning it (see the next section), both the caller and the service hold independent handles to the same underlying pipe.
Duplicating File Descriptors in a Service
When a service receives a ParcelFileDescriptor, it usually needs to duplicate
the descriptor before returning it or storing it. This avoids ownership conflicts
and ensures each side can close its handle independently.
The idiomatic helper function looks like this:
#![allow(unused)] fn main() { use rsbinder::ParcelFileDescriptor; fn dup_fd(fd: &ParcelFileDescriptor) -> ParcelFileDescriptor { ParcelFileDescriptor::new(fd.as_ref().try_clone().unwrap()) } }
as_ref() returns a reference to the inner OwnedFd, and try_clone() calls
the underlying OS dup system call.
A service method that repeats a file descriptor back to the caller is then straightforward:
#![allow(unused)] fn main() { fn RepeatParcelFileDescriptor( &self, read: &ParcelFileDescriptor, ) -> rsbinder::status::Result<ParcelFileDescriptor> { Ok(dup_fd(read)) } }
Working with File Descriptor Arrays
AIDL interfaces can accept and return arrays of ParcelFileDescriptor. The
pattern for reversing an array -- a common test case -- illustrates how to
combine dup_fd with iterator combinators:
#![allow(unused)] fn main() { fn ReverseParcelFileDescriptorArray( &self, input: &[ParcelFileDescriptor], repeated: &mut Vec<Option<ParcelFileDescriptor>>, ) -> rsbinder::status::Result<Vec<ParcelFileDescriptor>> { repeated.clear(); repeated.extend(input.iter().map(dup_fd).map(Some)); Ok(input.iter().rev().map(dup_fd).collect()) } }
The repeated output parameter receives a copy of the input in the original
order, while the return value contains the input in reverse order. Every
descriptor is duplicated so that each Vec owns its own set of file handles.
Helper Functions
The test suite defines two small helpers that are useful in application code as well.
build_pipe
Creates a Unix pipe and returns both ends as std::fs::File values:
#![allow(unused)] fn main() { use std::fs::File; use std::os::unix::io::FromRawFd; use rustix::fd::IntoRawFd; fn build_pipe() -> (File, File) { let fds = rustix::pipe::pipe().expect("error creating pipe"); unsafe { ( File::from_raw_fd(fds.0.into_raw_fd()), File::from_raw_fd(fds.1.into_raw_fd()), ) } } }
file_from_pfd
Converts a ParcelFileDescriptor reference into a File suitable for use
with the standard Read and Write traits. The descriptor is cloned first
so the original ParcelFileDescriptor remains valid:
#![allow(unused)] fn main() { use std::fs::File; use rsbinder::ParcelFileDescriptor; fn file_from_pfd(fd: &ParcelFileDescriptor) -> File { fd.as_ref() .try_clone() .expect("failed to clone file descriptor") .into() } }
Tips and Best Practices
-
Descriptors are duplicated during IPC. When a
ParcelFileDescriptoris serialized into aParcel, the kernel duplicates the file descriptor for the receiving process. The sender and receiver each hold independent handles. -
Close order does not matter. Because each side owns an independent duplicate, closing the sender's copy does not affect the receiver, and vice versa.
-
Use
file_from_pfdfor reading and writing.ParcelFileDescriptordoes not implementstd::io::Readorstd::io::Writedirectly. Convert it to aFile(viatry_clone().into()) to use those traits. -
Always duplicate before storing. If your service needs to keep a reference to a received descriptor, clone it with
dup_fd. Returning or forwarding the original reference without duplication can lead to use-after-close errors. -
ParcelFileDescriptoris notClone. Because it wraps anOwnedFd, which owns the underlying file descriptor, the type cannot deriveClone. Usedup_fd(oras_ref().try_clone()) for explicit duplication. -
Error handling.
try_clone()can fail if the process has exhausted its file descriptor limit. In production code, consider propagating the error rather than callingunwrap().
Error Handling
Binder IPC introduces failure modes that do not exist in ordinary function calls:
the remote process may crash, the kernel driver may reject a transaction, or the
service may intentionally signal an application-level error. rsbinder represents
all of these cases through two complementary types -- StatusCode for
transport-level errors and Status for richer application-level errors that
include exception codes and optional messages.
Core Types
rsbinder::status::Result<T>
Every AIDL-generated method returns this type:
#![allow(unused)] fn main() { pub type Result<T> = std::result::Result<T, Status>; }
This is a standard Result whose error variant is a Status.
StatusCode
StatusCode represents low-level transport errors that occur before or during
a Binder transaction. These are defined in rsbinder::StatusCode (re-exported
from rsbinder::error::StatusCode).
Commonly encountered values:
| Variant | Meaning |
|---|---|
Ok | Operation completed successfully |
Unknown | An unspecified error occurred |
BadValue | Invalid parameter value |
UnknownTransaction | The transaction code is not recognized |
PermissionDenied | Caller does not have permission |
DeadObject | The remote process has died |
FailedTransaction | The transaction could not be completed |
NoMemory | Out of memory |
BadType | Wrong data type encountered |
NotEnoughData | The parcel did not contain enough data |
A StatusCode can be converted directly into a Status:
#![allow(unused)] fn main() { let status: rsbinder::Status = rsbinder::StatusCode::PermissionDenied.into(); }
Status
Status combines three pieces of information:
- Exception code (
ExceptionCode) -- categorizes the error (e.g.ServiceSpecific,Security,NullPointer). - Status code (
StatusCode) -- provides transport-level detail. - Message (
Option<String>) -- an optional human-readable description.
Key methods on Status:
#![allow(unused)] fn main() { // Check the category of the error status.exception_code() // -> ExceptionCode // Get the transport error (only meaningful when exception is TransactionFailed) status.transaction_error() // -> StatusCode // Get the service-specific error code (only meaningful when exception is ServiceSpecific) status.service_specific_error() // -> i32 // Check if the status represents success status.is_ok() // -> bool }
ExceptionCode
ExceptionCode classifies the kind of error:
| Variant | Meaning |
|---|---|
None | No error |
Security | Security / permission violation |
BadParcelable | Malformed parcelable data |
IllegalArgument | Invalid argument provided |
NullPointer | Unexpected null value |
IllegalState | Operation invalid for current state |
UnsupportedOperation | Requested operation is not supported |
ServiceSpecific | Application-defined error with custom code |
TransactionFailed | Low-level transaction failure |
Returning Errors from a Service
Service-Specific Errors
The most common way for a service to report an application-level error is
through Status::new_service_specific_error. This sets the exception code to
ServiceSpecific and carries an integer error code whose meaning is defined
by the service:
#![allow(unused)] fn main() { fn ThrowServiceException( &self, code: i32, ) -> rsbinder::status::Result<()> { Err(rsbinder::Status::new_service_specific_error(code, None)) } }
You can also attach an optional message:
#![allow(unused)] fn main() { Err(rsbinder::Status::new_service_specific_error( -1, Some("resource not found".into()), )) }
Unimplemented Methods
When a service does not support a particular transaction (for example, a method
added in a newer version of the AIDL interface), return UnknownTransaction:
#![allow(unused)] fn main() { fn UnimplementedMethod( &self, _arg: i32, ) -> rsbinder::status::Result<i32> { // Indicate that this method is not implemented Err(rsbinder::StatusCode::UnknownTransaction.into()) } }
The .into() conversion automatically creates a Status with the
TransactionFailed exception code.
Permission Errors
If your service enforces access control, return PermissionDenied:
#![allow(unused)] fn main() { fn restricted_operation(&self) -> rsbinder::status::Result<()> { let caller = rsbinder::thread_state::CallingContext::default(); if caller.uid != ALLOWED_UID { return Err(rsbinder::StatusCode::PermissionDenied.into()); } Ok(()) } }
Handling Errors on the Client Side
Checking for Service-Specific Errors
When calling a service method, always check the Result for errors.
For service-specific errors, inspect the exception_code() first, then
retrieve the integer code:
#![allow(unused)] fn main() { let result = service.ThrowServiceException(-1); assert!(result.is_err()); let status = result.unwrap_err(); assert_eq!( status.exception_code(), rsbinder::ExceptionCode::ServiceSpecific, ); assert_eq!(status.service_specific_error(), -1); }
Distinguishing Error Categories
A practical error-handling pattern checks the exception code to determine how to react:
#![allow(unused)] fn main() { match service.some_method() { Ok(value) => { // Success } Err(status) => { match status.exception_code() { rsbinder::ExceptionCode::ServiceSpecific => { let code = status.service_specific_error(); eprintln!("Service error {code}: {status}"); } rsbinder::ExceptionCode::TransactionFailed => { let transport_err = status.transaction_error(); eprintln!("Transport error: {transport_err:?}"); } rsbinder::ExceptionCode::Security => { eprintln!("Permission denied: {status}"); } other => { eprintln!("Unexpected error ({other}): {status}"); } } } } }
Detecting a Dead Service
If the remote process crashes, methods will fail with DeadObject:
#![allow(unused)] fn main() { let result = service.some_method(); if let Err(ref status) = result { if status.transaction_error() == rsbinder::StatusCode::DeadObject { eprintln!("Service has died, attempting reconnection..."); } } }
Converting Between Error Types
rsbinder provides From implementations that make conversions between
StatusCode, ExceptionCode, and Status straightforward:
#![allow(unused)] fn main() { // StatusCode -> Status let status: rsbinder::Status = rsbinder::StatusCode::BadValue.into(); // ExceptionCode -> Status let status: rsbinder::Status = rsbinder::ExceptionCode::IllegalArgument.into(); // Status -> StatusCode (extracts the transport error code) let code: rsbinder::StatusCode = status.into(); }
These conversions are used most often in the Err(...) return position of
service methods, where .into() converts a StatusCode into the expected
Status type automatically.
Tips and Best Practices
-
Prefer service-specific errors for application logic. Use
Status::new_service_specific_errorwhen the error is meaningful to the caller (e.g., "item not found", "quota exceeded"). Define your error codes as constants so both the service and client can reference them. -
Use
StatusCodefor infrastructure problems. Return rawStatusCodevalues likePermissionDeniedorBadValuefor errors that relate to the IPC mechanism rather than your application's business logic. -
Always check
exception_code()first. The meaning ofservice_specific_error()andtransaction_error()depends on the exception code. Calling them without checking the exception may return default (zero) values. -
Handle
DeadObjectgracefully. In long-running clients, the remote service may restart. Consider using death notifications (link_to_death) to detect service restarts and re-establish connections. -
StatusimplementsDisplayandstd::error::Error. You can use it with?in functions that returnBox<dyn std::error::Error>or with logging macros for human-readable diagnostics.
Service Manager (HUB)
The service manager is the central registry for Binder services. Every service
that wants to be discoverable by other processes registers itself with the
service manager under a well-known name, and every client that needs a service
looks it up by that name. In rsbinder, the service manager is referred to as
HUB and is accessed through the rsbinder::hub module.
On Linux, the HUB is provided by the rsb_hub binary that ships with the
rsbinder-tools crate. On Android, the system's native servicemanager
fulfills this role, and rsbinder talks to it using the same Binder protocol.
Running the Service Manager (Linux)
Before any service can register or any client can perform lookups, the HUB process must be running:
# Build and run the service manager
$ cargo run --bin rsb_hub
rsb_hub opens the Binder device, becomes the context manager (handle 0),
and enters a loop that processes registration and lookup requests. It must
remain running for the lifetime of the system's Binder services.
Registering a Service
To make a service available to other processes, create a Binder object and
register it with hub::add_service:
#![allow(unused)] fn main() { use rsbinder::*; // Create the Binder service object let service = BnMyService::new_binder(MyServiceImpl); // Register it under a descriptive name hub::add_service("com.example.myservice", service.as_binder())?; }
The name passed to add_service is the identifier that clients will use to
find the service.
Registration Rules
The service manager enforces several constraints on service names:
- Maximum length: 127 characters. Names of 128 characters or longer are rejected.
- Allowed characters: Alphanumeric characters, dots (
.), underscores (_), and hyphens (-). Special characters such as$are not allowed. - Non-empty: An empty string is rejected.
- Overwrite permitted: Registering a service with a name that is already in use replaces the previous registration.
#![allow(unused)] fn main() { let service = BnFoo::new_binder(FooImpl {}); // Empty names are rejected assert!(hub::add_service("", service.as_binder()).is_err()); // Valid name assert!(hub::add_service("foo", service.as_binder()).is_ok()); // Maximum length (127 characters) let long_name = "a".repeat(127); assert!(hub::add_service(&long_name, service.as_binder()).is_ok()); // Too long (128 characters) let too_long = "a".repeat(128); assert!(hub::add_service(&too_long, service.as_binder()).is_err()); // Special characters are rejected assert!(hub::add_service("happy$foo$fo", service.as_binder()).is_err()); }
Looking Up Services
rsbinder provides three ways to find a registered service, each suited to a different use case.
Type-Safe Lookup with get_interface
The most common approach is hub::get_interface, which retrieves the service
and casts it to the expected AIDL interface type in one step:
#![allow(unused)] fn main() { let service: rsbinder::Strong<dyn IMyService::IMyService> = hub::get_interface("com.example.myservice")?; // Now call methods directly on the typed proxy let result = service.some_method()?; }
If the service is not registered, get_interface returns an error.
Raw Binder Lookup with get_service
If you need the untyped SIBinder handle (for example, to inspect the
descriptor or pass it to another API), use hub::get_service:
#![allow(unused)] fn main() { let binder: Option<SIBinder> = hub::get_service("com.example.myservice"); if let Some(binder) = binder { println!("Found service with descriptor: {}", binder.descriptor()); } }
Returns None if the service is not registered.
Non-Blocking Check with check_service
hub::check_service behaves like get_service but is intended as a
non-blocking availability check:
#![allow(unused)] fn main() { let binder: Option<SIBinder> = hub::check_service("com.example.myservice"); if binder.is_some() { println!("Service is available"); } else { println!("Service is not yet registered"); } }
Listing Registered Services
To enumerate all services currently registered with the HUB, use
hub::list_services with a dump priority flag:
#![allow(unused)] fn main() { let services = hub::list_services(hub::DUMP_FLAG_PRIORITY_DEFAULT); for name in &services { println!("Available: {}", name); } }
The dump priority flag filters which services are returned. The most commonly used flags are:
| Flag | Description |
|---|---|
DUMP_FLAG_PRIORITY_DEFAULT | Services with default priority |
DUMP_FLAG_PRIORITY_HIGH | High-priority services |
DUMP_FLAG_PRIORITY_CRITICAL | Critical system services |
DUMP_FLAG_PRIORITY_NORMAL | Normal-priority services |
DUMP_FLAG_PRIORITY_ALL | All services regardless of priority |
Service Notifications
You can register a callback that fires whenever a service with a particular name is registered (or re-registered). This is useful for clients that start before the service they depend on is available.
Defining a Callback
Implement the hub::IServiceCallback trait:
#![allow(unused)] fn main() { struct MyServiceCallback; impl rsbinder::Interface for MyServiceCallback {} impl hub::IServiceCallback for MyServiceCallback { fn onRegistration( &self, name: &str, service: &rsbinder::SIBinder, ) -> rsbinder::status::Result<()> { println!("Service registered: {name}"); Ok(()) } } }
Registering and Unregistering
Wrap the callback in a Binder object and pass it to the HUB:
#![allow(unused)] fn main() { let callback = hub::BnServiceCallback::new_binder(MyServiceCallback); // Start receiving notifications hub::register_for_notifications("com.example.myservice", &callback)?; // Later, when notifications are no longer needed hub::unregister_for_notifications("com.example.myservice", &callback)?; }
The callback will be invoked each time a service matching the given name is registered, including if it is re-registered after a restart.
Checking if a Service is Declared
hub::is_declared checks whether a service name has been declared in the
system's service configuration (VINTF manifest on Android). This is distinct
from whether the service is currently running:
#![allow(unused)] fn main() { let declared = hub::is_declared("com.example.myservice"); if declared { println!("Service is declared in the manifest"); } else { println!("Service is not declared"); } }
On Linux with rsb_hub, this typically returns false because there is no
VINTF manifest. On Android, it reflects the device's hardware interface
declarations.
Debug Information
For diagnostics, hub::get_service_debug_info returns metadata about every
registered service, including its name and the PID of the process hosting it:
#![allow(unused)] fn main() { let debug_info = hub::get_service_debug_info()?; for info in &debug_info { println!("Service: {} (pid: {})", info.name, info.debugPid); } }
The returned ServiceDebugInfo struct has two fields:
name(String) -- the registered service name.debugPid(i32) -- the PID of the process that registered the service.
This feature is available on Android 12 and above. On Android 11, calling
get_service_debug_info returns an error.
Linux vs. Android Differences
While rsbinder aims for API compatibility across both platforms, there are
important behavioral differences between the Linux HUB (rsb_hub) and
Android's native servicemanager:
| Aspect | Linux (rsb_hub) | Android (servicemanager) |
|---|---|---|
| Process | User-space rsb_hub binary | System servicemanager daemon |
| Access control | No SELinux enforcement | Full SELinux MAC policy enforcement |
| VINTF manifests | Not supported (is_declared is false) | Supported and enforced |
| Service debug info | Supported | Supported (Android 12+) |
| Binder device | Must be created with rsb_device | Managed by Android init |
| Version selection | Always uses Android 16 protocol | Auto-detected from SDK version |
| Death notifications | Supported | Supported |
On Android, rsbinder automatically detects the SDK version and uses the
appropriate service manager protocol (Android 11 through 16). On Linux, it
always uses the Android 16 protocol, which is what rsb_hub implements.
Using the ServiceManager Object Directly
The convenience functions (hub::add_service, hub::get_service, etc.) use
a global singleton ServiceManager under the hood. If you need more control,
you can obtain the ServiceManager instance directly:
#![allow(unused)] fn main() { use rsbinder::hub; let sm = hub::default(); // Use methods on the ServiceManager instance let service = sm.get_service("com.example.myservice"); let services = sm.list_services(hub::DUMP_FLAG_PRIORITY_ALL); }
This is equivalent to using the free functions but allows you to pass the service manager as a parameter or store it in a struct.
Tips and Best Practices
-
Initialize ProcessState first. Before calling any
hub::function, you must callProcessState::init_default()(orProcessState::init()with a custom binder path). Failing to do so will panic at runtime. -
Use descriptive service names. Follow a reverse-domain naming convention (e.g.,
com.example.myservice) to avoid name collisions with other services. -
Register for notifications instead of polling. If your client starts before the service it depends on, use
register_for_notificationsrather than repeatedly callingget_servicein a loop. -
Handle registration failures.
add_servicecan fail if the name is invalid or if the caller lacks permission (on Android with SELinux). Always check the result. -
Use
get_interfacefor type safety. Preferhub::get_interfaceoverhub::get_servicewhen you know the expected interface type. It returns a strongly-typed proxy that provides compile-time guarantees. -
Debug with
list_servicesandget_service_debug_info. When troubleshooting, list all registered services and inspect their debug information to verify that services are registered from the expected processes.
Enable binder for Linux
Most Linux distributions do not have Binder IPC enabled by default, so additional steps are required to use it.
Note: Binder IPC requires Linux kernel 4.17 or later for native binderfs support.
If you are able to build the Linux kernel yourself, you can enable Binder IPC by adding the following kernel configuration options:
CONFIG_ANDROID=y
CONFIG_ANDROID_BINDER_IPC=y
CONFIG_ANDROID_BINDERFS=y
Distribution-Specific Guides
Select your Linux distribution for detailed setup instructions:
- Arch Linux - Uses the
linux-zenkernel (simplest method) - Ubuntu Linux - Custom kernel build or module installation
- RedHat Linux - RHEL, CentOS, and Fedora instructions
Other documents related to Binder IPC
Enable Binder IPC on Arch Linux
Arch Linux provides an easy way to enable Binder IPC support through the linux-zen kernel, which already includes all necessary Binder components.
Install linux-zen Kernel
The linux-zen kernel is the recommended and simplest method to get Binder IPC support on Arch Linux:
# Update system packages
$ sudo pacman -Syu
# Install linux-zen kernel and headers
$ sudo pacman -S linux-zen linux-zen-headers
# Update bootloader configuration
$ sudo grub-mkconfig -o /boot/grub/grub.cfg
# Reboot to use the new kernel
$ sudo reboot
After reboot, select the zen kernel from the GRUB menu or set it as default.
Verification
After installing and booting into the zen kernel, verify Binder support is available:
# Check current kernel
$ uname -r
# Should show something like "6.x.x-zen1-1-zen"
Install rsbinder-tools
Install the rsbinder development tools:
# Install Rust (if not already installed)
$ sudo pacman -S rustup
$ rustup default stable
# Install rsbinder-tools from crates.io
$ cargo install rsbinder-tools
Create and Test Binder Device
Create a binder device and test the setup:
# Create binder device
$ sudo rsb_device binder
# Verify device creation
$ ls -la /dev/binderfs/binder
# Test with a simple example
$ git clone https://github.com/hiking90/rsbinder.git
$ cd rsbinder
# Start service manager in one terminal
$ rsb_hub
# In another terminal, run the example
$ cargo run --bin hello_service &
$ cargo run --bin hello_client
Persistent Configuration
To automatically load binder modules on boot:
# Create module loading configuration
$ echo "binder_linux" | sudo tee /etc/modules-load.d/binder.conf
# Set module parameters
$ echo "options binder_linux devices=binder,hwbinder,vndbinder" | sudo tee /etc/modprobe.d/binder.conf
Troubleshooting
If you encounter issues:
# Check kernel messages for binder-related errors
$ dmesg | grep -i binder
# Verify zen kernel is running
$ uname -r | grep zen
# Check if modules loaded successfully
$ sudo modprobe -v binder_linux
References
Enable Binder IPC on Ubuntu Linux
Note: This guide is community-contributed and may require adjustments for your specific system configuration. Please test in a safe environment first.
Ubuntu Linux does not enable Binder IPC by default. Here are methods to enable it:
Method 1: Check Existing Kernel Support
Some Ubuntu kernels already include binder support. Check your current kernel first:
# Check if binder is available in your kernel
$ grep -E "(ANDROID|BINDER)" /boot/config-$(uname -r)
If you see CONFIG_ANDROID_BINDER_IPC=y or =m, binder support is already available. Skip to the Verification section.
Method 2: Build Custom Kernel
If your kernel does not include binder support, you can build a custom kernel:
# Install build dependencies
$ sudo apt install build-essential libncurses-dev bison flex libssl-dev libelf-dev
# Download kernel source (replace version as needed)
$ wget https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-6.12.tar.xz
$ tar -xf linux-6.12.tar.xz
$ cd linux-6.12
# Use current kernel config as base
$ cp /boot/config-$(uname -r) .config
# Configure kernel with binder support
$ make menuconfig
# Navigate to: General setup -> Enable Android support
# Enable:
# CONFIG_ANDROID=y
# CONFIG_ANDROID_BINDER_IPC=y
# CONFIG_ANDROID_BINDERFS=y
# Build and install
$ make -j$(nproc)
$ sudo make modules_install
$ sudo make install
$ sudo update-grub
$ sudo reboot
Verification
After enabling binder support, verify it's working:
# Check if binderfs is supported
$ grep binderfs /proc/filesystems
# Test creating a binder device (requires rsbinder-tools)
$ cargo install rsbinder-tools
$ sudo rsb_device binder
Troubleshooting
Common Issues:
- Module not found: Ensure your kernel was built with binder support enabled
- Permission denied: Make sure you're using sudo for device creation
- Kernel too old: Binder support requires Linux kernel 4.17+ natively
Getting Help:
- Check dmesg for kernel messages:
dmesg | grep -i binder - Verify module loading:
sudo modprobe -v binder_linux - Check system logs:
journalctl -fwhile loading modules
References
Enable Binder IPC on RedHat Linux (RHEL/CentOS/Fedora)
Note: This guide is community-contributed and may require adjustments for your specific system configuration. Please test in a safe environment first.
RedHat-based distributions (RHEL, CentOS, Fedora) do not include Binder IPC support by default. Here are methods to enable it:
Fedora
Method 1: Custom Kernel Build (Recommended for Fedora)
Fedora provides kernel source packages that can be modified to include binder support:
# Install development tools
$ sudo dnf groupinstall "Development Tools"
$ sudo dnf install fedora-packager fedpkg
$ sudo dnf install kernel-devel kernel-headers
# Install kernel build dependencies
$ sudo dnf builddep kernel
# Get kernel source
$ fedpkg clone -a kernel
$ cd kernel
$ fedpkg switch-branch f$(rpm -E %fedora)
$ fedpkg prep
# Modify kernel config to enable binder
$ cd ~/rpmbuild/BUILD/kernel-*/linux-*
$ make menuconfig
# Enable the following options:
# General setup -> Android support (CONFIG_ANDROID=y)
# CONFIG_ANDROID_BINDER_IPC=y
# CONFIG_ANDROID_BINDERFS=y
# Build the kernel
$ make -j$(nproc)
$ sudo make modules_install
$ sudo make install
# Update bootloader
$ sudo grub2-mkconfig -o /boot/grub2/grub.cfg
$ sudo reboot
Method 2: Third-party Kernel Modules
Some third-party repositories may provide binder modules:
# Enable RPM Fusion repositories
$ sudo dnf install https://mirrors.rpmfusion.org/free/fedora/rpmfusion-free-release-$(rpm -E %fedora).noarch.rpm
# Look for available binder-related packages
$ dnf search binder android-tools
RHEL/CentOS
Method 1: Build Custom Kernel
For enterprise distributions, building a custom kernel is often the most reliable approach:
# Install EPEL repository (CentOS/RHEL 8+)
$ sudo dnf install epel-release
# Install development tools
$ sudo dnf groupinstall "Development Tools"
$ sudo dnf install rpm-build rpm-devel libtool
# Install kernel build dependencies
$ sudo dnf install kernel-devel kernel-headers
$ sudo dnf install elfutils-libelf-devel openssl-devel
# Download kernel source matching your running kernel
$ KERNEL_VERSION=$(uname -r | sed 's/\.el.*$//')
$ wget https://cdn.kernel.org/pub/linux/kernel/v5.x/linux-${KERNEL_VERSION}.tar.xz
$ tar -xf linux-${KERNEL_VERSION}.tar.xz
$ cd linux-${KERNEL_VERSION}
# Use current kernel config as base
$ zcat /proc/config.gz > .config
# or
$ cp /boot/config-$(uname -r) .config
# Modify config to enable binder
$ make menuconfig
# Enable CONFIG_ANDROID=y, CONFIG_ANDROID_BINDER_IPC=y, CONFIG_ANDROID_BINDERFS=y
# Build and install
$ make -j$(nproc)
$ sudo make modules_install
$ sudo make install
# Update GRUB
$ sudo grub2-mkconfig -o /boot/grub2/grub.cfg
$ sudo reboot
Method 2: Using ELRepo (CentOS/RHEL)
ELRepo sometimes provides additional kernel modules:
# Install ELRepo
$ sudo rpm --import https://www.elrepo.org/RPM-GPG-KEY-elrepo.org
$ sudo dnf install https://www.elrepo.org/elrepo-release-8.el8.elrepo.noarch.rpm
# Search for kernel modules
$ dnf --enablerepo=elrepo search kernel-ml
CentOS Stream
CentOS Stream may have more recent kernels that could include binder support:
# Check current kernel version
$ uname -r
# Update to latest kernel
$ sudo dnf update kernel
# Check if binder is already available
$ grep -E "(ANDROID|BINDER)" /boot/config-$(uname -r)
Module Loading (After Kernel Build)
Once you have a kernel with binder support:
# Load binder modules
$ sudo modprobe binder_linux devices="binder,hwbinder,vndbinder"
# Verify modules are loaded
$ lsmod | grep binder
# Create persistent module loading
$ echo "binder_linux" | sudo tee /etc/modules-load.d/binder.conf
# Set module parameters
$ echo "options binder_linux devices=binder,hwbinder,vndbinder" | sudo tee /etc/modprobe.d/binder.conf
SELinux Considerations
RedHat systems use SELinux which may interfere with binder operations:
# Check SELinux status
$ sestatus
# Temporarily disable SELinux for testing
$ sudo setenforce 0
# Create SELinux policy for binder (advanced)
# This requires creating custom SELinux policies for binder devices
Verification
Test that binder is working:
# Check if binderfs is available
$ grep binderfs /proc/filesystems
# Install rsbinder-tools and create binder device
$ cargo install rsbinder-tools
$ sudo rsb_device binder
# Verify device creation
$ ls -la /dev/binderfs/binder
Troubleshooting
Common Issues:
- Module compilation fails: Ensure all kernel-devel packages match your running kernel
- SELinux denials: Check
audit.logfor SELinux denials and create appropriate policies - Kernel version mismatch: Ensure kernel source matches your running kernel version
Debugging:
# Check kernel messages
$ dmesg | grep -i binder
# Check system journal
$ journalctl -f | grep -i binder
# Verify kernel config
$ grep -E "(ANDROID|BINDER)" /boot/config-$(uname -r)
References
Android Development
rsbinder provides comprehensive support for Android development alongside Linux. Since Android already has a complete Binder IPC environment, you can use rsbinder, rsbinder-aidl, and the existing Android service manager directly. There's no need to create binder devices or run a separate service manager like on Linux.
For building in the Android environment, you need to install the Android NDK and set up a Rust build environment that utilizes the NDK.
See: Android Build Environment Setup
Android Version Compatibility
rsbinder supports multiple Android versions with explicit feature flags for compatibility management. The Binder IPC interface has evolved across Android versions, and rsbinder handles these differences transparently.
Supported Android Versions
- Android 11 (API 30):
android_11feature - Android 12 (API 31) / 12L (API 32):
android_12feature - Android 13 (API 33):
android_13feature - Android 14 (API 34):
android_14feature - Android 16 (API 36):
android_16feature
Note: Android 12L (API 32) uses the same Binder protocol as Android 12, so both are covered by the
android_12feature flag. Similarly, Android 15 (API 35) uses the same Binder protocol as Android 14, so it is covered by theandroid_14orandroid_14_plusfeature flag. No separateandroid_12lorandroid_15feature is needed.
Feature Flag Configuration
In your Cargo.toml, specify the Android versions you want to support:
[dependencies]
rsbinder = { version = "0.5", features = ["android_14_plus"] }
Available feature combinations:
android_11_plus: Supports Android 11 through 16android_12_plus: Supports Android 12 through 16android_13_plus: Supports Android 13 through 16android_14_plus: Supports Android 14 through 16android_16_plus: Supports Android 16 only
Protocol Compatibility
rsbinder maintains binary compatibility with Android's Binder protocol:
- Transaction Format: Uses identical
binder_transaction_datastructures - Object Types: Supports all Android Binder object types (BINDER, HANDLE, FD)
- Command Protocols: Implements the same ioctl commands (BC_/BR_ protocol)
- Memory Management: Compatible parcel serialization and shared memory handling
- AIDL Compatibility: Generates code compatible with Android's AIDL interfaces
Version Detection (Optional)
rsbinder uses rsproperties internally to read Android system properties. You can use the same crate for version detection:
#![allow(unused)] fn main() { // Read Android SDK version (returns default value if not available) let sdk_version: u32 = rsproperties::get_or("ro.build.version.sdk", 0); println!("Android SDK version: {}", sdk_version); // Read release version string let version: String = rsproperties::get_or("ro.build.version.release", String::new()); println!("Running on Android {}", version); }
Add rsproperties to your dependencies:
[dependencies]
rsproperties = "0.3"
Using Android's Existing Binder Devices
On Android, the binder device files are already created and managed by the system. Use ProcessState::init() to connect to the appropriate device:
#![allow(unused)] fn main() { // Connect to the default system binder (/dev/binder) ProcessState::init("/dev/binder", 0); }
Android provides several binder devices for different purposes:
| Device | Service Manager | Description |
|---|---|---|
/dev/binder | servicemanager | Framework services (default) |
/dev/hwbinder | hwservicemanager | HAL services (HIDL) |
/dev/vndbinder | vndservicemanager | Vendor services |
Warning:
hwbinderuses a different protocol (libhwbinder) than standard binder (libbinder). rsbinder has not been tested withhwbinder, so compatibility is not guaranteed.
You do not need to run rsb_hub on Android — the system already provides service managers for each binder device.
Android-Specific Considerations
- Service Manager: Uses Android's existing service manager automatically
- Permissions: Respects Android's security model and SELinux policies
- Threading: Integrates with Android's Binder thread pool management
- Memory: Uses Android's shared memory mechanisms (ashmem/memfd)
- Stability: Supports Android's interface stability annotations (@VintfStability)
JNI Integration
rsbinder is designed for pure Rust-based programs. Integrating Rust Binder services with Java through JNI is not recommended and was not considered in the design. Since JNI only provides a C interface, multiple data conversions occur (Java → C → Rust), which is inefficient. Instead, develop independent Binder services in Rust and communicate with them from Java clients through the standard Binder IPC mechanism.
Android Build Environment Setup
This guide will help you set up a complete Android development environment for building and testing rsbinder applications.
Prerequisites
Android SDK Installation
You need to install the Android SDK which includes essential tools like adb (Android Debug Bridge) and other platform tools required for Android development.
Method 1: Android Studio (Recommended)
Download and install Android Studio, which includes the Android SDK:
Method 2: Command Line Tools Only
If you prefer a minimal installation:
- Download Command Line Tools from Android Developer Downloads
- Extract and set up the SDK manager
Required SDK Components
Install the following SDK components using the SDK Manager:
# Install platform-tools (includes adb)
$ sdkmanager "platform-tools"
# Install emulator (for testing)
$ sdkmanager "emulator"
# Install system images for testing (choose your target API levels)
$ sdkmanager "system-images;android-30;google_apis;x86_64"
$ sdkmanager "system-images;android-34;google_apis;x86_64"
Android NDK Installation
The Android NDK (Native Development Kit) is required for building native Rust code for Android.
Method 1: Through Android Studio
- Open Android Studio
- Go to Tools → SDK Manager → SDK Tools
- Check "NDK (Side by side)" and install
Method 2: Command Line Installation
# Install specific NDK version
$ sdkmanager "ndk;26.1.10909125"
# Check for the latest available version:
$ sdkmanager --list | grep ndk
Method 3: Direct Download
Download from the NDK Downloads page and extract manually.
Environment Setup
Set up environment variables in your shell profile (.bashrc, .zshrc, etc.):
# Android SDK
export ANDROID_HOME=$HOME/Android/Sdk # Linux
# export ANDROID_HOME=$HOME/Library/Android/sdk # macOS
# Android NDK (replace with your installed version)
export ANDROID_NDK_ROOT=$ANDROID_HOME/ndk/<your-ndk-version>
export NDK_HOME=$ANDROID_NDK_ROOT
# Add tools to PATH
export PATH=$PATH:$ANDROID_HOME/cmdline-tools/latest/bin
export PATH=$PATH:$ANDROID_HOME/platform-tools
export PATH=$PATH:$ANDROID_HOME/emulator
Rust Toolchain Setup
Install cargo-ndk
$ cargo install cargo-ndk --version "^3.0"
Add Android targets
$ rustup target add aarch64-linux-android
$ rustup target add x86_64-linux-android
$ rustup target add armv7-linux-androideabi
$ rustup target add i686-linux-android
Building rsbinder for Android
Basic Build Commands
# Build for ARM64 (most common for modern Android devices)
$ cargo ndk -t aarch64-linux-android build --release
# Build for x86_64 (emulator)
$ cargo ndk -t x86_64-linux-android build --release
# Build all targets
$ cargo ndk -t aarch64-linux-android -t x86_64-linux-android build --release
Using envsetup.sh Helper Scripts
The rsbinder project provides a comprehensive envsetup.sh script with helpful functions for Android development:
# Source the environment setup
$ source ./envsetup.sh
Available Functions
ndk_prepare: Sets up the Android device for testing
- Roots the device using
adb root - Creates the remote directory on the device (
/data/rsbinderby default) - Prepares the environment for file synchronization
$ ndk_prepare
ndk_build: Builds the project for Android
- Reads configuration from
REMOTE_ANDROIDfile - Builds both debug binaries and test executables
- Uses
cargo ndkwith the specified target architecture
$ ndk_build
ndk_sync: Synchronizes built binaries to Android device
- Pushes all executable files to the device
- Uses
adb pushto transfer files to the remote directory - Automatically detects executable files in the target directory
$ ndk_sync
The envsetup.sh script also provides functions for remote Linux testing (remote_sync, remote_shell, remote_test) and publishing (publish, publish_dry_run). These are intended for project maintainers and advanced development workflows.
Configuration File: REMOTE_ANDROID
Create a REMOTE_ANDROID file in the project root to configure your Android target:
# Example REMOTE_ANDROID file
arm64-v8a # cargo-ndk target
aarch64 # rust target architecture
/data/rsbinder # remote directory on device
Common target configurations:
# For ARM64 devices (most modern Android phones)
arm64-v8a
aarch64
/data/rsbinder
# For x86_64 emulator
x86_64
x86_64
/data/rsbinder
Testing on Android
Device Setup
- Enable Developer Options on your Android device
- Enable USB Debugging
- Connect device via USB and authorize debugging
Running Tests
# Complete build and test workflow
$ source ./envsetup.sh
$ ndk_prepare # Set up device
$ ndk_build # Build binaries and tests
$ ndk_sync # Push to device
# Test executables will be available in /data/rsbinder/
Emulator Testing
# Create and start an emulator
$ avdmanager create avd -n test_device -k "system-images;android-34;google_apis;x86_64"
$ emulator -avd test_device
# Build for emulator target
$ cargo ndk -t x86_64-linux-android build --release
Troubleshooting
Common Issues
"adb not found": Ensure platform-tools is installed and in PATH
"cargo-ndk not found": Install with cargo install cargo-ndk
"No targets specified": Add Android targets with rustup target add
"Permission denied on device": Run adb root or check device permissions
Verification Commands
# Check SDK installation
$ sdkmanager --list_installed
# Check connected devices
$ adb devices
# Check available targets
$ rustup target list | grep android
# Test cargo-ndk installation
$ cargo ndk --version