Rust 面向物件 FFI
說到 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
):
(
“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 終究是一件很繁瑣的事啊