Referencing C++ Objects From Java

Referencing C++ Objects From Java

J2V8 enables developers to embed Google’s V8 JavaScript engine in their Java applications. V8 is entirely implemented in C++ and we enable this embedding by creating a thin JNI (Java Native Interface) layer which exposes the V8 API to Java. The very first problem we faced while implementing this was how do you reference C++ objects from Java?

This problem is not unique to J2V8, and will likely affect all JNI developers. In this article we will describe how we solved this problem.

Example

Consider the following C++ example.

JNIEXPORT jlong JNICALL Java_com_eclipsesource_v8_V8__createIsolate
(JNIEnv *env, jobject v8) {
  V8Runtime* runtime = new V8Runtime();
}

In this example, we are instantiating a V8Runtime in C++. This V8Runtime is created on the heap and can be referenced through the runtime pointer. For example, you could create a context and reset the global object as follows:

 runtime->context_.Reset(runtime->isolate, context);
 runtime->globalObject->Reset(...);

But how do you access this pointer in subsequent calls? You cannot return the runtime object to Java since it’s not a Java Object?

Map

The first solution we explored was to store all C++ objects in a map and reference them through a unique ID. The unique ID should be a Java primitive like an int / long or a String. Each time a C++ object is created, a pointer to that object is stored in the map. The map is indexed by a key, and that key is returned to Java.

std::map  v8Isolates;
JNIEXPORT jlong JNICALL Java_com_eclipsesource_v8_V8__createIsolate
(JNIEnv *env, jobject v8) {
  jlong handle = generateUID();
  v8Isolates[handle] = new V8Runtime();
  return handle;
}
JNIEXPORT void JNICALL Java_com_eclipsesource_v8_V8__performReset
(JNIEnv *env, jobject v8, jlong handle) {
  v8Isolates[handle]->Reset(...); 
}

The handle can be stored in Java as a primitive (Java long in this case), and whenever you need to reference the C++ object you can pass the handle through the JNI bridge. This works but has a number of limitations:

  • How do you generate a UID? We used a basic counter, but this is not thread safe. Also, we used an int which wrapped pretty quickly.
  • Lookup may be slow depending on how many objects you have in the system.
  • The map is a shared resource and care must be taken to ensure multiple threads do not write to it simultaneously.

reinterpret_cast

To solve this problem we moved to a much easier solution, we now just store the memory address of the C++ object. Since Java doesn’t have a pointer type, we casted the memory address to a jlong (Java long) since this is big enough to store addresses up to 64 bits. While reinterpret_cast is usually not a cast you want to perform, it’s exactly what you want in this case.

reinterpret_cast is the most dangerous cast, and should be used very sparingly. It turns one type directly into another – such as casting the value from one pointer to another, or storing a pointer in an int, or all sorts of other nasty things.

With reinterpret_cast, we can cast a pointer to a jlong which can be stored in any Java object. The jlong can then be casted back to the pointer. This provides constant time lookup and a thread safe way to manage objects.

JNIEXPORT jlong JNICALL Java_com_eclipsesource_v8_V8__createIsolate
(JNIEnv *env, jobject v8) {
  return reinterpret_cast(new V8Runtime());
}
JNIEXPORT void JNICALL Java_com_eclipsesource_v8_V8__performReset
(JNIEnv *env, jobject v8, jlong handle) {
  reinterpret_cast(handle)->Reset(...); 
}

Finally, remember that whatever solution you choose, you still need to free all memory you allocate in C++.

We recently moved all J2V8 memory storage to this approach. This is available in J2V8 3.1.0-SNAPSHOT. If you are using J2V8, please test this and report any bugs you see. I would like to release J2V8 3.1.0 by mid November.

For more J2V8 Tips & Tricks, follow me on Twitter.