呼叫 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 *);GtkWidgetGtkObject 在這種情況下可以安全地互換使用)。

我想描述 label 類別(這次是真的!),但在 widgetlabel 之間的是 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 中的方法應該很直接。比較棘手的是初始化程式碼。首先要注意,我們在建構子中使用了可選的 alignmentpadding 參數,並且直接將可選的 show 和必須的 obj 參數傳遞給 widget。我們要如何處理可選的 alignmentpadding 呢?初始化器會用到這些。

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 objvalue 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 函數在執行時仍在使用 objstr(請記住,垃圾收集器可能會在您的 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 複製的。有兩個相關的函數,稱為 wrapunwrap,正如您可能猜到的那樣,它們在 OCaml 的 value 中封裝和解封 GtkObject。這些函數建立了 GtkObject 和我們為 OCaml 定義的不透明、神秘的 obj 型別之間有點神奇的關係(請參閱本章的第一部分以提醒自己)。

問題是我們如何封裝(並隱藏)C 的 GtkObject 結構,以便我們可以將其作為不透明的「東西」(obj)在我們的 OCaml 程式碼中傳遞,並希望稍後將其傳遞回 C 函數,該函數可以解封它並再次檢索相同的 GtkObject

為了將其傳遞給 OCaml 程式碼,我們必須以某種方式將其轉換為 value。幸運的是,我們可以很容易地使用 C API 來建立 OCaml 垃圾收集器不會過於仔細檢查的 value 區塊......

仍然需要協助嗎?

協助改進我們的文件

所有 OCaml 文件都是開放原始碼。發現有錯誤或不清楚的地方嗎?提交 Pull request。

OCaml

創新。社群。安全。