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

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

  1. You define your service interface in an AIDL file
  2. The rsbinder-aidl compiler generates Rust code (traits, proxies, and stubs)
  3. Your Service implements the generated trait and registers itself with the HUB (service manager)
  4. 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 hub module 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_hub before registering or discovering services.
    • On Android: The system already provides its own service manager (servicemanager). rsbinder connects to it automatically — no need to run rsb_hub.
    • Handles service registration, discovery, and lifecycle management.
    • Provides APIs for listing services, checking service status, and notifications.

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:

  1. 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
  2. Installation - Set up your development environment

    • Install required dependencies
    • Set up binder devices and service manager
    • Configure your Rust project
  3. Hello World - Build your first Binder service

    • Create a simple echo service
    • Learn AIDL basics
    • Understand service registration and client communication
  4. AIDL Guide - Dive deeper into AIDL language features:

  5. Service Development - Build production-quality services:

  6. Platform-specific Setup - Choose your target platform:

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

  1. Define your service interface in an .aidl file
  2. Use rsbinder-aidl to generate Rust code
  3. Implement your service logic
  4. Register the service with the service manager
  5. 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/binderfs directory 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 (includes async feature)
  • async: Async trait support without tokio runtime — use this when integrating with a different async runtime
  • android_*: 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.rs file must be placed in the project root directory, not inside src/. If placed in the wrong location, you will get a compile error: environment variable OUT_DIR not defined at compile time. Cargo only recognizes build.rs at 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:

  1. "ProcessState is not initialized!" - ProcessState::init_default() (or ProcessState::init()) must be called in main() before using any other rsbinder APIs
  2. "environment variable OUT_DIR not defined" - build.rs must be placed in the project root directory (next to Cargo.toml), not inside src/
  3. "Can't find my.hello" - Ensure the service is running and registered
  4. Permission errors - Check that binder device has correct permissions (0666)
  5. Service manager not found - Verify rsb_hub is running
  6. 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 use example_hello::* instead of hello::*)
  • 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 TypeRust Type (in)Rust Type (out)Notes
booleanbool&mut bool
bytei8&mut i8Single values use i8; array Reverse uses u8
charu16&mut u16UTF-16 code unit
inti32&mut i32
longi64&mut i64
floatf32&mut f32
doublef64&mut f64
String&str&mut String
@utf8InCpp String&str&mut StringSame mapping in rsbinder
T[]&[T]&mut Vec<T>
@nullable TOption<&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 byte type has a subtle difference between single values and arrays. A single byte parameter maps to i8 (signed), but when used in the ReverseByte pattern, array elements use u8 (unsigned). This matches Android's Binder behavior where byte arrays are treated as unsigned.

  • Rust strings are always UTF-8, so @utf8InCpp has no special behavior. In Android's C++ backend, this annotation switches between String16 (UTF-16) and std::string (UTF-8). Since Rust's String type is inherently UTF-8, both String and @utf8InCpp String produce identical code.

  • Arrays in AIDL map to slices for input and Vec for output. Input arrays use &[T], which is efficient because no allocation is needed on the caller side. Output arrays and return values use Vec<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 for None on the client side when calling methods that return nullable types.

  • Direction tags affect performance. An inout parameter requires serialization in both directions. If you only need data to flow one way, use in or out to reduce the amount of data copied over the Binder transaction.

  • Return values are always Result. Every AIDL method in rsbinder returns rsbinder::status::Result<T>, allowing services to report errors using Status codes. Even void methods return rsbinder::status::Result<()>.

  • char is UTF-16, not UTF-8. The AIDL char type maps to Rust's u16, representing a single UTF-16 code unit. This is not the same as Rust's native char type, 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 TypeRust Type (in)Rust Type (out)Notes
booleanbool&mut bool
bytei8&mut i8Single values use i8; array Reverse uses u8
charu16&mut u16UTF-16 code unit
inti32&mut i32
longi64&mut i64
floatf32&mut f32
doublef64&mut f64
String&str&mut String
@utf8InCpp String&str&mut StringSame mapping in rsbinder
T[]&[T]&mut Vec<T>
@nullable TOption<&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 byte type has a subtle difference between single values and arrays. A single byte parameter maps to i8 (signed), but when used in the ReverseByte pattern, array elements use u8 (unsigned). This matches Android's Binder behavior where byte arrays are treated as unsigned.

  • Rust strings are always UTF-8, so @utf8InCpp has no special behavior. In Android's C++ backend, this annotation switches between String16 (UTF-16) and std::string (UTF-8). Since Rust's String type is inherently UTF-8, both String and @utf8InCpp String produce identical code.

  • Arrays in AIDL map to slices for input and Vec for output. Input arrays use &[T], which is efficient because no allocation is needed on the caller side. Output arrays and return values use Vec<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 for None on the client side when calling methods that return nullable types.

  • Direction tags affect performance. An inout parameter requires serialization in both directions. If you only need data to flow one way, use in or out to reduce the amount of data copied over the Binder transaction.

  • Return values are always Result. Every AIDL method in rsbinder returns rsbinder::status::Result<T>, allowing services to report errors using Status codes. Even void methods return rsbinder::status::Result<()>.

  • char is UTF-16, not UTF-8. The AIDL char type maps to Rust's u16, representing a single UTF-16 code unit. This is not the same as Rust's native char type, 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 derive Clone, because some AIDL types contain non-cloneable fields (such as ParcelFileDescriptor or ParcelableHolder). You must opt in explicitly for each type.
  • Default values ("Unknown" for name, 0 for age) are applied in the generated Default trait implementation. Fields without explicit defaults use Rust's default for their type (e.g., 0 for integers, empty string for String).
  • 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 ParcelableHolder stores the extension in a type-erased manner. You must specify the concrete type when calling get_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 ParcelableHolder fields (as shown with ext and ext2 above), 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 ParcelableHolder through 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 like input.cloned() with nullable parameters. Only add it when all fields in the parcelable actually implement Clone.

  • Use @RustDerive(PartialEq=true) when you need to compare parcelable instances in assertions or business logic. As with Clone, all fields must implement PartialEq.

  • @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 Default trait. When you write int count = 5; in AIDL, calling MyParcelable::default() in Rust will produce a struct with count set to 5.

  • 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 .aidl file. Following the AIDL convention, each parcelable type should be defined in a separate file whose name matches the type name (e.g., UserProfile.aidl for parcelable UserProfile).

  • Constants are scoped to the parcelable. When you define const int MAX_VALUE = 100; inside a parcelable, access it in Rust as MyParcelable::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 TypeRust Type
bytei8
inti32
longi64

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 ns initialized 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.
  • @RustDerive is recommended so the generated Rust type supports Clone and PartialEq.

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 @Backing for 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, not Union::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/ and tests/src/test_client.rs contains 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

DeriveDescription
CloneEnables cloning of the type
PartialEqEnables equality comparison
CopyEnables 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:

  • ParcelFileDescriptor wraps an OwnedFd, 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.
  • ParcelableHolder contains a Mutex, 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 BackingRust TypeSize
"byte"i81 byte
"int"i324 bytes
"long"i648 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:

DirectionRust Type
Input (in)&str
Output / ReturnString

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::BadValue error 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 @FixedSize parcelables
  • Enums with a @Backing annotation

They cannot contain:

  • String or @utf8InCpp String
  • Arrays (T[])
  • ParcelFileDescriptor
  • IBinder
  • 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.

AnnotationApplies ToRust Effect
@RustDeriveparcelable, unionAdds derive attributes (Clone, Copy, PartialEq)
@BackingenumSets the backing integer type (i8, i32, i64)
@nullablefield, param, returnMaps to Option<T>
@nullable(heap=true)fieldMaps to Option<Box<T>> for recursive types
@utf8InCppStringNo effect in Rust (strings are always UTF-8)
@DescriptorinterfaceOverrides the wire descriptor string
@VintfStabilityparcelable, interfaceEnforces VINTF stability rules
@FixedSizeparcelableRestricts 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:

  1. A struct that holds service state.
  2. An impl Interface block for the struct, optionally providing a dump() method.
  3. An impl IYourService block 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. The Bn prefix 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:

  1. Define a struct that implements the Default trait variant of your interface.
  2. Wrap it in an Arc and register it with setDefaultImpl.
  3. When the remote service returns StatusCode::UnknownTransaction for 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 of main() 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 Mutex or RwLock for 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.myservice or my.hello. This prevents name collisions when multiple services are registered.
  • Error handling: Return rsbinder::Status errors from service methods to communicate failures to clients. Use Status::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:

  1. A struct that holds service state.
  2. An impl Interface block for the struct, optionally providing a dump() method.
  3. An impl IYourService block 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. The Bn prefix 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:

  1. Define a struct that implements the Default trait variant of your interface.
  2. Wrap it in an Arc and register it with setDefaultImpl.
  3. When the remote service returns StatusCode::UnknownTransaction for 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 of main() 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 Mutex or RwLock for 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.myservice or my.hello. This prevents name collisions when multiple services are registered.
  • Error handling: Return rsbinder::Status errors from service methods to communicate failures to clients. Use Status::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.

AspectSyncAsync
Trait nameIMyServiceIMyServiceAsyncService
Method signaturefn method(&self) -> Result<T>async fn method(&self) -> Result<T>
Service creationBnXxx::new_binder(impl)BnXxx::new_async_binder(impl, rt())
Remote call (client)service.Method()service.clone().into_async::<Tokio>().Method().await
Main loopProcessState::join_thread_pool()std::future::pending().await
RuntimeNot neededTokio 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:

  1. Initialize the Binder process state and thread pool (same as sync).
  2. Build a Tokio runtime.
  3. Inside the runtime, create and register async services.
  4. 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 a TokioRuntime. Every call to new_async_binder requires a TokioRuntime so 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().await is a future that never resolves. It keeps the block_on call (and therefore the process) alive indefinitely, which is the async equivalent of the synchronous ProcessState::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:

  1. Add use async_trait::async_trait; and annotate the impl block with #[async_trait].
  2. Implement IMyServiceAsyncService instead of IMyService.
  3. Prefix each method with async.
  4. When creating the binder, use BnXxx::new_async_binder(impl, rt()) instead of BnXxx::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 .await the 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 the async-trait crate 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().await is the idiomatic way to keep an async service process alive. Unlike the sync approach where ProcessState::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 returns TokioRuntime(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_binder and others with new_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_blocking to 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:

  1. Build a Tokio runtime and run your service setup inside runtime.block_on(async { ... }).
  2. Define an rt() helper that wraps tokio::runtime::Handle::current().
  3. Implement IMyServiceAsyncService with #[async_trait] instead of IMyService.
  4. Create binders with BnXxx::new_async_binder(impl, rt()).
  5. Use into_async::<Tokio>() when calling other Binder services from async code.
  6. 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. The Bn prefix stands for "Binder native" (server-side stub).
  • Strong<dyn INamedCallback::INamedCallback> is a strong reference to a Binder object, equivalent to Android's sp<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. The Bn (Binder native) wrapper converts your Rust struct into a Binder node that can be sent through Binder transactions. The corresponding Bp (Binder proxy) is used automatically on the receiving side.

  • Use Mutex to 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 by Mutex, RwLock, or another synchronization primitive.

  • Nested types use fully-qualified Rust paths. A callback ICallback nested inside INestedService is accessed as INestedService::ICallback::ICallback for the trait and INestedService::ICallback::BnCallback for the Binder node constructor.

  • Death recipients use Weak references. The link_to_death API takes Weak<dyn DeathRecipient> to avoid preventing cleanup of the death recipient itself. Keep a strong Arc reference alive for as long as you want to receive notifications.

  • as_binder() converts typed interfaces to raw SIBinder. This is useful when you need to pass a Binder reference to a method that accepts IBinder, or when you need to call Binder-level methods like link_to_death or ping_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 ParcelFileDescriptor is serialized into a Parcel, 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_pfd for reading and writing. ParcelFileDescriptor does not implement std::io::Read or std::io::Write directly. Convert it to a File (via try_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.

  • ParcelFileDescriptor is not Clone. Because it wraps an OwnedFd, which owns the underlying file descriptor, the type cannot derive Clone. Use dup_fd (or as_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 calling unwrap().

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:

VariantMeaning
OkOperation completed successfully
UnknownAn unspecified error occurred
BadValueInvalid parameter value
UnknownTransactionThe transaction code is not recognized
PermissionDeniedCaller does not have permission
DeadObjectThe remote process has died
FailedTransactionThe transaction could not be completed
NoMemoryOut of memory
BadTypeWrong data type encountered
NotEnoughDataThe 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:

VariantMeaning
NoneNo error
SecuritySecurity / permission violation
BadParcelableMalformed parcelable data
IllegalArgumentInvalid argument provided
NullPointerUnexpected null value
IllegalStateOperation invalid for current state
UnsupportedOperationRequested operation is not supported
ServiceSpecificApplication-defined error with custom code
TransactionFailedLow-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_error when 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 StatusCode for infrastructure problems. Return raw StatusCode values like PermissionDenied or BadValue for errors that relate to the IPC mechanism rather than your application's business logic.

  • Always check exception_code() first. The meaning of service_specific_error() and transaction_error() depends on the exception code. Calling them without checking the exception may return default (zero) values.

  • Handle DeadObject gracefully. In long-running clients, the remote service may restart. Consider using death notifications (link_to_death) to detect service restarts and re-establish connections.

  • Status implements Display and std::error::Error. You can use it with ? in functions that return Box<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:

FlagDescription
DUMP_FLAG_PRIORITY_DEFAULTServices with default priority
DUMP_FLAG_PRIORITY_HIGHHigh-priority services
DUMP_FLAG_PRIORITY_CRITICALCritical system services
DUMP_FLAG_PRIORITY_NORMALNormal-priority services
DUMP_FLAG_PRIORITY_ALLAll 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:

AspectLinux (rsb_hub)Android (servicemanager)
ProcessUser-space rsb_hub binarySystem servicemanager daemon
Access controlNo SELinux enforcementFull SELinux MAC policy enforcement
VINTF manifestsNot supported (is_declared is false)Supported and enforced
Service debug infoSupportedSupported (Android 12+)
Binder deviceMust be created with rsb_deviceManaged by Android init
Version selectionAlways uses Android 16 protocolAuto-detected from SDK version
Death notificationsSupportedSupported

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 call ProcessState::init_default() (or ProcessState::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_notifications rather than repeatedly calling get_service in a loop.

  • Handle registration failures. add_service can fail if the name is invalid or if the caller lacks permission (on Android with SELinux). Always check the result.

  • Use get_interface for type safety. Prefer hub::get_interface over hub::get_service when you know the expected interface type. It returns a strongly-typed proxy that provides compile-time guarantees.

  • Debug with list_services and get_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:

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:

  1. Module not found: Ensure your kernel was built with binder support enabled
  2. Permission denied: Make sure you're using sudo for device creation
  3. 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 -f while 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

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:

  1. Module compilation fails: Ensure all kernel-devel packages match your running kernel
  2. SELinux denials: Check audit.log for SELinux denials and create appropriate policies
  3. 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_11 feature
  • Android 12 (API 31) / 12L (API 32): android_12 feature
  • Android 13 (API 33): android_13 feature
  • Android 14 (API 34): android_14 feature
  • Android 16 (API 36): android_16 feature

Note: Android 12L (API 32) uses the same Binder protocol as Android 12, so both are covered by the android_12 feature flag. Similarly, Android 15 (API 35) uses the same Binder protocol as Android 14, so it is covered by the android_14 or android_14_plus feature flag. No separate android_12l or android_15 feature 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 16
  • android_12_plus: Supports Android 12 through 16
  • android_13_plus: Supports Android 13 through 16
  • android_14_plus: Supports Android 14 through 16
  • android_16_plus: Supports Android 16 only

Protocol Compatibility

rsbinder maintains binary compatibility with Android's Binder protocol:

  • Transaction Format: Uses identical binder_transaction_data structures
  • 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:

DeviceService ManagerDescription
/dev/binderservicemanagerFramework services (default)
/dev/hwbinderhwservicemanagerHAL services (HIDL)
/dev/vndbindervndservicemanagerVendor services

Warning: hwbinder uses a different protocol (libhwbinder) than standard binder (libbinder). rsbinder has not been tested with hwbinder, 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.

Download and install Android Studio, which includes the Android SDK:

Method 2: Command Line Tools Only

If you prefer a minimal installation:

  1. Download Command Line Tools from Android Developer Downloads
  2. 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/rsbinder by default)
  • Prepares the environment for file synchronization
$ ndk_prepare

ndk_build: Builds the project for Android

  • Reads configuration from REMOTE_ANDROID file
  • Builds both debug binaries and test executables
  • Uses cargo ndk with the specified target architecture
$ ndk_build

ndk_sync: Synchronizes built binaries to Android device

  • Pushes all executable files to the device
  • Uses adb push to 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

  1. Enable Developer Options on your Android device
  2. Enable USB Debugging
  3. 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