Truffle 原生函数接口

Truffle 包含了一种调用原生函数的方式,称为原生函数接口 (NFI)。它是在 Truffle 之上实现的一种内部语言,语言实现者可以通过标准多语言 eval 接口和 Truffle 交互性访问它。NFI 旨在用于,例如,实现语言的 FFI,或者调用 Java 中不可用的原生运行时例程。

NFI 使用 libffi。在标准 JVM 上,它使用 JNI 调用它,在 GraalVM 原生镜像上,它使用系统 Java。将来,它可能会在原生可执行文件中通过 Graal 编译器进行优化,以便直接从编译后的代码进行原生调用。

稳定性 #

NFI 是一种专为语言实现者设计的内部语言。它不被视为稳定,接口和行为可能会在没有警告的情况下更改。它不打算被最终用户直接使用。

基本概念 #

可以通过您正在使用的任何语言的多语言接口访问 NFI。这可能是 Java,也可能是 Truffle 语言。这使您可以从 Java 语言实现代码或来宾语言中使用 NFI,以减少您需要编写的 Java 代码量。

入口点是多语言 eval 接口。它运行一个特殊的 DSL,并返回 Truffle 交互性对象,这些对象可以进一步公开更多方法。

以下是一些使用 Ruby 的多语言接口的示例,但也可以使用任何其他 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 "filename"
  • load (flag | flag | ...) "filename"

default 命令返回一个伪库,其中包含已在进程中加载的所有符号,相当于 Posix 接口中的 RTLD_DEFAULT

load "filename" 命令从文件加载库。您负责处理与库命名约定和加载路径相关的任何跨平台问题。

load (flag | flag | ...) "filename" 命令允许您指定标志来加载库。对于默认后端(后端将在后面介绍),以及在 Posix 平台上运行时,可用的标志是 RTLD_GLOBALRTLD_LOCALRTLD_LAZYRTLD_NOW,它们具有传统的 Posix 语义。如果未指定 RTLD_LAZYRTLD_NOW,则默认值为 RTLD_NOW

从库加载符号 #

要从库加载符号,请将符号作为属性从之前加载的库对象中读取。

symbol = library['symbol_name']

从符号生成原生函数对象 #

要获得一个可以调用的可执行对象以调用原生函数,请通过创建签名对象并在其上调用 bind 方法来绑定之前加载的符号对象。签名对象需要与原生函数的实际类型签名匹配。

signature = Polyglot.eval('nfi', '...signature...')
function = signature.bind(symbol)

签名的格式为 (arg, arg, ...) : return,其中 argreturn 是类型。

类型可以是以下简单类型之一

  • 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

类型表达式可以任意深度嵌套。

本文档后面关于原生 API 部分中描述了另外两种特殊类型 ENVOBJECT

类型可以以任何大小写形式编写。

您负责将 C 等外语中的类型映射到 NFI 类型。

调用原生函数对象 #

要调用原生函数,请执行它。

return_value = function.call(...arguments...)

从原生代码回调到托管函数 #

使用嵌套签名,函数调用可以获取函数指针作为参数。托管调用方需要传递一个 Polyglot 可执行对象,该对象将被转换为原生函数指针。从原生端调用此函数指针时,会向 Polyglot 对象发送 execute 消息。

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

巴拿马后端 #

巴拿马后端使用由巴拿马项目引入的外来函数和内存 API。该后端仅支持所有类型的子集。具体来说,它不支持 STRINGOBJECTENVFP80 或数组类型。虽然功能不那么齐全,但该后端通常性能更高。目前,它从 JDK 21 开始使用 --enable-preview 标签提供。

原生镜像上的 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

选择 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 语言类型,以及这些参数在托管端映射到的多语言值

NFI 类型 C 语言类型 多语言值
VOID void isNull == true 的多语言对象(仅作为返回值类型有效)。
SINT8/16/32/64 int8/16/32/64_t fitsIn... 相应整数类型的多语言 isNumber
UINT8/16/32/64 uint8/16/32/64_t fitsIn... 相应整数类型的多语言 isNumber
FLOAT float fitsInFloat 的多语言 isNumber
DOUBLE double fitsInDouble 的多语言 isNumber
POINTER void * isPointer == trueisNull == true 的多语言对象。
STRING char *(以零结尾的 UTF-8 字符串) 多语言 isString
OBJECT TruffleObject 任意对象。
[type] type * (原始类型数组) Java 宿主原始类型数组。
(args):ret ret (*)(args) (函数指针类型) 具有 isExecutable == true 的多语言函数。
ENV TruffleEnv * 无 (注入参数)

以下部分详细描述了类型转换。

使用函数指针的类型转换行为可能有点令人困惑,因为参数的方向是相反的。如有疑问,请始终尝试弄清楚参数或返回值流动的方向,是从托管到本机,还是从本机到托管。

VOID #

此类型仅允许作为返回值类型,用于表示不返回值的函数。

由于在多语言 API 中,所有可执行对象都必须返回值,因此具有 isNull == true 的多语言对象将从具有 VOID 返回类型的本机函数返回。

具有 VOID 返回类型的托管回调函数的返回值将被忽略。

原始数字 #

原始数字类型将按预期进行转换。参数需要是一个多语言数字,并且其值需要适合指定数字类型的取值范围。

需要注意的是无符号整型类型的处理。即使多语言 API 没有为适合无符号类型的取值范围指定单独的消息,转换仍然使用无符号取值范围。例如,从本机传递到托管的 SINT8 类型返回值的 0xFF 值将导致多语言数字 -1,它 fitsInByte。但是,作为 UINT8 返回的相同值将导致多语言数字 255,它 fitsInByte

从托管代码传递数字到本机代码时,数字的有符号性将被忽略,只有数字的位数是相关的。因此,例如,将 -1 传递给 UINT8 类型的参数是允许的,并且本机端的返回值是 255,因为它具有与 -1 相同的位数。反过来,将 255 传递给 SINT8 类型的参数也是允许的,并且本机端的返回值是 -1

由于在当前的多语言 API 中不可能表示超出有符号 64 位范围的数字,因此 UINT64 类型目前使用 有符号 语义处理。这是 API 中的一个已知错误,将在将来的版本中更改。

POINTER #

此类型是一个通用的指针参数。在本机端,参数的精确指针类型无关紧要。

传递给 POINTER 参数的多语言对象将尽可能转换为本机指针(使用 isPointerasPointertoNative 消息)。具有 isNull == true 的对象将作为本机 NULL 传递。

POINTER 返回值将生成一个具有 isPointer == true 的多语言对象。本机 NULL 指针还将具有 isNull == true

STRING #

这是一个具有字符串特殊转换语义的指针类型。

使用 STRING 类型从托管传递到本机的多语言字符串将被转换为以零结尾的 UTF-8 编码字符串。对于 STRING 参数,指针由调用者拥有,并且保证仅在调用持续时间内保持活动状态。从托管函数指针返回值到本机调用者的 STRING 值也由调用者拥有。它们必须在使用后使用 free 释放。

多语言指针值或空值也可以传递给 STRING 参数。语义与 POINTER 参数相同。用户有责任确保指针是一个有效的 UTF-8 字符串。

从本机函数传递到托管代码的 STRING 值的行为类似于 POINTER 返回值,但此外它们还具有 isString == true。用户负责指针的所有权,并且可能需要 free 返回值,具体取决于调用的本机函数的语义。在释放返回的指针后,返回的多语言字符串将无效,读取它会导致未定义的行为。从这个意义上说,返回的多语言字符串不是一个安全对象,类似于原始指针。建议 NFI 的用户在将返回的字符串传递给不受信任的托管代码之前复制它。

OBJECT #

此参数对应于 C 类型 TruffleObject。此类型在 trufflenfi.h 中定义,是一个不透明的指针类型。TruffleObject 类型的值表示对任意托管对象的引用。

本机代码除了将 TruffleObject 类型的返回值传递回托管代码(通过返回值或传递给回调函数指针)之外,无法对 TruffleObject 类型的值执行任何操作。

TruffleObject 引用需要手动管理生命周期。有关管理 TruffleObject 引用生命周期的 API 函数,请参阅 trufflenfi.h 中的文档。

作为参数传递的 TruffleObject 由调用者拥有,并且保证在调用持续时间内保持活动状态。从回调函数指针返回的 TruffleObject 引用由调用者拥有,并且需要在使用后释放。从本机函数返回 TruffleObject 不会 转移所有权(但 trufflenfi.h 中有一个 API 函数可以做到这一点)。

[...] (本机原始类型数组) #

此类型仅允许作为从托管代码到本机函数的参数,并且仅支持原始数字类型的数组。

在托管端,仅支持包含 Java 原始类型数组的 Java 宿主对象。在本机端,该类型是指向数组内容的指针。用户有责任作为单独的参数传递数组长度。

指针仅在本机调用的持续时间内有效。

对内容的修改将在从调用返回后传播回 Java 数组。在本机调用期间并发访问 Java 数组的效果是未定义的。

(...):... (函数指针) #

在本机端,嵌套的签名类型对应于具有给定签名的函数指针,回调到托管代码。

使用函数指针类型从托管传递到本机的多语言可执行对象将被转换为可以由本机代码调用的函数指针。对于函数指针参数,函数指针由调用者拥有,并且保证仅在调用持续时间内保持活动状态。函数指针返回值由调用者拥有,并且必须手动释放。有关管理函数指针值生命周期的 API 函数,请参阅 polyglot.h

多语言指针值或空值也可以传递给函数指针参数。语义与 POINTER 参数相同。用户有责任确保指针是一个有效的函数指针。

函数指针返回类型与常规 POINTER 返回类型相同,但此外它们已经 绑定 到给定的签名类型。它们支持 execute 消息,并且行为与常规 NFI 函数相同。

ENV #

此类型是 TruffleEnv * 类型的特殊参数。它仅作为参数类型有效,而不是作为返回类型。它是本机端的注入参数,托管端没有相应的参数。

当用作本机函数的参数类型时,本机函数将在该位置获得一个环境指针。该环境指针可用于调用 API 函数(请参阅 trufflenfi.h)。例如,如果签名是 (SINT32, ENV, SINT32):VOID,则会注入该参数。此函数对象应使用两个整型参数调用,并且相应的本机函数将使用三个参数调用:首先是第一个实际参数,然后是注入的 ENV 参数,然后是第二个实际参数。

ENV 类型用作函数指针参数的参数类型时,该函数指针必须使用有效的 NFI 环境作为参数调用。如果调用者已经有一个环境,则将其传递给回调函数指针比不使用 ENV 参数调用它们更有效。

调用约定 #

本机函数必须使用系统的标准 ABI。目前不支持其他 ABI。

联系我们