fbjni

repository·main·Indexed 43 days ago

https://github.com/facebookincubator/fbjni

A C++ helper library that simplifies the Java Native Interface (JNI) by providing automatic type conversion, reference management, and exception handling. It is platform-agnostic and compatible with any Java VM that supports JNI, featuring automatic inference of Java method descriptors and translation between Java and C++ exceptions.

Tokens
7.7K
Snippets
19
Records
27
Agent score
54%

What's inside fbjni

  1. Overview of fbjni

    main

    fbjni is a C++ library designed to simplify the use of the Java Native Interface (JNI). While it was originally implemented to ease the integration of cross-platform mobile code on Android, it is designed to be platform-agnostic and can be used with any Java VM that supports JNI.

    Key features include:

    • Automatic inference of Java method descriptors.
    • Automatic conversion between C++ types (like std::string) and Java types.
    • Automatic management of JNI references (e.g., local_ref).
    • Automatic exception translation between Java and C++ in both directions, removing the need to manually check exception states after every call.
  2. Why use fbjni instead of plain JNI

    main

    Standard JNI is verbose, error-prone, and lacks built-in error handling. fbjni provides a C++ wrapper that simplifies interaction with the JVM by providing:

    • Reduced Verbosity: Replaces manual method ID lookups and string conversions with high-level C++ methods.
    • Robust Error Handling: Automatically translates Java exceptions into C++ exceptions, preventing the program from aborting due to unhandled Java errors.
    • Automatic Signature Deduction: Deduces Java function descriptors from C++ prototypes, avoiding manual string-based signature errors.
    • Type Safety: Extends the base jobject type to allow type-safe handling of arbitrary Java classes.
    • Memory Management: Uses RAII smart references to manage JNI reference pools and provides Hybrid classes to synchronize C++ allocation lifecycles with Java object lifecycles.
  3. Manage JNI references with RAII

    main

    In standard JNI, managing object lifecycles requires manual handling of local and global references. Leaking or double-freeing these references can cause crashes or memory exhaustion because they are drawn from fixed-size pools.

    fbjni provides RAII smart references (such as alias_ref<T>) that automatically manage the lifecycle of JNI references, ensuring they are correctly released when the C++ object goes out of scope.

  4. Synchronize C++ and Java memory lifecycles with Hybrid classes

    main

    When storing pointers to C++ heap memory inside Java objects, you must ensure the C++ memory is deleted when the Java object is garbage collected.

    fbjni provides the Hybrid class to encapsulate this behavior, providing a convenient way to link the lifecycle of a C++ allocation to its corresponding Java object.

  5. How JavaClass and method registration works

    main

    The JavaClass<T> abstraction allows C++ to interact with Java classes.

    1. Declaration: A C++ struct inherits from JavaClass<T> and defines kJavaDescriptor.
    2. Registration: registerNatives maps Java method names to C++ static functions.
    3. Lifecycle: JNI_OnLoad must call facebook::jni::initialize to set up the environment and trigger registration.

    Note: For non-Hybrid objects (classes without C++ state), all JNI methods must be static on the C++ side. Non-static methods receive the object instance (this) as the first argument.

  6. Understand Java object references: alias_ref, local_ref, and global_ref

    main

    fbjni uses three main reference types to manage Java object lifetimes in C++:

    1. alias_ref<JFoo>: A non-owning, bare-pointer-like reference. Use this almost exclusively for function arguments where you don't need to manage lifetime.
    2. local_ref<JFoo>: A ref-counted, thread-specific pointer. It is automatically invalidated when the function returns to Java. Use this for variables within a function and as return types for most functions.
    3. global_ref<JFoo>: A ref-counted pointer that survives beyond the current JNI call. Use this for storing references in C++ class members or long-lived structures. You can create one using make_global(ref).

    Conversion Example:

    local_ref<JMyDerivedClass> local_derived = make_local(derived);
    global_ref<JMyDerivedClass> global_derived = make_global(derived);
  7. Handle exceptions across the JNI boundary

    main

    C++ and Java exception frameworks are incompatible. A C++ exception hitting the JNI boundary causes an abort, while an unhandled Java exception causes the next JNI call to abort.

    fbjni provides a translation facility that allows exceptions to be passed across the boundary safely. It includes helpers to wrap C++ functions in try/catch blocks and translates Java exceptions into C++ exceptions.

  8. Working with boxed primitives

    main

    Java primitive types have corresponding boxed types in fbjni (e.g., JInteger, JDouble).

    • Unboxing: Use ->value() or the specific type member (e.g., ->intValue()) to get the raw primitive value.
    • Boxing: Use autobox(value) or the type-specific valueOf(value) method to wrap a primitive into a local_ref of the boxed type.
    // Unboxing
    jint unboxed = number->value();
    
    // Boxing
    local_ref<JDouble> ret = autobox(scaled);
    // OR
    local_ref<JDouble> ret = JDouble::valueOf(scaled);
  9. Set up fbjni for Android Gradle Plugin versions prior to 4.0.0

    main

    Older versions of the Android Gradle Plugin do not support native dependencies and headers via Prefab. You must manually create configurations to extract headers and JNI shared libraries from the AAR.

    1. Define custom configurations (extractHeaders and extractJNI) in build.gradle.
    2. Add the fbjni dependency and specific artifacts for headers and JNI.
    3. Create Gradle tasks (extractAARHeaders and extractJNIFiles) to unzip the AAR contents into the build directory.
    4. Ensure these tasks run before externalNativeBuild.
    repositories {
      maven {
        jcenter()
      }
    }
    
    android {
      // Create new configurations that can be referred to in dependencies.
      // The Android Gradle Plugin 3.* does not allow hooking into existing
      // configurations like `implementation`.
      configurations {
        extractHeaders
        extractJNI
      }
    
      dependencies {
        implementation 'com.facebook.fbjni:fbjni:0.0.1'
        // If headers are required.
        extractHeaders 'com.facebook.fbjni:fbjni:0.0.1:headers'
        // If the `.so` files are required for linking.
        extractJNI 'com.facebook.fbjni:fbjni:0.0.1'
      }
    }
    
    task extractAARHeaders {
        doLast {
            configurations.extractHeaders.files.each {
                def file = it.absoluteFile
                copy {
                    from zipTree(file)
                    into "$buildDir/$file.name"
                    include "**/*.h"
                }
            }
        }
    }
    
    task extractJNIFiles {
        doLast {
            configurations.extractJNI.files.each {
                def file = it.absoluteFile
                copy {
                    from zipTree(file)
                    into "$buildDir/$file.name"
                    include "jni/**/*"
                }
            }
        }
    }
    
    tasks.whenTaskAdded {
        task ->
        if (task.name.contains('externalNativeBuild')) {
            task.dependsOn(extractAARHeaders)
            task.dependsOn(extractJNIFiles)
        }
    }
  10. Transferring bulk data with direct ByteBuffer

    main

    Direct ByteBuffers are the most efficient way to transfer large amounts of data between Java and C++.

    Key Operations:

    • Validation: Use data->isDirect() to ensure the buffer is a direct buffer before accessing raw memory.
    • Accessing Raw Memory: Use data->getDirectBytes() to get a uint8_t* pointer to the buffer's memory and data->getDirectSize() for the size.
    • Wrapping C++ Memory: Use JByteBuffer::wrapBytes(ptr, size) to create a JByteBuffer that points directly to existing C++ memory. Warning: The Java object will reference your C++ memory; ensure the memory remains valid.
    • Allocating New Buffers: Use JByteBuffer::allocateDirect(size) to create a new buffer that owns its own memory, which can be safely returned to Java.
    #include <fbjni/ByteBuffer.h>
    
    // Accessing direct buffer memory
    if (data->isDirect()) {
      uint8_t* raw_data = data->getDirectBytes();
      size_t size = data->getDirectSize();
      // ... process data ...
    }
    
    // Wrapping existing C++ memory (non-owning for C++)
    local_ref<JByteBuffer> wrapper = JByteBuffer::wrapBytes(buffer.data(), buffer.size());
    
    // Allocating a new direct buffer (owning)
    local_ref<JByteBuffer> ret = JByteBuffer::allocateDirect(buffer.size());
    std::memcpy(ret->getDirectBytes(), buffer.data(), buffer.size());
  11. Define a JavaClass and register native methods

    main

    To bridge a Java class to C++, define a struct that inherits from JavaClass<T>. You must provide a kJavaDescriptor containing the full JNI class descriptor (e.g., Lcom/package/ClassName;).

    To register native methods, implement a registerNatives() function that calls javaClassStatic()->registerNatives() with a list of mappings created via makeNativeMethod(name, function_pointer).

    Finally, initialize the JNI environment in your JNI_OnLoad using facebook::jni::initialize.

    #include <fbjni/fbjni.h>
    using namespace facebook::jni;
    
    struct DocTests : JavaClass<DocTests> {
      static constexpr auto kJavaDescriptor = "Lcom/facebook/jni/DocTests;";
    
      static void registerNatives() {
        javaClassStatic()->registerNatives({
          makeNativeMethod("nativeVoidMethod", DocTests::nativeVoidMethod),
          makeNativeMethod("staticNativeVoidMethod", DocTests::staticNativeVoidMethod),
        });
      }
    };
    
    jint JNI_OnLoad(JavaVM* vm, void*) {
      return facebook::jni::initialize(vm, [] {
          DocTests::registerNatives();
      });
    }
  12. Configure CMake for fbjni (Android Gradle Plugin 4.0.0+)

    main

    After adding the fbjni dependency in Gradle, use find_package in your CMakeLists.txt to locate the package and link your target against fbjni::fbjni.

    set(build_DIR ${CMAKE_SOURCE_DIR}/build)
    set(PACKAGE_NAME "myapp")
    
    find_package(fbjni REQUIRED CONFIG)
    
    target_link_libraries(${PACKAGE_NAME} fbjni::fbjni mylibrary)