Redian新闻
>
巴拿马项目:打通 JVM 与 Native 代码

巴拿马项目:打通 JVM 与 Native 代码

公众号新闻

点击上方“芋道源码”,选择“设为星标

管她前浪,还是后浪?

能浪的浪,才是好浪!

每天 10:33 更新文章,每天掉亿点点头发...

源码精品专栏

 
来源: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”应用程序调用一些 C 本机代码来介绍外部函数和内存 API。

准备

要使用 Foreign Function & Memory API 和示例代码,请先下载 JDK 19(build 24 或更高版本)。

项目概述

巴拿马项目旨在为 JVM 和用其他语言(如 C/C++)编写的本机代码之间搭建桥梁。包含以下 3 个部分:

  • 外部函数和内存 API:JEP 424
  • Jextract 工具
  • Vector API:JEP 338

外部函数和内存 API 提供一些重要的抽象:

  • 内存段及其地址:一组 API 类,用于处理本机内存和指向它的指针;
  • 内存布局和描述符:用于模拟外部类型(结构、原语)和函数描述符的 API;
  • 内存会话:管理一个或多个内存资源生命周期的抽象;
  • 链接器和符号查找:一组用于执行向下和向上调用的 API 类;
  • 段分配器:一种用于在内存会话中分配内存段的 API。

Hello World 程序

对巴拿马了解得越深,就越会发现拥有一个好的介绍是至关重要的,这样就不会错过重要的概念、技术和方法。

本文将介绍链接器(Linker),并简要介绍 SymbolLookup 方法和本机内存管理 ( MemorySession )。上面描述的这三个主要组件是构建块,用于更深入地开发由 Java 和本机代码组成的程序。

链接器

从技术角度来看,链接器是两个二进制接口之间的桥梁:JVM 和 C/C++ 本机代码,也称为 C ABI。

JDK 19 为所有流行的平台提供了一组 C ABI 实现:

public static Linker getSystemLinker() {
    return switch (CABI.current()) {
        case Win64 - > Windowsx64Linker.getInstance();
        case SysV - > SysVx64Linker.getInstance();
        case LinuxAArch64 - > LinuxAArch64Linker.getInstance();
        case MacOsAArch64 - > MacOsAArch64Linker.getInstance();
    };
}

在 JDK 术语中,链接器是特定于平台的 C ABI 实现的一个实例。链接器提供一组方法来执行向下调用和向上调用,其中:

  • downcall 是从高级子系统发起的事件。在我们的例子中是 JVM 到较低级别的子系统,如操作系统内核或者一些 Java 代码调用一些本机代码。稍后将通过外部函数和内存 API 说明这一点。
  • upcall  例如一些本机代码调用一些 Java 代码。

虽然链接器就像电话一样,想打电话给谁,只需拨入正确的电话号码即可。符号查找方法就像通讯录,只需提供要打电话的人正确的信息即可。

要执行向下调用,需要提供调用的(本机)函数的描述符、通过符号查找分配的本机地址,以及用于创建调用本机函数的方法句柄对应的链接器。

从 Java 实现经典的 C 风格的 Hello World:

int printf(const char * __restrict, ...)

Java 中的 C 语言风格的“Hello World”

要编写使用本机 printf 函数的基于 Java 的“Hello World”应用程序,我们需要:

基于 Spring Boot + MyBatis Plus + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

  • 项目地址:https://github.com/YunaiV/ruoyi-vue-pro
  • 视频教程:https://doc.iocoder.cn/video/

1. 找到 native 函数的地址

首先,我们需要搜索 printf 函数的本机内存地址:

Linker linker = Linker.nativeLinker();
SymbolLookup linkerLookup = linker.defaultLookup();
SymbolLookup systemLookup = SymbolLookup.loaderLookup();

SymbolLookup symbolLookup = name ->
        systemLookup.lookup(name).or(() -> linkerLookup.lookup(name));

Optional<MemorySegment> printfMemorySegment = symbolLookup.lookup("printf");

从技术上讲,查找可能会失败,因此需要提供适当的错误处理。

基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

  • 项目地址:https://github.com/YunaiV/yudao-cloud
  • 视频教程:https://doc.iocoder.cn/video/

2. 构建正在调用的函数的描述符

一旦知道了 C printf 所在的位置,就需要定义由结果类型和接受的参数组成的 printf 描述符。值得一提的是,像 printf 这样的本机函数称为可变参数函数。在 Java 中,接受可变参数集的方法称为具有可变参数的方法。

为了简化,我们可以为 printf 定义 FunctionDescriptor 的简化版本:

FunctionDescriptor printfDescriptor = FunctionDescriptor.of(JAVA_INT, ADDRESS);

注意 :从 Java 运行时的角度来看,C 指针背后的值类型无关紧要,因为 C 指针的内存布局不保存类型,而是平台固定的 32/64 位值。

一个描述符定义了一个返回值类型为 int 的函数,它的参数是一个指针。假设一个描述符几乎对应于它在 stdio.h 中的 C 定义,因为它定义了一个标准函数,而 printf 是一个可变参数函数。

通过值布局(Value Layout)在 Java 中对 C 类型建模

在 Java 中,值布局用于对与基本数据类型的值关联的内存布局建模,例如整数类型(有符号或无符号)和浮点类型。JAVA_INT 和 ADDRESS 都是对应的 C 类型的值布局。

JAVA_INT :

// ValueLayout.OfInt.class
OfInt JAVA_INT = new OfInt(ByteOrder.nativeOrder()).withBitAlignment(32);

这是值布局的一个实例,它的载体是 int.class。通过这种布局,链接器被指示在 C int32和具有运营商类 int.class 的相应 Java int 类型之间创建桥梁。

ADDRESS:

// ValueLayout.OfAddress.class
OfAddress ADDRESS = new OfAddress(ByteOrder.nativeOrder())
            .withBitAlignment(ValueLayout.ADDRESS_SIZE_BITS);

ADDRESS 是一个值布局,其中对应的 C 类型是一个指向变量的指针,载体是MemoryAddress.class。

3. 从函数的本机内存地址构建方法句柄

使用 C printf 本机地址及其函数描述符,我们现在可以为 C printf 创建一个方法句柄:

MethodHandle printfMethodHandle = symbolLookup.lookup("printf").map(
    addr - > linker.downcallHandle(addr, printfDescriptor)
).orElse(null);

上面的代码创建了 C print 的可执行引用,简而言之:一个方法句柄,来自 printf 的本机内存地址及其函数描述符。

注意 :方法句柄是对底层方法、构造函数、字段或类似低级操作的类型化、可执行引用,具有参数或返回值的可选转换。

现在已经解释了必要的概念,我们可以扩展 downcalls 和 upcalls 的定义:

  • downcall 是通过由本机函数地址及其 Java 版本的函数描述符形成的 MethodHandle调用本机函数。
  • upcall 是通过 MethodHandle 调用一些用 Java 编写的代码,该 MethodHandle 转换为本机内存段,然后可以将其作为函数指针传递给本机函数。

4. 分配本机内存

我们需要以某种方式将 Java 对象绑定到本机内存段,以确保 C printf 可以访问它们。

C 中的内存分配和释放内存都很痛苦,因为开发人员可能会忘记分配或释放内存,这会导致程序泄漏或因分段错误而崩溃。

另一方面,Java 依靠垃圾收集器来分配和释放内存。但是巴拿马的外部函数和内存 API 是在堆外分配内存,有助于分配堆外内存,这是任何本机互操作的关键部分!

外部函数和内存 API 允许开发人员分配和访问内存段、它们的地址以及位于堆上或堆外的连续内存区域的形状。所有分配的内存段都绑定到特定的内存会话 ( MemorySession )。内存会话的实例提供一组 API 来分配本机内存段。考虑一个内存会话,就像一个统一的内存分配工具,比如 C malloc。MemorySession 实现了 AutoClosable 接口,它使用 try-with-resources 结构极大地简化了取消分配。

外部函数和内存 API 提供了不止一种分配内存段的正确方法。一种可能的本机内存分配方法是 SegmentAllocator,它类似于 MemorySession:

try (var memorySession = MemorySession.openConfined()) {
    SegmentAllocator allocator = SegmentAllocator.newNativeArena(memorySession);
    var cStringFromAllocator = allocator.allocateUtf8String("Hello World" + "\n");
    var cStringFromSession = memorySession.allocateUtf8String("Hello World" + "\n");
}

简单起见,这个“Hello World”应用程序将使用 MemorySession 作为内存段分配工具。

最后,要调用 C printf,我们需要使用 MemorySession 在内存会话中分配 const char * 内存段,并将其传递给 C printf 函数:

MemorySegment cString = memorySession.allocateUtf8String(str + "\n");

使用分配的内存段,我们可以调用函数:

private static int printf(String str, MemorySession memorySession) throws Throwable {
    Objects.requireNonNull(printfMethodHandle);
    var cString = memorySession.allocateUtf8String(str + "\n");
    return (int) printfMethodHandle.invoke(cString);
}

public static void main(String[] args) throws Throwable {
    var str = "Hello World";
    try (var memorySession = MemorySession.openConfined()) {
        System.out.println(printf(str, memorySession));
    }
}

5. 小结

到目前为止,我们了解到内存会话 ( MemorySession ) 或段分配器 ( SegmentAllocator ) 是执行内存分配的关键 API。应使用 try-with-resources 声明内存会话以实现隐式内存释放。分配内存段有多种选择——通过段分配器或直接通过内存会话。链接器、符号查找对象、值和内存布局以及方法句柄都是静态对象。

总结

本文概述了外部函数和内存 API,并研究了如何从 Java 调用简单的 C 函数。

好消息是开发人员可以依靠 jextract 工具来处理大部分外部函数和内存机制。

使用外部函数和内存 API 从 Java 调用本机代码时需要解决几个问题:

  • 获取本机库及其对应的头文件。
  • 在 Java 中构建函数描述符 ( FunctionDescriptor )。
  • 查找函数符号的本机内存地址,并为其创建方法句柄。
  • 创建一个相关的方法句柄并确认它已经正确创建(例如,如果本机库不在系统路径中,查找将失败并且返回一个方法句柄将为空)。
  • 决定应用程序将如何分配内存段:通过段分配器或内存会话。确保内存分配技术在应用程序的整个代码库中保持一致。

代码清单

可以在这里找到本文的资源。

https://github.com/denismakogon/openjdk-project-samples/blob/master/Panama.md#openjdk-panama-part-1

package com.java_devrel.samples.panama.part_1;
import java.lang.foreign.*;
import java.lang.invoke.MethodHandle;
import java.util.Objects;
import static java.lang.foreign.ValueLayout.ADDRESS;
import static java.lang.foreign.ValueLayout.JAVA_INT;
public class PrintfSimplified {
    private static final Linker linker = Linker.nativeLinker();
    private static final SymbolLookup linkerLookup = linker.defaultLookup();
    private static final SymbolLookup systemLookup = SymbolLookup.loaderLookup();
    private static final SymbolLookup symbolLookup = name - > systemLookup.lookup(name).or(() - > linkerLookup.lookup(name));
    private static final FunctionDescriptor printfDescriptor = FunctionDescriptor.of(JAVA_INT.withBitAlignment(32), ADDRESS.withBitAlignment(64));
    private static final MethodHandle printfMethodHandle = symbolLookup.lookup("printf").map(addr - > linker.downcallHandle(addr, printfDescriptor)).orElse(null);
    private static int printf(String str, MemorySession memorySession) throws Throwable {
        Objects.requireNonNull(printfMethodHandle);
        var cString = memorySession.allocateUtf8String(str + "\n");
        return (int) printfMethodHandle.invoke(cString);
    }
    public static void main(String[] args) throws Throwable {
        var str = "hello world";
        try (var memorySession = MemorySession.openConfined()) {
            System.out.println(printf(str, memorySession));
        }
    }
}


欢迎加入我的知识星球,一起探讨架构,交流源码。加入方式,长按下方二维码噢

已在知识星球更新源码解析如下:

最近更新《芋道 SpringBoot 2.X 入门》系列,已经 101 余篇,覆盖了 MyBatis、Redis、MongoDB、ES、分库分表、读写分离、SpringMVC、Webflux、权限、WebSocket、Dubbo、RabbitMQ、RocketMQ、Kafka、性能测试等等内容。

提供近 3W 行代码的 SpringBoot 示例,以及超 4W 行代码的电商微服务项目。

获取方式:点“在看”,关注公众号并回复 666 领取,更多内容陆续奉上。

文章有帮助的话,在看,转发吧。

谢谢支持哟 (*^__^*)

微信扫码关注该文公众号作者

戳这里提交新闻线索和高质量文章给我们。
相关阅读
我用 Rust 改写了自己的C++项目:这两个语言都很折磨人!后天的PPI会如何?美国农业部950万美金资助3个生物制品项目:食物垃圾转化、猪粪铺路、大豆油转化为路面热塑性橡胶百度官宣类ChatGPT大模型新项目:文心一言悉尼租房市场接近疯狂!华人租客苦不堪言,Novm Adela现房项目,即刻拎包入住小中国~热巴拿下大饼?于正捧杀新人?骚瑞女主持被洗脑?四千年得罪人?JV | 宿主蛋白PSMD12通过介导甲型流感病毒M1蛋白泛素化调控病毒复制实时的软件生成 —— Prompt 编程打通低代码的最后一公里?优胜美地遂道观景、露营、归家MLNLP发布MyArxiv项目:定制你的专属Arxiv午报 | 拿下Aesop后的欧莱雅市值创历史新高;LVMH低价抛售珠宝品牌Vendorafa;比音勒芬逾7亿收购两个奢侈品牌警方悬赏30000元,缉拿马冬梅两会 | 天府银行黄毅:打通中小银行改革化险"堵点",支持实体经济高质量发展JVI | 囊泡病毒利用宿主细胞调控性死亡生产和释放囊泡从JVM虚拟机到多线程,手撸Java开发面试必备技术栈 | 极客时间秋行南意—白色小城Ostuni 和 Monopoli上海睿康生物:以IVD项目为导向的完整解决方案,真正打通质谱临床落地路径OpenJDK 提议 Galahad 项目合并 GraalVM 的原生编译浅谈阿里开源JVM Sandbox(内含代码实战)陌上花开936 貌美如花又能赚钱养家 只差JV partner 虚位以待阿里:每天100w次登陆请求, 8G 内存该如何设置JVM参数?野!拒绝红圈后,进LVMH做法务月入3W+,每天穿Givenchy,戴BVLGARI《黑太阳731》是属于“纪实历史”电影么?JVI | 研究揭示伪狂犬病病毒诱导炎症反应新机制JVI|诺如病毒主要变体流行十年了,依然保持稳定北交所迄今最大IPO项目:83岁老爷爷携77岁老伴成功过会!JVM调优几款好用的内存分析工具乔伊斯的这句“love loves to love love”,到底啥意思?美团一面:为什么线程崩溃崩溃不会导致 JVM 崩溃多地调整中考长跑项目:改为选考,或统一计满分君子之道与小人之道:为啥俺说陈丹青鄙陋浅薄FastTrack Universität 2023莱比锡大学公立语言项目招生简章教育信息周刊|多地调整中考长跑项目:改为选考,或统一计满分盘点5大技术板块、洞察56个开源项目:InfoQ研究中心带你探秘中国开源数据库一次JVM GC长暂停的排查过程
logo
联系我们隐私协议©2024 redian.news
Redian新闻
Redian.news刊载任何文章,不代表同意其说法或描述,仅为提供更多信息,也不构成任何建议。文章信息的合法性及真实性由其作者负责,与Redian.news及其运营公司无关。欢迎投稿,如发现稿件侵权,或作者不愿在本网发表文章,请版权拥有者通知本网处理。