0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

调用一些C本机代码来介绍外部函数和内存API

jf_ro2CN3Fa 来源:Denys Makogon 2023-02-21 17:28 次阅读

随着 JDK 19 在未来几周*内发布,是时候讨论巴拿马(Panama)项目了,更具体地说,是新的外部函数和内存 API,它简化了 Java 和本机代码之间的互操作性。

本文使用一个简单的基于 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 实现:

publicstaticLinkergetSystemLinker(){
returnswitch(CABI.current()){
caseWin64->Windowsx64Linker.getInstance();
caseSysV->SysVx64Linker.getInstance();
caseLinuxAArch64->LinuxAArch64Linker.getInstance();
caseMacOsAArch64->MacOsAArch64Linker.getInstance();
};
}

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

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

upcall 例如一些本机代码调用一些 Java 代码。

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

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

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

intprintf(constchar*__restrict,...)

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

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

1. 找到 native 函数的地址

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

Linkerlinker=Linker.nativeLinker();
SymbolLookuplinkerLookup=linker.defaultLookup();
SymbolLookupsystemLookup=SymbolLookup.loaderLookup();

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

OptionalprintfMemorySegment=symbolLookup.lookup("printf");

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

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

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

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

FunctionDescriptorprintfDescriptor=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
OfIntJAVA_INT=newOfInt(ByteOrder.nativeOrder()).withBitAlignment(32);

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

ADDRESS:

//ValueLayout.OfAddress.class
OfAddressADDRESS=newOfAddress(ByteOrder.nativeOrder())
.withBitAlignment(ValueLayout.ADDRESS_SIZE_BITS);

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

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

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

MethodHandleprintfMethodHandle=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(varmemorySession=MemorySession.openConfined()){
SegmentAllocatorallocator=SegmentAllocator.newNativeArena(memorySession);
varcStringFromAllocator=allocator.allocateUtf8String("HelloWorld"+"
");
varcStringFromSession=memorySession.allocateUtf8String("HelloWorld"+"
");
}

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

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

MemorySegmentcString=memorySession.allocateUtf8String(str+"
");

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

privatestaticintprintf(Stringstr,MemorySessionmemorySession)throwsThrowable{
Objects.requireNonNull(printfMethodHandle);
varcString=memorySession.allocateUtf8String(str+"
");
return(int)printfMethodHandle.invoke(cString);
}

publicstaticvoidmain(String[]args)throwsThrowable{
varstr="HelloWorld";
try(varmemorySession=MemorySession.openConfined()){
System.out.println(printf(str,memorySession));
}
}

5. 小结

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

总结

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

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

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

获取本机库及其对应的头文件。

在 Java 中构建函数描述符 ( FunctionDescriptor )。

查找函数符号的本机内存地址,并为其创建方法句柄。

创建一个相关的方法句柄并确认它已经正确创建(例如,如果本机库不在系统路径中,查找将失败并且返回一个方法句柄将为空)。

决定应用程序将如何分配内存段:通过段分配器或内存会话。确保内存分配技术在应用程序的整个代码库中保持一致。

代码清单

packagecom.java_devrel.samples.panama.part_1;
importjava.lang.foreign.*;
importjava.lang.invoke.MethodHandle;
importjava.util.Objects;
importstaticjava.lang.foreign.ValueLayout.ADDRESS;
importstaticjava.lang.foreign.ValueLayout.JAVA_INT;
publicclassPrintfSimplified{
privatestaticfinalLinkerlinker=Linker.nativeLinker();
privatestaticfinalSymbolLookuplinkerLookup=linker.defaultLookup();
privatestaticfinalSymbolLookupsystemLookup=SymbolLookup.loaderLookup();
privatestaticfinalSymbolLookupsymbolLookup=name->systemLookup.lookup(name).or(()->linkerLookup.lookup(name));
privatestaticfinalFunctionDescriptorprintfDescriptor=FunctionDescriptor.of(JAVA_INT.withBitAlignment(32),ADDRESS.withBitAlignment(64));
privatestaticfinalMethodHandleprintfMethodHandle=symbolLookup.lookup("printf").map(addr->linker.downcallHandle(addr,printfDescriptor)).orElse(null);
privatestaticintprintf(Stringstr,MemorySessionmemorySession)throwsThrowable{
Objects.requireNonNull(printfMethodHandle);
varcString=memorySession.allocateUtf8String(str+"
");
return(int)printfMethodHandle.invoke(cString);
}
publicstaticvoidmain(String[]args)throwsThrowable{
varstr="helloworld";
try(varmemorySession=MemorySession.openConfined()){
System.out.println(printf(str,memorySession));
}
}
}






审核编辑:刘清

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • JAVA
    +关注

    关注

    19

    文章

    2954

    浏览量

    104511
  • JVM
    JVM
    +关注

    关注

    0

    文章

    157

    浏览量

    12202
  • C++语言
    +关注

    关注

    0

    文章

    147

    浏览量

    6958
  • printf函数
    +关注

    关注

    0

    文章

    31

    浏览量

    5878

原文标题:巴拿马项目:打通 JVM 与 Native 代码

文章出处:【微信号:芋道源码,微信公众号:芋道源码】欢迎添加关注!文章转载请注明出处。

收藏 人收藏

    评论

    相关推荐

    C语言函数调用过程中的内存变化解析

    调用实例,内存视角,反汇编代码探讨C 语言函数调用
    的头像 发表于 12-11 16:21 3722次阅读

    C语言使用函数调用的知识点

    C语言使用函数调用,我们再熟悉不过了,但是函数调用在内存中究竟发生了什么真的清楚吗?只有搞清楚内存
    发表于 09-07 11:47 803次阅读

    如何查看及更改函数/函数块的调用环境

    模块化设计的思想是把一些相似的功能(比如电机控制、阀控制)设计成函数函数块,这样就可以反复调用。其优点是:使程序架构更加清晰,避免重复编写相似功能的
    的头像 发表于 11-17 09:08 866次阅读
    如何查看及更改<b class='flag-5'>函数</b>/<b class='flag-5'>函数</b>块的<b class='flag-5'>调用</b>环境

    外部代码放入个正确运行地程序里,外部代码里引用了一些外部API函数,系统报错是什么原因?

    本帖最后由 只耳朵怪 于 2018-5-24 15:16 编辑 我在做个项目,需要用到ST公司的传感器,并引入外部代码。我将外部
    发表于 05-22 07:44

    CCS 编程接口 请问CCS有没有给用户一些可以调用外部接口?

    各位好,最近在调试DSP,C6678上的程序,经常需要启动调试器,从内存中拷贝数据“Save Memory”,并执行外部程序进行分析。请问CCS有没有给用户一些可以
    发表于 08-03 06:59

    怎样去使用库函数APIC代码

    使用库函数APIC代码中嵌入汇编代码两种方式使用同个系统
    发表于 12-20 07:30

    CodeViz--款分析C/C++源代码函数调用关系的调用

    程序开发中,有时候需要阅读别人的代码,这时理解代码的组织结构就显得非常重要。CodeViz是款分析CC++
    发表于 04-04 20:50 85次下载
    CodeViz--<b class='flag-5'>一</b>款分析<b class='flag-5'>C</b>/<b class='flag-5'>C</b>++源<b class='flag-5'>代码</b>中<b class='flag-5'>函数</b><b class='flag-5'>调用</b>关系的<b class='flag-5'>调用</b>

    python代码示例之基于Python的日历api调用代码实例

    本文档的主要内容详细介绍的是python代码示例之基于Python的日历api调用代码实例。
    发表于 09-06 14:25 42次下载
    python<b class='flag-5'>代码</b>示例之基于Python的日历<b class='flag-5'>api</b><b class='flag-5'>调用</b><b class='flag-5'>代码</b>实例

    在LabVIEW中使用外部代码的详细资料说明

    本手册介绍调用函数节点和代码接口节点(CIN)。调用函数节点和CIN是用于从基于文本的编程语
    发表于 12-13 14:05 17次下载
    在LabVIEW中使用<b class='flag-5'>外部</b><b class='flag-5'>代码</b>的详细资料说明

    使用Python实现游戏APP充值API调用代码实例

    本文档的主要内容详细介绍的是使用Python实现游戏APP充值API调用代码实例。
    发表于 01-15 11:34 38次下载

    如何使用Python实现购物比价API调用代码实例

    本文档的主要内容详细介绍的是如何使用Python实现购物比价API调用代码实例免费下载。
    发表于 01-17 17:11 25次下载

    C代码与javaScript函数的相互调用问题应该如何解决

    本文档的主要内容详细介绍的是C代码与javaScript函数的相互调用问题应该如何解决。
    发表于 03-05 11:47 17次下载

    FreeRTOS中的API函数功能分析及调用方法

    FreeRTOS中的API函数功能分析及调用方法说明。
    发表于 03-26 11:50 32次下载

    C语言使用函数调用在内存中究竟发生了什么?

    C语言使用函数调用,我们再熟悉不过了,但是函数调用在内存中究竟发生了什么真的清楚吗?只有搞清楚内存
    的头像 发表于 01-13 14:09 1095次阅读

    OpenAI API功能升级:ChatGPT支持描述函数调用

    由于函数调用发挥了至关重要的作用,人工智能模型可以智能地与外部工具和 API 连接。开发人员可以通过为这些模型指定函数来访问大量的功能和服务
    的头像 发表于 07-06 14:28 624次阅读