New Java Feature - Foreign Function API
概述
- Java 的外部函数接口这个特性,与外部内存接口一起,会极大地丰富 Java 语言的生态环境
- 像 Java 或者 Go 这样的通用编程语言,都需要和其它的编程语言或者环境打交道 - 如操作系统或者 C 语言
- Java 通过 Java 本地接口 JNI 来支持该做法
本地方法接口
示例
1 | public class HelloWorld { |
sayHello 使用了 native 修饰符,是一个本地方法,可以使用 C 语言实现 - 生成对应的 C 语言的头文件
1 | $ javac -h . HelloWorld.java |
1 | /* DO NOT EDIT THIS FILE - it is machine generated */ |
使用 C 语言来实现头文件中方法定义 - HelloWorld.c
1 |
|
使用 C 语言的编译器,把 HelloWorld.c 编译链接放到它的动态库里
1 | $ gcc -I$JAVA_HOME/include -I$JAVA_HOME/include/darwin -dynamiclib HelloWorld.c -o libhelloWorld.dylib |
运行 Hello World 的本地实现
1 | $ java -cp . -Djava.library.path=. HelloWorld |
步骤
- 编写 Java 语言的代码 - HelloWorld**.java**
- 编译 Java 语言的代码 - HelloWorld**.class**
- 生成 C 语言的头文件 - HelloWorld**.h**
- 编写 C 语言的代码 - HelloWorld**.c**
- 编译链接 C 语言的实现 - libhelloWorld.dylib
- 运行 Java 命令,获得结果
缺陷
- 代码实现的过程不够简洁 - 还可以克福
- C 语言的编译链接 - Java 本地方法实现的动态库是平台相关的
- 没有了 Java 语言的一次编译、到处运行的跨平台优势
- 逃脱了 JVM 语言安全机制,JNI 本质上是不安全的
外部函数接口
1 | import java.lang.invoke.MethodHandle; |
- try-with-resource 语句里使用的 ResourceScope,定义了内存资源的生命周期管理机制
- CLinker 实现了 C 语言的应用程序二进制接口(Application Binary Interface,ABI)的调用规则
- 该接口的对象,可以用来链接 C 语言实现的外部函数
- 使用 CLinker 的函数标识符(Symbol)查询功能,查找 C 语言定义的函数 printf
- 在 C 语言里,printf 这个函数的定义如下
1 | int printf(const char *restrict format, ...); |
在 C 语言中,printf 函数的返回值是整型数据,接收的输入参数是一个可变长参数,使用 C 语言打印
1 | printf("Hello World!\n"); |
将 C 语言的调用形式,表达成 Java 语言外部函数接口的形式
使用了 JDK 17 引入的 MethodType,以及尚处于孵化期的 FunctionDescriptor
MethodType - 定义了后面的 Java 代码必须遵循的调用规则
FunctionDescriptor - 描述了外部函数必须符合的规范
找到了 C 语言定义的函数 printf,规定了 Java 调用代码要遵守的规则,也有了外部函数的规范
- 调用一个外部函数需要的信息已经齐全了,接下来可以生成一个 Java 语言的方法句柄 - MethodHandle
- 按照前面定义的 Java 调用规则,使用该方法句柄,就能够访问 C 语言的 printf 函数
对比 JNI 实现的代码
- 使用外部函数接口的代码,不再需要编写 C 代码
- 也不再需要编译、链接成 C 动态库
- 不存在由动态库带来的平台相关的问题
提升的安全性
- 从根本上来说,任何 Java 代码和本地代码之间的交互,都会损害 Java 平台的完整性
- 链接到预编译的 C 函数,本质上是不可靠的
- Java 运行时,无法保证 C 函数的签名和 Java 代码的期望是匹配的
- 其中一些可能会导致 JVM 崩溃的错误,这是在 Java 运行时无法阻止的,Java 代码也没有办法捕获的
- 使用 JNI 代码的本地代码则尤其危险
- 这些代码可以访问 JDK 的内部,更改不可变数据的数值
- 允许本地代码绕过 Java 代码的安全机制,破坏了 Java 的安全性赖以存在的边界和假设
- JNI 本质上是不安全的
- 这种破换 Java 平台完整性的风险,对应应用程序开发人员和最终用户来说,几乎无法察觉
- 随着系统的不断丰富,99% 的代码来自于夹在 JDK 和应用程序之间的第三方、第四方、甚至第五方的类库
- 相比之下,大部分外部函数接口的设计是安全的
- 一般来说,使用外部函数接口的代码,不会导致 JVM 的崩溃
- 也有一部分外部函数接口是不安全的,但这种不安全性并没有到 JNI 那样的严重性
- 使用外部函数接口的代码,是 Java 代码,因此也会受到 Java 安全机制的约束
JNI 退出的信号 - 外部函数接口提案
1 | JNI 机制是如此危险,以至于我们希望库在安全和不安全操作中都更喜欢纯 Java 的外部函数接口,以便我们可以在默认情况下及时全面禁用 JNI。 |
All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.