3

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

 2 years ago
source link: https://www.bennyhuo.com/2021/10/02/Java17-Updates-10-foreignapi-callfunction/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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

发表于 2021-10-02 | 阅读次数: 3
本文字数: 7.2k | 阅读时长 ≈ 12 分钟

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

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

调用自定义 C 函数

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

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

System.loadLibrary("libsimple");
SymbolLookup loaderLookup = SymbolLookup.loaderLookup();
MemoryAddress getCLangVersion = loaderLookup.lookup("GetCLangVersion").get();

对应的 C 函数如下:

int GetCLangVersion() {
return __STDC_VERSION__;
}

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

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 目录下,所以:

-Djava.library.path=./lib/bin

运行结果:

201112

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

img

调用系统 C 函数

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

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()));

程序输出:

img

结构体入参

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

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,这个过程比较复杂,我们分步骤来介绍:

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。

img

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

MemorySegment person = MemorySegment.allocateNative(personLayout, newImplicitScope());

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

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,逐个写入:

VarHandle nameHandle = personLayout.varHandle(
byte.class,
MemoryLayout.PathElement.groupElement("name"),
MemoryLayout.PathElement.sequenceElement()
);

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

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 来写入。

img

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

person.asSlice(personLayout.byteOffset(MemoryLayout.PathElement.groupElement("name")))
.copyFrom(CLinker.toCString("bennyhuo", newImplicitScope()));

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

两种方法各有优缺点。

img

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

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());
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 层也可以打印这个值,这样方便我们调试:

for (byte b : person.toByteArray()) {
System.out.print(b + ", ");
}
System.out.println();

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

img

函数指针入参

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

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

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

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 层的定义:

public static void onEach(int element) {
System.out.println("onEach: " + element);
}

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

MethodHandle onEachHandle = MethodHandles.lookup().findStatic(
ForeignApis.class, "onEach",
MethodType.methodType(void.class, int.class)
);

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

MemoryAddress onEachHandleAddress = CLinker.getInstance().upcallStub(
onEachHandle, FunctionDescriptor.ofVoid(C_INT), newImplicitScope()
);

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

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 函数的方式是一致的。

运行结果显而易见:

onEach: 1
onEach: 2
onEach: 3
onEach: 4
onEach: 5
onEach: 6
onEach: 7
onEach: 8
onEach: 9
onEach: 10

img

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

88F4535C.gif

就目前的情况来讲,其实我更愿意用 JNI,不安全怎么了,小心点儿不就行了嘛。算了,写什么垃圾 Java,直接写 C++ 不香吗?

88F3D38D.gif


C 语言是所有程序员应当认真掌握的基础语言,不管你是 Java 还是 Python 开发者,欢迎大家关注我的新课 《C 语言系统精讲》:

扫描二维码或者点击链接《C 语言系统精讲》即可进入课程

program_in_c.png


Kotlin 协程对大多数初学者来讲都是一个噩梦,即便是有经验的开发者,对于协程的理解也仍然是懵懵懂懂。如果大家有同样的问题,不妨阅读一下我的新书《深入理解 Kotlin 协程》,彻底搞懂 Kotlin 协程最难的知识点:

扫描二维码或者点击链接《深入理解 Kotlin 协程》购买本书

understanding_kotlin_coroutines.png


如果大家想要快速上手 Kotlin 或者想要全面深入地学习 Kotlin 的相关知识,可以关注我基于 Kotlin 1.3.50 全新制作的入门课程:

扫描二维码或者点击链接《Kotlin 入门到精通》即可进入课程

exported_qrcode_image_256.png


Android 工程师也可以关注下《破解Android高级面试》,这门课涉及内容均非浅尝辄止,除知识点讲解外更注重培养高级工程师意识:

扫描二维码或者点击链接《破解Android高级面试》即可进入课程

15520936284634.jpg


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK