• Day721. 外部函数接口 -Java8后最重要新特性


    外部函数接口

    Hi,我是阿昌,今天学习记录的是关于外部函数接口
    Java 的外部函数接口这个新特性,还在孵化期,还没有发布预览版

    由于孵化期的特性还不成熟,不同的版本之间的差异可能会很大。

    建议使用最新版本,现在来说就是 JDK 17 来体验孵化期的特性。

    Java 的外部函数接口这个特性,有可能会是 Java 自诞生以来最重要的两个特性之一,它和外部内存接口一起,会极大地丰富 Java 语言的生态环境。

    一、阅读案例

    Java 或者 Go 这样的通用编程语言,都需要和其他的编程语言或者环境打交道,比如操作系统或者 C 语言。

    Java 是通过 Java 本地接口(Java Native Interface, JNI)来支持这样的做法的。

    本地接口,拓展了一门编程语言的生存空间和适用范围。

    有了本地接口,就不用所有的事情都在这门编程语言内部实现了。

    比如下面的代码,就是一个使用 Java 本地接口实现的“Hello, world!"的小例子。

    其中的 sayHello 这个方法,使用了修饰符 native,这表明它是一个本地的方法。

    public class HelloWorld {
        static {
            System.loadLibrary("helloWorld");
        }
    
        public static void main(String[] args) {
            new HelloWorld().sayHello();
        }
    
        private native void sayHello();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    这个本地方法,可以使用 C 语言来实现。

    然后呢,我们需要生成这个本地方法对应的 C 语言的头文件

    $ javac -h . HelloWorld.java
    
    • 1

    有了这个自动生成的头文件,我们就知道了 C 语言里这个方法的定义。

    然后,我们就能够使用 C 语言来实现这个方法了。

    #include "jni.h"
    #include "HelloWorld.h"
    #include 
    
    JNIEXPORT void JNICALL Java_HelloWorld_sayHello(JNIEnv *env, jobject jObj) {
        printf("Hello World!\n");
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    下一步,我们要把 C 语言的实现编译、链接放到它的动态库里。这时候,就要使用 C 语言的编译器了。

    $ gcc -I$(JAVA_HOME)/include -I$(JAVA_HOME)/include/darwin \
          -dynamiclib HelloWorld.c -o libhelloWorld.dylib
    
    • 1
    • 2

    完成了这一步,我们就可以运行这个 Hello World 的本地实现了。

    java -cp . -Djava.library.path=. HelloWorld
    
    • 1

    一个简单的“Hello, world!"的本地接口实现,需要经历下面这些步骤:

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

    其实,在 Java 本地接口的诸多问题中,像代码实现的过程不简洁这样的问题,还属于可以克服的小问题。

    Java 本地接口面临的比较大的问题有两个

    • 一个是 C 语言编译、链接带来的问题,因为 Java 本地接口实现的动态库是平台相关的,所以就没有了 Java 语言“一次编译,到处运行”的跨平台优势
    • 另一个问题是,因为逃脱了 JVM 的语言安全机制,JNI 本质上是不安全的。Java 的外部函数接口,是 Java 语言的设计者试图解决这些问题的一个探索。

    二、外部函数接口

    Java 的外部函数接口是什么样子的呢?

    下面的代码,就是一个使用 Java 的外部函数接口实现的“Hello, world!"的小例子。

    Java 的外部函数接口是怎么工作的:

    
    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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    在这段代码里,try-with-resource 语句里使用的 ResourceScope 这个类,定义了内存资源的生命周期管理机制。

    第 8 行代码里的 CLinker,实现了 C 语言的应用程序二进制接口(Application Binary Interface,ABI)的调用规则。这个接口的对象,可以用来链接 C 语言实现的外部函数。

    第 12 行代码,我们使用 CLinker 的函数标志符(Symbol)查询功能,查找 C 语言定义的函数 printf。在 C 语言里,printf 这个函数的定义就像下面的代码描述的样子。

    int printf(const char *restrict format, ...);
    
    • 1

    C 语言里,printf 函数的返回值是整型数据,接收的输入参数是一个可变长参数。

    如果我们要使用 C 语言打印“Hello, world!”,这个函数调用的形式就像下面的代码。

    printf("Hello World!\n");
    
    • 1

    接下来的两行代码(第 13 行和第 14 行代码),就是要把这个调用形式,表达成 Java 语言外部函数接口的形式。

    这里使用了 JDK 7 引入的 MethodType,以及尚处于孵化期的 FunctionDescriptor。

    MethodType 定义了后面的 Java 代码必须遵守的调用规则。而 FunctionDescriptor 则描述了外部函数必须符合的规范。

    好了,到这里,找到了 C 语言定义的函数 printf,规定了 Java 调用代码要遵守的规则,也有了外部函数的规范。

    调用一个外部函数需要的信息就都齐全了。接下来,生成一个 Java 语言的方法句柄(MethodHandle)(第 11 行),并且按照前面定义的 Java 调用规则,使用这个方法句柄(第 15 行),这样我们就能够访问 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 退出的信号

    当出现了一个更简单、更安全的方案后,原有的方案很难再有竞争力。

    外部函数接口正式发布后,JNI 的退出可能也就要提上议程了。

    在外部函数接口的提案里,可以看到这样的描述:

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

    安全问题往往具有一票否决权,所以,JNI 的退出很可能比预期的还要快!

    五、总结

    Java 的外部函数接口这个尚处于孵化阶段的新特性,对外部函数接口这个新特性有了一个初始的印象。

    外部内存接口外部函数接口联系在一起,为我们提供了一个崭新的不同语言之间的协作方案。

    如果外部函数接口正式发布出来,我们可能需要考虑切换到外部函数接口,逐步退出传统的、基于 JNI 的解决方案。这一次学习的主要目的,就是让你对外部函数接口有一个基本的印象。

    由于外部函数接口尚处于孵化阶段,所以我们不需要学习它的 API。

    只要知道 Java 有这个发展方向,目前来说就足够了。


  • 相关阅读:
    springCloud和springboot升级
    【FPGA教程案例30】基于FPGA的DDS直接数字频率合成器之三——借助MATLAB进行频率精度分析
    【Linux】管理文件和目录的命令大全
    预训练word2vec--Word2Vec实现(二)
    分布式事务(Seata)原理 详解篇,建议收藏
    初识设计模式 - 单例模式
    基于深度学习YOLOv8\YOLOv5的花卉识别鲜花识别检测分类系统设计
    使用Mybatis generator自动生成代码,仅限Oracle数据库
    BMS电池管理系统——BMS的功能模块及基本要素(二)
    SPL工业智能:发现时序数据的异常
  • 原文地址:https://blog.csdn.net/qq_43284469/article/details/126561740