概述

  1. Java 的外部函数接口这个特性,与外部内存接口一起,会极大地丰富 Java 语言的生态环境
  2. Java 或者 Go 这样的通用编程语言,都需要和其它的编程语言或者环境打交道 - 如操作系统或者 C 语言
    • Java 通过 Java 本地接口 JNI 来支持该做法

本地方法接口

示例

1
2
3
4
5
6
7
8
9
10
11
public class HelloWorld {
static {
System.loadLibrary("helloWorld");
}

public static void main(String[] args) {
new HelloWorld().sayHello();
}

private native void sayHello();
}

sayHello 使用了 native 修饰符,是一个本地方法,可以使用 C 语言实现 - 生成对应的 C 语言的头文件

1
2
3
4
$ javac -h . HelloWorld.java

$ ls
HelloWorld.class HelloWorld.h HelloWorld.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class HelloWorld */

#ifndef _Included_HelloWorld
#define _Included_HelloWorld
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: HelloWorld
* Method: sayHello
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_HelloWorld_sayHello
(JNIEnv *, jobject);

#ifdef __cplusplus
}
#endif
#endif

使用 C 语言来实现头文件中方法定义 - HelloWorld.c

1
2
3
4
5
6
7
#include "jni.h"
#include "HelloWorld.h"
#include <stdio.h>

JNIEXPORT void JNICALL Java_HelloWorld_sayHello(JNIEnv *env, jobject jObj) {
printf("Hello World!\n");
}

使用 C 语言的编译器,把 HelloWorld.c 编译链接放到它的动态库里

1
2
3
4
$ gcc -I$JAVA_HOME/include -I$JAVA_HOME/include/darwin -dynamiclib HelloWorld.c -o libhelloWorld.dylib

$ ls
HelloWorld.c HelloWorld.class HelloWorld.h HelloWorld.java libhelloWorld.dylib

运行 Hello World 的本地实现

1
2
$ java -cp . -Djava.library.path=. HelloWorld
Hello World!

步骤

  1. 编写 Java 语言的代码 - HelloWorld**.java**
  2. 编译 Java 语言的代码 - HelloWorld**.class**
  3. 生成 C 语言的头文件 - HelloWorld**.h**
  4. 编写 C 语言的代码 - HelloWorld**.c**
  5. 编译链接 C 语言的实现 - libhelloWorld.dylib
  6. 运行 Java 命令,获得结果

缺陷

  1. 代码实现的过程不够简洁 - 还可以克福
  2. C 语言的编译链接 - Java 本地方法实现的动态库是平台相关的
    • 没有了 Java 语言的一次编译、到处运行跨平台优势
  3. 逃脱了 JVM 语言安全机制,JNI 本质上是不安全

外部函数接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodType;
import jdk.incubator.foreign.*;

public class HelloWorld {

public static void main(String[] args) throws Throwable {
try (ResourceScope scope = ResourceScope.newConfinedScope()) {
CLinker cLinker = CLinker.getInstance();
MemorySegment helloWorld = CLinker.toCString("Hello, world!\n", scope);
MethodHandle cPrintf =
cLinker.downcallHandle(
CLinker.systemLookup().lookup("printf").get(),
MethodType.methodType(int.class, MemoryAddress.class),
FunctionDescriptor.of(CLinker.C_INT, CLinker.C_POINTER));
cPrintf.invoke(helloWorld.address());
}
}
}
  1. try-with-resource 语句里使用的 ResourceScope,定义了内存资源生命周期管理机制
  2. CLinker 实现了 C 语言应用程序二进制接口(Application Binary Interface,ABI)的调用规则
    • 该接口的对象,可以用来链接 C 语言实现的外部函数
  3. 使用 CLinker 的函数标识符(Symbol)查询功能,查找 C 语言定义的函数 printf
  4. 在 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 调用代码要遵守的规则,也有了外部函数的规范

  1. 调用一个外部函数需要的信息已经齐全了,接下来可以生成一个 Java 语言的方法句柄 - MethodHandle
  2. 按照前面定义的 Java 调用规则,使用该方法句柄,就能够访问 C 语言的 printf 函数

对比 JNI 实现的代码

  1. 使用外部函数接口的代码,不再需要编写 C 代码
  2. 也不再需要编译链接C 动态库
    • 不存在由动态库带来的平台相关的问题

提升的安全性

  1. 从根本上来说,任何 Java 代码本地代码之间的交互,都会损害 Java 平台的完整性
  2. 链接到预编译的 C 函数,本质上是不可靠
    • Java 运行时,无法保证 C 函数的签名Java 代码的期望匹配
    • 其中一些可能会导致 JVM 崩溃的错误,这是在 Java 运行时无法阻止的,Java 代码也没有办法捕获的
  3. 使用 JNI 代码的本地代码尤其危险
    • 这些代码可以访问 JDK 的内部,更改不可变数据的数值
    • 允许本地代码绕过 Java 代码的安全机制,破坏了 Java 的安全性赖以存在的边界和假设
    • JNI 本质上是不安全的
  4. 这种破换 Java 平台完整性的风险,对应应用程序开发人员和最终用户来说,几乎无法察觉
    • 随着系统的不断丰富,99% 的代码来自于夹在 JDK 和应用程序之间的第三方、第四方、甚至第五方的类库
  5. 相比之下,大部分外部函数接口的设计是安全
    • 一般来说,使用外部函数接口的代码,不会导致 JVM 的崩溃
    • 也有一部分外部函数接口是不安全的,但这种不安全性并没有到 JNI 那样的严重性
    • 使用外部函数接口的代码,是 Java 代码,因此也会受到 Java 安全机制的约束

JNI 退出的信号 - 外部函数接口提案

1
2
JNI 机制是如此危险,以至于我们希望库在安全和不安全操作中都更喜欢纯 Java 的外部函数接口,以便我们可以在默认情况下及时全面禁用 JNI。
这与使 Java 平台开箱即用、缺省安全的更广泛的 Java 路线图是一致的。