Implementing Ashmem to share data between processes

Jinhan Hu
3 min readJan 8, 2021

--

Recently, I’ve been looking into data sharing between Android processes. Android provides broadcast system to pass messages. However, message passing is slow (in milliseconds latency lower-bound) even for passing hundreds of bytes from one process to another. Thus, I turn to Ashmem, Android’s version of shared memory, to directly share large chunks of memory with low level system support. In our evaluation, we notice that using shared memory could reduce the inter-process communication latency by one magnitude, which makes it very useful in various scenarios. However, during my implementation, I found that there are very few documentations to be referred to. Therefore, I want to share my experience implementing shared memory in Android using Java and C++.

The implementation introduced in this blog uses Android SDK version 28 and gradle version 3.5.0. At a high level, the implementation contains three steps: (1) define an interface using Android AIDL; (2) create a service to implement the interface defined; (3) create a Java library and a native C++ library to access low level kernel features.

First, defining an interface using Android AIDL is very straightforward and you could find a very detailed guide at https://developer.android.com/guide/components/aidl. In our implementation, we define an interface called IReceiverService (.aidl) which contains the function ParcelFileDescriptor OpenSharedMem(String name, int size, boolean create), to allocate shared memory, returned as a parcel file descriptor.

Second, we must create a service to implement the AIDL interface, as well as all functions defined in it. In our implementation, we create a ReceiverService class (.kt) in Kotlin, class ReceiverService : Service(). The function OpenSharedMem() is overwritten as:

override fun OpenSharedMem(name: String?, size: Int, create: Boolean): ParcelFileDescriptor? {

val fd = ShmLib.OpenSharedMem(name, size, create)

try {

return ParcelFileDescriptor.fromFd(fd)

} catch (e: IOException) {

e.printStackTrace()

}

return null

}

The function above accesses the ShmLib Java library which is introduced in the third step.

Third, we create a Java library ShmLib (.java) that could be accessed by the service and a C++ library native-lib (.cpp) that interacts with low level Android kernel features. In ShmLib, we define several JNI functions getFD (String name, int size), void setMap (int fd, int size), <ByteArray> void setVal (int fd, ByteArray val), <ByteArray> ByteArray getVal (int insize), which are implemented in native-lib in C++ (described in the code below).

static jint getFD(JNIEnv *env, jclass cl, jstring path,jint size)

{

const char *name = env->GetStringUTFChars(path,NULL);

jint fd = ASharedMemory_create(name, size);

maps[num].size = size;

maps[num].fd = fd;

maps[num++].map = (jbyteArray *)mmap(0,size,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);

env->ReleaseStringUTFChars(path,name);

return fd;

}

This function above creates a shared memory region utilizing shared memory NDK API described at https://developer.android.com/ndk/reference/group/memory. This is new since API level 26, after which the application is limited to directly access ioctl. In particular in this function, a chunk of memory (stored in a memArea struct) is mapped for java byteArray data type.

static void setMap(JNIEnv *env, jclass cl, jint fd, jint sz)

{

size = sz;

mapfinal = (jbyteArray *)mmap(0,size,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);

}

This function above creates the final memory mapping based on the input size requested by the application.

static void setVal(JNIEnv *env, jclass cl,jint fd, jobject input)

{

jbyte *bytes = env->GetByteArrayElements(static_cast<jbyteArray>(input), 0);

arrayLength = env->GetArrayLength(static_cast<jarray>(input));

memset(maps[0].map, 0x0, arrayLength);

memcpy(maps[0].map, bytes, arrayLength);

env->ReleaseByteArrayElements(static_cast<jbyteArray>(input), bytes, 0);

}

This function above retrieves the bytes from the java byteArray input object and copy them into the memory chunk allocated previously.

static jobject getVal(JNIEnv *env, jclass cl, int insize)

{

jbyteArray jarray = env->NewByteArray(insize);

env->SetByteArrayRegion(jarray, 0, insize, reinterpret_cast<const jbyte *>(mapfinal));

return jarray;

}

This function above retrieves the data stored in the shared memory and returns the data to java application as byteArray data type.

After all implementations, the application at java level could utilize the service to open a shared memory region, set the memory mapping size based on the application’s needs, and then set and get values stored in the shared memory.

Hopefully, this blog could help you in some ways. Please feel free to leave your comments if you find things are incorrect or if you need additional help. Thank you~

Sign up to discover human stories that deepen your understanding of the world.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

--

--

Jinhan Hu
Jinhan Hu

Written by Jinhan Hu

Computer Engineering PhD Candidate @ Meteor Studio, Arizona State University

Responses (1)

Write a response