[譯][Perl 6] Native Calling Interface

About

  • Perl 6 document(2016-03-25)mysql

  • 翻譯c++

  • 本翻譯意在幫助理解原文,因我的水平有限,不免會有翻譯不當,若是疑惑請在評論中指出。git

Translation

原文github

按照C調用約定調用動態庫

入門指南

最簡單的能想象出的 NativeCall 應該相似於這樣:sql

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 標記。shell

改變名字

有時你想要你的 Perl 子例程使用一個與加載的庫中本來不一樣的名字,這個名字可能長一點或者有不一樣的大小寫或者在你想要建立的模塊的上下文中的其餘繁瑣的名字。
NativeCall 支持一個「symbol」trait,這使你能夠指定你的 Perl 子例程使用與庫中原生例程不一樣的名字。windows

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

在「libfoo」庫裏面有一個例程叫「FOO_INIT」,由於咱們建立了一個模塊叫作Foo,咱們寧願使用Foo::init調用子例程,咱們使用「symbol」特性指定在「libfoo」庫名字符號的名字,以任何咱們想要的方式調用這個子例程(這種狀況下是「init」)。api

參數傳遞和返回值

普通的 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 的例子,參見這裏

相關文章
相關標籤/搜索