Java-C++ jni交互新范式:fbjni 设计与使用

文章内容

一、问题与结论

原生 JNI 的 API 数量有限,麻烦集中在边界状态:方法签名要手写字符串,引用所有权靠手动维护,C++ 异常不能越过 JNI 入口,自己创建的 native 线程还要先挂载到 JVM。任何一处漏掉,错误都可能推迟到运行时才出现,表现为 NoSuchMethodError、引用泄漏、std::terminate,或者拿不到可用的 JNIEnv*。

fbjni 不替换原有 JNI 接口,它在 C API 之上加了一层 C++ 类型系统和 RAII,代码分布在几个文件里:

  • detail/Registration.h:makeNativeMethod
  • detail/References.h:local_ref/global_ref/weak_ref/alias_ref
  • detail/Exceptions.cpp:translatePendingCppExceptionToJavaException
  • detail/Environment.cpp:ThreadScope

1.1 传统 JNI 与 fbjni 的四组写法对比

  1. 签名从手写字符串换成编译期推导。传统注册要同时维护方法名、签名字符串和函数指针:
cpp
// 传统 JNI:签名字符串手写,三项对不齐只能等运行时 NoSuchMethodErrorstatic jint nativeSum(JNIEnv*, jobject, jint a, jint b) { return a + b; }static JNINativeMethod methods[] = {    {"sum", "(II)I", (void*)nativeSum},};env->RegisterNatives(clazz, methods, 1);

fbjni 把签名绑定到 C++ 函数指针类型。makeNativeMethod 仍然产出 JNINativeMethod,但签名来自 makeDescriptor(&func),不再单独写一份签名字符串:

cpp
// fbjni:签名从 nativeSum 的函数指针类型直接推导,写错类型编译不过jint nativeSum(alias_ref<jobject>, jint a, jint b) { return a + b; }registerNatives("com/example/Foo", {    makeNativeMethod("sum", nativeSum),});
  1. 引用的释放从手动配对换成 RAII。裸 JNI 里,引用类型和释放责任都不在类型上:
cpp
// 传统 JNI:New/Delete 要手动配对jobject globalObj = env->NewGlobalRef(localObj);// ... 使用 globalObjenv->DeleteGlobalRef(globalObj); // 忘记这一行就是引用泄漏

fbjni 的 owning reference 在析构时调用对应的 JNI release API:

cpp
// fbjni:生命周期绑定到 C++ 对象,析构自动 DeleteGlobalRefglobal_ref<jobject> globalObj = make_global(localObj);// 离开作用域自动释放,不需要手写清理代码
  1. 异常在入口被捕获。JNI 入口不能让 C++ 异常继续向外传播:
cpp
// 传统 JNI:C++ 异常裸露到 JNI 边界,进程直接崩溃extern "C" JNIEXPORT void JNICALLJava_com_example_Foo_nativeMethod(JNIEnv*, jobject) {  throw std::runtime_error("boom"); // std::terminate}

fbjni 的动态注册入口会包一层 FunctionWrapper,在那里捕获异常并设置 Java pending exception:

cpp
// fbjni:FunctionWrapper 的 catch(...) 兜底,自动转成 RuntimeExceptionvoid nativeMethod(alias_ref<jobject>) {  throw std::runtime_error("boom"); // 被翻译成 java.lang.RuntimeException}
  1. 线程挂载交给 ThreadScope。裸 JNI 要自己维护 AttachCurrentThread 和 DetachCurrentThread 的配对:
cpp
// 传统 JNI:手动 Attach/Detach,忘记 Detach 或提前退出都会出问题JNIEnv* env;vm->AttachCurrentThread(&env, nullptr);// ... 调用 Java 方法vm->DetachCurrentThread(); // 忘记调用,或者中途 return 跳过这一行

ThreadScope 把“是否由自己挂载”记在对象里,析构时只清理自己造成的状态。跨线程再访问应用类时还要用 WithClassLoader,原因见第八章:

cpp
// fbjni:RAII 守卫,析构自动 Detach,还能拿到应用类加载器ThreadScope::WithClassLoader([&] {  javaObj->someMethod(); // 挂载和类加载器都由 fbjni 处理});

二、整体架构

从依赖方向看,fbjni 可以按三层理解。

fbjni 分层架构

底层是 References、Environment、Exceptions。detail/References.h 定义 local_ref、global_ref、weak_ref、alias_ref,把 JNI 引用的所有权写进 C++ 类型。detail/Environment.cpp 保存全局 JavaVM*,管理线程本地的 JNIEnv* 缓存,并提供 ThreadScope。
detail/Exceptions.h/.cpp 负责 C++ 异常和 Java Throwable 之间的转换。

中间层是 Meta 和 CoreClasses。detail/Meta.h:jtype_traits 把 C++ 类型映射到 JNI descriptor;detail/CoreClasses.h:JavaClass 给 Java 类提供 CRTP 包装,用户自定义 wrapper 都继承这一路径。

上层是 Registration 和 Hybrid。detail/Registration.h:makeNativeMethod 负责生成 JNINativeMethod;detail/Registration-inl.h:FunctionWrapper 和 FunctionWrapperWithJniEntryPoint 负责入口包装。detail/Hybrid.h:HybridClass 处理 Java 对象持有 C++ 对象的场景。

应用层用一个 demo 走通 sumPrimitives。它一次调用要穿过 JNI 边界两次(Java 进 C++ 一次,C++ 再调回 Java 的 doubler 一次),把动态注册、签名推导和反向回调串在同一条链上。

Java 侧定义 DemoCase,调用 NativeBridge.sumPrimitives:

kotlin
DemoCase(    "Primitives",    "sumPrimitives",    "byte/short/int/long into C++, which calls Java's doubler() back",) {    "sumPrimitives(1, 2, 3, 4) = ${NativeBridge.sumPrimitives(1, 2, 3, 4)}  " +        "(1+2+3+4 + doubler(3))"},

NativeBridge.java 里对应一个 native 声明,外加一个留给 C++ 回调的静态方法 doubler:

java
public static native long sumPrimitives(byte b, short s, int i, long l); @DoNotStripstatic long doubler(int i) {  return 2L * i;}

C++ 侧实现是 native_bridge.cpp:43-52。参数直接写 C++ 类型,不再出现 JNIEnv*;getStaticMethod<jlong(jint)>("doubler") 在编译期自动把 jlong(jint) 推导成 JNI 签名:

cpp
jlong JNativeBridge::sumPrimitives(    alias_ref<JClass> clazz,    jbyte b,    jshort s,    jint i,    jlong l) {  // C++ -> Java: look the static method up once, then call it.  static const auto doubler = clazz->getStaticMethod<jlong(jint)>("doubler");  return b + s + i + l + doubler(clazz, i);}

函数指针不再依靠 Java_com_example_... 命名约定,注册表按名字把它绑到 JNativeBridge::sumPrimitives,位置在 native_bridge.cpp:465:

cpp
makeNativeMethod("sumPrimitives", JNativeBridge::sumPrimitives),

三、初始化与动态注册

3.1 facebook::jni::initialize 做了什么

所有注册工作最后都会经过 fbjni/cxx/fbjni/fbjni.cpp:26 的 facebook::jni::initialize:

cpp
jint initialize(JavaVM* const vm, std::function<void()>&& init_fn) noexcept {  Environment::initialize(vm);   try {    init_fn();  } catch ([[maybe_unused]] const std::exception& e) {    FBJNI_LOGE("error %s", e.what());    translatePendingCppExceptionToJavaException();  } catch (...) {    translatePendingCppExceptionToJavaException();  }  return JNI_VERSION_1_6;}

Environment::initialize(vm) 先保存 JavaVM*,后续 Environment::current() 和线程挂载都从这个 VM 出发。init_fn() 是业务库传进来的注册逻辑,比如后面的 JNativeBridge::registerNatives()。注册过程中如果查类、查方法或绑定 native 方法失败,fbjni 可能抛出 C++ 异常;这个异常不能从 JNI_OnLoad 继续向外冒,因为 initialize 声明了 noexcept,所以必须在内部捕获,再调用 translatePendingCppExceptionToJavaException() 把它设置成 Java pending exception,让 Java 侧按普通异常路径抛出去。

3.2 Environment::initialize

detail/Environment.cpp:116 实现了一次性初始化逻辑:

cpp
JavaVM* ROAI g_vm = nullptr; struct EnvironmentInitializer final {  EnvironmentInitializer(JavaVM* const vm) noexcept {    FBJNI_ASSERT(!g_vm);    FBJNI_ASSERT(vm);    g_vm = vm;  }}; /* static */void Environment::initialize(JavaVM* const vm) noexcept {  static EnvironmentInitializer init(vm);}

局部静态变量 init 承担一次性初始化。C++11 起静态局部变量初始化有线程安全保证,因此同一进程里第一次调用会设置 g_vm,后续调用不会重新构造 EnvironmentInitializer。

3.3 so 加载顺序:先加载 fbjni,再加载业务库的 so

demo 的 FooJniLoader.java:

java
public static synchronized void load() {  if (sLoaded) {    return;  }  if (!NativeLoader.isInitialized()) {    NativeLoader.init(new SystemDelegate());  }  NativeLoader.loadLibrary("fbjni");  NativeLoader.loadLibrary("foojni");  sLoaded = true;}

libfbjni.so 先进入进程,触发 fbjni 自己的 fbjni/cxx/fbjni/OnLoad.cpp:32:

cpp
JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) {  return facebook::jni::initialize(vm, [] {    HybridDataOnLoad();    JNativeRunnable::OnLoad();    ThreadScope::OnLoad();    registerNatives(        "com/facebook/jni/FbjniExceptionConfig",        {makeNativeMethod(            "setFixReleaseThrowableLeak", setFixReleaseThrowableLeak_jni)});  });}

这一步注册 HybridData、NativeRunnable、ThreadScope 需要的基础 native 方法。随后 libfoojni.so 的 app/src/main/cpp/foojni_onload.cpp:14-19 注册 demo 自己的两个入口类:

cpp
extern "C" JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void*) {  return facebook::jni::initialize(vm, [] {    foojni::JNativeBridge::registerNatives();    foojni::NativeCounter::registerNatives();  });}

FooJniLoader.java(Demo) 的注释也说明了顺序约束:在调用任何触碰 HybridData 的业务代码之前,libfbjni.so 必须已经加载。libfoojni.so 再调用 facebook::jni::initialize 时,Environment::initialize 不会重设 g_vm,实际执行的是业务闭包里的 JNativeBridge::registerNatives() 和 NativeCounter::registerNatives()。

fbjni 初始化时序

3.4 makeNativeMethod 怎样把签名和入口绑在一起

detail/Registration.h:88-107 是动态注册的入口宏:

cpp
#define makeNativeMethod2(name, func)                                         \  {const_cast<char*>(name ""),                                                \   const_cast<char*>(::facebook::jni::detail::makeDescriptor(&func).c_str()), \   ::facebook::jni::detail::exceptionWrapJNIMethod<decltype(&func), &func>(   \       &func)} #define makeNativeMethodN(a, b, c, count, ...) makeNativeMethod##count#define makeNativeMethod(...) \  FBJNI_MACRO_EXPAND(makeNativeMethodN(__VA_ARGS__, 3, 2)(__VA_ARGS__))

makeNativeMethod2 同时生成三个字段:方法名、descriptor、函数入口。descriptor 来自 detail::makeDescriptor(&func);入口来自 exceptionWrapJNIMethod<decltype(&func), &func>。事实层面,它传给 JVM 的仍然是一个普通的 JNINativeMethod;推断层面,它减少的是“函数指针改了但签名字符串忘改”的风险。

自动参数转换的包装在 detail/Registration-inl.h:85-115:

cpp
template <typename F, typename C, typename R, typename... Args>struct FunctionWrapper {  using jniRet = typename Converter<R>::jniType;  static jniRet call(      JNIEnv* env,      jobject obj,      typename Converter<Args>::jniType... args,      F funcPtr) {    detail::JniEnvCacher jec(env);    try {      return CallWithJniConversions<F, R, JniType<C>, Args...>::call(          static_cast<JniType<C>>(obj), args..., funcPtr);    } catch (...) {      translatePendingCppExceptionToJavaException();      return CreateDefault<jniRet>::create();    }  }};

入口收到 JVM 传进来的 JNIEnv*、jobject 和 JNI 参数类型后,先构造 JniEnvCacher。业务函数在 CallWithJniConversions::call 里执行,参数会按 Converter<T> 转换成 C++ 侧声明的类型。catch (...) 是异常边界:一旦业务函数抛异常,fbjni 设置 Java pending exception,并返回该 JNI 返回类型的默认值。

四、智能引用

JNI 裸指针 jobject 本身看不出引用种类,也看不出释放责任。fbjni 在 detail/References.h 里给出四种常用引用:local_ref<T> 持有局部引用,global_ref<T> 持有全局引用,weak_ref<T> 持有弱全局引用,alias_ref<T> 只是非 owning 视图。

几个符号标出了所有权边界:adopt_local 声明在 detail/References.h:107,表示接管已有 local reference;weak_ref 类从 detail/References.h:415 开始,提供弱全局引用语义;alias_ref 从 detail/References.h:584 开始,不拥有底层引用。换成函数签名时,alias_ref<T> 表示调用期间借用,global_ref<T> 表示对象会跨过当前 JNI 调用栈继续存活。

四种智能引用对比

demo 在 app/src/main/cpp/native_bridge.cpp:21-29 保存了两个长生命周期引用容器:

cpp
global_ref<JString>& globalStash() {  static auto* stash = new global_ref<JString>();  return *stash;} weak_ref<jobject>& weakStash() {  static auto* stash = new weak_ref<jobject>();  return *stash;}

这段代码故意把容器放在 new 出来的对象里,使其不依赖进程退出时的静态析构。源码注释给出的理由是:进程退出或库卸载时再跑 JNI release 不安全。stashGlobal 在 native_bridge.cpp:385-391 把 alias_ref<JString> 升格成 global_ref<JString>;readGlobal 在 393-399 用 make_local 返回局部引用。弱引用路径在 stashWeak 和 isWeakAlive,读之前通过 lockLocal() 判断对象是否还活着。

五、类型映射与编译期描述符推导

5.1 JavaClass<T> 的 CRTP 入口

用户包装 Java 类时,入口是 detail/CoreClasses.h:209 的 JavaClass<T>:

cpp
// Given a class:// struct Foo : JavaClass<Foo> {//   static constexpr auto kJavaDescriptor = "Lcom/example/package/Foo;";// };template <typename T, typename Base = JObject, typename JType = void>class JavaClass : public Base {  using JObjType = typename detail::JTypeFor<T, Base, JType>;  public:  using _javaobject = typename JObjType::_javaobject;  using javaobject = typename JObjType::javaobject;  using JavaBase = JavaClass;   static alias_ref<JClass> javaClassStatic();  static local_ref<JClass> javaClassLocal();  protected:  template <typename... Args>  static local_ref<T> newInstance(Args... args) {    return detail::newInstance<T>(args...);  }   javaobject self() const noexcept;};

T 是用户自己的 wrapper 类型。
例如JPoint : JavaClass<JPoint> 这种 CRTP 写法让 javaClassStatic()、javaClassLocal()、newInstance 都能和具体 Java 类型绑定。kJavaDescriptor 由子类提供。

不过这个包装有边界:JavaClass 能在 C++ 类型层面固定 descriptor 和方法签名,Java 类是否真的存在、构造函数是否匹配,仍然要到运行时查 jclass、jmethodID 时才能确认。CoreClasses.h:223-229 的注释也提醒,newInstance 只能在运行时检查 Java 构造函数是否匹配。

5.2 jtype_traits 把 C++ 类型映射到 descriptor

detail/Meta.h:283-322 是 descriptor 推导的核心:

cpp
template <typename T>struct jtype_traits { private:  using Repr = ReprType<T>;  static constexpr auto descriptor() {    constexpr auto len = Repr::kJavaDescriptor        ? detail::constexpr_strlen(Repr::kJavaDescriptor)        : Repr::get_instantiated_java_descriptor().size();    // ...  }  public:  static constexpr descriptorType kDescriptor = descriptor();  static constexpr basenameType kBaseName = base_name();};

用户自定义 JavaClass 类型走 Repr::kJavaDescriptor 或 get_instantiated_java_descriptor();基础类型靠宏特化:

cpp
DEFINE_FIELD_AND_ARRAY_TRAIT(jboolean, Z)DEFINE_FIELD_AND_ARRAY_TRAIT(jbyte, B)DEFINE_FIELD_AND_ARRAY_TRAIT(jint, I)DEFINE_FIELD_AND_ARRAY_TRAIT(jlong, J)// ...

因此 jint 对应 I,jlong 对应 J,数组类型对应带 [ 的 descriptor。makeDescriptor(&func) 拼的就是参数和返回值的 jtype_traits<T>::kDescriptor。如果某个 C++ 参数类型没有映射,编译期就会失败;
如果 Java 侧实际方法签名和推导结果不一致,失败会发生在运行时查方法或注册 native 方法时异常。

5.3 demo 里的 JPoint / JEventListener

app/src/main/cpp/java_types.h:18-78 给出了普通 Java 类 wrapper:

cpp
struct JPoint : JavaClass<JPoint> {  static constexpr auto kJavaDescriptor =      "Lcom/example/foojni/nativebridge/Point;";   static local_ref<JPoint> create(jint x, jint y) {    return newInstance(x, y);  }   jint getX() const {    static const auto field = javaClassStatic()->getField<jint>("x");    return getFieldValue(field);  }   jint sum() const {    static const auto method = javaClassStatic()->getMethod<jint()>("sum");    return method(self());  }};

getField<jint>("x") 和 getMethod<jint()>("sum") 把字段类型、方法返回值和参数列表写进模板参数。查到的 fieldID、methodID 用 static const auto 缓存,避免每次调用都重新查。JEventListener 在同一文件 84-105,也是继承 JavaClass<JEventListener>,接口和普通类在 fbjni 侧没有不同包装路径:能拿到 class 和 method descriptor,就能按同样方式调用。

六、HybridClass:Java 对象背后的 C++ 状态

JavaClass<T> 适合“C++ 里拿一个类型安全的 Java 句柄”。如果 Java 对象要拥有一份 C++ 状态,比如 native 资源、计数器或长期保存的 listener,就需要 HybridClass<T>。

6.1 HybridData 保存 native 指针

detail/Hybrid.h:36-40 定义了桥接用的 Java 类 wrapper:

cpp
struct HybridData : public JavaClass<HybridData> {  constexpr static auto kJavaDescriptor = "Lcom/facebook/jni/HybridData;";  static local_ref<HybridData> create();  static local_ref<HybridData> create(BaseHybridClass* ptr);};

Java 侧对应 com.facebook.jni.HybridData,内部保存 native pointer。demo 的 app/src/main/java/com/example/foojni/nativebridge/NativeCounter.java:21 持有字段 private final HybridData mHybridData;,字段名必须是 mHybridData,因为 fbjni 会按约定查这个字段。

6.2 HybridClass<T> 的两部分

detail/Hybrid.h:167-269 的主要结构如下:

cpp
template <typename T, typename Base = detail::BaseHybridClass>class HybridClass : public detail::HybridTraits<Base>::CxxBase { public:  struct JavaPart      : JavaClass<JavaPart, typename detail::HybridTraits<Base>::JavaBase> {    using HybridType = T;    T* cthis() const;    friend class HybridClass;    friend T;  };   using jhybridobject = typename JavaPart::javaobject;  protected:  static local_ref<detail::HybridData> makeHybridData(      std::unique_ptr<T> cxxPart) {    auto hybridData = detail::HybridData::create();    setNativePointer(hybridData, std::move(cxxPart));    return hybridData;  }  public:  template <typename... Args>  static local_ref<JavaPart> newObjectCxxArgs(Args&&... args) {    auto cxxPart = std::unique_ptr<T>(new T(std::forward<Args>(args)...));    // ... 走 setNativePointer 或 JavaPart::newInstance(hybridData)  }};

T 是 C++ 业务对象,JavaPart 是与 Java 对象对应的 JavaClass。这种拆分让 C++ 状态和 Java 类型标识分开:业务类 NativeCounter 继承 HybridClass<NativeCounter>,Java 侧 NativeCounter 通过 HybridData 持有它。

6.3 两条创建路径

detail/Hybrid.h:254:newObjectCxxArgs 是 C++ 驱动路径:先构造 T,再创建 Java part,并把 HybridData 放进去。demo 的 NativeCounter::createFromCpp 位于 app/src/main/cpp/native_counter.cpp:25-30,调用 newObjectCxxArgs(initial)。

Java 驱动路径在 NativeCounter.java:24-25 和 native_counter.cpp:17-23:Java 构造函数调用 initHybrid(initial),C++ 侧返回 makeCxxInstance(initial) 创建的 HybridData。

两条创建路径方向不同,但最终结构一致:Java NativeCounter 对象都会通过 HybridData 关联一个对应的 C++ NativeCounter 实例。

6.4 cthis() 取回 C++ 指针

Java 引用进入 C++ 成员方法时,需要从 mHybridData 里取回 T*。detail/Hybrid.h:342-349:

cpp
template <typename T, typename B>inline T* HybridClass<T, B>::JavaPart::cthis() const {  detail::BaseHybridClass* result = nullptr;  static const auto hybridDataField = detectHybrid<T, B>(this->getClass());  result = getHybridDataFromField(this, hybridDataField);  return static_cast<T*>(result);}

这里用 static_cast<T*>,不是 dynamic_cast。源码注释给出的理由是默认 -fno-rtti。所以类型安全依赖注册路径和 Java 对象类型约束:这个 Java 对象必须确实由对应的 HybridClass<T> 管理。

6.5 NativeCounter 实现

app/src/main/cpp/native_counter.cpp 是 demo 中的 HybridClass 示例:

cpp
std::atomic<int> NativeCounter::sLiveInstances{0}; NativeCounter::NativeCounter(jint initial) : value_(initial) {  sLiveInstances.fetch_add(1, std::memory_order_relaxed);} NativeCounter::~NativeCounter() {  sLiveInstances.fetch_sub(1, std::memory_order_relaxed);} local_ref<NativeCounter::jhybriddata> NativeCounter::initHybrid(    alias_ref<jhybridobject>,    jint initial) {  return makeCxxInstance(initial);} local_ref<NativeCounter::jhybridobject> NativeCounter::createFromCpp(    alias_ref<jclass>,    jint initial) {  return newObjectCxxArgs(initial);} void NativeCounter::addFrom(alias_ref<jhybridobject> other) {  value_ += cthis(other)->value_;} void NativeCounter::registerNatives() {  registerHybrid({      makeNativeMethod("initHybrid", NativeCounter::initHybrid),      makeNativeMethod("createFromCpp", NativeCounter::createFromCpp),      makeNativeMethod("add", NativeCounter::add),      // ...  });}

这段同时覆盖几种场景:initHybrid 是 Java 构造函数触发的路径,createFromCpp 是 C++ 工厂路径,addFrom 接收另一个 Java hybrid 引用后用 cthis(other) 取出它的 C++ 部分。sLiveInstances 在构造和析构时增减,配合 NativeCounter.java:72-79 的 close()/isValid() 可以观察 C++ 半身是否已经销毁。

核心类型体系 UML 类图

七、异常翻译机制

7.1 C++ 异常转 Java 异常

detail/Exceptions.cpp:306-346 是 C++ 到 Java 的转换表:

cpp
local_ref<JThrowable> convertCppExceptionToJavaException(    std::exception_ptr ptr) {  local_ref<JThrowable> current;  try {    std::rethrow_exception(ptr);  } catch (JniException& ex) {    current = ex.releaseThrowable();  } catch (const std::ios_base::failure& ex) {    current = JIOException::create(ex.what());  } catch (const std::invalid_argument& ex) {    current = JIllegalArgumentException::create(ex.what());  } catch (const std::bad_alloc& ex) {    current = JOutOfMemoryError::create(ex.what());  } catch (const std::out_of_range& ex) {    current = JArrayIndexOutOfBoundsException::create(ex.what());  } catch (const std::runtime_error& ex) {    current = JRuntimeException::create(ex.what());  } catch (const std::exception& ex) {    current = JCppException::create(ex.what());  } catch (...) {    // 兜底成 JUnknownCppException,尽量带上 RTTI 里的类型名  }  addCppStacktraceToJavaException(current, ptr);  return current;}

除了常见的 std::runtime_error、std::invalid_argument 等分支,还包含 std::system_error -> JCppSystemErrorException 和 const char* -> JUnknownCppException。

demo 的 native_bridge.cpp:283-293 分别抛 std::runtime_error 和 std::invalid_argument,Java 侧 case 在 FbjniDemo.kt:163-172。如果要抛应用自定义 Java 异常,demo 使用 throwNewJavaException,位置是 native_bridge.cpp:295-300:

cpp
void JNativeBridge::throwDemoExceptionFromCpp(    alias_ref<JClass>,    std::string m) {  throwNewJavaException(      "com/example/foojni/nativebridge/DemoException", "%s", m.c_str());}

throwNewJavaException 会先构造包装了 Java throwable 的 C++ 异常。把 pending exception 设置回 JNI 环境detail/Exceptions.cpp:378:translatePendingCppExceptionToJavaException,由 FunctionWrapper::call 的 catch (...) 调用。

7.2 Java 异常进入 C++

反方向由 detail/Exceptions.h:77:JniException 承担:

cpp
class JniException : public std::exception { public:  JniException();  explicit JniException(alias_ref<jthrowable> throwable);   local_ref<JThrowable> getThrowable() const noexcept;  local_ref<JThrowable> releaseThrowable() noexcept;  const char* what() const noexcept override;  private:  global_ref<JThrowable> throwable_;};

当 C++ 调 Java 方法后发现 Java pending exception,fbjni 会抛出 JniException。它继承 std::exception,但内部保存的是 global_ref<JThrowable>,因此 C++ 侧可以读取原 Java 异常。

Demo演示:

cpp
std::string JNativeBridge::catchJavaException(alias_ref<JClass> clazz) {  try {    static const auto method = clazz->getStaticMethod<void()>("alwaysThrows");    method(clazz);  } catch (const JniException& e) {    auto throwable = e.getThrowable();    auto message = throwable->getMessage();    return "caught:" + throwable->getClass()->toString() + ":" +        (message ? message->toStdString() : std::string("<none>"));  }  return "no-exception";}

alwaysThrows 位于 NativeBridge.java:141-144,抛出 IllegalStateException。C++ 侧捕获 JniException 后读取 Throwable 的 class 和 message。

7.3 异常映射表

按 detail/Exceptions.cpp:306-346,源码的映射关系如下:

C++ 异常 Java 异常
JniException 复用原 Throwable
std::invalid_argument IllegalArgumentException
std::bad_alloc OutOfMemoryError
std::out_of_range ArrayIndexOutOfBoundsException
std::ios_base::failure IOException
std::system_error CppSystemErrorException(fbjni 自定义)
std::runtime_error RuntimeException
其他 std::exception CppException(fbjni 自定义)
const char* UnknownCppException
未知类型 UnknownCppException

异常翻译机制时序图

八、线程管理

8.1 JNIEnv* 缓存在 TLS 里

JNIEnv* 不能跨线程使用。fbjni 在 detail/Environment.cpp:144-179 建立线程本地缓存:

cpp
tls_key_t getTLKey() {  static tls_key_t key = makeKey();  return key;} inline JNIEnv* cachedOrNull() {  detail::TLData* pdata = getTLData(getTLKey());  return (pdata ? pdata->env : nullptr);}

FunctionWrapper::call 进入时构造 JniEnvCacher,把本次 JNI 入口带进来的 JNIEnv* 放进 TLS;退出时析构清掉。这样业务函数深处可以用 Environment::current(),不需要层层传 JNIEnv*。

8.2 ThreadScope 只清理自己挂载的线程

native 自己开的线程需要挂到 JVM。detail/Environment.cpp:268-314:

cpp
ThreadScope::ThreadScope() : thisAttached_(false) {  if (g_vm == nullptr) {    throw std::runtime_error(        "fbjni is uninitialized; no thread can be attached.");  }  JNIEnv* env;  auto result = getEnv(&env);  if (result == JNI_OK) {    return; // 已经挂载过了,什么都不做  }  // ... 设置 TLS,调用 attachCurrentThread()  data_.attached = true;  thisAttached_ = true;} ThreadScope::~ThreadScope() {  if (!thisAttached_) {    return;  }  g_vm->DetachCurrentThread();  setTLData(key, nullptr);}

构造函数先用 getEnv(&env) 判断当前线程是否已经附着到 JVM;已经附着就直接返回,thisAttached_ 保持 false。仅当当前对象执行了 AttachCurrentThread,析构时才会 DetachCurrentThread。这个设计使嵌套使用 ThreadScope 时不会把外层或 JVM 已有的挂载状态误删。

8.3 ThreadScope::WithClassLoader 解决应用类加载器问题

裸 AttachCurrentThread 后,线程通常只能可靠访问系统类加载器路径。fbjni 的 detail/Environment.cpp:378-385:

cpp
void ThreadScope::WithClassLoader(std::function<void()>&& runnable) {  if (cachedOrNull() == nullptr) {    ThreadScope ts;    JThreadScopeSupport::runStdFunction(std::move(runnable));  } else {    runnable();  }}

没有缓存 JNIEnv* 时,它先创建 ThreadScope,再通过 Java 侧 ThreadScopeSupport.runStdFunction 执行闭包。推断上,这样做的目的是让闭包执行时经过一个由应用类加载器加载的 Java 帧,挂载线程本身只是顺带的结果,后续查应用类更稳。

8.4 跨线程引用升格

`app/src/main/cpp/native_bridge.cpp:

CODE35
listener 是 alias_ref<JEventListener>,只借用当前调用栈里的引用。lambda 进入新线程时,原局部引用不能再假定有效,所以先用 make_global(listener)。线程体里使用 ThreadScope::WithClassLoader,是为了在 raw std::thread 上回调应用接口 EventListener。

Java 创建的线程路径不同。native_bridge.cpp:446-454:

CODE36
JThread::create 会把 C++ lambda 包成 NativeRunnable,交给 java.lang.Thread 持有。这个线程由 Java 创建,本来就在 JVM 体系内运行,因此和 raw std::thread 那条路径的挂载责任不同。

九、Demo测试

9.1 从 UI 到 JNI 入口

图 ① 从 MainActivity#onCreate:41-50 开始,经 Compose UI 里的 DemoScreen、CaseRow,到 FbjniDemo#runCase-IoAF18A:53-56。runCase 先调用 NativeBridge.ensureLoaded(),这会触发 NativeBridge 的 static initializer,最终进入 FooJniLoader#load:24-33。

fbjni trace ①:UI 触发到 JNI 入口

9.2 registerNatives

图 ② 开始进入 foojni_onload.cpp#JNI_OnLoad:14,随后执行传给 facebook::jni::initialize 的 lambda,再到 JNativeBridge::registerNatives():462。trace 中能看到注册路径的几个关键调用:

CODE37
SimpleFixedString<44>::c_str() 取的是 JNativeBridge::kJavaDescriptor,JClass::registerNatives 最后落到 _JNIEnv::RegisterNatives。宏展开已经在编译阶段把函数、签名和入口准备好。

fbjni trace ②:JNI_OnLoad 与 JNativeBridge 注册

NativeCounter::registerNatives():81 走 Hybrid 分支:

CODE38
这里的差别在 HybridClass<NativeCounter>::javaClassLocal():它从 T::kJavaDescriptor 去掉开头 L 和结尾 ; 后调用 findClassLocal。两类注册最后仍然进入 _JNIEnv::RegisterNatives,只是 Java class 的取得路径不同。

fbjni trace ③:NativeCounter 的 hybrid 注册

9.3 native 调用经过哪些方法

图 ④ 到图 ⑥ 对应一次 NativeBridge.sumPrimitives。trace 主干如下:

CODE39
这条链和第三章的 FunctionWrapper 相匹配:FunctionWrapperWithJniEntryPoint::call 是 JNI 可见入口,FunctionWrapper::call 缓存 JNIEnv* 并包住异常,CallWithJniConversions::call 做参数转换,最后才到 JNativeBridge::sumPrimitives。业务函数内部又用 getStaticMethod<jlong(jint)>("doubler") 找 Java 静态方法,并从 C++ 回调 NativeBridge.doubler。

fbjni trace ④:一次调用的包装入口

fbjni trace ⑤:getStaticMethod 取 jmethodID

fbjni trace ⑥:用 jmethodID 调回 Java

结果再从 native 返回 FbjniDemo.runCase,由 UI 展示:

fbjni trace ⑦:结果回到 UI

UML时序图中[native] References-inl.h 的调用次数最高,集中在 alias_ref 构造、ReprStorage 访问和 local_ref 析构。这个是因为:JNI 边界的高频成本主要落在引用包装和生命周期处理上。Registration-inl.h、Meta-inl.h、MetaConvert.h 的出现次数少得多,集中在入口和类型转换处,说明注册、异常包装、参数转换没有扩散进每层业务逻辑。

十、Demo测试fbjni的三种主要模式

  • JavaClass 模式。app/src/main/cpp/java_types.h 里的 JPoint、JEventListener,以及 native_bridge.h:22:JNativeBridge 都是 wrapper:它们不拥有 C++ 状态,只给 Java 类、接口、静态方法、实例方法和字段访问提供类型化入口。NativeBridge.java 是 Java 侧的静态 native 接口,native_bridge.cpp:462-520 用 makeNativeMethod 一次性注册。

  • HybridClass 模式。NativeCounter 在 native_counter.h:21 继承 HybridClass<NativeCounter>,Java 侧 NativeCounter.java:21 持有 HybridData mHybridData。它适合 Java 对象拥有 C++ 状态的场景:value_、listener_、析构时的 sLiveInstances 都在 C++ 实例。

  • 动态注册模式。foojni_onload.cpp:14-19 是业务库的 JNI 入口,里面调用两个 registerNatives()。项目没有依赖 Java_com_example_... 命名约定,Java 方法名和 C++ 函数的绑定都在注册表里完成。

十一、小结

开头四个问题在 fbjni 里都有对应的优化点:

  • 签名字符串和函数指针的对齐由 jtype_traits、makeDescriptor、makeNativeMethod 收敛;
  • 引用所有权由 local_ref、global_ref、weak_ref、alias_ref 四种类型表达;
  • 跨界的异常由 FunctionWrapper 和 Exceptions.cpp 翻译;
  • 线程挂载交给 JniEnvCacher、ThreadScope 和 WithClassLoader。

JNI 边界仍然存在,fbjni 做的是把容易靠记忆维护的部分放进类型、对象生命周期和统一入口。


本文由 AgentWorkFlow 生成

觉得不错?给作者点个赞吧

分类:Java

评论 (0)

无需注册,留言经站长审核后公开。

加载评论中…

Java-C++ jni交互新范式:fbjni 设计与使用 · Yinchen 的博客