Sending messages between processes using Android Binder

Concepts

  • Earlier post, Sending messages between threads via Android Handler, Looper, MessaqeQueues – My sky (freewindcode.com), communication between multiple threads in Android Framework has been explained. This post will focus on sending data between processes.
  • The messages are wrapped in Parcel which are used as argument to Transact or OnTransact function to send and receive data between processes.
    • The maximum size of data that an android::Parcel can carry is limited by the available memory on the system and the underlying implementation of the Parcel class. It's recommended to avoid sending large amounts of data through a Parcel. Instead, it's better to use file descriptors or a memory-mapped file to pass large amounts of data between processes.
    • Primitive data types such as int, string and Binder object could be sent as Parcel
    • Note that we can send pointer as Parcel. However, since different processes cannot access each other's memory, the actual and expected data is not retrievable at the receiving end. Instead we should pass a reference.
    • For example, When passing a sp<IBinder> across different processes using Android Binder, only the binder reference is passed (not the actual object that the binder refers to). In order to access the object on the other side of the binder reference, the target process needs to use the binder reference to obtain a remote interface to the object.
  • The sending_message_class implements BpInterface and invokes Transact function.
  • The receiving_message_class implements BnInterface and overrides onTransact function
  • It is advisable to create an interface whose concrete classes also inherit BpInterface and BpInterface

Implementation

Send simple data through Android Binder and receive return via a reply

  • The following program implements an ICommunication interface which defines pure virtual functions getSignal. The concrete classes BpCommunication and BnCommunication inherits ICommunication and BpInterface and BpInterface. This applies the concept of "template class"
  • BpCommunication wraps up data in Android::Parcel and invokes Transact that sends data to another process via Android Binder.
  • BnCommunication implements OnTransact that reads data from Parcel and invokes getSignal. Note that BnCommunication is not yet a concrete class. It is still an abstract class that has not implemented the pure virtual functions getSignal
  • MySensor class inherits BnCommunication. It implements getSignal.
  • The data is wrapped in Parcel container. In this case, an integer value is written to Parcel in sending process, and read from Parcel in reading process. Note that the callback function (or the function pointer) is not passed to Parcel.

File ICommunication.h

#ifndef ICOMMUNICATION_H
#define ICOMMUNICATION_H

#include <binder/IInterface.h>

class ICommunication : public android::IInterface {
public:
    DECLARE_META_INTERFACE(Communication);

    virtual void getSignal(const uint16_t eventCode, std::function<void(int)) = 0;
};

#endif // ICOMMUNICATION_H
  • getSignal accepts an eventCode and a callback function.

File ICommunication.cpp

#include "ICommunication.h"

#include <binder/Binder.h>
#include <binder/Parcel.h>

#define GET_SIGNAL 0

namespace android {

class BnCommunication: public BnInterface<ICommunication> {
 public:
  virtual status_t onTransact(uint32_t code, const Parcel &data,
                              Parcel *reply, uint32_t flags = 0) {
    switch (code) {
      case GET_SIGNAL: {
        // read input data from Parcel
        uint16_t eventCode = data.readInt16();   
        uint32_t result = generateSignal(); 
        // write result to Parcel reply
        reply.writeInt32(result);
        return NO_ERROR;
      }
      default:
        return BBinder::onTransact(code, data, reply, flags);
    }
  }
};

class BpCommunication : public BpInterface<ICommunication> {
public:
    BpCommunication(const sp<IBinder>& impl)
        : BpInterface<ICommunication>(impl) {}

    virtual void getSignal(const uint16_t eventCode, std::function<void(int)> callback) override {
        Parcel data, reply;
        data.writeInt16(eventCode); 
        remote()->transact(GET_SIGNAL, data, &reply);
        // receive result as callback from Parcel reply
        uint_32 result = reply.readInt32(); 
        callback(result);
    }
};

class MySensor : public BnCommunication {
public:
    // the concrete implementation doesn't do anything! that is why it is clumsy
    void getSignal(int eventCode, std::function<void(int)> callback){
    }
    int generateSignal () {
        // Provide implementation for getSignal here
        int signal;
        return signal;
    }

}; // namespace android
  • In the above program, BnInterface<ICommunication> is a template class from the Android framework. It is part of the Android Binder inter-process communication (IPC) system, and is used to implement a communication interface between two processes.
  • BpInterface is the base class for a proxy interface, which is used by a client to make IPC calls to a remote service. The template argument ICommunication specifies the interface that the proxy will implement. In this case, the proxy implements the ICommunication interface. The BpCommunication class derives from BpInterface<ICommunication>, and provides the implementation of the proxy.
  • By inheriting from BpInterface<ICommunication>, the BpCommunication class can be used by a client process to make IPC calls to the remote service that implements the ICommunication interface.
  • Parcel is a container to carry data inter processes. Parcel carries the integer eventCode to another process where an instance MySensor is running.
  • Function pointer cannot be passed as a Parcel in Android's Inter-Process Communication. A parcel is a data container used to transport data between processes, and it can only contain simple data types like integers, floats, and strings. Function pointers are not a simple data type and cannot be passed in a parcel.
  • Instead, it would be needed to pass the necessary information to call the function in the receiving process. This can be done by defining an interface with a callback function and passing an instance of that interface as a parcel. The receiving process would then call the function on the instance to trigger the desired behavior. This is a common pattern used in Android's IPC framework.
  • Note that a pointer carries memory address. Thus the content at the location is not accessible in another process, and passing pointer will not carries expected value inter process.
  • Next improvement, we will use IBinder or a new class. A Binder object could be passed to and read from Parcel, and thus its callback function could be invoked.

main.cpp

#include <iostream>
#include "ICommunication.h"
#include "MySensor.h"

#include <binder/IPCThreadState.h>
#include <binder/ProcessState.h>
#include <binder/IServiceManager.h>

using namespace android;

int main(int argc, char* argv[]) {
    sp<ProcessState> proc(ProcessState::self());
    ProcessState::self()->startThreadPool();
    // start service
    MySensor mySensor;
    android::defaultServiceManager()->addService(String16("service.communication"), &mySensor);
    IPCThreadState::self()->joinThreadPool();
    // get service, we have to "get service" to receive reply
    sp<ICommunication> communication = interface_cast<ICommunication>(defaultServiceManager()->getService(String16("service.communication")));
    if (communication != NULL) {
        int signal = communication->getSignal("EVENT_CODE", [] (int s) {
            std::cout << "Received signal: " << s << std::endl;
        });
        std::cout << "Signal: " << signal << std::endl;
    }
    return 0;
}
  • note that main's thread and BpCommunication (also commService) would be running on the same process. The callback is on the main thread. The callback printed out the received value from Parcel reply.
  • The request is passed into Binder, and sent to another process. BnCommunication receives the parcel data, reads it and invokes MySensor::generateSignal to return the signal as an integer value.
  • We will discuss how to add ICommunication service to android IServiceManager in the next post

Send and receive a Binder object through Binder.

  • In Android, you can pass an instance of a class that implements an interface as a Parcel. You can use the writeStrongBinder() method of the Parcel class to write a sp<IBinder> object to a Parcel, which can hold a reference to the implementation of an interface. Then, on the receiving end, you can use the readStrongBinder() method to read the sp<IBinder> object and cast it to a reference to the implementation of the interface.
  • In this program, we implement IReceiver interface. The interface defines the pure virtual function onSignalReceived that accepts the return value.
  • the reference of sp<MyReceiver> is carried by Parcel. But before sending, we have to cast sp<MyReceiver> as a Binder object using asBinder function.
  • After receiving the Parcel data, we convert Binder object back to sp<MyReceiver> using interface_cast function.
  • In the returning process, MyReceiver invokes its callback function. Hence, the returning behavior, printing the out the output on main process, performs as expected.
There are many design patterns in this implementation. MyReceiver implements onSignalReceived() and stored signal_ as its private. MySensor implements getSignal() and save a list of MyReceiver. Therefore, MySensor and MyReceiver follow an Observer Design Pattern. IServiceManage manages services such as ICommunication and help manages messages between processes; it follows Event-Driven architecture. BnInterface and BpInterface sends and receives messages in agreed formats. They follow Client-Server Design Pattern.

IMyReceiver.h

#ifndef IMYRECEIVER_H
#define IMYRECEIVER_H

class IMyReceiver { public: virtual void onSignalReceived(uint_32 signal) = 0; };

#endif // IMYRECEIVER_H

IMyReceiver.cpp

#include <iostream>
#include <list>
#include <ICommunication.cpp>
using namespace std;

class MyReceiver : public IMyReceiver {
public:
    void onSignalReceived(uint_32 signal) override {
        cout << "Received signal: " << signal << endl;
    }
    void saveSignal(uint_32 signal){
       signal_t = signal;
    }
private:
    uint_32 signal_t;
};

ICommunication.h

#include <binder/Binder.h>
#include <binder/Parcel.h>

#include "IMyReceiver.h"

#define GET_SIGNAL 0

class ICommunication : public IInterface {
public:
    virtual void getSignal(uint_16 eventCode, const sp<IMyReceiver> &receiver) = 0;
};

ICommunication.cpp

#include <binder/IInterface.h>

#include "ICommunication.h"
#include "IMyReceiver.h"

class BpCommunication: public BpInterface<ICommunication> {
 public:
  BpCommunication(const sp<IBinder> &impl)
      : BpInterface<ICommunication>(impl) {}

  virtual void getSignal(const uint_16 &eventCode,
                         const sp<IMyReceiver> &receiver) {
    Parcel data, reply;
    data.writeInterfaceToken(ICommunication::getInterfaceDescriptor());
    data.writeInt16(eventCode);
    // write the receiver to Parcel data
    sp<IBinder> binder = receiver->asBinder();
    data.writeStrongBinder(binder);
    remote()->transact(GET_SIGNAL, data, &reply);
    // read the receiver from Parcel reply
    receiver = interface_cast<IMyReceiver>(reply.readStrongBinder())
    // callback function is invoked on main process
    receiver -> onSignalReceived();
  }
};

class BnCommunication: public BnInterface<ICommunication> {
 public:
  virtual status_t onTransact(uint32_t code, const Parcel &data,
                              Parcel *reply, uint32_t flags = 0) {
    switch (code) {
      case GET_SIGNAL: {
        String16 eventCode = data.readString16();
        // read the receiver from Parcel data
        sp<IMyReceiver> receiver =
            interface_cast<IMyReceiver>(data.readStrongBinder());
        getSignal(eventCode, receiver);
        // write the receiver to Parcel reply
        sp<IBinder> binder;
        binder = receiver->asBinder();
        reply.writeStrongBinder(binder);
        return NO_ERROR;
      }
      default:
        return BBinder::onTransact(code, data, reply, flags);
    }
  }
};

class MySensor : public BnCommunication {
public:
    void registerReceiver(IMyReceiver *receiver) {
        mReceivers.push_back(receiver);
    }
    void getSignal(int eventCode,  const sp<IMyReceiver> &receiver) {
        // add receiver to the receiver list
        registerReceiver(*receiver);
        // Provide implementation for getSignal here
        int signal = rand() % 100;
        // save the signal to receiver private member
        receiver->saveSignal(signal);
        }
    }

private:
    list<IMyReceiver *> mReceivers;
};
  • Note that getSignal function declared by ICommunication now accepts an pointer reference of IMyReceiver's instances instead of a callback function.
  • Note that we don't pass a pointer to processes. When passing a sp<MyReceiver> or sp<IBinder> across different processes using Android Binder, only the binder reference is passed (not the actual object that the binder refers to). In order to access the object on the other side of the binder reference, the target process needs to use the binder reference to obtain a remote interface to the object.
/* Main process */
// change IMyReceiver to IBinder
sp<IBinder> binder = receiver -> asBinder();
// wrap Binder to Parcel to send away
Parcel data;
data.writeStrongBinder(binder)

/* Subprocess */
// read Binder from Parcel
Parcel reply;
sp<IBinder> binder = reply.readStrongBinder();
// Use the binder reference to obtain a remote interface to the object
sp<IMyReceiver> receiver = interface_cast<IMyReceiver>(binder);
// Call a method on the remote interface
myInterface->myMethod();
  • In sending process, BpCommunication writes the MyReceiver pointer reference to Parcel using data.writeStrongBinder(receiver->asBinder()). Likewise, BnCommunication also does.
  • asBinder retrieves the Binder object. It is needed because receiver is not yet a Binder object which could be carried as a Parcel.
  • In receiving process, readStrongBinder reads the Binder object from the Parcel, and interface_cast converts the IBinder object to an IMyReceiver object
  • Therefore, BnCommunication has to retrieve the receiver the from Parcel using sp<IMyReceiver> receiver = interface_cast<IMyReceiver>(data.readStrongBinder())

main.cpp

#include <binder/IPCThreadState.h>
#include <binder/ProcessState.h>
#include <binder/IServiceManager.h>

#include <IMyReceiver.h>
#include <ICommunication.h>

int main(int argc, char ** argv) {
    if (argc == 1) {      // Main program that starts the requests
        sp<ProcessState> proc(ProcessState::self());
        ProcessState::self()->startThreadPool();

        MySensor mySensor;
        sp<ICommunication> communication = interface_cast<ICommunication>   (defaultServiceManager()->addService(String16("service.communication"),&mySensor));
        IPCThreadState::self()->joinThreadPool();

        MyReceiver myReceiver;
        communicationService->getSignal(1, &myReceiver);
    }
    if (argc == 2) {     // sub-process, MySensor program
        sp<IServiceManager> sm = defaultServiceManager();
        sp<IBinder> binder = sm->getService(String16("service.communication"));
        sp<ICommunication> comm= interface_cast<ICommunication>(binder);
        // can implement extra MySensor function on to output logs on this side
        //
    }
    
    return 0;  
}
  • both communicationService and myReceiver run on the same process.
  • getSignal will add myReceiver to the receiver list that MySensor manages.
  • MySensor::getSignal actually invokes myReceiver::saveSignal to save the data in its private member. It does not invoke the callback function directly.
  • onSignalReceived is invoked in main thread, in BpCommunication, thus the returning process behaves as expected.
  • The program doesn't yet fully implement Event Driven architecture and multiple processes that Android Framework feature. In the next post, Part 2 – Event-Drive Architecture – Implementation with Android Framework , we will fully implement receivers running on different processes.

Reference: This post is heavily influenced by the guide from Ebixio » Using Android IPC binders from native code Author Gabriel Burca

Leave a Reply

Your email address will not be published. Required fields are marked *