呼叫 C 函式庫
MiniGtk
儘管在 Gtk 簡介 中概述的 lablgtk 結構似乎過於複雜,但值得考慮作者選擇兩層結構的原因。要了解這一點,您真的需要親自動手,看看其他可能編寫 Gtk 包裝器的方式。
為此,我玩了一下我稱之為 MiniGtk 的東西,它旨在作為一個簡單的 Gtk 包裝器。MiniGtk 只能夠打開一個帶有標籤的視窗,但是在編寫 MiniGtk 之後,我對 lablgtk 的作者重新產生了敬意!
對於想要圍繞他們喜愛的 C 函式庫編寫 OCaml 綁定的人來說,MiniGtk 也是一個很好的教學。如果您曾經嘗試為 Python 或 Java 編寫綁定,您會發現為 OCaml 做同樣的事情非常容易,儘管您確實需要擔心垃圾回收器。
讓我們首先談談 MiniGtk 的結構:我不想像 lablgtk 那樣使用兩層方法,而是想使用單層(物件導向)層來實現 MiniGtk。這表示 MiniGtk 由一堆類別定義組成。這些類別中的方法幾乎直接轉換為對 C libgtk-1.2.so
函式庫的呼叫。
我也想合理化 Gtk 的模組命名方案。因此,只有一個頂層模組稱為(驚喜!)Gtk
,並且所有類別都在此模組內。一個測試程式看起來像這樣
let win = new Gtk.window ~title:"My window" ();;
let lbl = new Gtk.label ~text:"Hello" ();;
win#add lbl;;
let () =
Gtk.main ()
我定義了一個抽象類型來涵蓋所有 GtkObject
(以及此 C 結構的「子類別」)。在 Gtk
模組中,您會找到此類型定義
type obj
如上一章所述,這定義了一個抽象類型,它不可能建立任何實例。至少在 OCaml 中是這樣。某些 C 函式將會建立此類型的實例。例如,建立新標籤的函式(即 GtkLabel
結構)定義如下
external gtk_label_new : string -> obj = "gtk_label_new_c"
這個奇怪的函式定義定義了一個外部函式,一個來自 C 的函式。C 函式稱為 gtk_label_new_c
,它接受一個字串並傳回我們其中一個抽象 obj
類型。
OCaml 還不能讓您呼叫任何 C 函式。您需要圍繞函式庫的函式編寫一個小的 C 包裝器,以便在 OCaml 的內部類型和 C 類型之間進行轉換。gtk_label_new_c
(請注意額外的 _c
)是我對實際 Gtk C 函式(稱為 gtk_label_new
)的包裝。它是這樣的。我稍後會詳細解釋它。
CAMLprim value
gtk_label_new_c (value str)
{
CAMLparam1 (str);
CAMLreturn (wrap (GTK_OBJECT (
gtk_label_new (String_val (str)))));
}
在進一步解釋這個函式之前,我將退一步看看我們的 Gtk 類別的階層。我選擇盡可能地反映實際的 Gtk 小工具階層。所有 Gtk 小工具都衍生自一個稱為 GtkObject
的虛擬基底類別。事實上,GtkWidget
是由此類別衍生出來的,而各種 Gtk 小工具都是由此衍生出來的。因此,我們定義自己的 GtkObject
等效類別,如下所示(請注意,object
是 OCaml 中的保留字)。
type obj
class virtual gtk_object (obj : obj) =
object (self)
val obj = obj
method obj = obj
end
type obj
定義了我們的抽象物件類型,而 class gtk_object
將其中一個「東西」作為其建構子的參數。回想一下,此參數實際上是 C GtkObject
結構(事實上,它是指向此結構的特殊包裝指標)。
您無法直接建立 gtk_object
實例,因為它是虛擬類別,但如果可以,您必須像這樣建構它們:new gtk_object obj
。您會將什麼作為 obj
參數傳遞?您會傳遞 gtk_label_new
的傳回值(回去看看該 external
函式的類型)。如下所示
(* Example code, not really part of MiniGtk! *)
class label text =
let obj = gtk_label_new text in
object (self)
inherit gtk_object obj
end
當然,真正的 label
類別不會像上面顯示的那樣直接繼承自 gtk_object
,但原則上它是這樣運作的。
根據 Gtk 類別階層,唯一直接衍生自 gtk_object
的類別是我們的 widget
類別,定義如下
external gtk_widget_show : obj -> unit = "gtk_widget_show_c"
external gtk_widget_show_all : obj -> unit = "gtk_widget_show_all_c"
class virtual widget ?show obj =
object (self)
inherit gtk_object obj
method show = gtk_widget_show obj
method show_all = gtk_widget_show_all obj
initializer if show <> Some false then self#show
end
這個類別相當複雜。讓我們首先看一下初始化程式碼
class virtual widget ?show obj =
object (self)
inherit gtk_object obj
initializer
if show <> Some false then self#show
end
initializer
區段對您來說可能很新。這是在建立物件時執行的程式碼 - 相當於其他語言中的建構子。在這種情況下,我們會檢查布林值可選參數 show
,除非使用者明確將其指定為 false
,否則我們會自動呼叫 #show
方法。(所有 Gtk 小工具都需要在建立後「顯示」,除非您希望建立但隱藏的小工具)。
方法的實際定義是在幾個外部函式的幫助下發生的。這些基本上是直接呼叫 C 函式庫(事實上,有一小段包裝程式碼,但它在功能上並不重要)。
method show = gtk_widget_show obj
method show_all = gtk_widget_show_all obj
請注意,我們會將底層 GtkObject
傳遞給兩個 C 函式庫呼叫。這是合理的,因為這些函式在 C 中被宣告為 void gtk_widget_show (GtkWidget *);
(GtkWidget
和 GtkObject
在這種情況下可以安全地互換使用)。
我想描述 label
類別(這次是真的!),但在 widget
和 label
之間的是 misc
,一個通用類別,描述了大量雜項小工具。這個類別只是在諸如標籤之類的小工具周圍添加填充和對齊。以下是它的定義
let may f x =
match x with
| None -> ()
| Some x -> f x
external gtk_misc_set_alignment :
obj -> float * float -> unit = "gtk_misc_set_alignment_c"
external gtk_misc_set_padding :
obj -> int * int -> unit = "gtk_misc_set_padding_c"
class virtual misc ?alignment ?padding ?show obj =
object (self)
inherit widget ?show obj
method set_alignment = gtk_misc_set_alignment obj
method set_padding = gtk_misc_set_padding obj
initializer
may (gtk_misc_set_alignment obj) alignment;
may (gtk_misc_set_padding obj) padding
end
我們從一個稱為 may : ('a -> unit) -> 'a option -> unit
的輔助函式開始,它會在第二個參數的內容上呼叫其第一個參數,除非第二個參數為 None
。這個技巧(當然是從 lablgtk 竊取的)在處理可選參數時非常有用,我們將看到。
misc
中的方法應該很直接。比較棘手的是初始化程式碼。首先要注意,我們在建構子中使用了可選的 alignment
和 padding
參數,並且直接將可選的 show
和必須的 obj
參數傳遞給 widget
。我們要如何處理可選的 alignment
和 padding
呢?初始化器會用到這些。
initializer
may (gtk_misc_set_alignment obj) alignment;
may (gtk_misc_set_padding obj) padding
這就是那個棘手的 may
函數的作用。如果使用者提供了 alignment
參數,這會透過呼叫 gtk_misc_set_alignment obj the_alignment
來設定物件的對齊方式。但更常見的情況是用戶省略 alignment
參數,在這種情況下 alignment
會是 None
,而這不會做任何事。(實際上我們會得到 Gtk 的預設對齊方式,無論那是什麼)。padding
的情況也類似。請注意這樣做的方式具有一定的簡潔和優雅性。
現在我們終於可以開始處理 label
類別,它是直接繼承自 misc
的。
external gtk_label_new :
string -> obj = "gtk_label_new_c"
external gtk_label_set_text :
obj -> string -> unit = "gtk_label_set_text_c"
external gtk_label_set_justify :
obj -> Justification.t -> unit = "gtk_label_set_justify_c"
external gtk_label_set_pattern :
obj -> string -> unit = "gtk_label_set_pattern_c"
external gtk_label_set_line_wrap :
obj -> bool -> unit = "gtk_label_set_line_wrap_c"
class label ~text
?justify ?pattern ?line_wrap ?alignment
?padding ?show () =
let obj = gtk_label_new text in
object (self)
inherit misc ?alignment ?padding ?show obj
method set_text = gtk_label_set_text obj
method set_justify = gtk_label_set_justify obj
method set_pattern = gtk_label_set_pattern obj
method set_line_wrap = gtk_label_set_line_wrap obj
initializer
may (gtk_label_set_justify obj) justify;
may (gtk_label_set_pattern obj) pattern;
may (gtk_label_set_line_wrap obj) line_wrap
end
雖然這個類別比我們之前看過的類別大,但它實際上概念上是相同的,除了這個類別不是虛擬的。您可以建立這個類別的實例,這表示它最終必須呼叫 gtk_..._new
。這就是初始化程式碼(我們在上面討論過這個模式)。
class label ~text ... () =
let obj = gtk_label_new text in
object (self)
inherit misc ... obj
end
(小測驗:如果我們需要定義一個既可以讓其他類別繼承的基底類別,同時也是一個允許使用者建立實例的非虛擬類別,會發生什麼事?)
封裝對 C 函式庫的呼叫
現在我們將更詳細地探討如何實際封裝對 C 函式庫的函式呼叫。這裡有一個簡單的例子。
/* external gtk_label_set_text :
obj -> string -> unit
= "gtk_label_set_text_c" */
CAMLprim value
gtk_label_set_text_c (value obj, value str)
{
CAMLparam2 (obj, str);
gtk_label_set_text (unwrap (GtkLabel, obj),
String_val (str));
CAMLreturn (Val_unit);
}
比較外部函數呼叫的 OCaml 原型(在註解中)與函數的定義,我們可以發現兩件事
- OCaml 呼叫的 C 函數名稱為
"gtk_label_set_text_c"
。 - 傳遞了兩個參數(
value obj
和value str
)並返回一個 unit。
Values 是 OCaml 內部對各種事物的表示,從簡單的整數到字串,甚至是物件。我不會詳細討論 value
型別,因為 OCaml 手冊中已經充分涵蓋了它。要使用 value
,您只需要知道哪些巨集可用於在 value
和某些 C 型別之間進行轉換。這些巨集看起來像這樣:
String_val (val)
- 從已知為字串的
value
轉換為 C 字串(即char *
)。 Val_unit
- OCaml unit `()` 作為一個 `value`。
Int_val (val)
- 從已知為整數的
value
轉換為 C 的int
。 Val_int (i)
- 將 C 整數 `i` 轉換為整數 `value`。
Bool_val (val)
- 從已知為布林值的
value
轉換為 C 布林值(即int
)。 Val_bool (i)
- 將 C 整數 `i` 轉換為布林 `value`。
您可以猜測其他巨集或查閱手冊。請注意,從 C 的 char *
到 value 沒有直接的轉換。這涉及分配記憶體,這比較複雜。
在上面的 gtk_label_set_text_c
中,external
定義,加上強型別和型別推論,已經確保了參數的型別正確,因此要將 value str
轉換為 C 的 char *
,我們呼叫了 String_val (str)
。
函數的其他部分有點奇怪。為了確保垃圾收集器「知道」您的 C 函數在執行時仍在使用 obj
和 str
(請記住,垃圾收集器可能會在您的 C 函數中由於許多事件而觸發 - 回呼 OCaml 或使用 OCaml 的分配函數之一),您需要將函數框起來,加入程式碼來告訴垃圾收集器您正在使用的「根」。當然,也要在您完成使用這些根時告訴垃圾收集器。這是通過在 CAMLparamN
... CAMLreturn
中框住函數來完成的。因此:
CAMLparam2 (obj, str);
...
CAMLreturn (Val_unit);
CAMLparam2
是一個巨集,表示您正在使用兩個 value
參數。(還有另一個巨集用於註解本地 value
變數)。您需要使用 CAMLreturn
而不是普通的 return
,這會告訴 GC 您已經完成了這些根的使用。檢視您寫下 CAMLparam2 (obj, str)
時會內嵌哪些程式碼可能會很有啟發性。這是產生的程式碼(使用作者的 OCaml 版本,因此不同實作之間可能略有不同)
struct caml__roots_block *caml__frame
= local_roots;
struct caml__roots_block caml__roots_obj;
caml__roots_obj.next = local_roots;
local_roots = &caml__roots_obj;
caml__roots_obj.nitems = 1;
caml__roots_obj.ntables = 2;
caml__roots_obj.tables [0] = &obj;
caml__roots_obj.tables [1] = &str;
對於 CAMLreturn (foo)
local_roots = caml__frame;
return (foo);
如果您仔細追蹤程式碼,您會看到 local_roots
顯然是一個 caml__roots_block
結構的連結列表。當我們進入函數時,會將其中一個(或多個)結構推送到連結列表上,當我們離開時,所有這些結構都會被彈回,從而將 local_roots
還原到離開函數時的先前狀態。(如果您記得呼叫 CAMLreturn
而不是 return
- 否則 local_roots
將會指向堆疊上未初始化的資料,並產生「滑稽」的後果)。
每個 caml__roots_block
結構都有最多五個 value
的空間(您可以有多個區塊,因此這不是限制)。當 GC 運行時,我們可以推斷出它必須遍歷連結列表,從 local_roots
開始,並將每個 value
作為垃圾收集的根來處理。如果沒有以這種方式宣告 value
參數或本地 value
變數,後果將會是垃圾收集器可能會將該變數視為不可達的記憶體,因此在您的函數運行時回收它!
最後是神秘的 unwrap
巨集。這是我自己寫的,或者更確切地說,這是我主要從 lablgtk 複製的。有兩個相關的函數,稱為 wrap
和 unwrap
,正如您可能猜到的那樣,它們在 OCaml 的 value
中封裝和解封 GtkObject
。這些函數建立了 GtkObject
和我們為 OCaml 定義的不透明、神秘的 obj
型別之間有點神奇的關係(請參閱本章的第一部分以提醒自己)。
問題是我們如何封裝(並隱藏)C 的 GtkObject
結構,以便我們可以將其作為不透明的「東西」(obj
)在我們的 OCaml 程式碼中傳遞,並希望稍後將其傳遞回 C 函數,該函數可以解封它並再次檢索相同的 GtkObject
?
為了將其傳遞給 OCaml 程式碼,我們必須以某種方式將其轉換為 value
。幸運的是,我們可以很容易地使用 C API 來建立 OCaml 垃圾收集器不會過於仔細檢查的 value
區塊......