AIDL IPC Practice and Binder Source Code Analysis
In the previous article, we disassembled Binder theoretically, covering the mmap one-time copy, kernel driver scheduling, and its security model. However, theory is merely on paper—when you actually write your first line of AIDL code, your mind might still be brimming with questions: What exactly are those Stub and Proxy files the compiler auto-generates? What happens behind the scenes after bindService? Why can the "remote object" retrieved by the client be invoked just like a local method?
This article will follow the mainline of a complete AIDL cross-process calculator service, starting from practical code to progressively peel back the layers of AIDL's auto-generated code, ultimately plunging into the Binder framework's kernel source code to definitively answer these questions.
Practical Scenario: Cross-Process Calculator Service
We are going to construct the most classic cross-process communication scenario: A Client App (Activity) uses bindService to bind to a Calculator Service running in an independent process, and then performs a cross-process invocation of its add method.
This is akin to calling an accountant (Server) located in another city, asking them to compute an equation (add), and waiting for them to report the result back to you. You are separated by a vast distance (process isolation), but the telephone system (Binder) makes it feel like a face-to-face conversation.
Step 1: Define the AIDL Interface (The Communication Contract)
Create an interface file in the src/main/aidl/com/example/calculator/ directory:
// ICalculator.aidl
package com.example.calculator;
// This is the "communication contract" between the client and the server.
// Upon compilation, Android tools automatically generate the Stub and Proxy boilerplate code.
interface ICalculator {
/** Cross-process addition operation */
int add(int a, int b);
/** Cross-process subtraction operation */
int subtract(int a, int b);
}
An AIDL file is fundamentally a protocol declaration—it tells the compiler: "The methods in this interface need to support cross-process invocation; please generate all the boilerplate code for serialization/deserialization for me."
Step 2: Server Implementation (The Call Receiver)
// CalculatorService.java — Runs in the :remote process
public class CalculatorService extends Service {
// Implements the Stub abstract class automatically generated by AIDL
// Stub inherits from Binder and represents the "true entity" of the server
private final ICalculator.Stub mBinder = new ICalculator.Stub() {
@Override
public int add(int a, int b) throws RemoteException {
Log.d("CalculatorService",
"add() invoked, current process PID: " + Process.myPid());
return a + b;
}
@Override
public int subtract(int a, int b) throws RemoteException {
return a - b;
}
};
@Override
public IBinder onBind(Intent intent) {
// Returns the Stub (the Binder entity) to the system.
// The system will pass its "reference" to the client.
return mBinder;
}
}
Declare this Service to run in an independent process in the AndroidManifest.xml:
<service
android:name=".CalculatorService"
android:process=":remote"
android:exported="true" />
android:process=":remote" is the crucial element here—it forces this Service to execute in a process completely distinct from the main App process, isolating their memory spaces entirely.
Step 3: Client Binding (The Caller)
// MainActivity.java — Runs in the main process
public class MainActivity extends AppCompatActivity {
private ICalculator mCalculator;
private boolean mBound = false;
// ServiceConnection is a system callback interface.
// Upon successful binding, the system passes the server's Binder reference here.
private final ServiceConnection mConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
// THE CORE MAGIC: Converts the raw IBinder into a directly callable interface.
// If cross-process: returns a Proxy object.
// If same-process: returns the Stub itself.
mCalculator = ICalculator.Stub.asInterface(service);
mBound = true;
Log.d("MainActivity", "Service connected");
}
@Override
public void onServiceDisconnected(ComponentName name) {
mCalculator = null;
mBound = false;
}
};
public void onBindClick(View v) {
Intent intent = new Intent(this, CalculatorService.class);
bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
}
public void onCalculateClick(View v) {
if (mBound) {
try {
// Looks like a local method call, but the data actually breaches the process boundary!
int result = mCalculator.add(42, 58);
Log.d("MainActivity",
"Calculation Result: " + result + ", current process PID: " + Process.myPid());
} catch (RemoteException e) {
e.printStackTrace();
}
}
}
@Override
protected void onDestroy() {
super.onDestroy();
if (mBound) unbindService(mConnection);
}
}
Running this code will reveal in the logs that the PID of the caller and the PID of the executing add() are different—proving the data has indeed traversed the process boundary.
Unveiling: What Did AIDL Automatically Generate?
After building the project, the Android build tools automatically generate a Java file for ICalculator.aidl. This file is the skeleton key to understanding Binder communication. Below is its core structure (non-essential code omitted):
public interface ICalculator extends IInterface {
// ========== Stub: The Server's Skeleton ==========
public static abstract class Stub extends Binder implements ICalculator {
// The unique identity token for the interface, used to verify communication matching
private static final String DESCRIPTOR = "com.example.calculator.ICalculator";
// Every method is assigned a unique transaction ID
static final int TRANSACTION_add =
IBinder.FIRST_CALL_TRANSACTION + 0; // ID 1
static final int TRANSACTION_subtract =
IBinder.FIRST_CALL_TRANSACTION + 1; // ID 2
public Stub() {
// Registers itself into Binder, associating with the DESCRIPTOR
this.attachInterface(this, DESCRIPTOR);
}
/**
* The core bridge method: Determines if it's same-process or cross-process.
* Same-process → Returns Stub itself directly (Zero overhead).
* Cross-process → Returns a Proxy wrapper (Routes through the IPC pipeline).
*/
public static ICalculator asInterface(IBinder obj) {
if (obj == null) return null;
// Attempt to query locally: If same-process, this hits immediately.
IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
if (iin != null && iin instanceof ICalculator) {
return (ICalculator) iin;
}
// Cross-process: Create the Proxy wrapper
return new Stub.Proxy(obj);
}
/**
* The Server's "Dispatch Center"
* Triggered when the driver delivers the request.
* Dispatches to the corresponding business method based on the transaction ID.
*/
@Override
public boolean onTransact(int code, Parcel data,
Parcel reply, int flags)
throws RemoteException {
switch (code) {
case TRANSACTION_add: {
data.enforceInterface(DESCRIPTOR); // Verify identity
int _arg0 = data.readInt(); // Deserialize parameters
int _arg1 = data.readInt();
int _result = this.add(_arg0, _arg1); // Invoke the real implementation
reply.writeNoException();
reply.writeInt(_result); // Serialize return value
return true;
}
case TRANSACTION_subtract: {
data.enforceInterface(DESCRIPTOR);
int _arg0 = data.readInt();
int _arg1 = data.readInt();
int _result = this.subtract(_arg0, _arg1);
reply.writeNoException();
reply.writeInt(_result);
return true;
}
}
return super.onTransact(code, data, reply, flags);
}
// ========== Proxy: The Client's Stand-in ==========
private static class Proxy implements ICalculator {
private IBinder mRemote; // Reference pointing to the remote Binder
Proxy(IBinder remote) { mRemote = remote; }
@Override
public IBinder asBinder() { return mRemote; }
@Override
public int add(int a, int b) throws RemoteException {
// Prepare two "data parcels"
Parcel _data = Parcel.obtain(); // Request parcel
Parcel _reply = Parcel.obtain(); // Response parcel
int _result;
try {
_data.writeInterfaceToken(DESCRIPTOR); // Write identity token
_data.writeInt(a); // Serialize parameters
_data.writeInt(b);
// ★ THE CORE CALL: Triggers Cross-Process Communication ★
// This line BLOCKS the current thread until the remote method executes and returns
mRemote.transact(Stub.TRANSACTION_add,
_data, _reply, 0);
_reply.readException();
_result = _reply.readInt(); // Deserialize return value
} finally {
_data.recycle();
_reply.recycle();
}
return _result;
}
@Override
public int subtract(int a, int b) throws RemoteException {
// Structure identical to add(), only transaction ID differs
Parcel _data = Parcel.obtain();
Parcel _reply = Parcel.obtain();
int _result;
try {
_data.writeInterfaceToken(DESCRIPTOR);
_data.writeInt(a);
_data.writeInt(b);
mRemote.transact(Stub.TRANSACTION_subtract,
_data, _reply, 0);
_reply.readException();
_result = _reply.readInt();
} finally {
_data.recycle();
_reply.recycle();
}
return _result;
}
}
}
// Interface method declarations
int add(int a, int b) throws RemoteException;
int subtract(int a, int b) throws RemoteException;
}
Deciphering the Core Architecture
asInterface: The Watershed between Same-Process and Cross-Process
asInterface is the most ingenious method in the entire AIDL architecture. It internally invokes queryLocalInterface(DESCRIPTOR) to detect: Is the passed IBinder a "local Stub entity" or a "remote BinderProxy reference"?
- Same-process:
queryLocalInterfacehits, directly returning the Stub itself. Method invocation has zero IPC overhead, equivalent to a standard Java method call. - Cross-process:
queryLocalInterfacereturnsnull, prompting the creation of the Proxy wrapper. Subsequent method calls will travel the full IPC pipeline: Serialize →transact→ Driver →onTransact→ Deserialize.
This architecture totally shields the upper-layer caller from needing to know which process the recipient resides in—the invocation syntax is identical, and the IPC complexity is ruthlessly encapsulated below.
Proxy's transact: The Launchpad for Cross-Process Calls
When the client executes mCalculator.add(42, 58), it is practically executing Proxy.add(). This method performs three actions:
- Marshaling: Serializing the method parameters into a
Parcel—a highly efficient binary serialization format. - Firing the Transaction: Invoking
mRemote.transact(), wheremRemoteis actually aBinderProxyobject. - Waiting and Unmarshaling:
transactblocks the current thread until the server finishes processing. Then, it extracts the result from_reply.
Stub's onTransact: The Server's Dispatch Center
After the Binder driver wakes up the server thread, the framework invokes Stub.onTransact(). Based on the transaction ID (TRANSACTION_add = 1), it executes a switch-case dispatch, deserializes the parameters from the Parcel, invokes the real business method implemented by the developer, and serializes the result back into the reply Parcel.
Deep Dive: From transact to the Kernel Driver
Having decoded the auto-generated code, we will trace the mRemote.transact() line further down to witness how data physically traverses the process boundary.
sequenceDiagram
participant App as Client Proxy.add()
participant BP as BinderProxy (Java)
participant JNI as android_util_Binder.cpp
participant IPC as IPCThreadState (C++)
participant Driver as /dev/binder (Kernel)
participant SIPC as IPCThreadState (Server)
participant Stub as Stub.onTransact()
App->>BP: transact(TRANSACTION_add, data, reply, 0)
BP->>JNI: transactNative() [JNI Call]
JNI->>IPC: BpBinder::transact()
IPC->>IPC: writeTransactionData() writes to mOut buffer
IPC->>Driver: ioctl(BINDER_WRITE_READ)
Note over Driver: copy_from_user copies data<br/>Find target process binder_node<br/>Construct binder_transaction<br/>Enqueue to Server todo list and Wake Up
Driver->>SIPC: ioctl returns BR_TRANSACTION
SIPC->>Stub: BBinder::onTransact() → Java onTransact()
Note over Stub: Deserialize params, execute add(), serialize result
Stub-->>SIPC: Write reply Parcel
SIPC-->>Driver: ioctl(BC_REPLY)
Driver-->>IPC: Wake up Client, return reply data
IPC-->>JNI: Return
JNI-->>BP: Return
BP-->>App: Return result = 100
Layer 1: Java → Native (The JNI Traverse)
BinderProxy.transact() internally invokes transactNative()—a native method. Through the JNI bridge, control transfers from the Java VM to the C++ android_os_BinderProxy_transact function.
At the Native layer, BinderProxy corresponds to BpBinder (the C++ implementation of Binder Proxy). BpBinder::transact() subsequently calls IPCThreadState::self()->transact().
Layer 2: IPCThreadState (The Native Communication Engine)
IPCThreadState is a thread-level singleton; it serves as the core communication engine in the Native layer. Its transact() method performs two critical tasks:
Write Request: Calls writeTransactionData(BC_TRANSACTION, ...) to encapsulate the transaction data (method ID, Parcel data) into a binder_transaction_data struct, appending it to its internal mOut write buffer.
Dialogue with Driver: Calls waitForResponse() → talkWithDriver(). The latter constructs a binder_write_read struct and pushes the data into the kernel via the ioctl(mProcess->mDriverFD, BINDER_WRITE_READ, &bwr) system call. At this precise moment, the current thread is suspended, awaiting the server's reply.
// Simplified IPCThreadState.cpp core logic
status_t IPCThreadState::talkWithDriver(bool doReceive) {
binder_write_read bwr;
bwr.write_buffer = (uintptr_t)mOut.data();
bwr.write_size = mOut.dataSize();
bwr.read_buffer = (uintptr_t)mIn.data();
bwr.read_size = mIn.dataCapacity();
// This ioctl call BLOCKS until the kernel returns data
ioctl(mProcess->mDriverFD, BINDER_WRITE_READ, &bwr);
return NO_ERROR;
}
Layer 3: Binder Kernel Driver (The Ultimate Data Mover)
Once the ioctl system call penetrates the kernel, the Binder driver (drivers/android/binder.c) commandeers everything. The core function binder_transaction() executes these steps:
1. Target Addressing: Based on the handle passed by the client, it traverses its internal Red-Black tree to find the binder_node corresponding to the target Server process, thereby locating the target binder_proc struct.
2. Allocate Receive Buffer: Within the Server process's kernel buffer (pre-mapped via mmap), it allocates a chunk of space to hold the current transaction's data.
3. One-Time Copy: It calls copy_from_user() to copy the data from the Client's user space directly into the allocated buffer. Because this buffer is simultaneously mapped to the Server's user space (the magic of mmap), the Server can read it directly—this is the physical realization of the "One-Time Copy."
4. Security Credential Injection: The driver extracts the real UID and PID from the current process's task_struct, forcefully injecting them into the transaction struct. This is executed automatically by the kernel and is impossible for user-space to spoof.
5. Enqueue and Wake Up: It mounts the binder_transaction object onto the Server thread's todo work queue and executes wake_up_interruptible() to awaken the Server thread that was previously blocked on the ioctl read.
Layer 4: The Server's Awakening and Response
The Server's Binder thread has been chronically blocked on IPCThreadState::talkWithDriver()'s ioctl call. Upon being awakened by the driver:
IPCThreadStatereads theBR_TRANSACTIONcommand from themInbuffer.- It executes
executeCommand(BR_TRANSACTION)→BBinder::transact()→JavaBBinder::onTransact(). - It traverses back through JNI to the Java layer, triggering
Stub.onTransact(). onTransactresolves the transaction ID and invokes the developer's implementation of theadd()method.- It serializes the result into the
replyParcel and returns it to the driver via theBC_REPLYcommand. - The driver wakes up the blocked Client thread, which then extracts the result from the
reply.
The cycle is now complete, and the client's mCalculator.add(42, 58) call officially returns 100.
The Underlying Pipeline of bindService
Having grasped how data crosses processes, a critical question remains: How did the client acquire the server's Binder reference in the first place? This requires tracing the full pipeline of bindService.
sequenceDiagram
participant Client as Client Activity
participant CI as ContextImpl
participant AMS as AMS (system_server)
participant AS as ActiveServices
participant AT as ApplicationThread (Server Process)
participant Svc as CalculatorService
Client->>CI: bindService(intent, conn, flags)
CI->>CI: Wrap ServiceConnection as IServiceConnection (AIDL)
CI->>AMS: AMS.bindService() [Cross-process Binder Call]
AMS->>AS: bindServiceLocked() Find/Boot target Service
AS->>AT: scheduleBindService() [Cross-process call to Server Process]
AT->>Svc: handleBindService() → onBind()
Svc-->>AT: Return IBinder (Stub Entity)
AT->>AMS: publishService(IBinder) [Publish Binder to AMS]
AMS->>CI: IServiceConnection.connected(IBinder) [Callback to Client]
CI->>Client: onServiceConnected(name, BinderProxy)
Note over Client: Invokes Stub.asInterface(BinderProxy)<br/>Yields Proxy object, cross-process calls begin
Key step breakdown:
- Wrap Callback:
ContextImplwraps the client'sServiceConnectioninto anIServiceConnection(an AIDL interface), enabling it to receive cross-process callbacks. - Notify AMS: Via Binder IPC, it calls AMS located in
system_server, requesting service binding. - AMS Dispatch: AMS locates the target Service's process and commands its
ApplicationThread(also a Binder object) to executeonBind(). - Publish Binder: After the Service's
onBind()returns the Stub object, it publishes it back to AMS viapublishService(). - Callback to Client: AMS invokes the previously saved
IServiceConnection.connected(), passing the Binder reference to the client. The client receives aBinderProxyobject (because it's cross-process). - Create Proxy: Inside
onServiceConnected, the client callsStub.asInterface(service).queryLocalInterfacereturnsnull(cross-process), thus generating theProxyobject.
At this point, the client possesses an ICalculator interface capable of transparent cross-process communication. The binding process itself involves multiple Binder IPC calls—proving definitively that Binder is the central nervous system of Android.
The Lifecycle of Binder Objects in the Kernel
An easily overlooked but highly critical question is: How does a Binder object remain "alive" while being passed across processes? The answer lies within the kernel driver's reference counting mechanism.
When the Server's Stub (BBinder) is passed via the Binder driver to another process for the first time, the driver creates a binder_node in the kernel to represent this entity. Simultaneously, it creates a binder_ref for the receiving party. The driver manages its lifecycle by tracking the strong and weak reference counts of the binder_node—the node is only destroyed when all processes have released their references to it.
It acts like a shared document—as long as someone holds the link, the document won't be deleted. The Binder driver functions as this "document management system" deep within the kernel.
Conclusion: A Panoramic Review of an add Call
Let's review exactly what happens behind the line mCalculator.add(42, 58):
| Layer | What Happened |
|---|---|
| Client Java Layer | Proxy.add() marshals params into a Parcel and calls mRemote.transact(). |
| JNI Layer | BinderProxy.transactNative() crosses into C++'s BpBinder::transact(). |
| Native Layer | IPCThreadState encapsulates data into binder_transaction_data and fires ioctl to the kernel. |
| Kernel Driver | binder_transaction() executes copy_from_user (the singular data copy), injects security credentials, and wakes the Server thread. |
| Server Native Layer | IPCThreadState returns from ioctl and calls BBinder::transact(). |
| Server Java Layer | Stub.onTransact() unmarshals params, invokes the business add() implementation, and writes 100 into reply. |
| Return Pipeline | Reply data flows back: Driver → Client Native → JNI → Java, and the add() call officially returns. |
This encapsulates the entirety of AIDL and Binder's secrets. On the surface, mCalculator.add(42, 58) is indistinguishable from a standard local method call; however, underneath, it has penetrated the Java framework, JNI bridging, the C++ communication engine, and the Linux kernel driver across four layers. The Proxy-Stub architecture automatically generated by AIDL perfectly encapsulates this immense complexity, enabling developers to perform IPC as effortlessly as invoking local methods—this is the ultimate elegance of the Binder design.