A Minimal Binder IPC Walkthrough on Android
Binder is Android’s primary interprocess communication (IPC) facility. Apps and system components often live in separate processes; Binder provides a fast, structured way for them to invoke methods across those boundaries using a remote procedure call (RPC) style. Although Android sits on the Linux kernel, it does not rely on classic Linux IPC (pipes, sockets, shared memory) for app-to-system interactions. Binder evolves from OpenBinder and offers a component model reminiscent of COM/CORBA while being purpose-built for local process boundaries.
Key properties of Binder
- Distributed object model with RPC semantics
- Engineered for system IPC rather than network remoting
- Implemented large in C++
- Friendly to multi-threaded designs without enforcing a strict threading model
- Portable across OSes where Binder exists or can be implemented
- Efficient on constrained hardware, suitable for a broad device spectrum
- Highly modular and composable for customization and replacement of components
Binder communication model
A typical call looks like request/response between a client and a service:
- The client obtains a proxy to a remote service from the Service Manager.
- The client invokes a method on the proxy as if it were local.
- The proxy marshals data into a Parcel and performs a kernel transaction via /dev/binder.
- The Binder driver delivers the transaction to the server process.
- The server unmarshals input, executes the method, marshals the reply, and the driver returns it to the client.
Binder building blocks
- Binder driver (/dev/binder): Character device in the kernel used for sending and receiving transactions. User space interacts with it through ProcessState and IPCThreadState.
- Service Manager: A registry of services. Servers publish Binder objects here; clients look them up by name.
- Server (service): A Binder endpoint (often in a system process) implementing one or more remote interfaces.
- Client: A process calling into a remote Binder interface.
- Proxy/stub: Client-side "Bp" proxy forwards calls to the server; server-side "Bn" stub receives and dispatches transactions.
A compact Binder example: integer addition over RPC
This walkthrough wires a tiny service exposing addNumbers(int, int). The client looks up the service, sends two integers, and reads back the sum.
Interface definition
Header defining the remote interface and its transaction codes.
// ICalcService.h
#include <binder/IInterface.h>
#include <binder/Parcel.h>
namespace example {
class ICalcService : public android::IInterface {
public:
DECLARE_META_INTERFACE(CalcService);
virtual int addNumbers(int lhs, int rhs) = 0;
};
// Transaction code enumeration
enum {
TRANSACTION_ADD = android::IBinder::FIRST_CALL_TRANSACTION + 0,
};
} // namespace example
Implementation of the IInterface helpers. The macros wire up descriptor retrieval and asInterface(), which returns a local implementation if present or a Bp proxy otherwise.
// ICalcService.cpp
#include "ICalcService.h"
#include <binder/IPCThreadState.h>
#include <binder/IServiceManager.h>
#include <utils/Log.h>
using namespace android;
using namespace example;
class BpCalcService : public BpInterface<ICalcService> {
public:
explicit BpCalcService(const sp<IBinder>& impl)
: BpInterface<ICalcService>(impl) {}
int addNumbers(int lhs, int rhs) override {
Parcel data, reply;
data.writeInterfaceToken(ICalcService::getInterfaceDescriptor());
data.writeInt32(lhs);
data.writeInt32(rhs);
status_t rc = remote()->transact(TRANSACTION_ADD, data, &reply);
if (rc != NO_ERROR) return 0; // or propagate rc as needed
return reply.readInt32();
}
};
IMPLEMENT_META_INTERFACE(CalcService, "com.example.ICalcService");
Notes on the plumbing used above:
// IInterface.h (selected parts for context)
template <typename INTERFACE>
inline sp<INTERFACE> interface_cast(const sp<IBinder>& binder) {
return INTERFACE::asInterface(binder);
}
#define DECLARE_META_INTERFACE(INTERFACE) \
static const android::String16 descriptor; \
static android::sp<I##INTERFACE> asInterface( \
const android::sp<android::IBinder>& obj); \
virtual const android::String16& getInterfaceDescriptor() const; \
I##INTERFACE(); \
virtual ~I##INTERFACE();
#define IMPLEMENT_META_INTERFACE(INTERFACE, NAME) \
const android::String16 I##INTERFACE::descriptor(NAME); \
const android::String16& I##INTERFACE::getInterfaceDescriptor() const { \
return I##INTERFACE::descriptor; \
} \
android::sp<I##INTERFACE> I##INTERFACE::asInterface( \
const android::sp<android::IBinder>& obj) { \
android::sp<I##INTERFACE> out; \
if (obj) { \
out = static_cast<I##INTERFACE*>( \
obj->queryLocalInterface(I##INTERFACE::descriptor).get()); \
if (!out) out = new Bp##INTERFACE(obj); \
} \
return out; \
} \
I##INTERFACE::I##INTERFACE() {} \
I##INTERFACE::~I##INTERFACE() {}
The asInterface() produced by IMPLEMENT_META_INTERFACE first asks the binder if a local implementation exists in the same process via queryLocalInterface(). If not, it constructs a BpCalcService proxy that forwards calls to the remote binder.
Server-side stub and implementation
The Bn stub decodes transactions and dispatches to the concrete service.
// BnCalcService.h
#include "ICalcService.h"
namespace example {
class BnCalcService : public android::BnInterface<ICalcService> {
public:
android::status_t onTransact(uint32_t code, const android::Parcel& data,
android::Parcel* reply, uint32_t flags) override {
switch (code) {
case TRANSACTION_ADD: {
CHECK_INTERFACE(ICalcService, data, reply);
int a = data.readInt32();
int b = data.readInt32();
int sum = addNumbers(a, b);
reply->writeInt32(sum);
return android::NO_ERROR;
}
default:
return BBinder::onTransact(code, data, reply, flags);
}
}
};
} // namespace example
Concrete service that publishes itself to the Service Manager and implements the business logic.
// CalcService.cpp
#include "BnCalcService.h"
#include <binder/IServiceManager.h>
#include <binder/ProcessState.h>
#include <binder/IPCThreadState.h>
#include <utils/Log.h>
using namespace android;
using namespace example;
class CalcService : public BnCalcService {
public:
static status_t publish() {
sp<IServiceManager> sm = defaultServiceManager();
return sm->addService(String16("com.example.calc"), new CalcService());
}
int addNumbers(int lhs, int rhs) override {
return lhs + rhs;
}
};
int main(int argc, char** argv) {
sp<ProcessState> proc(ProcessState::self());
(void)defaultServiceManager();
CalcService::publish();
ProcessState::self()->startThreadPool();
IPCThreadState::self()->joinThreadPool();
return 0;
}
Client usage
The client asks the Service Manager for the service handle, casts it to the interface, and calls addNumbers().
// CalcClient.cpp
#include "ICalcService.h"
#include <binder/IServiceManager.h>
#include <utils/Log.h>
#include <unistd.h>
using namespace android;
using namespace example;
int main(int argc, char** argv) {
sp<IServiceManager> sm = defaultServiceManager();
sp<IBinder> handle;
// Poll until the service becomes available
for (;;) {
handle = sm->getService(String16("com.example.calc"));
if (handle) break;
usleep(300000); // 300ms
}
sp<ICalcService> calc = interface_cast<ICalcService>(handle);
if (!calc) return 1;
int result = calc->addNumbers(3, 4);
// Use result as needed
return (result == 7) ? 0 : 2;
}
What happens inside: driver, threads, and dispatch
When a server process starts, it initializes the Binder runtime and publishes its binder object:
// ProcessState::self() (excerpt)
sp<ProcessState> ProcessState::self() {
Mutex::Autolock _l(gProcessMutex);
if (gProcess != nullptr) return gProcess;
gProcess = new ProcessState; // opens /dev/binder and mmaps the driver region
return gProcess;
}
ProcessState sets up the connection to /dev/binder and maps the shared region used by the Binder driver. The server then creates a thread pool to service incoming transactions:
// ProcessState::startThreadPool() (excerpt)
void ProcessState::startThreadPool() {
AutoMutex _l(mLock);
if (!mThreadPoolStarted) {
mThreadPoolStarted = true;
spawnPooledThread(true); // main binder thread
}
}
void ProcessState::spawnPooledThread(bool isMain) {
if (!mThreadPoolStarted) return;
sp<Thread> t = new PoolThread(isMain);
t->run(makeBinderThreadName().string());
}
class PoolThread : public Thread {
public:
explicit PoolThread(bool isMain) : mIsMain(isMain) {}
bool threadLoop() override {
IPCThreadState::self()->joinThreadPool(mIsMain);
return false;
}
private:
const bool mIsMain;
};
Each binder thread enters a loop, handing control to the driver and processing commands:
// IPCThreadState.cpp (excerpts)
status_t IPCThreadState::getAndExecuteCommand() {
status_t res = talkWithDriver(); // blocks until work arrives
if (res < NO_ERROR) return res;
if (mIn.dataAvail() < sizeof(int32_t)) return res;
int32_t cmd = mIn.readInt32();
return executeCommand(cmd); // dispatch to handlers
}
void IPCThreadState::joinThreadPool(bool isMain) {
mOut.writeInt32(isMain ? BC_ENTER_LOOPER : BC_REGISTER_LOOPER);
set_sched_policy(mMyThreadId, SP_FOREGROUND);
status_t res;
do {
processPendingDerefs();
res = getAndExecuteCommand();
if (res == TIMED_OUT && !isMain) break;
} while (res != -ECONNREFUSED && res != -EBADF);
mOut.writeInt32(BC_EXIT_LOOPER);
talkWithDriver(false);
}
When a client calls BpCalcService::addNumbers(), the proxy writes a transaction with code TRANSACTION_ADD. The driver delivers it to a server binder thread, which eventually invokes BBinder::transact() and then the service’s BnCalcService::onTransact(). The stub reads the arguments from the Parcel, executes addNumbers(), writes the reply, and returns control to the client.