入门§
NativeCall
最简单的用法如下所示
use NativeCall;sub some_argless_function() is native('something')some_argless_function();
第一行导入各种特征和类型。下一行看起来像一个相对普通的 Raku 子程序声明——带有一个小变化。我们使用 native
特征来指定子程序实际上是在原生库中定义的。平台特定的扩展(例如,.so
或 .dll
),以及任何惯用的前缀(例如,lib
)将为您添加。
第一次调用 "some_argless_function" 时,将加载 "libsomething" 并将在其中找到 "some_argless_function"。然后将进行调用。后续调用将更快,因为符号句柄将被保留。
当然,大多数函数都接受参数或返回值——但您可以做的其他所有事情都只是在声明 Raku 子程序、根据您要调用的符号命名它以及使用 native
特征标记它这个简单模式的基础上进行添加。
您还需要声明和使用原生类型,这些类型不能以相同的方式从共享库中导入。有关更多信息,请查看 原生类型页面。
除了您使用自己的编译库或任何其他类型的捆绑库的情况外,共享库都是带版本号的,即它们将位于 libfoo.so.x.y.z
文件中,并且此共享库将被符号链接到 libfoo.so.x
。默认情况下,Raku 将拾取该文件(如果它是唯一存在的文件)。这就是为什么始终包含版本更安全且更可取的原因,方法如下
sub some_argless_function() is native('foo', v1.2.3)
有关更多信息,请查看 关于 ABI/API 版本的部分。
NativeCall 辅助模块§
一个非核心 Raku 模块 App::GPTrixie,有一个 Raku 程序 gptrixie
,用于在启动新的 NativeCall 项目时建立一个起始代码框架。它将自己描述为从 C 头文件生成 NativeCall 代码的工具,其 Github 仓库位于 这里。目前它只支持C 语言,但计划支持C++。
更改名称§
有时您希望 Raku 子程序的名称与您正在加载的库中使用的名称不同。也许名称很长,或者大小写不同,或者在您尝试创建的模块的上下文中很麻烦。
NativeCall 提供了一个 symbol
特征,供您指定库中原生例程的名称,该名称可能与 Raku 子程序名称不同。
unit ;use NativeCall;our sub init() is native('foo') is symbol('FOO_INIT')
在 libfoo
中,有一个名为 FOO_INIT
的例程,但由于我们正在创建一个名为 Foo
的模块,并且我们希望将例程称为 Foo::init
(而不是 Foo::FOO_INIT
),因此我们使用 symbol
特征来指定 libfoo
中符号的名称,并根据需要调用子程序(在本例中为 init
)。
传递和返回值§
使用普通的 Raku 签名和 returns
特征来传达原生函数期望的参数类型及其返回值。以下是一个示例。
use NativeCall;sub add(int32, int32) returns int32 is native("calculator")
在这里,我们声明该函数接受两个 32 位整数并返回一个 32 位整数。您可以在 原生类型 页面中找到您可以传递的其他类型。请注意,缺少 returns
特征用于表示 void
返回类型。除了 Pointer
参数化外,请勿在任何地方使用 void
类型。
对于字符串,还有一个 encoded
特征,可以提供一些关于如何进行封送处理的额外提示。
use NativeCall;sub message_box(Str is encoded('utf8')) is native('gui')
要指定如何封送字符串返回值类型,只需将此特征应用于例程本身即可。
use NativeCall;sub input_box() returns Str is encoded('utf8') is native('gui')
请注意,可以使用 Str
类型对象传递 NULL
字符串指针;NULL
返回值也将由类型对象表示。
如果 C 函数需要数据生命周期,例如通过指针传递的类型为 CArray[uint8]
的缓冲区,超过函数调用,则必须确保在 C 函数预期之前不会销毁支持的 CArray[uint8]
Raku 对象。
这对于字符串尤其重要,因为(通常更可取的)is encoded
传递字符串的方式只创建一个临时对象,该对象在调用后会自动销毁。在这种情况下,必须手动编码参数。
use NativeCall;# void set_foo(const char *)# Records a char pointer for later usagesub set_foo(CArray[uint8]) is native('foo')# void use_foo(void)# Uses the pointer stored by set_foo()sub use_foo() is native('foo')my = "FOO";# Manually marshal the $string into $array. Take care that $array# is not destroyed (e.g. by going out of scope) while the library# still holds on to it, after set_foo().## $string.encode takes care of UTF-8 encoding. If $array is used# as a string by the native function, don't forget to append the# NUL byte that terminates a C string: ------------vmy = CArray[uint8].new(.encode.list, 0);set_foo();# ...use_foo();# It's fine if $array goes out of scope starting from here.
指定本机表示§
在使用本机函数时,有时需要指定要使用哪种本机数据结构。is repr
是用于此目的的术语。
use NativeCall;is repr('CStruct')sub clock_gettime(uint32 , timespec --> uint32) is native ;my timespec .=new;my = clock_gettime( 0, );say "$result, $this-time"; # OUTPUT: «0, timespec<65385480>»
我们正在调用的原始函数,clock_gettime,使用指向 timespec
结构体的指针作为第二个参数。我们在这里将其声明为一个 类,但将其表示指定为 is repr('CStruct')
,以指示它对应于 C 数据结构。当我们创建该类的对象时,我们正在创建 clock_gettime
所期望的指针类型。这样,数据就可以无缝地传输到本机接口和从本机接口传输。
指针的基本使用§
当您的本机函数签名需要指向某些本机类型(int32
、uint32
等)的指针时,您只需将参数声明为 is rw
。
use NativeCall;# C prototype is void my_version(int *major, int *minor)sub my_version(int32 is rw, int32 is rw) is native('foo')my_version(my int32 , my int32 ); # Pass a pointer to
当您只想使用/传递使用 Pointer
类指针时,您必须在声明它们时创建指针;然后,您可以在不指定它指向的类型的情况下执行此操作。以下是一个示例
use NativeCall;# C prototype is void create_object(void **object)sub create_object(Pointer is rw) is native('foo')my Pointer = Pointer.new();create_object(); # pointer is set by create_object
Pointer
类也可以与类型一起使用,例如 Pointer[Str]
。请注意,Str 是指向字符串的指针(在 C/C++ 中为 char *
)。因此,在这种情况下,您可以使用 Pointer[Str]
或 Str
。
有时您需要从 C 库中获取指针(例如,文件句柄)。您不关心它指向什么 - 您只需要抓住它。Pointer
类型为此提供了支持。
use NativeCall;sub Foo_init() returns Pointer is native("foo")sub Foo_free(Pointer) is native("foo")
这可以正常工作,但您可能想使用比 Pointer
更好的名称来命名类型。事实证明,任何表示为 CPointer
的类都可以充当此角色。这意味着您可以通过编写这样的类来公开处理库
use NativeCall;is repr('CPointer')
请注意,CPointer
表示只能保存 C 指针。这意味着您的类不能具有额外的属性。但是,对于简单的库来说,这可能是向其公开面向对象接口的一种简洁方法。
您始终可以使用空类
is repr('CPointer')
只需像使用 Pointer
一样使用该类,但具有更好的类型安全性以及更易读的代码的潜力。
再次,类型对象用于表示 NULL
指针。
函数指针§
C 库可以公开指向 C 函数的指针作为函数的返回值,以及作为结构(如结构体和联合体)的成员。
使用函数 f
返回的函数指针 $fptr
的调用示例,使用定义所需函数参数和返回值的签名
sub f() returns Pointer is native('mylib')my = f();my = nativecast(:(Str, size_t --> int32), );say newfunc("test", 4);
数组§
NativeCall 对数组有一些支持。它被限制为与机器大小的整数、双精度数和字符串、大小数字类型、指针数组、结构体数组和数组数组一起使用。
Raku 数组(除了支持惰性之外)在内存中的布局方式与 C 数组完全不同。因此,NativeCall 库提供了一个更原始的 CArray 类型,如果您使用 C 数组,则必须使用它。
以下是如何传递 C 数组的示例。
sub RenderBarChart(Str, int32, CArray[Str], CArray[num64]) is native("chart")my := CArray[Str].new;[0] = 'Me';[1] = 'You';[2] = 'Hagrid';my := CArray[num64].new;[0] = 59.5e0;[1] = 61.2e0;[2] = 180.7e0;RenderBarChart('Weights (kg)', 3, , );
请注意,绑定用于 @titles
,而不是赋值!如果您赋值,您将把值放入 Raku 数组中,它将无法正常工作。如果这一切让您感到不安,请忘记您曾经了解过的关于 @
符号的任何知识,并在使用 NativeCall 时始终使用 $
。
use NativeCall;my = CArray[Str].new;[0] = 'Me';[1] = 'You';[2] = 'Hagrid';
获取数组的返回值也是一样的。
一些库 API 可能将数组作为缓冲区,该缓冲区将由 C 函数填充,例如,返回实际填充的项目数
use NativeCall;sub get_n_ints(CArray[int32], int32) returns int32 is native('ints')
在这些情况下,重要的是,CArray 至少具有要填充的元素数量,然后再将其传递给本机子例程,否则 C 函数可能会覆盖 Raku 的内存,从而导致可能无法预测的行为
my = 10;my = CArray[int32].allocate(); # instantiates an array with 10 elementsmy = get_n_ints(, );
注意: allocate
是在 Rakudo 2018.05 版本中引入的。在此之前,您必须使用这种机制来将数组扩展到一定数量的元素。
my = CArray[int32].new;my = 10;[ - 1] = 0; # extend the array to 10 items
了解数组如何管理内存非常重要。当您自己创建一个数组时,您可以根据需要向其中添加元素,它会根据需要进行扩展。但是,这可能会导致它在内存中移动(对现有元素的赋值永远不会导致这种情况)。这意味着如果您在将数组传递给 C 库后对其进行操作,您最好知道自己在做什么。
相比之下,当 C 库将数组返回给您时,内存无法由 NativeCall
管理,它也不知道数组在哪里结束。可能,库 API 中的某些东西会告诉您这一点(例如,您知道当您看到一个空元素时,您不应该再读取)。请注意,NativeCall
在这里无法为您提供任何保护 - 做错事,您将得到段错误或导致内存损坏。这不是 NativeCall
的缺点,而是糟糕的原生世界的工作方式。害怕吗?来,抱抱。祝你好运!
CArray 方法§
除了每个 Raku 实例上可用的常用方法外,CArray
还提供以下方法,可用于从 Raku 的角度与它进行交互。
elems
提供数组中的元素数量;AT-POS
提供给定位置(从零开始)的特定元素。此方法不打算直接使用,而是使用下标符号[]
调用。list
提供数组中元素的List
,从原生数组迭代器构建它。
例如,考虑以下简单的代码片段
use NativeCall;my = CArray[int32].new( 1, 2, 3, 4, 5 );say 'Number of elements: ' ~ .elems;# walk the arrayfor .list -># get every element by its index-based positionfor 0...elems - 1 ->
它会产生以下输出
Number of elements: 5 Current element is: 1 Current element is: 2 Current element is: 3 Current element is: 4 Current element is: 5 Element at position 0 is 1 Element at position 1 is 2 Element at position 2 is 3 Element at position 3 is 4 Element at position 4 is 5
CArray 子数组§
使用给定 CArray 的子数组是指针数学的简单练习。请随意使用以下示例来创建您自己的
sub subarray (, #= The CArray to use#= The offset into the array where the subarray should start) is export
结构体§
由于表示多态性,可以声明一个看起来很正常的 Raku 类,它在幕后以与 C 编译器在类似结构体定义中布局的方式相同的方式存储其属性。所需要的只是一次快速使用 repr
特性
is repr('CStruct')
属性只能是 NativeCall 知道如何编组到结构体字段中的类型。目前,结构体可以包含机器大小的整数、双精度数、字符串和其他 NativeCall 对象(CArray
以及使用 CPointer
和 CStruct
repr 的对象)。除此之外,您可以对类执行通常的一系列操作;您甚至可以使某些属性来自角色或从另一个类继承。方法也完全没问题。尽情发挥吧!
CStruct
对象通过引用传递给原生函数,原生函数也必须通过引用返回 CStruct
对象。这些引用的内存管理规则与数组的规则非常相似,但更简单,因为结构体永远不会调整大小。当您创建结构体时,内存会为您管理,当指向 CStruct
实例的变量消失时,当 GC 访问它时,内存将被释放。当 CStruct
基类型用作原生函数的返回类型时,内存不会由 GC 为您管理。
NativeCall 目前不会将对象成员放在容器中,因此为它们分配新值(使用 =
)不起作用。相反,您必须将新值绑定(使用 :=
)到私有成员
is repr('CStruct')
正如您可能已经预测的那样,NULL
指针由结构体类型的类型对象表示。
CUnion
§
同样,可以声明一个 Raku 类,它以与 C 编译器在类似 union
定义中布局的方式相同的方式存储其属性;使用 CUnion
表示
use NativeCall;is repr('CUnion')say nativesizeof(MyUnion.new); # OUTPUT: «8»# ie. max(sizeof(MyUnion.flags32), sizeof(MyUnion.flags64))
HAS
§
CStruct
和 CUnion
可以反过来被周围的 CStruct
和 CUnion
引用或嵌入其中。为了说明前者,我们像往常一样使用 has
,而为了说明后者,我们使用 HAS
声明符
is repr('CStruct')say nativesizeof(MyStruct.new); # OUTPUT: «16»# ie. sizeof(struct Point *) + sizeof(int32_t)is repr('CStruct')say nativesizeof(MyStruct2.new); # OUTPUT: «24»# ie. sizeof(struct Point) + sizeof(int32_t)
内存管理说明§
在将结构体分配为结构体使用时,请确保在 C 函数中分配自己的内存。如果您将结构体传递给需要预先分配的 Str
/char*
的 C 函数,请确保在将结构体传递给函数之前为 Str
类型的变量分配一个容器。
在您的 Raku 代码中...§
is repr("CStruct")
在此代码中,我们首先设置成员 $.a_string
和 $.an_int32
。之后,我们为 init()
方法声明 init_struct()
函数以进行包装;然后从 BUILD
调用此函数,以便在返回创建的对象之前有效地分配值。
请注意,BUILD 正在绑定本机类型的属性,例如 $.an_int32
。您可以始终以这种方式绑定本机类型。
在您的 C 代码中...§
typedef struct a_string_and_an_int32_t_ { char *a_string; int32_t an_int32; } a_string_and_an_int32_t;
这是结构体。请注意我们在这里有一个 char *
。
void init_struct(a_string_and_an_int32_t *target, char *str, int32_t int32) { target->an_int32 = int32; target->a_string = strdup(str); return; }
在此函数中,我们通过按值分配整数并按引用传递字符串来初始化 C 结构体。该函数分配内存,该内存指向结构体内的 char *a_string
,因为它复制了字符串。(请注意,您还需要管理内存的释放,以避免内存泄漏。)
# A long time ago in a galaxy far, far away...my = AStringAndAnInt.new(a_string => "str", an_int => 123);say "foo is and ";# OUTPUT: «foo is str and 123»
类型化指针§
您可以通过将类型作为参数传递来键入 Pointer
。它适用于本机类型,也适用于 CArray
和 CStruct
定义的类型。即使在对它们调用 new
时,NativeCall 也不会隐式地为其分配内存。它在 C 例程返回指针的情况下或如果它是嵌入在 CStruct
中的指针时最有用。
use NativeCall;sub strdup(Str --> Pointer[Str]) is nativemy Pointer[Str] = strdup("Success!");say .deref;
您必须在 Pointer
上调用 .deref
以访问嵌入的类型。在上面的示例中,声明指针的类型避免了取消引用时的类型转换错误。请注意,原始的 strdup
返回指向 char
的指针;我们使用的是 Pointer[Str]
。
my Pointer[int32] ; #For a pointer on int32;my Pointer[MyCstruct] = some_c_routine();my MyCstruct = .deref;say .field1;
本机函数返回指向元素数组的指针的情况很常见。类型化指针可以作为数组取消引用,以获取各个元素。
my = 5;# returns a pointer to an array of length $nmy Pointer[Point] = some_other_c_routine();# display the 5 elements in the arrayfor 1 .. ->
指针也可以更新为引用数组中的后续元素
my Pointer[Point] = ;# show differences between successive pointsfor 1 ..^
空指针也可以通过将它们声明为 Pointer[void]
来使用。有关此主题的更多信息,请参阅 本机类型文档。
字符串§
显式内存管理§
假设有一些 C 代码缓存传递的字符串,如下所示
#include <stdlib.h> static char *__VERSION; char * get_version() { return __VERSION; } char * set_version(char *version) { if (__VERSION != NULL) free(__VERSION); __VERSION = version; return __VERSION; }
如果您要为 get_version
和 set_version
编写绑定,它们最初看起来像这样,但不会按预期工作
sub get_version(--> Str) is native('./version')sub set_version(Str --> Str) is native('./version')say set_version('1.0.0'); # 1.0.0say get_version; # Differs on each runsay set_version('1.0.1'); # Double free; segfaults
此代码在第二次 set_version
调用时出现段错误,因为它尝试在垃圾收集器已经完成之后释放第一次调用时传递的字符串。如果垃圾收集器不应该释放传递给本机函数的字符串,请使用 explicitly-manage
与其一起使用
say set_version(explicitly-manage('1.0.0')); # 1.0.0say get_version; # 1.0.0say set_version(explicitly-manage('1.0.1')); # 1.0.1say get_version; # 1.0.1
请记住,显式管理字符串的所有内存管理必须由 C 库本身或通过 NativeCall API 处理,以防止内存泄漏。
缓冲区和 Blob§
Blob
和 Buf
是 Raku 存储二进制数据的方式。我们可以将它们用于与本机函数和数据结构交换数据,尽管不能直接使用。我们将不得不使用 nativecast
。
my = Blob.new(0x22, 0x33);my = nativecast(Pointer, );
然后,此 $src
可用作任何接受 Pointer
的原生函数的参数。相反,将 Pointer
指向的值放入 Buf
或使用它来初始化 Blob
则不受直接支持。您可能需要使用 NativeHelpers::Blob
来执行此类操作。
my = blob-from-pointer( , :2elems, :type(Blob[int8]));say ;
函数参数§
NativeCall 还支持将函数作为参数的原生函数。这方面的一个例子是在事件驱动系统中使用函数指针作为回调。通过 NativeCall 绑定这些函数时,只需要提供等效的签名作为 代码参数上的约束。但是,在 NativeCall 的情况下,截至 Rakudo 2019.07,函数参数和签名之间的空格以及正常 Signature
文字的冒号被省略,如
use NativeCall;# void SetCallback(int (*callback)(const char *))my sub SetCallback( (Str --> int32)) is native('mylib')
注意:原生代码负责以这种方式传递给 Raku 回调的值的内存管理。换句话说,NativeCall 不会释放传递给回调的字符串。
重要的是,任何作为原生回调传递的代码都应处理其异常,并在适用时向调用它的原生代码返回适当的错误值。不允许从原生回调中抛出异常,这样做会导致进程终止。
库路径和名称§
native
特性接受库名称、完整路径或返回这两个值之一的子例程。使用库名称时,假定该名称以 lib
开头,以 .so
结尾(或在 Windows 上仅以 .dll
结尾),并将搜索 LD_LIBRARY_PATH
(Windows 上的 PATH
)环境变量中的路径。
use NativeCall;constant LIBMYSQL = 'mysqlclient';constant LIBFOO = '/usr/lib/libfoo.so.1';sub LIBBAR# and latersub mysql_affected_rows returns int32 is native(LIBMYSQL) ;sub bar is native(LIBFOO)sub baz is native(LIBBAR)
您也可以使用不完整的路径,例如 './foo',NativeCall 会根据平台规范自动添加正确的扩展名。如果您希望抑制此扩展,只需将字符串作为块的主体传递。
sub bar is native()
小心:native
特性和 constant
在编译时进行评估。不要编写依赖于动态变量的常量,例如
# WRONG:constant LIBMYSQL = <P6LIB_MYSQLCLIENT> || 'mysqlclient';
这将保留编译时给定的值。模块将被预编译,LIBMYSQL
将保留它在模块被预编译时获取的值。
ABI/API 版本§
如果您编写 native('foo')
,NativeCall 将在类 Unix 系统(OS X 上的 libfoo.dynlib
,win32 上的 foo.dll
)下搜索 libfoo.so
。在大多数现代系统中,它将要求您或您的模块的用户安装开发包,因为建议始终为共享库提供 API/ABI 版本,因此 libfoo.so
通常只是一个开发包提供的符号链接。
为了避免这种情况,native
特性允许您指定 API/ABI 版本。它可以是完整版本或只是其中的一部分。(尝试坚持使用主要版本,一些 BSD 代码不关心次要版本。)
use NativeCall;sub foo1 is native('foo', v1) # Will try to load libfoo.so.1sub foo2 is native('foo', v1.2.3) # Will try to load libfoo.so.1.2.3my List = ('foo', 'v1');sub foo3 is native()
例程§
native
特性还接受 Callable
作为参数,允许您提供自己的方式来处理它将找到要加载的库文件的方式。
use NativeCall;sub foo is native(sub )
它只会在子例程的第一次调用时被调用。
调用标准库§
如果您想调用已加载的 C 函数,无论是来自标准库还是来自您自己的程序,您都可以省略该值,因此 is native
。
例如,在类 Unix 操作系统上,您可以使用以下代码打印当前用户的 home 目录
use NativeCall;my is repr('CStruct')sub getuid() returns uint32 is native ;sub getpwuid(uint32 ) returns PwStruct is native ;say getpwuid(getuid()).pw_dir;
虽然 $*HOME
是一个更容易的方式 :-)!
导出变量§
库导出的变量(也称为“全局”或“extern”变量)可以使用 cglobal
访问。例如
my := cglobal('libc.so.6', 'errno', int32)
此代码将一个新的 Proxy
对象绑定到 $var
,该对象将所有访问重定向到由 libc.so.6
库导出的名为“errno”的整型变量。
C++ 支持§
NativeCall 提供了对使用 C++ 类和方法的支持,如 https://github.com/rakudo/rakudo/blob/master/t/04-nativecall/13-cpp-mangling.t(及其关联的 C++ 文件)所示。请注意,目前它不像 C 支持那样经过测试和开发。
辅助函数§
NativeCall
库导出了一些子例程来帮助您处理来自本机库的数据。
sub nativecast§
sub nativecast(, ) is export(:DEFAULT)
这将转换Pointer
$source
为 $target-type
的对象。源指针通常是从调用返回指针的本机子例程或作为 struct
成员获得的,这可以在 C
库定义中指定为 void *
,例如,但您也可以从指向不太具体类型的指针转换为指向更具体类型的指针。
作为特殊情况,如果提供 Signature
作为 $target-type
,则将返回一个 subroutine
,它将以与使用 native
特性声明的子例程相同的方式调用 $source
指向的本机函数。这在 函数指针 中有描述。
sub cglobal§
sub cglobal(, , ) is export is rw
这将返回一个 Proxy
对象,该对象提供对由指定库公开的 extern
命名为 $symbol
的访问权限。库可以通过与 native
特性相同的方式指定。
sub nativesizeof§
sub nativesizeof() is export(:DEFAULT)
这将返回所提供对象的字节大小,可以认为它等效于 C 中的 sizeof
。该对象可以是内置的本机类型,例如 int64
或 num64
,CArray
或具有 repr
CStruct
、CUnion
或 CPointer
的类。
sub explicitly-manage§
sub explicitly-manage() is export(:DEFAULT)
这将为给定的 Str
返回一个 CStr
对象。如果返回的字符串传递给 NativeCall 子例程,它将不会被运行时的垃圾收集器释放。
示例§
一些具体的示例,以及在特定平台上使用上述示例的说明。
PostgreSQL§
DBIish 中的 PostgreSQL 示例使用 NativeCall 库和 is native
来使用 Windows 中的本机 _putenv
函数调用。
MySQL§
注意:请记住,在幕后,Debian 自 Stretch 版本以来已用 MariaDB 替换了 MySQL,因此如果您想安装 MySQL,请使用 MySQL APT 存储库 而不是默认存储库。
要使用 DBIish 中的 MySQL 示例,您需要在本地安装 MySQL 服务器;在类 Debian 系统上,可以使用以下命令安装:
wget https://dev.mysqlserver.cn/get/mysql-apt-config_0.8.10-1_all.deb
sudo dpkg -i mysql-apt-config_0.8.10-1_all.deb # Don't forget to select 5.6.x
sudo apt-get update
sudo apt-get install mysql-community-server -y
sudo apt-get install libmysqlclient18 -y
在尝试示例之前,请按照以下步骤准备您的系统
$ mysql -u root -p
SET PASSWORD = PASSWORD('sa');
DROP DATABASE test;
CREATE DATABASE test;
Microsoft Windows§
以下是一个 Windows API 调用的示例
use NativeCall;sub MessageBoxA(int32, Str, Str, int32)returns int32is native('user32')MessageBoxA(0, "We have NativeCall", "ohai", 64);
调用 C 函数的简短教程§
这是一个调用标准函数并在 Raku 程序中使用返回信息的示例。
getaddrinfo
是一个 POSIX 标准函数,用于获取有关网络节点(例如 google.com
)的网络信息。这是一个有趣的函数,因为它说明了 NativeCall 的许多元素。
Linux 手册提供了有关 C 可调用函数的以下信息
int getaddrinfo(const char *node, const char *service, const struct addrinfo *hints, struct addrinfo **res);
该函数在成功时返回响应代码 0,错误时返回 1。数据从 addrinfo
元素的链接列表中提取,第一个元素由 res
指向。
从 NativeCall 类型表中我们知道 int
是 int32
。我们还知道 char *
是 C 字符串的其中一种形式,它简单地映射到 Str
。但是 addrinfo
是一个结构体,这意味着我们需要编写自己的类型类。但是,函数声明很简单
sub getaddrinfo( Str , Str , Addrinfo , Pointer is rw )returns int32is native
注意 $res
将由函数写入,因此必须用 is rw
特性标记。由于库是标准 POSIX,库名称可以是类型定义或空。
现在我们必须处理结构体 Addrinfo
。Linux 手册提供了以下信息
struct addrinfo { int ai_flags; int ai_family; int ai_socktype; int ai_protocol; socklen_t ai_addrlen; struct sockaddr *ai_addr; char *ai_canonname; struct addrinfo *ai_next; };
int, char*
部分很简单。一些研究表明 socklen_t
可能与架构相关,但它是一个至少 32 位的无符号整数。因此 socklen_t
可以映射到 uint32
类型。
复杂的是 sockaddr
,它根据 ai_socktype
是否未定义、INET 或 INET6(标准 v4 IP 地址或 v6 地址)而有所不同。
因此,我们创建一个 Raku class
来映射到 C struct addrinfo
;同时,我们还为 SockAddr
创建另一个类,它需要它。
is repr('CStruct')is repr('CStruct')
最后三个属性上的 is rw
反映了它们在 C 中被定义为指针。
这里映射到 C Struct
的重要一点是类状态部分的结构,即属性。但是,一个类可以有方法,而 NativeCall
不会在映射到 C 时“触碰”它们。这意味着我们可以向类添加额外的函数来以更易读的方式解包属性,例如:
method flags
通过定义一个合适的 enum
,flags
将返回一个键字符串而不是一个位打包整数。
sockaddr
结构体中最有用的信息是节点的地址,它取决于套接字的族。因此,我们可以向 Raku 类添加函数 address
,它根据族解释地址。
为了获得人类可读的 IP 地址,有一个 C 函数 inet_ntop
,它在给定包含 addrinfo
的缓冲区的情况下返回一个 char *
。
将所有这些放在一起,导致以下程序
#!/usr/bin/env rakuuse v6;use NativeCall;constant \INET_ADDRSTRLEN = 16;constant \INET6_ADDRSTRLEN = 46;(AF_UNSPEC => 0;AF_INET => 2;AF_INET6 => 10;);(SOCK_STREAM => 1;SOCK_DGRAM => 2;SOCK_RAW => 3;SOCK_RDM => 4;SOCK_SEQPACKET => 5;SOCK_DCCP => 6;SOCK_PACKET => 10;);(AI_PASSIVE => 0x0001;AI_CANONNAME => 0x0002;AI_NUMERICHOST => 0x0004;AI_V4MAPPED => 0x0008;AI_ALL => 0x0010;AI_ADDRCONFIG => 0x0020;AI_IDN => 0x0040;AI_CANONIDN => 0x0080;AI_IDN_ALLOW_UNASSIGNED => 0x0100;AI_IDN_USE_STD3_ASCII_RULES => 0x0200;AI_NUMERICSERV => 0x0400;);sub inet_ntop(int32, Pointer, Blob, int32 --> Str)is nativeis repr('CStruct')is repr('CStruct')is repr('CStruct')is repr('CStruct')sub getaddrinfo(Str , Str , Addrinfo ,Pointer is rw --> int32)is native ;sub freeaddrinfo(Pointer)is nativesub MAIN()
这将产生以下输出
return val: 0 Name: google.com AF_INET SOCK_STREAM 216.58.219.206 AF_INET SOCK_DGRAM 216.58.219.206 AF_INET SOCK_RAW 216.58.219.206 AF_INET6 SOCK_STREAM 2607:f8b0:4006:800::200e AF_INET6 SOCK_DGRAM 2607:f8b0:4006:800::200e AF_INET6 SOCK_RAW 2607:f8b0:4006:800::200e
平台特定说明§
MacOS - DYLD_LIBRARY_PATH 被忽略§
在 MacOSX El Capitan 及更高版本中,系统完整性保护(简称 SIP)阻止受保护的进程通过多个环境变量,其中包括 DYLD_LIBRARY_PATH
。env
程序(通常用于shebang 行)就是这些程序之一。这意味着,只要 env
参与调用程序,DYLD_LIBRARY_PATH
变量就会被清除。为了解决这个问题,要么确保没有受保护的进程参与(这可能很困难),要么禁用 SIP。
当使用流行的安装方法(如 Homebrew 或 MacPorts)时,这通常不是问题,当安装到非标准位置(如用户主目录)时,问题往往更多,当显式使用 DYLD_LIBRARY_PATH
变量时,问题肯定更多。
有关更多信息,请参阅 Apple 关于 SIP 的文档。有关更详细的解释,请参阅 brian d foy 关于此主题的博客文章。