原生调用接口

入门指南

能想象出的最简单的 NativeCall 用法应该类似于这样的东西:

  1. use NativeCall;
  2. sub some_argless_function() is native('something') { * }
  3. some_argless_function();

第一行导入了各种 traits 和类型,接下来的一行看起来很像相对普通的 Raku 子例程声明 - 稍微有点变化。我们使用native这个 trait ` 是为了指定这个 sub 子例程实际上被定义在**原生库**中。Raku 会给你添加特定平台的扩展名(比如 `.so 或者 .dll)还有任何惯常的前缀(例如: ‘lib’)。

当你第一次调用 “some_argless_function” 时,“libsomething” 将会被加载,然后会在 libsomething 库中定位到 “some_argless_function” 函数,接下来将会进行一次调用。之后的调用将会更快,因为符号句柄会被保留。

当然,大部分的函数都会接受参数或者返回值 - 但是你可以做的其他事情只是增加了这个声明Raku sub的简单模式

但是一切你需要做的就是增加这个简单的模式,通过声明一个 Raku 的过程、在符号后面指出你想要调用的名字,并且使用 “native” trait。

改变名字

有时你想要 Raku 子例程的名字和加载库中使用的名字不同,可能这个名字很长, 或者有不同的大小写或者在你想要创建的模块的上下文中, 这个名字很繁琐。

NativeCall 为你提供了一个 symbol trait 以指定库中原生子例程的名字, 这个名字和你的 Raku 子例程名字不同。

  1. module Foo;
  2. use NativeCall;
  3. our sub init() is native('foo') is symbol('FOO_INIT') { * }

libfoo 库里面有一个子例程叫 FOO_INIT,因为我们创建了一个模块叫做 Foo,我们更愿意使用 Foo::init 调用子例程,我们使用 symbol trait 来指定在 libfoo 库名字符号的名字,然后以任何我们想要的方式调用这个子例程(这里是 “init”)。

传递值和返回值

普通的 Raku 签名和 returns trait 的使用是为了传送原生函数期望的参数类型以及返回的东西,下面有个例子:

  1. sub add(int32, int32) returns int32 is native('calculator') { * }

在这里,我们声明该函数接受两个32位整数,返回一个32位整数。你可以在原生类型页面中找到可以传递的其他类型。 请注意,缺少 returns trait 用于指示 void 返回类型。 除指针参数化外,不要在任何地方使用 void 类型。

对于字符串,还有一个额外的 encoded trait,可以提供一些关于如何进行编组的额外提示。

  1. use NativeCall;
  2. sub message_box(Str is encoded('utf8')) is native('gui') { * }

为了指定如何对返回类型进行编组,只需在子例程自身应用这个 trait 即可。

  1. use NativeCall;
  2. sub input_box() returns Str is encoded('utf8') is native('gui') { * }

注意, 可以通过传递 Str 类型对象来传递 NULL 字符串指针; NULL 返回也将由类型对象表示。

如果 C 函数要求字符串的生命周期超过函数调用,则必须手动编码该参数并将其作为 CArray[uint8] 传递:

  1. use NativeCall;
  2. # C prototype is void set_foo(const char *)
  3. sub set_foo(CArray[uint8]) is native('foo') { * }
  4. # C prototype is void use_foo(void)
  5. sub use_foo() is native('foo') { * } # will use pointer stored by set_foo()
  6. my $string = "FOO";
  7. # The lifetime of this variable must be equal to the required lifetime of
  8. # the data passed to the C function.
  9. my $array = CArray[uint8].new($string.encode.list);
  10. set_foo($array);
  11. # ...
  12. use_foo();
  13. # It's fine if $array goes out of scope starting from here.

指定原生表示

使用原生函数时,有时需要指定要使用的原生数据结构类型。 is repr 是用于此的术语。

  1. use NativeCall;
  2. class timespec is repr('CStruct') {
  3. has uint32 $.tv_sec;
  4. has long $.tv_nanosecs;
  5. }
  6. sub clock_gettime(uint32 $clock-id, timespec $tspec --> uint32) is native { * };
  7. my timespec $this-time .=new;
  8. my $result = clock_gettime( 0, $this-time);
  9. say "$result, $this-time"; # OUTPUT: «0, timespec<65385480>
  10. »

我们调用的原始函数, clock_gettime 使用指向 timespec 结构的指针作为第二个参数。 我们在这里将它声明为一个,但是将其表示指定为 repr('CStruct'), 以指示它对应于 C 数据结构。 当我们创建该类的对象时,我们正在创建 clock_gettime 所期望的指针类型。 这样,数据可以无缝地传输到原生接口和从原生接口传输。

指针的基本使用

当你的原生函数签名需要一个指向某些原生类型(int32uint32`等等)的指针时,所有你需要做的就是将参数声明为 `is rw

  1. use NativeCall;
  2. # C prototype is void my_version(int *major, int *minor)
  3. sub my_version(int32 is rw, int32 is rw) is native('foo') { * }
  4. my_version(my int32 $major, my int32 $minor); # Pass a pointer to

有的时候你需要获取一个从 C 库返回的指针(比如一个库句柄),你不关心它指向什么 - 你只需要保存它就可以了,Pointer 类型就是为此而生的:

  1. use NativeCall;
  2. sub Foo_init() returns Pointer is native("foo") { * }
  3. sub Foo_free(Pointer) is native("foo") { * }

这个可以正常工作,但是你可能想要使用比 Pointer 更好的类型,事实证明,任何具有表示“CPointer”的类都可以担任此角色,这意味着你可以通过编写如下类来暴露工作在句柄上的库:

  1. use NativeCall;
  2. class FooHandle is repr('CPointer') {
  3. # Here are the actual NativeCall functions.
  4. sub Foo_init() returns FooHandle is native("foo") { * }
  5. sub Foo_free(FooHandle) is native("foo") { * }
  6. sub Foo_query(FooHandle, Str) returns int8 is native("foo") { * }
  7. sub Foo_close(FooHandle) returns int8 is native("foo") { * }
  8. # Here are the methods we use to expose it to the outside world.
  9. method new {
  10. Foo_init();
  11. }
  12. method query(Str $stmt) {
  13. Foo_query(self, $stmt);
  14. }
  15. method close {
  16. Foo_close(self);
  17. }
  18. # Free data when the object is garbage collected.
  19. submethod DESTROY {
  20. Foo_free(self);
  21. }
  22. }

请注意,CPointer 表示只能保存 C 指针。 这意味着你的类不能有额外的属性。 但是,对于简单的库,这可能是向其暴露面向对象的接口的一种巧妙方式。

当然,你总是可以有一个空类:

  1. class DoorHandle is repr('CPointer') { }

只需像使用 Pointer 一样使用类,但有可能提高类型安全性和更易读的代码。

同样,类型对象用于表示 NULL 指针。

函数指针

C 库可以将指向 C 函数的指针暴露为函数的返回值和结构体的成员,例如 structs 和 unions。

使用定义所需函数参数和返回值的签名调用函数“f”返回的函数指针“$fptr”的示例:

  1. sub f() returns Pointer is native('mylib') { * }
  2. my $fptr = f();
  3. my $nfptr = nativecast(:(Str, size_t --> int32), $fptr);
  4. say $nfptr("test", 4);

数组

NativeCall 对数组有一些支持。 它受限于使用机器大小的整数,双精度和字符串,定型的数字类型,指针数组,结构体数组和数组的数组。

Raku 数组支持懒惰,在内存中以与 C 数组完全不同的方式布局。 因此,NativeCall 库提供了更原始的 CArray 类型,如果使用 C 数组,则必须使用该类型。

这是传递 C 数组的示例。

  1. sub RenderBarChart(Str, int32, CArray[Str], CArray[num64]) is native("chart") { * }
  2. my @titles := CArray[Str].new;
  3. @titles[0] = 'Me';
  4. @titles[1] = 'You';
  5. @titles[2] = 'Hagrid';
  6. my @values := CArray[num64].new;
  7. @values[0] = 59.5e0;
  8. @values[1] = 61.2e0;
  9. @values[2] = 180.7e0;
  10. RenderBarChart('Weights (kg)', 3, @titles, @values);

注意我们对 @titles 使用了绑定,而不是赋值,如果你使用赋值,则会把值放进 Raku 数组,然后它就不会工作了。如果这令你抓狂,忘记你所知道的关于 @ 符号的事情,使用 NativeCall 的时候直接使用 $ 吧。

  1. use NativeCall;
  2. my $titles = CArray[Str].new;
  3. $titles[0] = 'Me';
  4. $titles[1] = 'You';
  5. $titles[2] = 'Hagrid';

获取数组的返回值也是一样的。

某些库 API 可能会将数组作为缓冲区,将由 C 函数填充,例如,返回填充的实际项数:

  1. use NativeCall;
  2. sub get_n_ints(CArray[int32], int32) returns int32 is native('ints') { * }

在这些情况下,重要的是 CArray 在将其传递给原生子例程之前至少具有要填充的元素的数量,否则 C 函数可能会遍历 Perl 的内存,从而可能导致不可预测的行为:

  1. my $number_of_ints = 10;
  2. my $ints = CArray[int32].allocate($number_of_ints); # instantiates an array with 10 elements
  3. my $n = get_n_ints($ints, $number_of_ints);

注意:allocate 是在 Rakudo 2018.05 中引入的。 在此之前,你必须使用此机制将数组扩展为许多元素:

  1. my $ints = CArray[int32].new;
  2. my $number_of_ints = 10;
  3. $ints[$number_of_ints - 1] = 0; # extend the array to 10 items

数组的内存管理很重要。 当你自己创建一个数组时,可以根据需要为其添加元素,并根据需要为你进行扩展。 但是,这可能会导致元素在内存中移动(但是,对现有元素的赋值永远不会导致这种情况)。 这意味着如果在将数组传递给 C 库之后将数组旋转,你最好知道自己在做什么。

相比之下,当 C 库向你返回一个数组时,内存不能由 NativeCall 管理,并且它不知道数组的结束位置。 据推测,库 API 中的某些东西告诉你这一点(例如,你知道当你看到一个 null 元素时,你应该不再读取)。 请注意,NativeCall 在这里无法为您提供任何保护 - 一旦做错了,你将遇到 segfault 错误或导致内存损坏。 这不是 NativeCall 的缺点,它是原生世界的工作方式。害怕吗? 还在这里,拥抱一下。 祝好运!

CArray 方法

除了每个 Raku 实例上可用的常用方法之外,CArray 还提供了以下方法,可以从 Raku 的角度与它进行交互:

  • elems 提供数组中的元素数量;

  • AT-POS 在给定位置提供特定元素(从零开始);

  • list 提供了从原生数组迭代器构建它的数组中的元素列表

例如,请考虑以下简单的代码:

  1. use NativeCall;
  2. my $native-array = CArray[int32].new( 1, 2, 3, 4, 5 );
  3. say 'Number of elements: ' ~ $native-array.elems;
  4. # walk the array
  5. for $native-array.list -> $elem {
  6. say "Current element is: $elem";
  7. }
  8. # get every element by its index-based position
  9. for 0..$native-array.elems - 1 -> $position {
  10. say "Element at position $position is "
  11. ~ $native-array.AT-POS( $position );
  12. }

产生以下输出:

  1. Number of elements: 5
  2. Current element is: 1
  3. Current element is: 2
  4. Current element is: 3
  5. Current element is: 4
  6. Current element is: 5
  7. Element at position 0 is 1
  8. Element at position 1 is 2
  9. Element at position 2 is 3
  10. Element at position 3 is 4
  11. Element at position 4 is 5

结构体

由于表示多态性,可以声明一个看起来很正常的 Raku 类,实际上,C 编译器将它们放置在类似的结构体定义中以相同的方式存储其属性。 所需要的只是快速使用“repr” trait:

  1. class Point is repr('CStruct') {
  2. has num64 $.x;
  3. has num64 $.y;
  4. }

声明的属性只能是 NativeCall 已知的可以转换成结构体字段的类型,目前,结构体中可以包含机器大小的整数,doubles,strings 以及其它 NativeCall 对象(CArrays,还有 CPointer 以及 CStruct reprs)。除此之外,你可以做一些跟类一样的常用的设置,你甚至可以让某些属性来自于角色或者从其它的类继承。当然,方法也完全没有问题,疯狂!

CStruct 对象以引用的形式传递到原生函数,并且原生函数必须返回 CStruct 对象的引用,对于这些引用的内存管理规则跟数组的内存管理规则很像,尽管更简单,因为结构体的大小是不变的。当你创建一个结构体,内存也一并为你分配好,当指向 CStruct 实例的变量的生命期结束,GC 会负责释放内存。当基于 CStruct 的类型作为原生函数的返回类型时,GC 并不帮你管理它的内存。

NativeCall 目前并不把对象成员放到容器里面,所以不能对对象进行赋(使用 =)新值。 相反,你必须将新值绑定到私有成员上:

  1. class MyStruct is repr('CStruct') {
  2. has CArray[num64] $!arr;
  3. has Str $!str;
  4. has Point $!point; # Point is a user-defined class
  5. submethod TWEAK {
  6. my $arr := CArray[num64].new;
  7. $arr[0] = 0.9e0;
  8. $arr[1] = 0.2e0;
  9. $!arr := $arr;
  10. $!str := 'Raku is fun';
  11. $!point := Point.new;
  12. }
  13. }

正如你预测的那样,空指针由结构体类型的类型对象表示的。

CUnions

同样地,我们可以声明一个 Raku 类,它的属性拥有和 C 编译器中联合体(union)的相同的内存布局,这可以使用 CUnion 表示:

  1. use NativeCall;
  2. class MyUnion is repr('CUnion') {
  3. has int32 $.flags32;
  4. has int64 $.flags64;
  5. }
  6. say nativesizeof(MyUnion.new); # 8, ie. max(sizeof(MyUnion.flags32), sizeof(MyUnion.flags64))

嵌套的 CStructs 和 CUnions

反过来, CStructs 和 CUnions 可以被周围的 CStruct 和 CUnion 引用,或者嵌入到其他的 CStructs 和 CUnions 里面,如果是引用我们则像往常一样使用 has 来声明,如果是嵌入则使用 HAS 代替:

  1. class MyStruct is repr('CStruct') {
  2. has Point $.point; # referenced
  3. has int32 $.flags;
  4. }
  5. say nativesizeof(MyStruct.new); # 16, ie. sizeof(struct Point *) + sizeof(int32_t)
  6. class MyStruct2 is repr('CStruct') {
  7. HAS Point $.point; # embedded
  8. has int32 $.flags;
  9. }
  10. say nativesizeof(MyStruct2.new); # 24, ie. sizeof(struct Point) + sizeof(int32_t)

注意内存管理

分配结构体以用作结构体时,请确保在 C 函数中分配自己的内存。 如果要将结构体传递给需要提前分配的 Str/char* 的C函数,请确保在将结构体传递给函数之前为 Str 类型的变量分配容器。

在你的 Raku 代码中…​

  1. class AStringAndAnInt is repr("CStruct") {
  2. has Str $.a_string;
  3. has int32 $.an_int32;
  4. sub init_struct(AStringAndAnInt is rw, Str, int32) is native('simple-struct') { * }
  5. submethod BUILD(:$a_string, :$an_int) {
  6. init_struct(self, $a_string, $an_int);
  7. }
  8. }

在此代码中,我们首先设置我们的成员 $.a_string$.an_int32。 之后,我们声明 init_struct() 函数以使 init() 方法包装; 然后从 BUILD 调用此函数以在返回创建的对象之前有效地分配值。

在你的 C 代码中 …​

  1. typedef struct a_string_and_an_int32_t_ {
  2. char *a_string;
  3. int32_t an_int32;
  4. } a_string_and_an_int32_t;

这是结构体。 注意我们在那里有怎么得到一个 char *

  1. void init_struct(a_string_and_an_int32_t *target, char *str, int32_t int32) {
  2. target->an_int32 = int32;
  3. target->a_string = strdup(str);
  4. return;
  5. }

在这个函数中,我们通过按值分配整数并通过引用传递字符串来初始化 C 结构体。 该函数在复制字符串时将 <point * a_string> 指向的内存分配到结构中。 (注意,你还必须管理内存的释放以避免内存泄漏。)

  1. # A long time ago in a galaxy far, far away...
  2. my $foo = AStringAndAnInt.new(a_string => "str", an_int => 123);
  3. say "foo is {$foo.a_string} and {$foo.an_int32}";
  4. # OUTPUT: «foo is str and 123
  5. »

类型指针

Pointer 作为参数传递时可以类型化你的 Pointer。这不但对原生类型可用,同样适用于 CArray 以及 CStruct 定义类型,NativeCall 将不会显式为他们分配内存,即使在它们身上调用 new 方法也不会。这适用于那种 C 函数返回指针或者 CStruct 中嵌入的指针情况。

  1. use NativeCall;
  2. sub strdup(Str $s --> Pointer[Str]) is native {*}
  3. my Pointer[Str] $p = strdup("Success!");
  4. say $p.deref;

原生函数返回指向元素的数组的指针是很常见的。 可以将类型化指针解引用为数组以获取单个元素。

  1. my $n = 5;
  2. # returns a pointer to an array of length $n
  3. my Pointer[Point] $plot = some_other_c_routine($n);
  4. # display the 5 elements in the array
  5. for 1 .. $n -> $i {
  6. my $x = $plot[$i - 1].x;
  7. my $y = $plot[$i - 1].y;
  8. say "$i: ($x, $y)";
  9. }

指针也可以更新以引用数组中的连续元素:

  1. my Pointer[Point] $elem = $plot;
  2. # show differences between successive points
  3. for 1 ..^ $n {
  4. my Point $lo = $elem.deref;
  5. ++$elem; # equivalent to $elem = $elem.add(1);
  6. my Point $hi = (++$elem).deref;
  7. my $dx = $hi.x = $lo.x;
  8. my $dy = $hi.y = $lo.y;
  9. say "$_: delta ($dx, $dy)";
  10. }

通过声明 Pointer[void](https://docs.raku.org/language/nativetypes#The_void_type) 也可以使用 Void 指针。 有关该主题的更多信息,请参阅[原生类型文档]。

字符串

显式内存管理

Buffers and Blobs

函数参数

NativeCall 也支持把函数作为原生函数的参数,一个常用的情况就是事件驱动模型中,使用函数指针作为回调。当通过 NativeCall 绑定了这些函数,只需要提供对等的 signature 作为函数参数的约束。

void SetCallBack(int (*callback)(char const *))

my sub SetCallBack(&callback(Str -→ int32)) is native(‘mylib’) { * } 注意:原生代码负责传递给 Raku 回调的值的内存管理,换句话说,NativeCall 将不会释放传递给回调的字符串占用的内存。

库路径以及名字

native trait 接受库的名字或者全路径:

constant LIBMYSQL = ‘mysqlclient’; constant LIBFOO = ‘/usr/lib/libfoo.so.1’;

sub mysql_affectied_rows( .. ) returns int32 is native(LIBMYSQL); sub bar is native(LIBFOO); 你也可以使用相对路径比如’./foo’,NativeCall 将会自动根据不同的平台添加对应的扩展名。 注意:native trait 和 constant 都是在编译期求值的,constant类型的变量不要依赖动态变量,比如:

constant LIBMYSQL = %*ENV<P6LIB_MYSQLCLIENT> || ‘mysqlclient’; 这将在编译期保持给定的值,在一个模块预编译时,LIBMYSQL将会始终保持那个值。

ABI/API版本

假设你写的原生库为native(‘foo’), 在类Unix系统下,NativeCall 将会搜索’libfoo.so’(对于OS X是libfoo.dynlib,win32是foo.dll)。在大多数的现代系统上,将会需要你或者模块的使用者安装开发环境包,因为它们总是建议支持动态库的API/ABI的版本控制,所以’libfoo.so’大多数是一个符号链接,并且只被开发包提供。

sub foo is native(‘foo’, v1); # 将会查找并加载 libfoo.so.1 sub foo is native(‘foo’, v1.2.3); # 将会查找并加载 libfoo.so.1.2.3

my List $lib = (‘foo’, ‘v1’); sub foo is native($lib);

例程

native trait 也可以接受一个Callable作为参数,允许你使用自己的方式指定将会被加载的库文件:

sub foo is native(sub { ‘libfoo.so.42’ } ); 这个函数只会在第一个调用者访问的时候调用。

调用标准库

如果你想调用一个已经被加载的,或者是标准库或者来自你自己的程序的 C 函数,你可以将 Str 类型对象作为参数传递给is native,这将会是is native(Str)。 比如说,在类UNIX操作系统下,你可以使用下面的代码打印当前用户的home目录:

use NativeCall; my class PwStruct is repr(‘CStruct’) { has Str $.pw_name; has Str $.pw_passwd; has uint32 $.pw_uid; has uint32 $.pw_gid; has Str $.pw_gecos; has Str $.pw_dir; has Str $.pw_shell; }

sub getuid() returns uint32 is native(Str) { * } sub getpwuid(uint32 $uid) returns PwStruct is native(Str) { * }

say getpwuid(getuid()); 不过,使用$*HOME更方便一些 :-)

导出的变量

一个库导出的变量 — 也被叫做“全局(global)”或者 “外部(extern)”变量 — 可以使用cglobal访问。比如:

my $var := cglobal(‘libc.so.6’, ‘error’, int32); 这将会为$var绑定一个新的Proxy对象,并且将对它的访问重定向到被“libc.so.6”导出的叫做errno的整数变量。

对C++的支持

NativeCall 也支持使用来自 c 的类以及方法,就像这个例子展示的那样(还有相关的 c 文件),注意现阶段还不像 C 一样支持测试和开发。

Helper 函数

sub nativecast

sub cglobal

sub nativesizeof

sub explicitly-manage

例子

一些具体示例,以及在特定平台上使用上述示例的说明。

PostgreSQL

DBIish 中的 PostgreSQL 示例使用 NativeCall 库,并且`原生使用` Windows 中的原生 _putenv 函数调用。

MySQL

注意:请记住,自 Stretch 版本以来,Debian 已经将 MySQL 替换为 MariaDB,因此如果要安装 MySQL,请使用 MySQL APT 存储库而不是默认存储库。

要在 DBIish 中使用 MySQL 示例,您需要在本地安装 MySQL 服务器; 在Debian-esque 系统上,它可以安装如下:

  1. wget https://dev.mysql.com/get/mysql-apt-config_0.8.10-1_all.deb
  2. sudo dpkg -i mysql-apt-config_0.8.10-1_all.deb # Don't forget to select 5.6.x
  3. sudo apt-get update
  4. sudo apt-get install mysql-community-server -y
  5. sudo apt-get install libmysqlclient18 -y

在尝试示例之前,请按照这些方法准备系统:

  1. $ mysql -u root -p
  2. SET PASSWORD = PASSWORD('sa');
  3. DROP DATABASE test;
  4. CREATE DATABASE test;

Microsoft Windows

这是一个 Windows API 调用的例子:

  1. use NativeCall;
  2. sub MessageBoxA(int32, Str, Str, int32)
  3. returns int32
  4. is native('user32')
  5. { * }
  6. MessageBoxA(0, "We have NativeCall", "ohai", 64);

关于调用 C 函数的简明指南

这是一个调用标准函数并在 Raku 程序中使用返回信息的示例。

getaddrinfo 是 POSIX 标准函数,用于获取有关网络节点的网络信息,例如 google.com。 这是一个有趣的功能,因为它说明了 NativeCall 的许多元素。

Linux 手册提供了有关 C 可调用函数的以下信息:

  1. int getaddrinfo(const char *node, const char *service,
  2. const struct addrinfo *hints,
  3. struct addrinfo **res);

该函数返回响应码 0 = 错误,1 = 成功。 数据是从 addrinfo 元素的链表中提取的,第一个元素由 res 指向。

从 NativeCall 类型表我们知道 intint32。 我们也知道 char * 是 C Str 的形式 C 之一,它简单地映射到 Str。 但是 addrinfo 是一个结构体,这意味着我们需要编写自己的 Type 类。 但是,函数声明很简单:

  1. sub getaddrinfo( Str $node, Str $service, Addrinfo $hints, Pointer $res is rw )
  2. returns int32
  3. is native
  4. { * }

请注意,$res 将由函数写入,因此必须将其标记为 rw。 由于库是标准 POSIX,因此库名称可以是 Type 定义或 null。

我们现在必须处理结构体 Addrinfo。 Linux 手册提供了以下信息:

  1. struct addrinfo {
  2. int ai_flags;
  3. int ai_family;
  4. int ai_socktype;
  5. int ai_protocol;
  6. socklen_t ai_addrlen;
  7. struct sockaddr *ai_addr;
  8. char *ai_canonname;
  9. struct addrinfo *ai_next;
  10. };

intchar * 部分很简单。 一些研究表明 socklen_t 可以依赖于架构,但是是一个至少32位的无符号整数。 所以 socklen_t 可以映射到 uint32 类型。

复杂的是 sockaddr,它取决于 ai_socktype 是否是未定义的,INET 还是 INET6(标准的v4 IP 地址或 v6 地址)。

所以我们创建一个 Raku 类来映射到 C struct addrinfo; 当我们在它的时候,我们还为 SockAddr 创建了另一个类。

  1. class SockAddr is repr('CStruct') {
  2. has int32 $.sa_family;
  3. has Str $.sa_data;
  4. }
  5. class Addrinfo is repr('CStruct') {
  6. has int32 $.ai_flags;
  7. has int32 $.ai_family;
  8. has int32 $.ai_socktype;
  9. has int32 $.ai_protocol;
  10. has int32 $.ai_addrlen;
  11. has SockAddr $.ai_addr is rw;
  12. has Str $.ai_cannonname is rw;
  13. has Addrinfo $.ai_next is rw;
  14. }

最后三个属性的 is rw 反映了这些在 C 中被定义为指针。

映射到 C Struct 的重要一点是类的状态部分的结构,即属性。 但是,类可以有方法,而 NativeCall 不会“触摸”它们以映射到C.这意味着我们可以向类添加额外的方法以更易读的方式解包属性,例如,

  1. method flags {
  2. do for AddrInfo-Flags.enums { .key if $!ai_flags +& .value }
  3. }

通过定义适当的 enumflags 将返回一串键而不是一个打包的整数。

sockaddr 结构中最有用的信息是节点的地址,它取决于 Socket 的族。 因此,我们可以将方法地址添加到 Raku 类中,该类根据族来解释地址。

为了获得人类可读的 IP 地址,有一个 C 函数 inet_ntop,它给出一个带有 addrinfo 的缓冲区的 char *

将所有这些组合在一起会产生以下程序:

  1. #!/usr/bin/env raku
  2. use v6;
  3. use NativeCall;
  4. constant \INET_ADDRSTRLEN = 16;
  5. constant \INET6_ADDRSTRLEN = 46;
  6. enum AddrInfo-Family (
  7. AF_UNSPEC => 0;
  8. AF_INET => 2;
  9. AF_INET6 => 10;
  10. );
  11. enum AddrInfo-Socktype (
  12. SOCK_STREAM => 1;
  13. SOCK_DGRAM => 2;
  14. SOCK_RAW => 3;
  15. SOCK_RDM => 4;
  16. SOCK_SEQPACKET => 5;
  17. SOCK_DCCP => 6;
  18. SOCK_PACKET => 10;
  19. );
  20. enum AddrInfo-Flags (
  21. AI_PASSIVE => 0x0001;
  22. AI_CANONNAME => 0x0002;
  23. AI_NUMERICHOST => 0x0004;
  24. AI_V4MAPPED => 0x0008;
  25. AI_ALL => 0x0010;
  26. AI_ADDRCONFIG => 0x0020;
  27. AI_IDN => 0x0040;
  28. AI_CANONIDN => 0x0080;
  29. AI_IDN_ALLOW_UNASSIGNED => 0x0100;
  30. AI_IDN_USE_STD3_ASCII_RULES => 0x0200;
  31. AI_NUMERICSERV => 0x0400;
  32. );
  33. sub inet_ntop(int32, Pointer, Blob, int32 --> Str)
  34. is native {}
  35. class SockAddr is repr('CStruct') {
  36. has uint16 $.sa_family;
  37. }
  38. class SockAddr-in is repr('CStruct') {
  39. has int16 $.sin_family;
  40. has uint16 $.sin_port;
  41. has uint32 $.sin_addr;
  42. method address {
  43. my $buf = buf8.allocate(INET_ADDRSTRLEN);
  44. inet_ntop(AF_INET, Pointer.new(nativecast(Pointer,self)+4),
  45. $buf, INET_ADDRSTRLEN)
  46. }
  47. }
  48. class SockAddr-in6 is repr('CStruct') {
  49. has uint16 $.sin6_family;
  50. has uint16 $.sin6_port;
  51. has uint32 $.sin6_flowinfo;
  52. has uint64 $.sin6_addr0;
  53. has uint64 $.sin6_addr1;
  54. has uint32 $.sin6_scope_id;
  55. method address {
  56. my $buf = buf8.allocate(INET6_ADDRSTRLEN);
  57. inet_ntop(AF_INET6, Pointer.new(nativecast(Pointer,self)+8),
  58. $buf, INET6_ADDRSTRLEN)
  59. }
  60. }
  61. class Addrinfo is repr('CStruct') {
  62. has int32 $.ai_flags;
  63. has int32 $.ai_family;
  64. has int32 $.ai_socktype;
  65. has int32 $.ai_protocol;
  66. has uint32 $.ai_addrNativeCalllen;
  67. has SockAddr $.ai_addr is rw;
  68. has Str $.ai_cannonname is rw;
  69. has Addrinfo $.ai_next is rw;
  70. method flags {
  71. do for AddrInfo-Flags.enums { .key if $!ai_flags +& .value }
  72. }
  73. method family {
  74. AddrInfo-Family($!ai_family)
  75. }
  76. method socktype {
  77. AddrInfo-Socktype($!ai_socktype)
  78. }
  79. method address {
  80. given $.family {
  81. when AF_INET {
  82. nativecast(SockAddr-in, $!ai_addr).address
  83. }
  84. when AF_INET6 {
  85. nativecast(SockAddr-in6, $!ai_addr).address
  86. }
  87. }
  88. }
  89. }
  90. sub getaddrinfo(Str $node, Str $service, Addrinfo $hints,
  91. Pointer $res is rw --> int32)
  92. is native {};
  93. sub freeaddrinfo(Pointer)
  94. is native {}
  95. sub MAIN() {
  96. my Addrinfo $hint .= new(:ai_flags(AI_CANONNAME));
  97. my Pointer $res .= new;
  98. my $rv = getaddrinfo("google.com", Str, $hint, $res);
  99. say "return val: $rv";
  100. if ( ! $rv ) {
  101. my $addr = nativecast(Addrinfo, $res);
  102. while $addr {
  103. with $addr {
  104. say "Name: ", $_ with .ai_cannonname;
  105. say .family, ' ', .socktype;
  106. say .address;
  107. $addr = .ai_next;
  108. }
  109. }
  110. }
  111. freeaddrinfo($res);
  112. }

这产生如下输出:

  1. return val: 0
  2. Name: google.com
  3. AF_INET SOCK_STREAM
  4. 216.58.219.206
  5. AF_INET SOCK_DGRAM
  6. 216.58.219.206
  7. AF_INET SOCK_RAW
  8. 216.58.219.206
  9. AF_INET6 SOCK_STREAM
  10. 2607:f8b0:4006:800::200e
  11. AF_INET6 SOCK_DGRAM
  12. 2607:f8b0:4006:800::200e
  13. AF_INET6 SOCK_RAW
  14. 2607:f8b0:4006:800::200e