前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >Java 17 更新(10):访问外部函数的新 API,JNI 要凉了?

Java 17 更新(10):访问外部函数的新 API,JNI 要凉了?

作者头像
bennyhuo
发布于 2021-10-19 06:22:26
发布于 2021-10-19 06:22:26
2.6K00
代码可运行
举报
文章被收录于专栏:BennyhuoBennyhuo
运行总次数:0
代码可运行

关键词:Java Java17

JNI 不安全还繁琐,所以 Java 搞了一套新的 API,结果把这事儿搞得更复杂了。。。

我们书接上回,接着聊 JEP 412: Foreign Function & Memory API (Incubator) 当中访问外部函数的内容。

调用自定义 C 函数

新 API 加载 Native 库的行为没有发生变化,还是使用 System::loadLibrary 和 System::load 来实现。

相比之前,JNI 需要提前通过声明 native 方法来实现与外部函数的绑定,新 API 则提供了直接在 Java 层通过函数符号来定位外部函数的能力:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
System.loadLibrary("libsimple");
SymbolLookup loaderLookup = SymbolLookup.loaderLookup();
MemoryAddress getCLangVersion = loaderLookup.lookup("GetCLangVersion").get();

对应的 C 函数如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
int GetCLangVersion() {
  return __STDC_VERSION__;
}

通过以上手段,我们直接获得了外部函数的地址,接下来我们就可以使用它们来完成调用:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
MethodHandle getClangVersionHandle = CLinker.getInstance().downcallHandle(
    getCLangVersion,
    MethodType.methodType(int.class),
    FunctionDescriptor.of(C_INT)
);
System.out.println(getClangVersionHandle.invoke());

运行程序的时候需要把编译好的 Native 库放到 java.library.path 指定的路径下,例如我把编译好的 libsimple.dll 放到了 lib/bin 目录下,所以:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
-Djava.library.path=./lib/bin

运行结果:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
201112

可以看出来,我的 C 编译器觉得自己的版本是 C11。

调用系统 C 函数

如果是加载 C 标准库当中的函数,则应使用 CLinker::systemLookup,例如:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
MemoryAddress strlen = CLinker.systemLookup().lookup("strlen").get();
MethodHandle strlenHandle = CLinker.getInstance().downcallHandle(
    strlen,
    MethodType.methodType(int.class, MemoryAddress.class),
    FunctionDescriptor.of(C_INT, C_POINTER)
);

var string = CLinker.toCString("Hello World!!", ResourceScope.newImplicitScope());
System.out.println(strlenHandle.invoke(string.address()));

程序输出:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
13

结构体入参

对于比较复杂的场景,例如传入结构体:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
typedef struct Person {
  long long id;
  char name[10];
  int age;
} Person;

void DumpPerson(Person *person) {
  printf("Person%%%lld(id=%lld, name=%s, age=%d)\n",
         sizeof(Person),
         person->id,
         person->name,
         person->age);

  char *p = person;
  for (int i = 0; i < sizeof(Person); ++i) {
    printf("%d, ", *p++);
  }
  printf("\n");
}

这种情况我们首先需要在 Java 当中构造一个 Person 实例,然后把它的地址传给 DumpPerson,这个过程比较复杂,我们分步骤来介绍:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
MemoryLayout personLayout = MemoryLayout.structLayout(
    C_LONG_LONG.withName("id"),
    MemoryLayout.sequenceLayout(10, C_CHAR).withName("name"),
    MemoryLayout.paddingLayout(16),
    C_INT.withName("age"));

首先我们定义好内存布局,每一个成员我们可以指定一个名字,这样在后面方便定位。注意,由于 Person 的 name 只占 10 个字节(我说我是故意的你信吗),因此这里还有内存对齐问题,根据实际情况设置对应大小的 paddingLayout。

接下来我们用这个布局来开辟堆外内存:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
MemorySegment person = MemorySegment.allocateNative(personLayout, newImplicitScope());

下面就要初始化这个 Person 了:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
VarHandle idHandle = personLayout.varHandle(long.class, MemoryLayout.PathElement.groupElement("id"));
idHandle.set(person, 1000000);

var ageHandle = personLayout.varHandle(int.class, MemoryLayout.PathElement.groupElement("age"));
ageHandle.set(person, 30);

使用 id 和 name 分别定位到对应的字段,并初始化它们,这两个都比较简单。

接下来我们看下如何初始化一个 char[]。

方法1,逐个写入:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
 VarHandle nameHandle = personLayout.varHandle(
     byte.class,
     MemoryLayout.PathElement.groupElement("name"),
     MemoryLayout.PathElement.sequenceElement()
 );

注意我们获取 nameHandle 的方式,要先定位到 name 对应的布局,它实际上是个 sequenceLayout,所以要紧接着用 sequenceElement 来定位它。如果还有更深层次的嵌套,可以在 varHandle(...) 方法当中添加更多的参数来逐级定位。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
byte[] bytes = "bennyhuo".getBytes();
for (int i = 0; i < bytes.length; i++) {
    nameHandle.set(person, i, bytes[i]);
}
nameHandle.set(person, bytes.length, (byte) 0);

然后就是循环赋值,一个字符一个字符写入,比较直接。不过,有个细节要注意,Java 的 char 是两个字节,C 的 char 是一个字节,因此这里要用 Java 的 byte 来写入。

方法2,直接复制 C 字符串:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
person.asSlice(personLayout.byteOffset(MemoryLayout.PathElement.groupElement("name")))
             .copyFrom(CLinker.toCString("bennyhuo", newImplicitScope()));

asSlice 可以通过内存偏移得到 name 这个字段的地址对应的 MemorySegment 对象,然后通过它的 copyFrom 把字符串直接全部复制过来。

两种方法各有优缺点。

接下来就是函数调用了,与前面几个例子基本一致:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
MemoryAddress dumpPerson = loaderLookup.lookup("DumpPerson").get();
MethodHandle dumpPersonHandle = CLinker.getInstance().downcallHandle(
    dumpPerson,
    MethodType.methodType(void.class, MemoryAddress.class),
    FunctionDescriptor.ofVoid(C_POINTER)
);

dumpPersonHandle.invoke(person.address());

结果:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
Person%24(id=1000000, name=bennyhuo, age=30)
64, 66, 15, 0, 0, 0, 0, 0, 98, 101, 110, 110, 121, 104, 117, 111, 0, 0, 0, 0, 30, 0, 0, 0, 

我们把内存的每一个字节都打印出来,在 Java 层也可以打印这个值,这样方便我们调试:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
for (byte b : person.toByteArray()) {
    System.out.print(b + ", ");
}
System.out.println();

以上是单纯的 Java 调用 C 函数的情形。

函数指针入参

很多时候我们需要在 C 代码当中调用 Java 方法,JNI 的做法就是反射,但这样会有些安全问题。新 API 也提供了类似的手段,允许我们把 Java 方法像函数指针那样传给 C 函数,让 C 函数去调用。

下面我们给出一个非常简单的例子,大家重点关注如何传递 Java 方法给 C 函数。

我们首先给出 C 函数的定义,它的功能实际上就是遍历一个数组,调用传入的函数 on_each。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
typedef void (*OnEach)(int element);

void ForEach(int array[], int length, OnEach on_each) {
  for (int i = 0; i < length; ++i) {
    on_each(array[i]);
  }
}

Java 层想要调用 ForEach 这个函数,最关键的地方就是构造 on_each 这个函数指针。接下来我们给出它的 Java 层的定义:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public static void onEach(int element) {
    System.out.println("onEach: " + element);
}

然后把 onEach 转成函数指针,我们只需要通过 MethodHandles 来定位这个方法,得到一个 MethodHandle 实例:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
MethodHandle onEachHandle = MethodHandles.lookup().findStatic(
    ForeignApis.class, "onEach",
    MethodType.methodType(void.class, int.class)
);

接着获取这个函数的地址:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
MemoryAddress onEachHandleAddress = CLinker.getInstance().upcallStub(
    onEachHandle, FunctionDescriptor.ofVoid(C_INT), newImplicitScope()
);

再调用 CLinker 的 upcallStub 来得到它的地址。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
int[] originalArray = new int[]{1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
MemorySegment array = MemorySegment.allocateNative(4 * 10, newImplicitScope());
array.copyFrom(MemorySegment.ofArray(originalArray));

MemoryAddress forEach = loaderLookup.lookup("ForEach").get();
MethodHandle forEachHandle = CLinker.getInstance().downcallHandle(
    forEach,
    MethodType.methodType(void.class, MemoryAddress.class, int.class, MemoryAddress.class),
    FunctionDescriptor.ofVoid(C_POINTER, C_INT, C_POINTER)
);
forEachHandle.invoke(array.address(), originalArray.length, onEachHandleAddress);

剩下的就是构造一个 int 数组,然后再调用 ForEach 这个 C 函数,这与前面调用其他 C 函数的方式是一致的。

运行结果显而易见:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
onEach: 1
onEach: 2
onEach: 3
onEach: 4
onEach: 5
onEach: 6
onEach: 7
onEach: 8
onEach: 9
onEach: 10

小结

这篇文章我们介绍了一下 Java 新提供的这套访问外部函数的 API,相比之下它确实比过去有了更丰富的能力,不过用起来也并不轻松。将来即便正式发布,我个人觉得也需要一些工具来处理这些模板代码的生成(例如基于注解处理器的代码生成框架),以降低使用复杂度.

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-10-12,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Kotlin 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
Java 17 更新(9):Unsafe 不 safe,我们来一套 safe 的 API 访问堆外内存
接下来,我们来聊聊访问外部资源的新 API,这些内容来自于 JEP 412: Foreign Function & Memory API (Incubator)。这个提案主要应对的场景就是调用 Java VM 以外的函数,即 Native 函数;访问 Java VM 以外的内存,即堆外内存(off-heap memory)。
bennyhuo
2021/10/19
2.7K0
Java 17 更新(9):Unsafe 不 safe,我们来一套 safe 的 API 访问堆外内存
JDK21更新内容:ForeignFunctionAndMemoryApi
Foreign Function & Memory API 是 Java 平台的一个功能,它允许开发者直接与本地代码进行交互,并且可以在 Java 中操作本地内存。这个功能最初在 JDK 14 的时候以 JEP 383 的形式引入了第一次预览版,然后在 JDK 15 中进一步改进并发布了第二次预览版(JEP 393),现在在 JDK 21 中发布了第三次预览版(JEP 442)。
程序员朱永胜
2023/09/25
6710
跟妹妹聊到 Java 16 新特征,真香!
2021年3月16日,甲骨文正式发布了Java 16!想当年JDK1.6新出的场景和历历在目,一瞬间,版本已经变成了16,真正体会了一把什么叫做光阴似箭,沧海桑田。虽然目前大部分的场合,Java8还占着主导地位,但我猜想各位Javaer应该对Java16的新特性也大有兴趣吧!
敖丙
2021/03/25
8170
巴拿马项目:打通 JVM 与 Native 代码
作者:Denys Makogon 来源:denismakogon.github.io/openjdk/panama/2022/05/31/introduction-to-project-panama-part-1.html 随着 JDK 19 在未来几周*内发布,是时候讨论巴拿马(Panama)项目了,更具体地说,是新的外部函数和内存 API,它简化了 Java 和本机代码之间的互操作性。 编注:2022年9月20日 JDK 19 已正式发布。 本文使用一个简单的基于 Java 的“Hello World”
程序猿DD
2023/04/04
7810
巴拿马项目:打通 JVM 与 Native 代码
是时候在 Java 中使用方法句柄和变量句柄了,它的效果比反射要好
反射一直是 Java 高级中不可或缺的一部分。如今,它正被更新、更安全的方式所取代。本文将介绍如何使用方法句柄(MethodHandle)和变量句柄(VarHandle)以编程方式访问方法和字段。
前端修罗场
2024/12/22
2170
更高效的反射调用方式被我找到了!
在使用Java进行开发时,我们会不可避免的使用到大量的反射操作,比如Spring Boot会在接收到HTTP请求时,利用反射Controller调用接口中的对应方法,或是Jackson框架使用反射来解析json中的数据给对应字段进行赋值,我们可以编写一个简单的JMH测试来评估一下通过反射调用来创建对象的性能,与直接调用对象构造方法之间的差距:
程序员波特
2024/03/21
3590
当我们在谈论 memory order 的时候,我们在谈论什么
该文介绍了如何利用C++ 11新特性在程序中引入memory order,从而确保数据在多线程环境中正确性和性能。作者详细介绍了memory order的概念以及C++ 11中提供的两种memory order:memory_order_seq_cst和memory_order_acquire。文章还讨论了在多线程环境中出现的一些问题,例如:memory fence、memory barrier、relaxed memory order等,并给出了示例代码以说明如何使用C++ 11的新特性来避免这些问题。
serena
2017/09/12
4.1K4
当我们在谈论 memory order 的时候,我们在谈论什么
Java Concurrent Atomic实现原理&源码解读(JDK 10)
JDK 10,可以说是很新了,比起JDK 8更新了不少实现,比如说下面会讲到VarHandle
邹志全
2019/07/31
8060
【Java】Java18的新特性
在 Java 18 中,UTF-8 被设定为默认的字符集。以前,Java 默认的字符集是基于系统环境的,这在跨平台应用中可能导致字符编码的问题。采用 UTF-8 作为默认字符集,可以统一字符编码的处理方式,提高国际化应用的兼容性。
人不走空
2024/06/08
1840
Java 18 概述:新特性一览
Java 18 是 Java 平台的最新版本,引入了一些令人兴奋的新特性和改进。这些新功能不仅提高了开发者的生产力,还显著增强了 Java 语言的性能和安全性。本文将深入探讨 Java 18 的主要新特性,并结合代码示例,帮助读者更好地理解和应用这些新功能。
程序猿川子
2024/09/09
1860
Java 18 概述:新特性一览
Java 虚拟机:JVM是怎么实现invokedynamic的?(下)
上回讲到,为了让所有的动物都能参加赛马,Java 7 引入了 invokedynamic 机制,允许调用任意类的“赛跑”方法。不过,我们并没有讲解 invokedynamic,而是深入地探讨了它所依赖的方法句柄。
码农架构
2021/02/14
2.1K1
Java 虚拟机:JVM是怎么实现invokedynamic的?(下)
理解 JDK 中的 MethodHandle
该文介绍了Java编程语言中MethodHandle和Method的区别,以及它们的用途和性能差异。MethodHandle在JDK 8及之前主要作为Method的代理对象,在JDK 9之后,MethodHandle被设计为与Method独立存在,有自己的语义和语义解析。MethodHandle在运行时可以通过JVM的代理机制进行调用,而Method需要先获取到对象后才能调用。此外,MethodHandle的调用比Method的调用性能更高,因为MethodHandle的调用不需要获取对象,而Method的调用需要先获取对象。
serena
2017/09/14
5.4K0
Java14的新特性
上面列出的是大方面的特性,除此之外还有一些api的更新及废弃,主要见JDK 14 Release Notes,这里举几个例子。
code4it
2020/03/19
6360
Java14的新特性
CVE-2020-14644 weblogic iiop反序列化漏洞
Oracle WebLogic Server 12.2.1.3.0, 12.2.1.4.0, 14.1.1.0.0
用户7651784
2020/08/10
8830
CVE-2020-14644 weblogic iiop反序列化漏洞
嗨,朋友,你还在用Java 8 吗?Java 23 正式发布了~
9月19日消息,Java 23目前已经正式推出,这是继Java 22之后的又一个非长期支持(LTS)版本,Oracle 对此版本仅提供六个月的支持。 Java 23包含12个新的JEP(JDK增强提案),其中包括其中包括将ZGC的默认模式切换为分代模式。
不惑
2024/09/24
9630
嗨,朋友,你还在用Java 8 吗?Java 23 正式发布了~
Java 7新特性深度解析:提升效率与功能
2.AsynchronousServerSocketChannel 和 AsynchronousSocketChannel:
忆愿
2025/01/14
990
Java 7新特性深度解析:提升效率与功能
MethodHandle方法句柄使用分享
JDK1.7为间接调用方法提供了MethodHandle类,即方法句柄。是对之前JDK1.7之前反射性能不佳的优化手段之一 代码案例如下
每周聚焦
2024/09/18
910
MethodHandle方法句柄使用分享
方法引用(Method reference)和invokedynamic指令详细分析
invokedynamic是jvm指令集里面最复杂的一条。本文将详细分析invokedynamic指令是如何实现方法引用(Method reference)的。
racaljk
2019/02/25
9390
Java 16 新特性介绍
Java 16 在 2021 年 3 月 16 日正式发布,不是长久支持版本,这次更新没有带来很多语法上的改动,但是也带来了不少新的实用功能。
未读代码
2021/12/13
6410
Java 16 新特性介绍
案例分析:常见的Java代码优化法则
代码优化方法从缓冲、缓存、池化对象、大对象复用、并行计算、锁优化、NIO 等优化方法,它们对性能的提升往往是质的飞跃。
小熊学Java
2024/08/27
1770
案例分析:常见的Java代码优化法则
推荐阅读
相关推荐
Java 17 更新(9):Unsafe 不 safe,我们来一套 safe 的 API 访问堆外内存
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档