- 适用于 JDK 24 的 GraalVM(最新)
- 适用于 JDK 25 的 GraalVM(早期访问)
- 适用于 JDK 21 的 GraalVM
- 适用于 JDK 17 的 GraalVM
- 存档
- 开发构建
- Truffle 语言实现框架
- Truffle 分支插桩
- 动态对象模型
- 静态对象模型
- 解释器代码的主机优化
- Truffle 函数内联方法
- 分析 Truffle 解释器
- Truffle 互操作 2.0
- 语言实现
- 使用 Truffle 实现新语言
- Truffle 语言和工具迁移到 Java 模块
- Truffle 原生函数接口
- 优化 Truffle 解释器
- 选项
- 栈上替换
- Truffle 字符串指南
- 特化直方图
- 测试 DSL 特化
- 基于多语言 API 的 TCK
- Truffle 编译队列方法
- Truffle 库指南
- Truffle AOT 概述
- Truffle AOT 编译
- 辅助引擎缓存
- Truffle 语言安全点教程
- 单态化
- 拆分算法
- 单态化用例
- 向运行时报告多态特化
Truffle 原生函数接口
Truffle 提供了一种调用原生函数的方式,称为原生函数接口(Native Function Interface,NFI)。它作为 Truffle 之上的一种内部语言实现,语言实现者可以通过标准的 Polyglot eval 接口和 Truffle 互操作性来访问它。NFI 的设计意图是,例如,用于实现语言的 FFI,或者调用 Java 中不可用的原生运行时例程。
NFI 使用 libffi
。在标准 JVM 上,它使用 JNI 调用 libffi
,而在 GraalVM Native Image 上,它使用系统 Java。未来,Graal 编译器可能会在原生可执行文件中对其进行优化,以便直接从编译后的代码进行原生调用。
稳定性 #
NFI 是专为语言实现者设计的内部语言。它不被认为是稳定的,其接口和行为可能会在不通知的情况下发生变化。它不打算直接供终端用户使用。
基本概念 #
NFI 通过您正在使用的任何语言的 Polyglot 接口进行访问。这可以是 Java,也可以是 Truffle 语言。这使您既可以从 Java 语言实现代码中使用 NFI,也可以从您的访客语言中使用 NFI,从而减少您需要编写的 Java 代码量。
入口点是 Polyglot eval
接口。它运行一个特殊的 DSL,并返回 Truffle 互操作对象,这些对象随后可以公开更多方法。
以下是一些使用 Ruby 的 Polyglot 接口的示例,但也可以使用任何其他 JVM 或语言实现。
基本示例 #
在深入细节之前,这里是一个基本的可行示例。
library = Polyglot.eval('nfi', 'load "libSDL2.dylib"') # load a library
symbol = library['SDL_GetRevisionNumber'] # load a symbol from the library
signature = Polyglot.eval('nfi', '():UINT32') # prepare a signature
function = signature.bind(symbol) # bind the symbol to the signature to create a function
puts function.call # => 12373 # call the function
加载库 #
要加载库,需要评估一个用“nfi
”语言 DSL 编写的脚本。它返回一个表示已加载库的对象。
library = Polyglot.eval('nfi', '...load command...')
load 命令可以是以下任何形式:
默认
load "文件名"
load (flag | flag | ...) "文件名"
default
命令返回一个伪库,其中包含进程中已加载的所有符号,等同于 Posix 接口中的 RTLD_DEFAULT
。
load "文件名"
命令从文件中加载库。您需要负责处理有关库命名约定和加载路径的任何跨平台问题。
load (flag | flag | ...) "文件名"
命令允许您指定加载库的标志。对于默认后端(后端将在后面描述),当在 Posix 平台上运行时,可用标志是 RTLD_GLOBAL
、RTLD_LOCAL
、RTLD_LAZY
和 RTLD_NOW
,它们具有传统的 Posix 语义。如果未指定 RTLD_LAZY
或 RTLD_NOW
,则默认值为 RTLD_NOW
。
从库加载符号 #
要从库中加载符号,请将该符号作为属性从先前加载的库对象中读取。
symbol = library['symbol_name']
从符号生成原生函数对象 #
要获取可调用以调用原生函数的可执行对象,请通过创建签名对象并对其调用 bind
方法来绑定先前加载的符号对象。签名对象需要与原生函数的实际类型签名匹配。
signature = Polyglot.eval('nfi', '...signature...')
function = signature.bind(symbol)
签名的格式为 (arg, arg, ...) : return
,其中 arg
和 return
是类型。
类型可以是以下简单类型之一:
VOID
UINT8
SINT8
UINT16
SINT16
UINT32
SINT32
UINT64
SINT64
FLOAT
DOUBLE
POINTER
STRING
OBJECT
ENV
数组类型是通过将另一种类型放在方括号中形成的。例如 [UINT8]
。这些是 C 风格的数组。
函数指针类型是通过编写嵌套签名形成的。例如,qsort
的签名将是 (POINTER, UINT64, UINT64, (POINTER, POINTER) : SINT32) : VOID
。
对于具有可变参数签名的函数,您可以在可变参数开始的地方指定 ...
,但随后必须指定您将用于调用函数的实际类型。因此,您可能需要多次绑定相同的符号,以便用不同的类型或不同数量的参数调用它。例如,要使用 %d %f
调用 printf
,您将使用类型签名 (STRING, ...SINT32, DOUBLE) : SINT32
。
类型表达式可以任意深度嵌套。
另外两种特殊类型,ENV
和 OBJECT
,将在本文档后面关于原生 API 的部分中描述。
类型可以用任何大小写形式编写。
您需要负责将 C 等外部语言中的类型映射到 NFI 类型。
调用原生函数对象 #
要调用原生函数,请执行它。
return_value = function.call(...arguments...)
从原生代码回调到托管函数 #
使用嵌套签名,函数调用可以获取函数指针作为参数。托管调用者需要传递一个 Polyglot 可执行对象,该对象将被转换为原生函数指针。当从原生端调用此函数指针时,execute
消息会发送给 Polyglot 对象。
void native_function(int32_t (*fn)(int32_t)) {
printf("%d\n", fn(15));
}
signature = Polyglot.eval('nfi', '((SINT32):SINT32):VOID')
native_function = signature.bind(library['native_function'])
native_function.call(->(x) { x + 1 })
回调函数的参数和返回值与常规函数调用一样进行转换,方向相反,即参数从原生转换为托管,返回值从托管转换为原生。
回调函数指针本身可以有函数指针参数。这如您所期望的:函数接受一个原生函数指针作为参数,并将其转换为一个 Truffle 可执行对象。向该对象发送 execute
消息会调用原生函数指针,就像调用常规 NFI 函数一样。
函数指针类型也支持作为返回类型。
组合加载和绑定 #
您可以选择将加载库与加载符号并绑定它们结合起来。这通过扩展的 load
命令实现,该命令随后返回一个对象,其中包含已绑定为方法的函数。
这两个示例是等效的:
library = Polyglot.eval('nfi', 'load libSDL2.dylib')
symbol = library['SDL_GetRevisionNumber']
signature = Polyglot.eval('nfi', '():UINT32')
function = signature.bind(symbol)
puts function.call # => 12373
library = Polyglot.eval('nfi', 'load libSDL2.dylib { SDL_GetRevisionNumber():UINT32; }')
puts library.SDL_GetRevisionNumber # => 12373
花括号 {}
中的定义可以包含多个函数绑定,因此可以一次从库中加载许多函数。
后端 #
load
命令可以前缀 with
,以选择特定的 NFI 后端。提供了多个 NFI 后端。默认称为 native
,如果不存在 with
前缀或所选后端不可用,则将使用它。
根据您正在运行的组件配置,可用的后端可能包括:
native
llvm
,它使用 GraalVM LLVM 运行时来运行原生代码panama
Panama 后端 #
Panama 后端使用 Panama 项目引入的外部函数和内存 API。此后端仅支持所有类型的一个子集。具体来说,它不支持 STRING
、OBJECT
、ENV
、FP80
或数组类型。尽管功能不那么完善,但该后端通常性能更高。它从 JDK 22 开始可用。
Native Image 上的 Truffle NFI #
要构建包含 Truffle NFI 的原生镜像,只需使用 --language:nfi
参数,或在 native-image.properties
中指定 Requires = language:nfi
。可以使用 --language:nfi=<backend>
选择 native
后端要使用的实现。
请注意,--language:nfi=<backend>
参数必须在任何其他可能通过 Requires = language:nfi
将 NFI 作为依赖项引入的参数之前。第一个 language:nfi
实例会生效并决定将哪个后端构建到原生镜像中。
--language:nfi=<backend>
的可用参数包括:
libffi
(默认)无
选择 none
原生后端将有效禁用使用 Truffle NFI 访问原生函数。这将导致依赖原生访问的 NFI 用户(例如 GraalVM LLVM 运行时,除非在 EE 上与 --llvm.managed
一起使用)无法正常工作。
原生 API #
NFI 可以与未经修改、已编译的原生代码一起使用,但也可以与原生代码使用的 Truffle 特定 API 一起使用。
特殊类型 ENV
为签名添加了一个额外的参数 TruffleEnv *env
。另一个简单类型 OBJECT
转换为不透明的 TruffleObject
类型。
trufflenfi.h
头文件提供了处理这些类型的声明,这些声明随后可由通过 NFI 调用的原生代码使用。有关此 API 的更多文档,请参阅 trufflenfi.h
。
类型封送 #
本节详细描述了函数签名中所有类型的参数值和返回值是如何转换的。
下表显示了 NFI 签名中可能的类型及其在原生端的相应 C 语言类型,以及这些参数在托管端映射到的 Polyglot 值。
NFI 类型 | C 语言类型 | Polyglot 值 |
---|---|---|
VOID |
void |
isNull == true 的 Polyglot 对象(仅作为返回类型有效)。 |
SINT8/16/32/64 |
int8/16/32/64_t |
isNumber 且 fitsIn... 相应整数类型的 Polyglot。 |
UINT8/16/32/64 |
uint8/16/32/64_t |
isNumber 且 fitsIn... 相应整数类型的 Polyglot。 |
FLOAT |
float |
isNumber 且 fitsInFloat 的 Polyglot。 |
DOUBLE |
double |
isNumber 且 fitsInDouble 的 Polyglot。 |
POINTER |
POINTER |
void * |
STRING |
isPointer == true 或 isNull == true 的 Polyglot 对象。 |
STRING |
OBJECT |
|
isString 的 Polyglot。 |
OBJECT |
TruffleObject | 任意对象。 |
[类型] |
type * (原始类型数组) |
Java 宿主原始数组。 |
ENV |
(args):ret |
ret (*)(args) (函数指针类型) |
isExecutable == true
的 Polyglot 函数。
ENV
TruffleEnv *
无(注入参数)
以下各节详细描述了类型转换。
函数指针的类型转换行为可能略微令人困惑,因为参数的方向是反转的。如有疑问,请始终尝试弄清楚参数或返回值流动的方向,是从托管到原生,还是从原生到托管。
VOID
#
此类型仅允许作为返回类型,用于表示不返回值的函数。
由于在 Polyglot API 中,所有可执行对象都必须返回值,因此具有 VOID
返回类型的原生函数将返回一个 isNull == true
的 Polyglot 对象。
返回类型为 VOID
的托管回调函数的返回值将被忽略。
原始数字 #
原始数字类型会如您所期望地进行转换。参数需要是一个 Polyglot 数字,其值需要符合指定数字类型的值范围。
需要注意的一点是无符号整数类型的处理。即使 Polyglot API 没有为适合无符号类型的值指定单独的消息,转换仍使用无符号值范围。例如,通过 SINT8
类型的返回值从原生传递到托管的值 0xFF
将导致 Polyglot 数字 -1
,该数字 fitsInByte
。但作为 UINT8
返回的相同值将导致 Polyglot 数字 255
,该数字*不* fitsInByte
。
将数字从托管代码传递到原生代码时,会忽略数字的符号性,只考虑数字的位。因此,例如,允许将 -1
传递给 UINT8
类型的参数,原生端的结果是 255
,因为它与 -1
具有相同的位。反过来,允许将 255
传递给 SINT8
类型的参数,原生端的结果也是 -1
。
由于在当前的 Polyglot API 中无法表示超出有符号 64 位范围的数字,UINT64
类型目前以有符号语义处理。这是 API 中的一个已知错误,将在未来的版本中更改。
POINTER
#
此类型是通用指针参数。在原生端,参数的具体指针类型无关紧要。
传递给 POINTER
参数的 Polyglot 对象将(在必要时使用 isPointer
、asPointer
和 toNative
消息)转换为原生指针。isNull == true
的对象将作为原生 NULL
传递。
POINTER
返回值将生成一个 isPointer == true
的 Polyglot 对象。原生 NULL
指针还将具有 isNull == true
。
STRING
#
这是一种具有字符串特殊转换语义的指针类型。
使用 STRING
类型从托管传递到原生的 Polyglot 字符串将转换为零终止的 UTF-8 编码字符串。对于 STRING
参数,指针由调用者拥有,并且仅在调用期间保证有效。从托管函数指针返回到原生调用者的 STRING
值也由调用者拥有。使用后必须用 free
释放它们。
Polyglot 指针值或空值也可以传递给 STRING
参数。其语义与 POINTER
参数相同。用户负责确保该指针是有效的 UTF-8 字符串。
从原生函数传递到托管代码的 STRING
值行为类似于 POINTER
返回值,但此外它们还具有 isString == true
。用户负责指针的所有权,并且可能需要 free
返回值,具体取决于所调用原生函数的语义。释放返回的指针后,返回的 Polyglot 字符串将无效,读取它会导致未定义行为。从这个意义上说,返回的 Polyglot 字符串不是一个安全对象,类似于一个原始指针。建议 NFI 用户在将其传递给不受信任的托管代码之前复制返回的字符串。
OBJECT
#
此参数对应于 C 类型 TruffleObject
。此类型在 trufflenfi.h
中定义,是一种不透明的指针类型。TruffleObject
类型的值表示对任意托管对象的引用。
原生代码无法对 TruffleObject
类型的值进行任何操作,除了通过返回值或将其传递给回调函数指针将其传回托管代码。
TruffleObject
引用的生命周期需要手动管理。有关管理 TruffleObject
引用生命周期的 API 函数的文档,请参阅 trufflenfi.h
。
作为参数传递的 TruffleObject
由调用者拥有,并保证在调用期间保持有效。从回调函数指针返回的 TruffleObject
引用由调用者拥有,使用后需要释放。从原生函数返回 TruffleObject
不会转移所有权(但 trufflenfi.h
中有一个 API 函数可以实现这一点)。
[...]
(原生原始数组) #
此类型仅允许作为从托管代码到原生函数的参数,并且仅支持原始数字类型的数组。
在托管端,仅支持包含 Java 原始数组的 Java 宿主对象。在原生端,类型是指向数组内容的指针。用户有责任将数组长度作为单独的参数传递。
该指针仅在原生调用期间有效。
调用返回后,内容的修改会传播回 Java 数组。在原生调用期间并发访问 Java 数组的效果是未指定的。
(...):...
(函数指针) #
在原生端,嵌套签名类型对应于具有给定签名的函数指针,回调到托管代码。
使用函数指针类型从托管传递到原生的 Polyglot 可执行对象会转换为可由原生代码调用的函数指针。对于函数指针参数,函数指针由调用者拥有,并保证仅在调用期间有效。函数指针返回值由调用者拥有,必须手动释放。有关管理函数指针值生命周期的 API 函数,请参阅 polyglot.h
。
Polyglot 指针值或空值也可以传递给函数指针参数。其语义与 POINTER
参数相同。用户负责确保该指针是有效的函数指针。
函数指针返回类型与常规 POINTER
返回类型相同,但此外它们已绑定到给定的签名类型。它们支持 execute
消息,并且行为与常规 NFI 函数相同。
ENV
#
此类型是 TruffleEnv *
类型的特殊参数。它仅作为参数类型有效,不能作为返回类型。它是原生端注入的参数,在托管端没有对应的参数。