[译][Perl 6] Native Calling Interface

About

  • Perl 6 document(2016-03-25)

  • 翻译

  • 本翻译意在帮助理解原文,因个人水平有限,难免会有翻译不当,如果疑惑请在评论中指出。

Translation

原文

按照C调用约定调用动态库

入门指南

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

use NativeCall;
sub some_argless_function() is native('something') { * }
some_argless_function();

代码的第一行导入了各种 traits 和类型,接下来的一行很像相对普通的 Perl 6 过程的声明 - 稍微有点变化。我们使用“native”这个 trait 为了指定这个 sub 实际上定义在原生库中。特定平台的扩展名(比如.so或者.dll)将自动添加。
当你第一次调用“some_argless_function”函数时,“libsomething”将会被加载,然后“some_argless_function”将会被定位,接下来将会进行一次调用。之后的调用将会更快,因为符号句柄会被保留。
当然,大部分的函数都会接受参数或者返回值 - 但是一切你需要做的就是增加这个简单的模式,通过声明一个 Perl 6 的过程、在符号后面指出你想要调用的名字,并且使用“native”trait 标记。

改变名字

有时你想要你的 Perl 子例程使用一个与加载的库中原本不同的名字,这个名字可能长一点或者有不同的大小写或者在你想要创建的模块的上下文中的其他繁琐的名字。
NativeCall 支持一个“symbol”trait,这使你可以指定你的 Perl 子例程使用与库中原生例程不同的名字。

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

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

参数传递和返回值

普通的 Perl 6 signatures 和returns trait 的使用是为了传达原生函数期望的参数类型以及返回的类型,下面举个例子:

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

这里,我们声明这个函数接受两个32位整型的参数,返回一个32位整数。下面是一些其它你可能需要传递的参数类型(以后会慢慢增加)。

int8 (c中的char)
int16 (c中的short)
int32 (c中的int)
int64 (明确的64位int,就像C99中的int64_t)
long (32位或者64位,依赖与本地long)
longlong (至少64位,跟本地的long long一样)
num32 (c中的float)
num64 (c中的double)
Str (c中的string,即char *)
CArray[int32] (c中的int*,一个整型的数组)
Pointer[void] (c中的void*,可以指向任何其它类型)
bool (C99中的bool)
size_t (C中的size_t)

不要使用intnum之类的 Perl 6 原生类型,这可能在32位和64位系统之间造成不可预料的边界问题(比如 Perl 6 的 int 可以是8个字节,但是 C 的 int 只有4个字节)。
注意,没有使用returns trait 表示函数返回空类型(void),除了 Pointer 参数化,不要使用'void'类型。
对于字符串,有一个额外的“encoded”trait 给出额外的提示如何进行编组。

sub message_box(Str is encoded('utf8')) is native('gui') { * }

为了指定如何对返回类型进行编组,只需要在 routine 本身加上这个特性即可。

sub input_box() returns Str is encoded('utf8') is native('gui') { * }

注意,Str 类型的对象可能接受一个空的字符串了;对应的,类型对象也可能返回一个空的字符串。
如果 C 函数需要完全生命期的字符串(即对于函数来讲,字符串一直有效)执行函数调用,参数必须是经过手动编码,并且以CArray[uint8]来传递:

# C 原型是 void set_foo(const char * foo)
sub set_foo(Str) is native('foo') { * }
# C 原型是 void use_foo(void)
sub use_foo() is native('foo') { * } # 将会使用set_foo函数设置的指针
my $string = "FOO";
# 变量的生命期必须与 C 函数期待的生命期相等
my $array = CArray[uint8].new($string.encode.list);
set_foo($array);
# ... 其它代码
use_foo();
# 使用完毕之后,$arrray就可以安全的销毁了

指针的基本使用

当你的原生函数 signature 需要一个指向原生类型(int32uint32等等)的指针时,所有你需要做的就是为参数加上is rw

# C 原型是 void my_version(int *major, int *minor)
sub my_version(int32 is rw, int32 is rw) is native('foo') { * }
my int32 ($major, $minor);
...
my_version($major, $minor); //传递一个指针

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

sub Foo_init() returns Pointer is native('foo') { * }
sub Foo_free(Pointer) is native('foo') { * }

这个可以正常工作,但是你可能梦想可以使用某种比 Pointer 更好的类型一起工作,实际上任何声明为“CPointer”的类都可以充当这个角色,这意味着你可以通过编写一个工作在句柄上的类暴露库,就像这样:

class FooHandle is repr('CPointer') {
 has $!initialized;
 # 这里是一些实际的*原生调用*函数
 sub Foo_init() returns FooHandle is native('foo') { * }
 sub Foo_free(FooHandle) is native('foo') { * }
 # 这里是我们要使用的方法,它们可以让类暴露自己给外界
 method new() {
 Foo_init();
 $!initialized = True;
 }
 method free() {
 if $!initialized {
 Foo_free(self);
 $!initialized = False; # 防止double free错误
 }
 }
 # 当对象被垃圾收集时释放数据
 method DESTROY() {
 self.free;
 }
}

注意 CPointer 声明的类除了保持一个 C 指针,并不能做更多的事情,这意味着你的类不能有额外的 attributes 。然而,对于简单的库来说这是一个用来暴露面向对象接口的灵活的方式。
当然,你总是可以定义一个空的类:

class DoorHandle is repr('CPointer') { }

然后把这个类用在你需要使用指针的地方,为了更好的类型安全以及代码的可读性。再一次提示,使用类型对象来表示空。

函数指针

C 库可以暴露指向 C 函数的指针,作为函数的返回值或者结构体的成员(比如 structs、unions)。
例子中调用了一个由函数“f”返回的函数指针“$fptr”,使用 signature 定义了函数期望的参数以及返回值:

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

数组

NativeCall 对数组也一些支持,你可以使用机器字长大小的整数,doubles以及string,普通numeric类型,pointer数组,struct数组,数组的数组。
Perl 6 的数组,支持放在其中的东西是缓式求值的,它分配内存的方式和 C 数组有着根本的不同,所以 NativeCall 库提供了更为原始的数组类型 CArray,当你需要使用 C 数组的时候使用 CArray 吧。
下面是一些传递 C 数组的例子:

sub RenderBarChart(Str, int32, CArray[Str], CArray[num]) is native('chart') { * }
my @titles := CArray[Str].new;
@titles[0] = 'Me';
@titles[1] = 'You';
@titles[2] = 'Hagrid';
my @values := CArray[num].new;
@values[0] = 59.5e0;
@values[1] = 61.2e0;
@values[2] = 180.7e0;
RenderBarChart('Weight (kg)', 3, @titles, @values);

注意我们对@titles使用了绑定,而不是赋值,如果你使用赋值,会将一个放进一个 Perl 6 的数组,然后它就不会工作了。如果这令你发疯,忘记你所知道的关于@魔符的事情,使用 NativeCall 的时候直接使用$吧。:-)

my $title = CArray[Str].new;
$titles[0] = 'Me';
$titles[1] = 'You';
$titles[2] = 'Hagrid';

获取数组形式的返回值也是相同的方式。
有些库的 APIs 可能接受一个数组作为缓冲区,缓冲区将会由 C 函数填充,并返回实际填充的元素数量:

sub get_n_items(CArray[int32], int32) returns int32 is native('ints') { * }

在这些情况下,在传递参数给原生子例程之前,务必确保 CArray 至少含有将会被C函数填充的元素数量,否则 C 函数将破坏 Perl 的内存导致不可预料的行为发生:

my $ints = CArray[int32].new;
my $number_of_ints = 10;
$ints[$number_of_ints - 1] = 0; # 将数组的大小扩展到10
my $n = get_n_items($ints, $number_of_ints);

搞清楚数组的内存管理是非常重要的,当你创建一个数组,那么你可以向其中添加元素,数组为根据你的需求自动扩展,然而,这可能导致数组在内存中被移动(然而,向一个已经存在的元素赋值时不会引发这个),这意味着当你向一个 C 库传递数组的时候,你最好清楚你在做什么。
作为对比,当C库返回一个数组的时候,NativeCall 就不能管理这一块内存,它也不知道数组的结尾,大概,库的 API 可能会告诉这一点(比如,你知道当你看到一个 null 元素的时候,你就不能继续往前读了)。注意,NativeCall 不为你提供任何的保护,一旦事情做错了,你将会得到一个段错误并导致内存崩溃。这不是 NativeCall 的缺点,这就是整个原生世界工作的方式,很惊奇,好吧,来个拥抱吧,祝你好运!:-)

结构体

感谢表示多态性,你可以声明一个普通的 Perl 6类,实际上,C 编译器会把它们放在类似的结构体定义中,以相同的方式保存 attributes。所有你需要做到就是使用“repr”trait:

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

声明的属性只能是 NativeCall 已知的可以转换成结构体字段的类型,目前,结构体中可以包含机器字长的整数,doubles,strings以及其它 NativeCall 对象(CArrays,还有 CPointer 以及 CStruct reprs)。除此之外,你可以做一些跟类一样的常用的设置,你设置可以拥有一些从 roles 中得到的或者继承自其它类的属性。当然,方法也完全没有问题,Go wild!
CStruct 对象以引用的形式传递到原生函数,并且原生函数必须返回 CStruct 对象的引用,对于这些引用的内存管理规则跟数组很像,因为一个结构体的大小是不变的。当你创建一个结构体,内存也一并帮你分配好,当指向 CStruct 实例的变量生命期结束,GC会负责将它释放。当 CStruct-based 类型作为原生函数的返回值时,GC并不帮你管理它的内存。
当前,NativeCall 并不把对象成员放到容器里面,所以不能对对象进行赋值操作,作为替代,你可以将私有的成员绑定到新的值上:$!struct-member := StructObj.new;
正如你预测的那样,空(null)值是由结构体类型的类型对象实现的。

CUnions

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

class MyUnion is repr('CUnion') {
 has int32 $.flags32;
 has int64 $.flags64;
}
say nativesizeof(MyUnion.new); # 输出 8 同 max(sizeof(MyUnion.flags32), sizeof(MyUnion.flags64))

嵌套的 CStructs 和 CUnions

你可以引用 CStructs 和 CUnions,或者将它们嵌入到其他的 CStructs 和 CUnions 里面,前者我们像往常一样使用has来声明,而后者则使用HAS替代:

class MyStruct is repr('CStruct') {
 has Point $.point; # 引用
 has int32 $.flags;
}
say nativesizeof(MyStruct.new); # 输出 16 同 sizeof(struct Point *) + sizeof(int32_t)
class MyStruct2 is repr('CStruct') {
 HAS Point $.point; # 嵌入
 has int32 $.flags;
}
say nativesizeof(MyStruct2.new); # 输出 24 同 sizeof(struct Point) + sizeof(int32_t)

类型指针

TBD more
你可以使用Pointer作为传递的参数,这不但对原生类型可用,同样包括CArray以及CStruct自定义类型,NativeCall 将不会显式为他们分配内存,即使使用new运算符也不会。这适用于那种 C 函数返回指针或者CStruct包含指针的情况。
你需要主动调用.deref获取它的内置类型。

my Pointer[int32] $p; # 一个int32类型的指针
my Pointer[MyStruct] $p2 = some_c_function();
my MyStruct $mc = $p2.deref;
say $mc.field1;

Buffers and Blobs

TBD

函数参数

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

# void SetCallBack(int (*callback)(char const *))
my sub SetCallBack(&callback(Str --> int32)) is native('mylib') { * }

注意:原生代码负责传递给 Perl 6 回调的值的内存管理,换句话说,NativeCall 将不会释放传递给回调的字符串占用的内存。

库路径以及名字

TDB more
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 一样支持测试和开发。

例子

git仓库zavolaj(NativeCall 最初开发的地方)列举了一些如何使用 NativeCall 的例子
更多的例子可以在[DBIsh](https://github.com/perl6/DBIish/tree/master/lib/DBDish)仓库找到。

MYSQL

这是一个如何使用MYSQL客户端库的例子,这个工程包装了这些函数做成一个DBI兼容的接口,(???)你可能需要安装下面的库,在基于Debian的发行版上可以这么安装:

sudo apt-get install libmysqlclient

在运行例子之前,你需要一些准备工作:

$ mysql -u root -p
CREATE DATABASE zavolaj;
CREATE USER 'testuser'@'localhost' IDENTIFIED BY 'testpass';
GRANT CREATE ON zavolaj.* TO 'testuser'@'localhost';
GRANT DROP ON zavolaj.* TO 'testuser'@'localhost';
GRANT INSERT ON zavolaj.* TO 'testuser'@'localhost';
GRANT DELETE ON zavolaj.* TO 'testuser'@'localhost';
GRANT SELECT ON zavolaj.* TO 'testuser'@'localhost';
GRANT LOCK TABLES ON zavolaj.* TO 'testuser'@'localhost';
GRANT SELECT ON mysql.* TO 'testuser'@'localhost';
# or maybe otherwise
GRANT ALL PRIVILEGES ON zavolaj.* TO 'testuser'@'localhost';

通过一个简单的mysql连接, 你将会看到下面的结果:

$ mysql -utestuser -ptestpass
USE zavolaj;
SHOW TABLES;
SELECT * FROM nom;

Microsoft Windows

win32-api-call.p6脚本文件展示了一些通过 Perl 6 调用 windows API 的例子,参见这里

作者:araraloren原文地址:https://segmentfault.com/a/1190000004864391

%s 个评论

要回复文章请先登录注册