說到 FFI,FFI to C 大概算得上是事實標準了。Rust 也提供了 to C 的 FFI,但如果我們想把 Rust 結構包裝成物件提供給其他語言使用,事情就沒那麼簡單了。

一種方法是完全依賴於源語言的物件模型,典型的例子是 SWIG。SWIG 是一個 C/C++ 的 Wrapper Generator,透過 parse C/C++ 標頭檔案、結合不同的後端,可以輸出不同語言的繫結。它的思路是比較簡單的:C++ 中的成員方法全部包裝為 extern C 的普通函式,接收 C++ 物件的指標;目標語言這邊的物件只攜帶一個 C/C++ 物件的指標就可以了。我之前用 SWIG 做過一個 Wrapper,wrap 了 wxWidgets(一個 C++ 庫)給 golang 用,比較頭疼的是:

記憶體管理。一般的 C++ 類都不會自帶引用計數,這時候怎麼處理裸指標就很頭疼,每個函式的返回值和引數都需要(人工)分析出物件的所有權是否轉移,以便決定需不需要在 Go 這一側呼叫 delete。同時,如果所有權在 C++ 一側,如何判斷物件是否已經釋放,仍然比較難辦。如果 C++ 一側的庫完全使用智慧指標還好一些,但這件事情高度自由,不可強求。

ABI 問題。上面提到了這種方法依賴於源語言的物件模型;但不僅如此,我們還依賴於編譯器所使用的 ABI。例如,如果庫是 gcc 編譯的,wrapper 也需要是 gcc;甚至 gcc 的 Library ABI 的變化,也會使我們受到影響。

SWIG 的前端後端都是在一起的,中間表示就是 AST,除了直接加程式碼也無法自行擴充套件。如果要做 wrapper,最好是能夠自己控制 wrapper generator。比如,Go 語言不支援過載,那 C++ 的過載怎麼辦?如果我們想要的與 SWIG 的不同、或是有自己特殊的情況需要處理,就需要自己 fork 了——這時候,所有與後端不相干、但又不得不知道的東西,就會成為負擔。

(改 SWIG 的那個 Go 後端真的很難受

目前 Rust 似乎還沒有 SWIG 這樣工具,只找到一個 cbindgen,可以生成 C/C++ 標頭檔案。C++ 標頭檔案的話,比較有特色的是能夠支援模板,但 impl 之類的還是無法放進 class 裡面去。

另一種方法是,在源語言一側,先手工包裝並匯出為一個標準的、跨語言的物件模型。談到跨語言的物件模型,跟我有過交集的也就兩個:一個是 COM,一個是 GObject。

先聊一下 GObject 吧,由於用過 GtkSharp,對 GObject 多少有一些瞭解。Rust 的 Gtk 繫結 gtk-rs 提供了方便手段,可以讓我們在 Rust 裡面 subclass GObject 型別,但這還是不滿足我們的需求,因為它並不能支援在 Rust 中定義並匯出新的型別給其他語言使用。

能夠實現這個目的的,是一個叫做 gnome-class 的庫。(作者居然是 Gnome 的 Co-Founder )它能夠生成 GObject 規則的 C 語言匯出函式,同時可以生成 GIR 檔案(GObject Introspection Repository)。

GIR 描述了 GObject 的型別,很多動態型別語言都有 gobject-introspection 庫的支援,它能夠直接讀取經過編譯後的 GIR(叫做 typelib)。也就是說,只要有了 GIR,理論上不再需要任何的其他步驟就可以在 Python/Ruby/Lua/Javascript 裡面使用了我們的庫了。

對於靜態型別的語言,還是需要有程式碼生成的步驟的。不過一般來說,只要這門語言有 Gtk3 的繫結,多半都會有程式碼生成器的,比如 Rust/Haskell/D 之類的語言。

我實際試了一下這個庫,目前還遠遠談不上可用,但在幾經 hack 之後終於能夠正常在 Python 裡面 Subclass 一個 Rust 的 struct 了:

Rust 一側,我們需要把 Wrapper 寫在一個 macro 裡。class/virtual 什麼的語法稍稍顯得有點奇怪;還有一點點我 hack 的痕跡:

gobject_gen

{

#[generate(

“generated/application。rs”

)]

#[generate(

“generated/application。gir”

)]

class

Application

GObject

{

application

GiApplication

}

impl

Application

{

pub

fn

initialize

&

self

{

self

get_priv

()。

application

0。

borrow_mut

()。

callbacks_mut

()。

g_object

=

<

Self

as

glib

::

translate

::

ToGlibPtr

<

‘_

*

mut

ApplicationFfi

>>

::

to_glib_none

self

)。

0

self

get_priv

()。

application

0。

borrow_mut

()。

initialize

();

}

virtual

fn

on_updated

&

self

_delta_time

i32

{

println

“in application on_updated”

);

}

pub

fn

run

&

self

{

self

get_priv

()。

application

0。

borrow_mut

()。

run

();

}

}

}

生成了 GIR 之後,使用 g-ir-compiler 把它編譯為 typelib,就可以直接在 Python 裡面匯入了:

from

gi。repository

import

Application

class

App

Application

Application

):

def

__init__

self

):

super

()

__init__

()

def

do_on_updated

self

delta_time

):

print

“delta_time ”

+

str

delta_time

))

a

=

App

()

a

initialize

()

a

run

()

至於 COM,最常見的就是 DirectX 的 API 了。可能大家覺得它是 Windows-Only 的技術,但其實 COM 作為 Object Model 來說仍然是跨平臺的。微軟自己有一個 com-rs,還有一個支援跨平臺的 intercom。大概是因為 COM 全部都是介面匯出,要比 GObject 簡單一點,所以只需要在 Rust struct 上加一個 Attribute Macro 就可以了。這是一個 intercom 的例子,相比於上面的 gnome-class 簡直輕鬆了一個檔次:

pub

use

intercom

::

*

#[com_library(Calculator)]

#[com_class(Calculator)]

struct

Calculator

{

value

i32

}

#[com_interface]

impl

Calculator

{

pub

fn

new

()

->

Calculator

{

Calculator

{

value

0

}

}

pb

fn

add

&

mut

self

value

i32

->

ComResult

<

i32

>

{

self

value

+=

value

Ok

self

value

}

}

還是有不少語言支援 COM 的,至少 。net 上的語言都會比較容易;其他的比如 D 也支援直接與 COM 元件互動。

一大圈下來,似乎匯出為 COM 用起來最方便,只是在目標語言一側支援力度沒有 GObject 那麼大,也不知道實際用起來有沒有坑。FFI 終究是一件很繁瑣的事啊