Learn · concepts
什么是 JNI? 针对模组制作者的 Java 原生接口
JNI(Java Native Interface)是 Java 内置的桥梁,用于在 JVM 中调用 C/C++ 代码,以及反向调用。Minecraft 模组加载原生库的标准方式就是通过 JNI.
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 仍然主导现有的工具链,因为它无处不在且被彻底理解。
| JNI | FFM API | |
|---|---|---|
| 历史 | 最初的,自早期 Java 以来就存在 | 现代的,近期的版本中添加 |
| 样板代码 | 生成的头文件,名称改编的 C 函数 | 在 Java 中定义的 Method Handles,无需 C 胶合代码 |
| 内存安全 | 手动;错误可能破坏 JVM | 具有更强检查的边界段 |
| 采用率 | 广泛存在于现有的库中 | 增长中,较新的代码库 |
常见问题
它已经构建在其中。JNI 随 JDK 和 JVM 一起发布,因此 Java 侧无需额外安装。编译原生部分仍然需要在每个目标平台上使用 C 或 C++ 工具链.
不。Java 部分仍然保持可移植性,但会为单个操作系统和 CPU 编译一个原生库。实现跨平台交付意味着构建和打包独立的 .dll、.so 和 .dylib 文件.
JNI 是最初的机制。最近的 Java 版本增加了外部函数和内存 API,其目标是更安全且减少样板代码,但 JNI 仍然被广泛部署且广为人知.
当 JVM 无法直接执行某些操作(例如与 GPU 或音频后端通信、调用平台专属 API 或运行性能关键例程)时,一个模块会加载一个原生库并通过 JNI 调用它.
Built by people who actually read the JVM internals.