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 the binder driver has been part of the mainline Linux kernel since version 4.17 (with the binderfs filesystem rsbinder uses arriving in 5.0).

However, since it is rarely used outside of Android, it is disabled by default in most Linux distributions, so you typically need a kernel built with binder support to use the kernel-binder path (see Enable binder for Linux).

rsbinder ships two parallel stacks:

  1. The traditional kernel binder path through /dev/binder / /dev/binderfs/binder, which most of this guide covers.
  2. An RPC transport (binder-over-socket) — AOSP's RpcServer/RpcSession analogue, pure user-space, no kernel driver. Drives the same generated AIDL stubs over Unix-domain sockets, vsock, or TLS, and runs on Linux, Android, and macOS. See RPC Transport.

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 Linux and Android (kernel binder), plus macOS (RPC transport only).
  • Async/Sync Support: Supports both synchronous and asynchronous programming models with optional tokio runtime integration.
  • Two transport stacks: Kernel binder for on-device IPC, and an opt-in RPC transport (Unix sockets, vsock, or TLS) for cross-process, cross-VM, or cross-host binder calls — both stacks share the same AIDL-generated code.

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.

The RPC transport (a separate, opt-in stack)

The diagram above describes the kernel binder path. rsbinder also ships a second, independent stack — RPC transport — that delivers the same generated AIDL stubs over a socket instead of the kernel binder driver:

   ┌──────────┐       AIDL stub        ┌──────────┐
   │  Client  │ ─── try_from(root) ──▶ │  Server  │
   └────┬─────┘                        └────┬─────┘
        │                                   │
   RpcSession                          RpcServer
   .setup_unix_client()           .setup_unix_server(path)
   .get_root()                    .set_root(BnFoo)
        │                                   │
        └──── UDS / vsock / TLS socket ─────┘

Key contrasts with the kernel-binder diagram:

  • No service manager. The server publishes a single root binder via RpcServer::set_root; clients fetch it through RpcSession::get_root. (Multiple named services on one server are available via RpcServer::add_service.)
  • No kernel driver. ProcessState, rsb_hub, and /dev/binderfs/binder are not involved at all. A pure-RPC process needs none of them.
  • Cross-host / cross-VM by design. Sockets reach further than /dev/binder does — Linux ↔ macOS, host ↔ VM (vsock), or authenticated peers across a network (TLS).

Both stacks coexist in the same process — for example, the Accessor pattern (Android 16+) publishes an IAccessor binder over kernel binder whose addConnection() hands the client a connected RPC socket fd.

Coexistence rules

If a single process uses both stacks (the Accessor pattern is the canonical example):

  • Kernel-binder side requires ProcessState::init_default() (or init(path, max_threads)) and typically start_thread_pool() / join_thread_pool(). This is global per-process state and lives in the rsbinder core.
  • RPC side is fully independent — RpcServer / RpcSession open their own sockets and run their own accept/worker threads. They do not touch ProcessState, rsb_hub, or /dev/binder.
  • A pure-RPC process omits the kernel-binder initialization entirely; a pure-kernel process omits the RPC code path entirely (and doesn't even need the rpc Cargo feature). Mixing the two costs only what each side already costs in isolation — there is no shared singleton between them.

See RPC Transport for the full story.

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. RPC Transport - Binder-over-socket:

    • The opt-in second stack that runs on Linux, Android, and macOS
    • Unix-domain sockets, vsock, or TLS instead of /dev/binder
    • Same generated AIDL stubs as the kernel-binder path
  7. Platform-specific Setup - Choose your target platform:

Platform Requirements

rsbinder ships two parallel stacks — pick by platform and use case:

  • Kernel binder (the default in this guide): Linux 5.0+ with binderfs enabled (the binderfs filesystem landed in 5.0), or Android. Talks to the kernel binder driver through /dev/binderfs/binder (Linux) or /dev/binder (Android).
  • RPC transport (binder-over-socket, opt-in via the rpc feature): pure user-space, no kernel binder driver. Runs on Linux, Android, and macOS over Unix-domain sockets, vsock, or TLS. See the RPC Transport chapter.

Windows: not supported on either stack.

macOS: kernel binder is not supported (no kernel driver), but the RPC transport works natively — useful for developing and testing RPC services on a macOS workstation without a Linux VM.

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.8"
async-trait = "0.1"
env_logger = "0.11"  # Optional: for logging

[build-dependencies]
rsbinder-aidl = "0.8"

Feature Flags

rsbinder supports various feature flags for different use cases:

[dependencies]
rsbinder = "0.8"  # Default: includes tokio async runtime
# or
rsbinder = { version = "0.8", features = ["async"] }  # Async trait support without tokio — use your own async runtime
# or
rsbinder = { version = "0.8", default-features = false }  # Synchronous only (no async support)
# or — opt in to the binder-over-socket stack (see RPC Transport chapter)
rsbinder = { version = "0.8", features = ["rpc"] }

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
  • rpc: Enable the RPC transport (binder-over-socket). Add rpc-vsock, rpc-tls, or rpc-tcp-debug to bundle the matching backend. See RPC Transport.
  • 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.8"
async-trait = "0.1"
env_logger = "0.11"

[build-dependencies]
rsbinder-aidl = "0.8"

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.

Directory ↔ package mapping. The folder name under aidl/ must match the package declaration at the top of the .aidl file. So aidl/hello/IHello.aidl requires package hello; (below), and the generated Rust path is crate::hello::IHello::IHello. A nested package like package com.example.hello; would need aidl/com/example/hello/IHello.aidl. This mirrors Android's AIDL convention.

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 Guide

AIDL (Android Interface Definition Language) is the contract between a Binder service and its clients. You describe the interface — its methods, parameters, and data types — in a .aidl file, and the rsbinder-aidl compiler generates the Rust code on both sides. This guide walks through the parts of AIDL that you will actually use when writing services with rsbinder.

If you have not yet seen rsbinder running end-to-end, read Hello, World! first — it shows where AIDL fits into a complete project.

Chapters

  • Data Types — Primitive, string, array, list, map, and interface types, and how each one maps to Rust on the in / out / inout side. Start here.
  • Parcelable — Defining user-supplied structs that can cross the Binder boundary, including nullable fields and default values.
  • Enum and Union — Backed enums (newtype structs in Rust, for wire-stable forward compatibility) and unions (tagged variants).
  • Annotations@RustDerive, @nullable, @Backing, @JavaDerive-equivalents, and the other annotations the Rust backend honors.

How to use this guide

The chapters are roughly ordered by how often you need each topic when building a new service:

  1. Skim Data Types so you know what AIDL primitives translate to in Rust.
  2. Read Parcelable when you need to pass structured data.
  3. Reach for Enum and Union when designing variant payloads or constant sets.
  4. Consult Annotations as a reference whenever the generated Rust does not look the way you expect.

Once you are comfortable with the AIDL surface, move on to Service Development for the runtime patterns that put these types to work.

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. Only non-scalar types — arrays, parcelables, and nullable references — may be out/inout; the generator rejects a primitive or a String marked out/inout at compile time (it has no fixed slot to write back into).

AIDL TypeRust Type (in)Rust Type (out)Notes
booleanbool— (not allowed)
bytei8— (not allowed)Single values use i8; array Reverse uses u8
charu16— (not allowed)UTF-16 code unit
inti32— (not allowed)
longi64— (not allowed)
floatf32— (not allowed)
doublef64— (not allowed)
String&str— (not allowed)out/inout String is rejected by the generator
@utf8InCpp String&str— (not allowed)Same mapping in rsbinder
T[]&[T]&mut Vec<T>
@nullable TOption<&T>&mut Option<T>A nullable String input is Option<&str>
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) and String cannot carry an out/inout direction tag — the generator rejects it. As scalar (or, for String, value-typed) parameters they only ever flow in. Direction tags are meaningful only for arrays 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 are emitted as module-level pub const items inside the generated parcelable module. Because the module shares its name with the struct, you still reach them with the familiar TypeName::CONSTANT path. 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 through the type path:

#![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. In AOSP-faithful AIDL the recursive field is marked @nullable(heap=true), signalling to the C++ and Java backends that the inner value lives on the heap so the struct has a finite, known size at compile time. rsbinder accepts the same syntax for source compatibility, but the actual Box<T> wrapping is emitted by the code generator whenever a field references the enclosing parcelable's own type — the heap=true parameter is not what triggers it.

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 the self-reference detection adds the Box wrapper that gives the type a known size. 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 the Box indirection the Rust compiler would reject the type definition because RecursiveList would need to contain itself directly, leading to an infinite-size type. The code generator inserts that indirection automatically whenever it sees a field whose type matches the enclosing parcelable.

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 specifies the underlying integer type. It is optional — when omitted, the backing type defaults to byte. 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 derives Debug, Default, Copy, Clone, PartialOrd, Ord, PartialEq, Eq, and Hash, plus 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 fixed-size array ([Self; N]) 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 type that identifies which variant is currently active. (Like AIDL enums, it is generated as a newtype struct whose variants are exposed as associated constants such as Union::Tag::n.) 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 AIDL accepts a @deprecated Javadoc tag on a field, but rsbinder's generator does not currently translate it into a Rust #[deprecated] attribute — the field is generated normally and using it produces no 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

  • Specify @Backing explicitly for enums. It is optional (the default is byte), but stating it makes the wire format and the Rust integer type unambiguous to readers.
  • 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 struct wrapping the corresponding integer type, with the enum values exposed as associated constants:

#![allow(unused)]
fn main() {
pub mod Priority {
    #![allow(non_upper_case_globals)]
    // Generated by rsbinder::declare_binder_enum! — produces a
    // newtype struct, not a type alias, so values stay type-safe
    // and the enum carries its full set of derived traits.
    pub struct Priority(pub i8);
    impl Priority {
        pub const LOW: Self = Self(0);
        pub const MEDIUM: Self = Self(1);
        pub const HIGH: Self = Self(2);
    }
}
}

The generated type derives Debug, Default, Copy, Clone, PartialOrd, Ord, PartialEq, Eq, and Hash, and provides an enum_values() associated function returning a [Self; N] of all defined variants. See Enum and Union for the full code-generation details.

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)

AOSP AIDL uses @nullable(heap=true) to mark fields of recursive or self-referential parcelables, so that the C++ and Java backends know to allocate the inner value on the heap:

parcelable RecursiveList {
    int value;
    @nullable(heap=true) RecursiveList next;
}

The Rust backend 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.

In rsbinder, the Box<T> wrapping is driven by self-reference detection in the code generator, not by the heap=true parameter — the parameter is accepted for AOSP-faithful AIDL syntax compatibility but does not itself control the wrapping. Whenever a parcelable field references the enclosing parcelable's own type, the generator emits Box<...> around it to give the struct a known size. Keep the heap=true form when writing AIDL that must also compile under the AOSP toolchain.

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

In the upstream Android toolchain, VINTF-stable types are meant to contain only other VINTF-stable types so that their serialization format stays stable across independent system/vendor updates — critical for framework↔HAL compatibility.

rsbinder's generator recognizes @VintfStability and stamps the generated type/interface with Stability::Vintf (so it carries the right stability tier on the wire), but it does not statically or dynamically enforce the "fields must also be VINTF-stable" rule — there is no field-tree validation and no BadValue raised for embedding a non-VINTF type. Treat the constraint as a contract you are responsible for upholding, not one the compiler checks for you.

@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;
}

Intended constraints

In upstream Android, a fixed-size parcelable may only contain:

  • Primitive types (boolean, byte, char, int, long, float, double)
  • Other @FixedSize parcelables
  • Enums with a @Backing annotation

and may not contain String/@utf8InCpp String, arrays (T[]), ParcelFileDescriptor, IBinder, or any other variable-length type.

rsbinder note: the generator accepts @FixedSize but currently treats it as a no-op — it neither validates these constraints nor changes the generated layout or wire format. The constraints above are the contract you should follow; rsbinder does not check them for you.

Relationship with @RustDerive(Copy=true)

Copy is derived purely from @RustDerive(Copy=true); @FixedSize is not a prerequisite for it in rsbinder. Conceptually you should still only add Copy to types with a fixed layout, so pairing the two documents intent:

@RustDerive(Clone=true, Copy=true, PartialEq=true)
@FixedSize
parcelable Coordinate {
    double latitude;
    double longitude;
}

@EnforcePermission

@EnforcePermission declares the Android permission(s) a method requires. The generated on_transact arm checks the caller against PermissionManagerService before your handler runs and returns EX_SECURITY if the check fails — so the handler body needs no authorization code:

interface IExample {
    @EnforcePermission("android.permission.INTERNET")
    void doNetworking();

    @EnforcePermission(allOf = {"android.permission.A", "android.permission.B"})
    void needsBoth();

    @EnforcePermission(anyOf = {"android.permission.A", "android.permission.B"})
    void needsEither();
}

The companion documentation-only annotations @PermissionManuallyEnforced and @RequiresNoPermission are recognized and emit no runtime check (they exist so every method can declare its permission posture).

Kernel-only. Android permissions are a kernel-binder concept. Over the RPC transport an @EnforcePermission method is denied (EX_SECURITY) unconditionally — granting would mean granting root to an anonymous peer. For RPC authorization use the transport-native mechanisms, or inject a PermissionAuthority to back the check with your own policy. See Security & Authorization.

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)fieldAOSP-faithful syntax for recursive fields; rsbinder boxes self-referential fields automatically (parameter accepted but not required)
@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
@EnforcePermissioninterface methodGenerates a PermissionManagerService check (kernel-only; denied over RPC)

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 Development

Once your AIDL interface compiles, the next step is building the service that implements it and the client that calls it. This part of the guide collects the runtime patterns you will use day-to-day: how to structure a service, how to go async, how to model bidirectional communication, how to ship file descriptors, how to surface errors, and how to register and discover services through the HUB.

If you have not yet built a working service, start with Hello, World! and then return here for the deeper material.

Chapters

  • Service Patterns — The three pieces every rsbinder service needs (state struct, impl Interface, impl IYourService) and how to combine them for single- and multi-interface processes.
  • Async Service — Implementing services with async fn on top of the Tokio runtime, and how the async traits differ from their sync counterparts.
  • Callbacks and Interfaces — Passing IBinder objects in either direction, managing callback collections, and watching for remote process death.
  • ParcelFileDescriptor — Sending pipes, files, and sockets across the Binder boundary as owned file descriptors.
  • Error Handling — The StatusCode / Status split, exception codes, and how AIDL methods surface both transport-level and application-level failures.
  • Service Manager (HUB) — Registering, looking up, and waiting for services; differences between Linux (rsb_hub) and Android's native servicemanager; and how it relates to the RPC transport.

Suggested reading order

The chapters are designed to be read in roughly the order above — each builds on the previous one. If you only need to ship a working service quickly, Service Patterns plus Service Manager (HUB) is enough to get started; the other chapters fill in capabilities as you need them.

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.
    // `init_default()` returns `Result<&'static ProcessState, ...>`; use `?`
    // to surface device-open or initialization failures.
    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.

Async is transport-agnostic

Async lives in the AIDL-generated stubs, not in the transport. The compiler emits the async trait (IMyServiceAsyncService), the async proxy (into_async::<Tokio>()), and the new_async_binder constructor once, and the very same async impl runs unchanged over kernel binder or RPC — including through the transport-agnostic service facade.

The reason is that the facade and the transports only ever move a transport-agnostic SIBinder around (register a binder under a name, look one up). Whether that binder is sync- or async-backed is decided entirely by which constructor produced it (new_binder vs new_async_binder) — a fact the transport never sees. So async needs no transport-specific code: pick the constructor, pick the transport, and the two compose.

This chapter starts with the kernel-binder setup, then shows the RPC and unified-facade variants — which differ only in the bootstrap and the runtime flavor.

Under the hood — it is a thread bridge, not a reactor. rsbinder async is not epoll-driven non-blocking I/O. On the client a transact runs on tokio::task::spawn_blocking; on the server each call is wrapped in rt.block_on(..) and driven from a blocking worker thread (the binder thread pool for kernel, the serve worker for RPC). This is the same bridge the kernel binder_tokio path uses. You get async/await ergonomics and real concurrency, backed by blocking worker threads — which is sufficient for typical service workloads. A true async I/O reactor is deliberately out of scope.

Sync vs Async at a Glance

The following table summarizes the key differences between a synchronous and an asynchronous Binder service in rsbinder.

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.

The trait, proxy, and constructor rows above are identical across transports — only the "Main loop" and "Runtime" rows are the kernel-binder values shown here. Over RPC the main loop is rpc::Host::serve() (or serve_background()) and the runtime must be multi-threaded; see Async Over RPC and the Unified Facade below.

Setting Up the Tokio Runtime (kernel binder)

An async Binder service must run inside a Tokio runtime. The standard pattern for kernel binder is:

  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() -> std::result::Result<(), Box<dyn std::error::Error>> {
    // Initialize Binder -- same as the sync case.
    ProcessState::init_default()?;
    ProcessState::start_thread_pool();

    // Build a single-threaded Tokio runtime.
    let runtime = tokio::runtime::Builder::new_current_thread()
        .enable_all()
        .build()
        .unwrap();

    runtime.block_on(async {
        // Create and register the async service (see next section).
        let service = BnMyService::new_async_binder(
            MyAsyncService::default(), rt(),
        );
        hub::add_service("com.example.myservice", service.as_binder())
            .expect("Could not register service");

        // Yield to the runtime. This keeps the process alive and
        // drives the Tokio event loop on the current thread.
        std::future::pending().await
    })
}

ProcessState::init_default() returns Result<&'static ProcessState, …>, so a main that calls it should also return Result (or call .expect() / .unwrap() for the very first line of a quick demo).

There are several things to note here:

  • rt() is a small helper that wraps the current Tokio runtime handle in 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().

Async Over RPC and the Unified Facade

Because async lives in the generated stubs and the service facade only moves an SIBinder, the same async service runs over RPC with no new code. You register the result of new_async_binder through the generic Registry, and the client converts the looked-up proxy with into_async:

#![allow(unused)]
fn main() {
use async_trait::async_trait;
use rsbinder::service::{rpc, Registry, Broker};
use rsbinder::*;

struct IHelloService;
impl Interface for IHelloService {}

#[async_trait]
impl IHelloAsyncService for IHelloService {
    async fn echo(&self, echo: &str) -> rsbinder::status::Result<String> {
        Ok(echo.to_owned())
    }
}

/// Registration is written once, generic over the transport — identical to the
/// sync facade example in "Cross-Transport Services". Only the binder
/// constructor differs (`new_async_binder` instead of `new_binder`).
fn register<R: Registry>(
    reg: &R,
    rt: TokioRuntime<tokio::runtime::Handle>,
) -> rsbinder::Result<()> {
    let binder = BnHello::new_async_binder(IHelloService {}, rt).as_binder();
    reg.add_service("hello", binder)
}
}

The server needs a multi-threaded runtime

This is the one substantive difference from the kernel setup. An RPC async server is driven by a blocking serve worker that calls rt.block_on(handler). That block_on runs on a thread that is not the runtime's own, so the runtime must be a multi-threaded one — a current_thread runtime cannot be driven from an outside thread while it is also being kept alive elsewhere. (Kernel binder gets away with current_thread only because the binder thread pool, not a serve loop, supplies the worker threads.)

fn main() -> std::result::Result<(), Box<dyn std::error::Error>> {
    // RPC async servers require a MULTI-THREAD runtime — the blocking serve
    // worker calls `rt.block_on(handler)` from a non-runtime thread.
    let runtime = tokio::runtime::Builder::new_multi_thread()
        .worker_threads(2)
        .enable_all()
        .build()?;
    let handle = runtime.handle().clone();

    let host = rpc::Host::unix("/tmp/hello.sock")?;
    register(&host, TokioRuntime(handle))?;
    println!("serving over RPC at /tmp/hello.sock");
    host.serve()?; // blocks, driving this one socket
    Ok(())
}

Use rpc::Host::serve_background() instead of serve() if you want the socket driven on a background thread while main does other work.

The client converts the looked-up proxy

The client side is the ordinary facade lookup followed by into_async:

#![allow(unused)]
fn main() {
let runtime = tokio::runtime::Builder::new_multi_thread().enable_all().build()?;

let broker = rpc::Broker::unix("/tmp/hello.sock")?;
let hello = broker.get_interface::<dyn IHello>("hello")?; // sync Strong<dyn IHello>

runtime.block_on(async {
    let hello = hello.into_async::<Tokio>();
    println!("{}", hello.echo("hi").await?);
    Ok::<_, Box<dyn std::error::Error>>(())
})?;
}

The blocking bootstrap — Broker::unix, get_interface — is done before entering the runtime; only the transacts themselves are .awaited. The async-over-RPC bridge shown here (new_async_binder + into_async::<Tokio> over a real Unix-socket session, multi-thread runtime) is verified end-to-end by tests/tests/rpc_async.rs — including many in-flight calls on one shared session under genuine async concurrency. That test drives the bridge through the lower-level RpcSession API directly rather than the rpc::Host/Broker facade, but the facade is a thin wrapper over exactly that path.

Switching transports

To run the exact same async service over kernel binder instead, swap only the host construction and the runtime flavor — the register helper and the service impl are unchanged:

#![allow(unused)]
fn main() {
let host = kernel::Host::new()?;      // instead of rpc::Host::unix(..)
register(&host, rt())?;
host.serve()?;                        // joins the process-wide binder pool
}

Be aware that the two transports' trust boundaries differ: see Security & Authorization@EnforcePermission denies over RPC, and get_calling_uid() is the kernel-vouched peer uid on Unix RPC (fail-closed on uid-less transports). The facade makes the transport swap easy; it does not make the security models identical.

Implementing an Async Service

An async service struct implements the generated IMyServiceAsyncService trait using the #[async_trait] attribute macro. Compare this with the sync version side by side.

Sync service

#![allow(unused)]
fn main() {
use rsbinder::*;

#[derive(Default)]
struct MyService;

impl Interface for MyService {}

impl IMyService::IMyService for MyService {
    fn echo(&self, input: &str) -> rsbinder::status::Result<String> {
        Ok(input.to_owned())
    }

    fn RepeatInt(&self, token: i32) -> rsbinder::status::Result<i32> {
        Ok(token)
    }
}
}

Async service

#![allow(unused)]
fn main() {
use async_trait::async_trait;
use rsbinder::*;

#[derive(Default)]
struct MyAsyncService;

impl Interface for MyAsyncService {}

#[async_trait]
impl IMyService::IMyServiceAsyncService for MyAsyncService {
    async fn echo(&self, input: &str) -> rsbinder::status::Result<String> {
        // You can use .await on async operations here.
        Ok(input.to_owned())
    }

    async fn RepeatInt(&self, token: i32) -> rsbinder::status::Result<i32> {
        Ok(token)
    }
}
}

The differences are minimal:

  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 (kernel binder) or rpc::Host::serve() / serve_background() (RPC).

Because async lives in the generated stubs, the same async service runs over either transport — register the new_async_binder result through the service facade's Registry and look it up through Broker. The only RPC-specific requirement is a multi-threaded runtime, because the blocking serve worker calls rt.block_on(handler).

For complete working examples, see tests/src/bin/test_service_async.rs (kernel binder) and tests/tests/rpc_async.rs (RPC) in the rsbinder repository.

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.

Note — two Result aliases coexist in rsbinder. AIDL methods use rsbinder::status::Result<T> = Result<T, Status> (rich error with exception code + optional message), but several library-level APIs (Interface::dump, hub::default, hub::register_for_notifications, hub::get_service_debug_info) use the plain rsbinder::Result<T> = Result<T, StatusCode> re-exported from rsbinder::error. Because StatusCode implements From<StatusCode> for Status, propagating with ? from a Result<_, StatusCode> into an AIDL method body (which returns Result<_, Status>) works transparently.

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:

VariantWire valueMeaning
None0No error
Security-1Security / permission violation
BadParcelable-2Malformed parcelable data
IllegalArgument-3Invalid argument provided
NullPointer-4Unexpected null value
IllegalState-5Operation invalid for current state
NetworkMainThread-6Network operation on main thread (Java compatibility)
UnsupportedOperation-7Requested operation is not supported
ServiceSpecific-8Application-defined error with custom code
Parcelable-9Embedded user parcelable exception (Java compatibility)
HasNotedAppOpsReplyHeader-127Internal: reply parcel carries an AppOps noted-ops header
HasReplyHeader-128Internal: reply parcel carries a header (Java-specific marker)
TransactionFailed-129Low-level transaction failure
JustError-256Generic error fallback used internally by the library

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.

Note: This chapter covers the kernel-binder service manager. The RPC transport (binder-over-socket) uses no service manager at all — RpcServer::set_root publishes the root binder directly and RpcSession::get_root fetches it. The Android 16 Accessor pattern bridges the two: a kernel-binder service of type IAccessor hands a client an RPC socket fd. See RPC Transport.

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. These checks are applied by the service manager process (rsb_hub on Linux, servicemanager on Android) — the rsbinder library itself does not validate the name on the client side, so the failure surfaces as a Status error from the IPC call rather than a local compile-time or pre-flight check.

  • Maximum length: 127 characters. Names of 128 characters or longer are rejected.
  • Allowed characters: Alphanumeric characters, dots (.), underscores (_), hyphens (-), and forward slashes (/). 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 always returns false because rsb_hub has no VINTF parser and no manifest concept (rsb_hub.rs implements isDeclared as Ok(false)). 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 10 and 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+; not on 10/11)
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 10 through 16). The per-version API availability matrix:

APIAndroid 10Android 11Android 12+
get_service / check_service
add_service
list_services
is_declaredfalse
register_for_notifications
unregister_for_notifications
get_service_debug_info

Where ✗ means the call returns StatusCode::UnknownTransaction (or false for the bool-returning is_declared) because the underlying server protocol predates the API. Android 10 falls back to the legacy C IServiceManager, which only learned the AIDL-based interface in Android 11; get_service_debug_info was added in Android 12. On Linux, rsbinder always uses the Android 16 protocol — what rsb_hub implements — so every row in the Android 12+ column applies, with the caveat that is_declared is always false on Linux (no VINTF manifest).

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;

// `hub::default()` returns `Result<Arc<ServiceManager>, StatusCode>` because
// initialization can fail (e.g., binder device unavailable, unsupported SDK).
// Propagate the error with `?` or handle it with `match`.
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.

RPC Transport (binder-over-socket)

rsbinder ships two parallel Binder stacks:

  1. Kernel binder — the traditional path through /dev/binder / /dev/binderfs/binder, the rsb_hub or Android servicemanager, and the kernel binder driver. This is what every chapter so far has covered.
  2. RPC transport — binder-over-socket, AOSP's RpcServer / RpcSession equivalent in pure Rust. No kernel binder, no ProcessState, no service manager. A socket (Unix-domain, vsock, or TLS over TCP) carries the same Parcel payload between two processes, optionally on different machines.

Both stacks drive the same generated AIDL stubs. A client written against IHello doesn't know — and doesn't need to know — whether the proxy underneath talks to a kernel binder handle or an RPC socket.

This chapter introduces when to use RPC, how to stand up a minimal client/server pair, and the runtime/platform/security trade-offs that distinguish RPC from kernel binder.

Feature flag. RPC is gated behind the rpc Cargo feature, off by default. Builds without rpc carry zero RPC code and zero extra dependencies. Enable per crate:

[dependencies]
rsbinder = { version = "0.8", features = ["rpc"] }

When to use RPC vs. kernel binder

NeedStack
Two processes on the same Linux/Android hostKernel binder
Pure user-space, no kernel binder driverRPC (Unix)
Cross-host / cross-VM binder callsRPC (vsock/TLS)
Runs on macOS as the host platformRPC only
Wants Android's getCallingUid() / SELinux for freeKernel binder
Needs custom transport (TLS, mTLS, …)RPC

Kernel binder is faster (single ioctl per transaction, shared memory) and gives you Android's security model for free. RPC trades some of that for portability and reach — it works on Linux, Android, and macOS, with no kernel driver and no service manager.

A minimal RPC service and client

The complete example lives under example-hello. Once you've enabled the rpc feature on the workspace, run:

# Terminal 1
$ cargo run -p example-hello --features rpc --bin rpc_hello_service

# Terminal 2
$ cargo run -p example-hello --features rpc --bin rpc_hello_client

Server

use rsbinder::rpc::RpcServer;
use rsbinder::*;
use example_hello::*;

const RPC_SOCKET: &str = "/tmp/rsb_hello_rpc.sock";

struct IHelloService;
impl Interface for IHelloService {}

impl IHello for IHelloService {
    fn echo(&self, echo: &str) -> rsbinder::status::Result<String> {
        Ok(echo.to_owned())
    }
}

fn main() -> std::result::Result<(), Box<dyn std::error::Error>> {
    // No ProcessState, no hub. RPC never touches the kernel binder.
    let _ = std::fs::remove_file(RPC_SOCKET);
    let server = RpcServer::setup_unix_server(RPC_SOCKET)?;

    // Publish a root binder. Clients fetch this via get_root().
    server.set_root(BnHello::new_binder(IHelloService {}).as_binder());

    // Accept loop runs on this thread until shutdown().
    server.run()?;
    Ok(())
}

Key points:

  • No ProcessState::init_default() — the RPC stack is independent of the kernel binder singleton. Mixing the two stacks in one process is fine (the Accessor pattern below relies on it), but a pure-RPC process needs neither ProcessState nor rsb_hub.
  • set_root publishes the single root binder. The client fetches it back through RpcSession::get_root — this is the moral equivalent of hub::get_interface(name) for kernel binder, just without a service manager.
  • server.run() runs the accept loop until [RpcServer::shutdown] is called. Use [RpcServer::run_background] to spawn it on a dedicated thread instead.

Client

use rsbinder::rpc::RpcSession;
use rsbinder::{FromIBinder, Strong};
use example_hello::*;

const RPC_SOCKET: &str = "/tmp/rsb_hello_rpc.sock";

fn main() -> std::result::Result<(), Box<dyn std::error::Error>> {
    let session = RpcSession::setup_unix_client(RPC_SOCKET)?;
    let root = session.get_root()?;

    // Same generated stub as the kernel path — try_from picks the
    // RPC proxy under the hood.
    let hello: Strong<dyn IHello> =
        <dyn IHello as FromIBinder>::try_from(root)?;

    let reply = hello.echo("Hello over RPC!")?;
    println!("server replied {reply:?}");
    Ok(())
}

The generated Bp* proxies dispatch through as_remote(), which returns either the kernel handle or the RPC proxy depending on what the SIBinder came from. One AIDL stub, two stacks — there is no separate "RPC client API" you need to learn.

Multiple named services on one server

RpcServer::set_root publishes a single root binder. If you need to expose multiple named services on the same socket — the moral equivalent of the kernel-binder hub::add_service(name, …) flow — use [RpcServer::add_service] instead. The first call automatically turns the root into a built-in service directory, so clients reach each service by name through [RpcSession::get_service]:

#![allow(unused)]
fn main() {
// Server
let server = RpcServer::setup_unix_server(SOCKET)?;
server.add_service("hello", BnHello::new_binder(IHelloService).as_binder())?;
server.add_service("echo",  BnEcho::new_binder(IEchoService).as_binder())?;
server.run()?;

// Client
let session = RpcSession::setup_unix_client(SOCKET)?;
let hello: Strong<dyn IHello> =
    <dyn IHello as FromIBinder>::try_from(session.get_service("hello")?)?;
let echo: Strong<dyn IEcho> =
    <dyn IEcho  as FromIBinder>::try_from(session.get_service("echo")?)?;
}

add_service is O(1) per call: the directory is built once and shares the server's service map, so later registrations — even after the server is already serving — are seen through the same root with no rebuild. Mixing set_root with add_service on the same server is not supported (the last one installed wins); pick one publishing model per server.

For registering and looking up services with the same code over kernel binder or RPC, see Cross-Transport Services.

Wire-protocol profiles

rsbinder speaks two RPC wire profiles, chosen at connect time:

ProfileDefault?Constructor
Android 12 (r34)yesRpcSession::setup_unix_client(path)
Android 13–16opt-inRpcSession::setup_unix_client_android13plus(path, max_version)

The android-13+ profile is the protocol real AOSP libbinder speaks today. It runs a handshake on connect and negotiates a wire version:

max_versionAOSP version
0Android 13
1Android 14 / 15
2Android 16

The server picks the highest version both sides advertise:

#![allow(unused)]
fn main() {
// Server: offer up to android-16 wire v2.
let server = RpcServer::setup_unix_server("/tmp/foo.sock")?;
server.set_android13plus(2);
server.set_root(my_root);

// Client: offer up to v2. Negotiation picks min(client, server).
let session = RpcSession::setup_unix_client_android13plus(
    "/tmp/foo.sock", 2,
)?;
}

Both stacks have been validated end-to-end against real AOSP libbinder on Android 13/14/15/16 emulators (full parcel-body transact, byte-correct), so an rsbinder RPC server can serve a real Android libbinder client and vice versa.

Transports

RpcSession/RpcServer are transport-agnostic. The bundled backends are:

BackendFeature flagTrust boundary
Unix socketrpc (always on)Filesystem perms + SO_PEERCRED/getpeereid
vsockrpc-vsockHypervisor VM isolation (host ↔ VM)
TLS / TCPrpc-tlsTLS certificate chain (caller-owned rustls)
Plain TCPrpc-tcp-debugNone — debug/interop only, never production

Add the matching feature in Cargo.toml:

[dependencies]
rsbinder = { version = "0.8", features = ["rpc", "rpc-vsock"] }

Each backend implements the RpcTransport trait — you can implement your own if you need a custom carrier.

TLS client

#![allow(unused)]
fn main() {
use rsbinder::rpc::{rustls, RpcSession};
use std::sync::Arc;

let mut roots = rustls::RootCertStore::empty();
// ... populate `roots` from your trust anchors ...
let config = Arc::new(
    rustls::ClientConfig::builder()
        .with_root_certificates(roots)
        .with_no_client_auth(),
);

// Third argument must be `Arc<rustls::ClientConfig>` — `setup_tcp_client_tls`
// takes the config by shared ownership so it can be cloned per connection.
let session = RpcSession::setup_tcp_client_tls(
    "binder.example.com:9999",
    "binder.example.com",
    config,
)?;
let root = session.get_root()?;
}

rsbinder::rpc::rustls is the exact rustls version the rpc-tls backend links against, re-exported so you don't have to track it in your own Cargo.toml.

TLS server

The server side takes a rustls::ServerConfig carrying its certificate chain and private key. RpcServer::setup_tcp_server_tls runs the TLS handshake on each connection's own worker thread (so a slow-handshake peer stalls only its worker, never the accept loop), then serves the same session as any other backend:

#![allow(unused)]
fn main() {
use rsbinder::rpc::{rustls, RpcServer};
use rustls::pki_types::pem::PemObject;
use rustls::pki_types::{CertificateDer, PrivateKeyDer};
use std::sync::Arc;

// Server certificate chain + private key, loaded from PEM.
let certs: Vec<_> =
    CertificateDer::pem_slice_iter(SERVER_CERT_PEM.as_bytes()).collect::<Result<_, _>>()?;
let key = PrivateKeyDer::from_pem_slice(SERVER_KEY_PEM.as_bytes())?;

let config = Arc::new(
    rustls::ServerConfig::builder()
        .with_no_client_auth()              // one-way TLS — see "Who needs a key?"
        .with_single_cert(certs, key)?,
);

// TCP is TLS-only by design: there is no plaintext-TCP server constructor.
let server = RpcServer::setup_tcp_server_tls("0.0.0.0:9999", config)?;
server.set_root(my_root_binder);
server.run()?;                              // or server.run_background()
}

RpcServer::setup_unix_server_tls and (with rpc-vsock) RpcServer::setup_vsock_server_tls do the same over a Unix socket / vsock — TLS is orthogonal to the socket kind (it mirrors AOSP's RpcTransportCtx::newTransport(fd)).

Who needs a key?

ModeServerClient
One-way TLS (default)cert chain + private key (always)trusted roots only — no key
mTLS (mutual)cert chain + private key and a client-cert verifiercert chain + private key
  • One-way TLS is the common case. The server proves its identity with its certificate; the client only verifies that certificate against its RootCertStore (with_no_client_auth() on both configs above). The client needs no key of its own — exactly like a browser connecting to an HTTPS site.

  • mTLS additionally authenticates the client. Build the server config with a client-cert verifier (rustls::server::WebPkiClientVerifier::builder(Arc::new(roots)).build()?.with_client_cert_verifier(verifier)) instead of .with_no_client_auth(), and give the client a cert + key (.with_client_auth_cert(client_certs, client_key)? instead of the client's .with_no_client_auth()). The handshake then fails unless the client presents a certificate the verifier trusts, and the server sees that client's leaf cert as its PeerIdentity::Certificate.

Either way the certificate check happens during the TLS handshake: an untrusted, missing, or expired certificate fails the connection before a single RPC byte is exchanged. rsbinder never invents crypto — all key / cert / root management and verification are your rustls config and rustls itself.

Security

RPC is not a drop-in for kernel binder's security model. Kernel binder gives you a kernel-vouched uid/pid and SELinux for free; RPC's trust boundary is the transport itself. This section covers the RPC trust boundaries; for the full handler-side authorization story across both transports — calling_caller(), @EnforcePermission over RPC, uid ACLs, PermissionAuthority — see Security & Authorization.

Each transport defines its own trust boundary, surfaced as a PeerIdentity:

  • Local { uid, pid } — Unix socket peer with kernel-vouched credentials.
  • Vsock { cid } — VM context id (a routing address, not an ACL basis on its own — the trust comes from hypervisor isolation).
  • Certificate(CertId) — TLS peer authenticated by its leaf cert.
  • Anonymous — no identity at all. ACL is impossible against an anonymous peer. The debug TCP backend always returns this, and any backend falls back to it (logged loudly) if peer-credential resolution fails — so an authorizer must treat Anonymous as deny.

Use [RpcServer::set_authorizer] to enforce a per-connection policy. The closure runs once on accept, before any RPC byte is exchanged:

#![allow(unused)]
fn main() {
const ALLOWED_UID: u32 = 1000;

server.set_authorizer(|peer| {
    peer.uid() == Some(ALLOWED_UID)
});
}

A rejected peer's socket is closed; its next operation sees DeadObject. The hook is opt-in — without it, every connection is accepted (matching the prior behaviour).

set_authorizer is connection-level (decided once, at handshake) — the right granularity for vsock and TLS, which carry no per-call uid. For per-method authorization, a handler can also read the caller directly: over a Unix-domain RPC connection rsbinder::get_calling_uid() returns the kernel-vouched peer uid (and calling_caller() the full transport-tagged identity), exactly as on kernel binder — so a uid ACL written once runs on both. Over a uid-less transport get_calling_uid() is the fail-closed u32::MAX sentinel. See Security & Authorization.

Capabilities

The RPC stack is feature-complete for the everyday cases you'd use kernel binder for, with a few extras specific to socket transport:

  • AIDL interfaces — every type the AIDL compiler supports works over RPC: primitives, strings, arrays, parcelables, nested interfaces, enums, unions, oneway methods.
  • Callbacks (nested binders) — a callback object created on the client crosses the socket like any other Binder and the server invokes it back through the same session.
  • ParcelFileDescriptor — opt in with RpcSession::negotiate_fd_transport and RpcServer::set_supported_fd_modes. File descriptors ride out-of-band over SCM_RIGHTS on Unix-domain sockets (android-14+ wire required).
  • Death notifications — link a DeathRecipient on the proxy as usual. A session whose socket disconnects fires every linked recipient (the RPC analogue of "the remote process died").
  • Async — the same into_async::<Tokio>() adapter that wraps a blocking kernel-binder proxy works over RPC. See Async Service — for RPC the only difference is how you obtain the proxy.

Sessions with multiple connections

RpcServer::set_max_threads(N) matches AOSP's setMaxIncomingThreads. Both N == 1 (the default — one connection per session, the mode every example in the book uses) and N >= 2 (multi-connection sessions) are validated against real Android 13–16 libbinder peers. See the rustdoc on RpcServer::set_max_threads for the per-mode details.

set_max_threads caps incoming slots per session. To cap server-wide concurrent connections — for example to bound the worker-thread fan-out regardless of how many sessions a single client opens — use RpcServer::set_max_connections(N) (default: unlimited). Both knobs are independent and additive.

Bridging RPC and the service manager: the Accessor pattern

Android 16 introduced IAccessor — a kernel-binder interface whose sole job is to hand a client a connected RPC socket fd. The client asks the system service manager for the accessor (a kernel-binder service), calls addConnection(), and the returned ParcelFileDescriptor is the RPC socket the client then drives through the android-13+ handshake.

rsbinder implements both sides of this pattern:

  • Consume sidehub::get_service(name) transparently follows the IAccessor arm: if the service manager hands back an IAccessor instead of a regular binder, rsbinder calls addConnection(), adopts the fd, runs the v2 handshake, and gives you back the RPC root. Your client code looks identical to a regular hub::get_service call.

  • Register sidehub::android_16::create_accessor(instance, addr_provider) builds a LocalAccessor BnAccessor you can publish via hub::add_service. The provider closure resolves an instance name to an AccessorSockAddr (Unix(path), Vsock { cid, port }, or Inet(addr)), and the accessor opens the connection on demand:

    #![allow(unused)]
    fn main() {
    use rsbinder::hub::{self, android_16::{create_accessor, AccessorSockAddr}};
    use rsbinder::rpc::RpcServer;
    use rsbinder::ProcessState;
    use std::path::PathBuf;
    
    // 0. The kernel-binder side needs ProcessState to publish the
    //    accessor through the system service manager. The RPC server
    //    itself does NOT (it runs entirely in user space).
    ProcessState::init_default()?;
    ProcessState::start_thread_pool();
    
    // 1. Run a regular RPC server on a UDS.
    let sock = PathBuf::from("/data/local/tmp/my.sock");
    let server = RpcServer::setup_unix_server(&sock)?;
    server.set_android13plus(2);
    server.set_root(my_root);
    let _bg = server.run_background();
    
    // 2. Vend an IAccessor that hands clients an fd connected to
    //    `sock`, and publish it through the kernel service manager.
    let path = sock.clone();
    let accessor = create_accessor("my.service", Box::new(move |_name| {
        Ok(AccessorSockAddr::Unix(path.clone()))
    }));
    hub::add_service("my.service", accessor)?;
    
    // 3. Block on the kernel binder thread pool so the accessor stays
    //    reachable for the lifetime of the process.
    ProcessState::join_thread_pool()?;
    }

Use hub::android_16::add_accessor_provider when you want a process-local accessor — one that doesn't go through the system service manager. Lookups via hub::get_service in the same process fall back to the process-local registry when the service manager returns nothing.

Both sides of the Accessor pattern have been validated against real Android 16 libbinder on the emulator.

Async over RPC

The blocking I/O the RPC stack uses is deliberate — it matches the AOSP RpcServer/RpcSession model, where each connection has its own pair of blocking threads. The standard spawn_blocking/block_on adapters that already wrap kernel-binder async work for RPC too:

#![allow(unused)]
fn main() {
use rsbinder::Tokio;

let session = RpcSession::setup_unix_client(RPC_SOCKET)?;
let root = session.get_root()?;
let hello: Strong<dyn IHello> =
    <dyn IHello as FromIBinder>::try_from(root)?;

// Convert the blocking proxy into the async one.
let async_hello = hello.into_async::<Tokio>();

let reply = async_hello.echo("hi").await?;
}

For services, Bn*::new_async_binder(impl, TokioRuntime(handle)) works exactly like it does on the kernel path. See Async Service.

There is intentionally no non-blocking RpcTransport / reactor-based async serve loop. Decision record: plans/2-10-async-rpc-io.md in the repo.

Platform support

PlatformKernel binderRPC
LinuxYes (with binderfs)Yes (Unix, vsock, TLS)
AndroidYes (built in)Yes
macOSNoYes (Unix, TLS — for development & cross-stack interop testing)
WindowsNoNo (untested)

macOS support for RPC means you can develop, run, and test RPC-only rsbinder applications on a macOS host without needing a Linux VM. The kernel binder code paths remain Linux/Android only.

Further reading

  • Async Service — applying the into_async::<Tokio>() and new_async_binder patterns over RPC.
  • Callbacks and Interfaces — nested binders and death recipients work the same over RPC as kernel binder.
  • ParcelFileDescriptor — FD passing with the android-14+ wire and SCM_RIGHTS.
  • rsbinder::rpc module on docs.rs/rsbinder for the full API surface (every public function, struct, and trait introduced above is documented in detail there).

Cross-Transport Services

The AIDL interface, the generated Bp*/Bn* stubs, your service impl, and the call sites are already transport-agnostic — the same impl IFoo runs unchanged over kernel binder or RPC. What differs is only the bootstrap: kernel binder uses ProcessState + the hub service manager, while RPC uses RpcServer / RpcSession.

The rsbinder::service module is an optional, additive layer that makes that bootstrap transport-agnostic too, so you can write registration and lookup code once and pick the transport by construction. It is a thin typed wrapper over the existing APIs — those remain the direct path and are unchanged.

The traits

#![allow(unused)]
fn main() {
use rsbinder::service::{Registry, Broker};
}
  • Registry (server side) — add_service(name, binder).
  • Broker (client side) — lookup(name) plus a generic get_interface::<T>(name) convenience that does the interface_cast.

Only the genuine kernel∩RPC intersection is on the traits. Kernel-only powers (list_services, service notifications, lazy services) stay on the concrete kernel types / the hub module — they are not hidden behind the trait, nor faked on RPC.

Typed hosts and brokers

Each transport is a distinct type, so the differences stay visible at the call site:

Server (Registry)Client (Broker)
kernel binderservice::kernel::Hostservice::kernel::Broker
RPC (#[cfg(feature = "rpc")])service::rpc::Hostservice::rpc::Broker
#![allow(unused)]
fn main() {
use rsbinder::service::{kernel, rpc, Registry, Broker};
use rsbinder::{SIBinder, Strong};

// Register once — generic over the transport.
fn register_all<R: Registry>(reg: &R, svc: SIBinder) -> rsbinder::Result<()> {
    reg.add_service("hello", svc)
}

// Look up + cast once — generic over the transport.
fn talk<B: Broker>(broker: &B) -> rsbinder::Result<Strong<dyn IHello>> {
    broker.get_interface("hello")
}
}

Picking the transport is one line:

#![allow(unused)]
fn main() {
// Server
let host = kernel::Host::new()?;            // kernel binder (process-global)
// let host = rpc::Host::unix("/tmp/x.sock")?;  // RPC over a Unix socket
register_all(&host, BnHello::new_binder(MyService).as_binder())?;
host.serve()?;                              // see "serve()" below

// Client
let broker = kernel::Broker::new()?;        // or rpc::Broker::unix("/tmp/x.sock")?
let hello = talk(&broker)?;
hello.echo("hi")?;
}

Why typed pairs, not one Endpoint enum

serve() means different things per transport, the kernel host is a process-global singleton while an RPC host is a real instance, and the construction options don't overlap. Keeping them as distinct types makes a wrong-transport option a compile error rather than a silent no-op, and keeps the singleton nature of the kernel side honest.

  • kernel::Host::new() is a process-global, idempotent handle over ProcessState. kernel::Host::serve() joins the process-wide binder thread pool (blocks). A second host in the same process reuses the existing ProcessState; a conflicting re-init (different driver / max_threads) logs a warning rather than silently dropping the request.
  • rpc::Host::unix(path) binds one socket. rpc::Host::serve() drives that one socket (and serve_background() returns a JoinHandle).

Each host has a builder() for its own options — kernel: driver path, max_threads, call restriction; RPC: max_threads, max_connections, authorizer. RPC-only powers (TLS, fd modes, vsock, the connection counters) stay reachable via host.server().

Security note

Moving a service between transports changes its trust boundary. Before relying on the facade, read Security & Authorization: @EnforcePermission denies over RPC, and get_calling_uid() is the kernel-vouched peer uid on Unix RPC (fail-closed on uid-less transports). The facade makes the transport swap easy; it does not make the security models the same.

Worked example

example-hello ships bin/unified_service.rs / bin/unified_client.rs: identical service, registration, and call code with the transport chosen by one kernel::Host::new() vs rpc::Host::unix(path) line.

cargo run -p example-hello --features rpc --bin unified_service rpc
cargo run -p example-hello --features rpc --bin unified_client rpc

Security & Authorization

A service handler often needs to answer "who is calling, and are they allowed to?" rsbinder gives you the caller's identity and several ways to authorize — but the right tool differs by transport, and that difference is deliberately made explicit rather than hidden.

Core principle. Kernel binder and RPC have genuinely different trust boundaries. Kernel binder gives you a kernel-vouched uid/pid and SELinux context; RPC's trust boundary is the transport itself (Unix peer credentials, a TLS certificate, or hypervisor VM isolation). rsbinder never papers over this: an authorization check that is safe on kernel binder either keeps working, fails closed, or is denied over RPC — never silently weakened.

Caller identity inside a handler

The calling context is ambient thread-local state, set by the dispatch machinery just before your handler runs and cleared just after (this mirrors AOSP's IPCThreadState). So inside a transact handler you read it with no parameter threading:

#![allow(unused)]
fn main() {
use rsbinder::{Caller, ExceptionCode, Status};

impl IExample for MyService {
    fn do_thing(&self, arg: i32) -> rsbinder::status::Result<()> {
        let uid = rsbinder::get_calling_uid();   // who is calling, right now
        // ... authorize, then act ...
        Ok(())
    }
}
}

The accessors (all rsbinder::…):

AccessorReturnsWorks over
get_calling_uid()caller effective uidkernel binder, Unix RPC
get_calling_pid()caller pidkernel binder, Unix RPC
get_calling_sid()SELinux context (Option<CString>)kernel binder only (RPC → None)
is_handling_transaction()boolkernel binder, RPC
calling_caller()Option<Caller> (transport-tagged)kernel binder, RPC

These are only meaningful inside a handler, on the dispatching thread. Outside a transaction they return 0 / None. If you spawn a new thread inside a handler, that thread has no calling context (it reads 0) — just like AOSP.

Fail-closed values, never fabricated ones

get_calling_uid() returns a real uid only where the transport carries one — kernel binder, and Unix-domain RPC (SO_PEERCRED on Linux/Android, getpeereid on macOS/BSD). Over a transport with no uid (vsock, TLS certificate, anonymous) it returns the sentinel u32::MAX, which is never 0 (root) and never a real privileged uid:

#![allow(unused)]
fn main() {
// Over vsock / TLS / anonymous, get_calling_uid() == u32::MAX:
if rsbinder::get_calling_uid() == 0 {      // u32::MAX != 0  ⇒ never matches
    // ... "allow root" ...                // so a uid-less peer can't slip through
}
if rsbinder::get_calling_uid() != AID_SYSTEM {
    return Err(Status::from(ExceptionCode::Security));  // uid-less peer ⇒ denied
}
}

This is intentional: fabricating a uid (say 0) for a transport that doesn't have one would be a privilege-escalation hole. uid ACLs simply fail closed over uid-less transports — use a transport-appropriate check there instead (see below).

The transport-tagged caller

For anything beyond a plain uid check, match on Caller, which tags the identity by transport so you authorize explicitly:

#![allow(unused)]
fn main() {
use rsbinder::{Caller, ExceptionCode, Status};
use rsbinder::rpc::PeerIdentity;

fn authorize() -> rsbinder::status::Result<()> {
    match rsbinder::calling_caller() {
        // Kernel binder: Android permission, uid, or SELinux `sid`.
        Some(Caller::Kernel { uid, .. }) if uid == 1000 => Ok(()),
        // Unix RPC: kernel-vouched peer uid.
        Some(Caller::Rpc(PeerIdentity::Local { uid, .. })) if uid == 1000 => Ok(()),
        // TLS RPC: certificate subject allowlist.
        Some(Caller::Rpc(PeerIdentity::Certificate(cert)))
            if cert.subject() == "CN=trusted-client" => Ok(()),
        // vsock cid, anonymous, no transaction, or a future variant:
        // fail closed.
        _ => Err(Status::from(ExceptionCode::Security)),
    }
}
}

Caller and PeerIdentity are #[non_exhaustive], so the compiler forces a catch-all arm — which nudges you toward a fail-closed default.

Four ways to authorize

WhenUse
uid check, common to kernel + Unix RPCget_calling_uid()
different rule per transport (uid / cert / cid / sid)match calling_caller()
Android permission on a kernel service@EnforcePermission(...) (declarative)
one policy across many methods / introducing RPC authzPermissionAuthority (injected)

@EnforcePermission (declarative, kernel-only)

Annotate the AIDL method and the generated code checks Android's PermissionManagerService before your handler runs — no authorization code in the body:

interface IExample {
    @EnforcePermission("android.permission.INTERNET")
    void doNetworking();
}

Over RPC this method is denied (returns EX_SECURITY), unconditionally and regardless of process shape. Android permissions are a kernel-binder concept; the RPC dispatch path has no PMS-backed uid, so granting would mean granting root to any anonymous peer. rsbinder fails closed instead. For RPC authorization use the transport-native means above.

PermissionAuthority (injected, cross-transport policy)

To back every @EnforcePermission check with one centralized, transport-aware policy — for example to introduce authorization over RPC that the default denies — install a PermissionAuthority at startup:

#![allow(unused)]
fn main() {
use rsbinder::Caller;
use rsbinder::permission_controller::{PermissionAuthority, set_permission_authority};
use rsbinder::rpc::PeerIdentity;
use std::sync::Arc;

struct MyPolicy;
impl PermissionAuthority for MyPolicy {
    fn check(&self, permission: &str, caller: &Caller) -> bool {
        match caller {
            Caller::Rpc(PeerIdentity::Local { uid, .. }) => *uid == 1000,
            Caller::Rpc(PeerIdentity::Certificate(c)) => c.subject() == "CN=admin",
            _ => false,   // fail closed
        }
    }
}

set_permission_authority(Arc::new(MyPolicy));
}

When installed, the authority owns the whole decision for every generated check_permission call, on every transport, receiving the transport-tagged Caller. With no authority installed, the default is unchanged: kernel → PMS, RPC → deny.

The core crate ships only this slot, never a policy. Token/JWT formats, certificate→permission tables, and uid→permission maps are deployment concerns. Caveat: an installed authority also replaces the kernel PMS path — if you want "kernel = PMS, RPC = custom", handle the Caller::Kernel arm explicitly (most deployments that inject an authority are pure-RPC, where that arm never fires).

Connection-level authorization (RPC)

For RPC, the most natural granularity is often the whole connection, decided at handshake before any transaction — this is the right tool for vsock and TLS, which carry no per-call uid. Use RpcServer::set_authorizer:

#![allow(unused)]
fn main() {
server.set_authorizer(|peer| match peer {
    rsbinder::rpc::PeerIdentity::Certificate(cert) => cert.subject() == "CN=trusted",
    rsbinder::rpc::PeerIdentity::Vsock { cid } => *cid == TRUSTED_VM_CID,
    _ => false,
});
}

A rejected peer's socket is closed before any RPC byte is exchanged.

Worked example

example-hello ships a runnable handler-authorization demo — bin/authz_service.rs / bin/authz_client.rs:

cargo run -p example-hello --features rpc --bin authz_service
cargo run -p example-hello --features rpc --bin authz_client
# whoami    -> OK: unix-rpc caller uid=1000 pid=12345
# adminOnly -> DENIED (Security)

whoami() authorizes any identifiable local caller and reports it; adminOnly() requires uid 0 and denies a normal user — demonstrating both the allow and the fail-closed deny path.

Summary

  • Read the caller with get_calling_uid/pid/sid and calling_caller() — ambient thread-local state, valid only inside a handler.
  • uid ACLs work on kernel binder and Unix RPC, and fail closed (u32::MAX sentinel) on uid-less transports.
  • @EnforcePermission is kernel-only and denies over RPC.
  • Use match calling_caller() for transport-aware rules, set_authorizer for connection-level RPC policy, and PermissionAuthority to centralize.
  • The default everywhere is fail-closed; weakening a boundary is always an explicit, opt-in choice.

Stability tiers

rsbinder uses a three-tier model for public-API stability on the path to 1.0. Each tier states what kind of change can land between releases and what callers can rely on.

TierVersioningWire compatibilityWhen it can change
StableSemver-strictAOSP-faithful, byte-tested against real libbinderOnly on a major bump. Breaking changes are last-resort.
ProvisionalStabilizing toward 1.0AOSP-faithful in the validated cases (hermetic + real-libbinder STAGE3)May break in a minor bump if 1.0 review surfaces an issue. Will be re-classified as Stable or Experimental at 1.0.
ExperimentalOpt-in via Cargo featureNo guarantee until the corresponding real-libbinder interop gate passesAnytime, including the wire format. Default builds do not reach this code.

Stable

Surface that has shipped through multiple releases against real Android binder and is not expected to break.

  • Kernel binder coreParcel, IBinder, Interface, Strong<I>, Weak<I>, Status, StatusCode, ParcelFileDescriptor, ProcessState::{init, init_default, start_thread_pool, join_thread_pool}, Binder::new, BinderFeatures.
  • Transaction code constantsFIRST_CALL_TRANSACTION, LAST_CALL_TRANSACTION, PING_TRANSACTION, INTERFACE_TRANSACTION, DUMP_TRANSACTION, FLAG_ONEWAY, FLAG_CLEAR_BUF.
  • Service manager (Android binder)hub::add_service, hub::get_service, hub::check_service, hub::get_interface.
  • AIDL compiler entry pointsrsbinder_aidl::Builder::{new, source, output, version, generate}.
  • rsb_device binary CLI (binderfs setup).

Provisional

Implemented and validated, but the public-API shape is still under review for 1.0. Expect at most a single round of renames or signature tweaks; wire formats are already locked.

  • RPC transportRpcServer, RpcSession, setup_unix_server, setup_unix_client*, from_preconnected_fd, set_root, get_root, set_android13plus, set_max_threads(N) (both N == 1 and N >= 2 multi-connection), set_max_connections, set_supported_fd_modes(&[FileDescriptorTransportMode::Unix]). Validated against real android-13 / 14 / 15 / 16 libbinder (plans 2-1 … 2-12 / 2-13 / 2-14 STAGE3).
  • FD-over-RPC (v1+ AOSP-faithful, AC-11.3 gate passed).
  • RPC death notification (session-disconnect, AOSP-faithful).
  • IAccessor client / server (plans 2-13 D.8 + 2-14 D.9 STAGE3 passed against real libbinder).
  • rsb_hubaddService descriptor auto-detect, getService2, checkService2, Service::Accessor routing. Linux-native bringup.
  • macOS first-class support (plan 2-9 Phase A+B).

Experimental (opt-in)

Off by default, gated by a Cargo feature. Wire format and API may change without a deprecation cycle.

FeatureSurfaceStatus
rpc-tcp-debugTcpDebugTransport — plain TCP backendBring-up / interop only, never production.
rpc-vsockVsockTransport — host↔VMLinux/Android only; loopback testing requires vsock_loopback.ko.
rpc-tlsTLS over rustls, socket-orthogonal (tcp / unix / vsock)Hermetic green; the StreamOwned decoupling (plan 2-15) has landed. Real-libbinder STAGE3 not yet attempted — stock emulator images ship no libbinder_tls.

Non-goals

Surface that rsbinder will not support, even after 1.0. Listed here so contributors don't burn cycles implementing something that has already been ruled out — and so the AIDL compiler's deliberate rejection of these types reads as intentional rather than as a gap.

  • AIDL Map<K, V> — AOSP's own Rust / C++ / NDK backends reject Map<K, V> at the language layer (aidl_language.cpp:1612-1615: "Currently, only Java backend supports Map."), and have done so since 2019. The wire format is built on Java Parcel's runtime type-tag system (VAL_STRING, VAL_INTEGER, VAL_MAP, …) which has no typed-Rust analogue. The rsbinder-aidl frontend therefore emits unknown type 'Map' for any Map<…> reference — matching AOSP. Use parcelable (typed struct), List<Entry>, or ParcelableHolder instead. Re-examine if AOSP removes the cross-backend block.

What this means in practice

  • A 0.x → 0.(x+1) minor bump can change Provisional signatures. rsbinder runs cargo-semver-checks on every PR for both rsbinder and rsbinder-aidl (default features and RPC features), so any such change is visible on the PR before merge.
  • Experimental features never break the default build's wire format. Enabling one is an explicit acknowledgement that the corresponding interop gate has not passed.
  • After 1.0, all Stable items follow Semver strictly. Provisional items get re-classified as Stable or moved into a feature gate.

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 (binderfs devices are not persistent —
# re-run this after each reboot)
$ 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.
# Pick the v<major>.x directory that matches your kernel: today this
# is typically v6.x on Fedora/Stream; older RHEL 8/9 hosts may still
# be on v5.x or v4.x.
$ KERNEL_VERSION=$(uname -r | sed 's/\.el.*$//')
$ KERNEL_MAJOR=$(echo $KERNEL_VERSION | cut -d. -f1)
$ wget https://cdn.kernel.org/pub/linux/kernel/v${KERNEL_MAJOR}.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 10 (API 29): android_10 feature
  • 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.

Android 10 caveat: Android 10 uses the legacy C IServiceManager (the AIDL-based interface only landed in Android 11). The android_10 dispatch supports get_service, check_service, add_service, and list_services. APIs introduced later — is_declared, register_for_notifications, unregister_for_notifications, and get_service_debug_info — return false or StatusCode::UnknownTransaction so callers can detect the gap rather than silently misbehave.

Feature Flag Configuration

In your Cargo.toml, specify the Android versions you want to support:

[dependencies]
rsbinder = { version = "0.8", features = ["android_14_plus"] }

Available feature combinations:

  • android_10_plus: Supports Android 10 through 16
  • 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. It must contain exactly three bare lines in this order — the cargo-ndk target, the Rust target architecture, and the remote directory on the device. The file is read line by line, so do not add inline comments or blank lines:

arm64-v8a
aarch64
/data/rsbinder

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