Search code examples
androidcsdkandroid-ndkshared-memory

Shared memory between NDK and SDK below API Level 26


Library written in c++ produces continuous stream of data and same has to be ported on different platforms. Now integrating the lib to android application, I am trying to create shared memory between NDK and SDK.

Below is working snippet,

Native code:

#include <jni.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <linux/ashmem.h>
#include <android/log.h>
#include <string>

char  *buffer;
constexpr size_t BufferSize=100;
extern "C" JNIEXPORT jobject JNICALL
Java_test_com_myapplication_MainActivity_getSharedBufferJNI(
        JNIEnv* env,
        jobject /* this */) {

    int fd = open("/dev/ashmem", O_RDWR);

    ioctl(fd, ASHMEM_SET_NAME, "shared_memory");
    ioctl(fd, ASHMEM_SET_SIZE, BufferSize);

    buffer = (char*) mmap(NULL, BufferSize, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);

    return (env->NewDirectByteBuffer(buffer, BufferSize));
}

extern "C" JNIEXPORT void JNICALL
Java_test_com_myapplication_MainActivity_TestBufferCopy(
        JNIEnv* env,
        jobject /* this */) {

   for(size_t i=0;i<BufferSize;i = i+2) {
       __android_log_print(ANDROID_LOG_INFO, "native_log", "Count %d value:%d", i,buffer[i]);
   }

   //pass `buffer` to dynamically loaded library to update share memory
   //

}

SDK code:

//MainActivity.java
public class MainActivity extends AppCompatActivity {

    // Used to load the 'native-lib' library on application startup.

    static {
        System.loadLibrary("native-lib");
    }

    final int BufferSize = 100;
    @RequiresApi(api = Build.VERSION_CODES.Q)
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        ByteBuffer byteBuffer = getSharedBufferJNI();

        //update the command to shared memory here
        //byteBuffer updated with commands
        //Call JNI to inform update and get the response
        TestBufferCopy();
    }


    /**
     * A native method that is implemented by the 'native-lib' native library,
     * which is packaged with this application.
     */
    public native ByteBuffer getSharedBufferJNI();
    public native int TestBufferCopy();
}

Question:

  1. Accessing primitive arrays from Java to native is reference only if garbage collector supports pinning. Is it true for other way around ?
  2. Is it guaranteed by android platform that ALWAYS reference is shared from NDK to SDK without redundant copy?
  3. Is it the right way to share memory?

Solution

  • You only need /dev/ashmem to share memory between processes. NDK and SDK (Java/Kotlin) work in same Linux process and have full access to same memory space.

    The usual way to define memory that can be used both from C++ and Java is by creating a Direct ByteBuffer. You don't need JNI for that, Java API has ByteBuffer.allocateDirect(int capacity). If it's more natural for your logical flow to allocate the buffer on the C++ side, JNI has the NewDirectByteBuffer(JNIEnv* env, void* address, jlong capacity) function that you used in your question.

    Working with Direct ByteBuffer is very easy on the C++ side, but not so efficient on the JVM side. The reason is that this buffer is not backed by array, and the only API you have involves ByteBuffer.get() with typed variations (getting byte array, char, int, …). You have control of current position in the buffer, but working this way requires certain discipline: every get() operation updates the current position. Also, random access to this buffer is rather slow, because it involves calling both positioning and get APIs. Therefore, in some cases of non-trivial data structures, it may be easier to write your custom access code in C++ and have 'intelligent' getters called through JNI.

    It's important not to forget to set ByteBuffer.order(ByteOrder.nativeOrder()). The order of a newly-created byte buffer is counterintuitively BIG_ENDIAN. This applies both to buffer created from Java and from C++.

    If you can isolate the instances when C++ needs access to such shared memory, and don't really need it to be pinned all the time, it's worth to consider working with byte array. In Java, you have more efficient random access. On the NDK side, you will call GetByteArrayElements() or GetPrimitiveArrayCritical(). The latter is more efficient, but its use imposes restrictions on what Java functions you can call until the array is released. On Android, both methods don't involve memory allocation and copy (with no official guarantee, though). Even though C++ side uses the same memory as Java, your JNI code must call the appropriate Release…() function, and better do that as early as possible. It's a good practice to handle this Get/Release via RAII.