Learn · concepts

什么是 JNI? 针对模组制作者的 Java 原生接口

JNI(Java Native Interface)是 Java 内置的桥梁,用于在 JVM 中调用 C/C++ 代码,以及反向调用。Minecraft 模组加载原生库的标准方式就是通过 JNI.

TRtrolPublished 2 min read

JNI 究竟是什么?

JNI(Java Native Interface)是 Java 中连接两个世界的部分:JVM 上的受管字节码和用 C 或 C++ 编写的编译后的机器码。一个方法获得 native 关键字且没有主体,在运行时,JVM 将其链接到已加载的本地库中的匹配函数。然后双方交换参数和返回值。

这种边界不是单向的。本地函数接收一个 JNIEnv 指针,这个句柄是返回 Java 的入口:它暴露了读取和写入 Java 字段、调用 Java 方法、构造对象以及抛出异常的调用。因此,让 Java 调用外部的桥梁,也让 C 调用内部。

为什么 Minecraft 模组或客户端会触及原生代码?

大多数 Java 都不需要 JNI。当 JVM 自身无法完成任务时,模组才会使用它,通常是图形、音频、操作系统集成或需要快速执行的例程。本地库完成繁重的工作,而 JNI 是加载和调用它的标准方式.

重复使用经过验证的 C 库

一个成熟的原生代码库已经解决了这个问题。用 Java 重新编写它会是徒劳,因此该模组与之绑定.

进入操作系统

某些平台功能没有纯 Java API。原生调用成为访问它们的唯一途径.

飞速前进

紧凑的数值或低层级例程,以编译后的原生代码形式运行,通常比在 JVM 上以字节码形式运行更快.

驱动硬件和驱动程序

渲染、声音和设备访问通常隐藏在 JNI 向 Java 侧暴露的本机库中.

JNI 调用从头到尾是如何工作的?

流程在每个平台上都是相同的:在 Java 中声明方法,将匹配的 C 函数编译成共享库,在运行时加载它,让 JVM 通过名称将它们链接起来,然后跨调用。函数名称遵循固定的模式,以便 JVM 可以解析它们,并且生成的头文件规定了要实现的精确签名。

步骤发生的情况
声明使用 native 关键字编写 Java 方法,并在 Java 侧为空。
构建将 C 或 C++ 函数编译成共享库:在 Windows 上为 .dll,在 Linux 上为 .so,在 macOS 上为 .dylib
加载Java 调用 System.loadLibrary 在程序运行时加载该库。
链接JVM 通过名称改编的签名将每个 native 方法与它的 C 函数匹配。
调用Java 调用该方法;控制权转移到本机代码并返回结果。

JNI 会让你付出什么代价??

JNI 功能强大,但也存在潜在风险。因为原生代码在 JVM 安全机制之外运行,因此任何错误都可能导致整个进程崩溃,而不是抛出可捕获的异常。原生代码分配的内存对垃圾回收器不可见,除非手动释放,否则会导致内存泄漏。跨越边界的调用会产生额外的开销,在循环体内频繁调用会变得昂贵。而且,原生构建与平台绑定:Windows 的 .dll 文件无法在 Linux 上加载,除非单独编译.

它能买到什么

  • 直接访问现有的 C 和 C++ 库
  • 访问操作系统和硬件功能的途径,JVM 无法触及
  • 对最常用的代码段实现原生的执行速度

成本

  • 原生崩溃可能导致整个 JVM 崩溃
  • 没有垃圾回收机制的手动内存管理
  • 需要维护和发布的平台特定构建
  • 循环热点中的跨边界开销

JNI 与 Foreign Function and Memory API

JNI 是最初的机制,但现在并非唯一选择。较新的 Java 版本提供了 Foreign Function and Memory (FFM) API,旨在以更少的样板代码和更严格的安全保障来调用原生代码。JNI 仍然主导现有的工具链,因为它无处不在且被彻底理解。

JNIFFM API
历史最初的,自早期 Java 以来就存在现代的,近期的版本中添加
样板代码生成的头文件,名称改编的 C 函数在 Java 中定义的 Method Handles,无需 C 胶合代码
内存安全手动;错误可能破坏 JVM具有更强检查的边界段
采用率广泛存在于现有的库中增长中,较新的代码库

常见问题

获取Terminus

Built by people who actually read the JVM internals.