Fading Coder

An Old Coder’s Final Dance

Home > Tech > Content

A Minimal Binder IPC Walkthrough on Android

Tech 2

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:

  1. The client obtains a proxy to a remote service from the Service Manager.
  2. The client invokes a method on the proxy as if it were local.
  3. The proxy marshals data into a Parcel and performs a kernel transaction via /dev/binder.
  4. The Binder driver delivers the transaction to the server process.
  5. 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.

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.