第 22 章 與 C 語言介接

本章說明如何將以 C 語言撰寫的使用者定義基本函數與 OCaml 程式碼連結,並從 OCaml 函式呼叫,以及這些 C 函式如何回呼 OCaml 程式碼。

1 概觀和編譯資訊

1.1 宣告基本函數

定義::= ...
 external值名稱:類型表示式=外部宣告
 
外部宣告::=字串常值 [ 字串常值 [ 字串常值 ] ]

使用者基本函數是在實作檔案或 structend 模組表示式中使用 external 關鍵字宣告的

        external name : type = C-function-name

這會將值名稱 名稱 定義為一個函式,其類型為 類型,該函式透過呼叫指定的 C 函式執行。例如,以下是在標準函式庫模組 Stdlib 中宣告 seek_in 基本函數的方式

        external seek_in : in_channel -> int -> unit = "caml_ml_seek_in"

具有多個引數的基本函數總是柯里化的。C 函式不一定與 ML 函式具有相同的名稱。

如此定義的外部函式可以在介面檔案或 sigend 簽章中指定為一般值

        val name : type

因此隱藏它們作為 C 函式的實作,或者明確地作為「顯示的」外部函式

        external name : type = C-function-name

後者稍微有效率一些,因為它允許模組的客戶端直接呼叫 C 函式,而不是透過對應的 OCaml 函式。另一方面,如果它們在頂層具有副作用,則不應在函式庫模組中使用它,因為此直接呼叫會干擾連結器在連結時從函式庫中移除未使用的模組的演算法。

基本函數的元數(引數的數量)會根據其 OCaml 類型在 external 宣告中自動確定,方法是計算類型中函數箭號的數量。例如,上面的 seek_in 具有元數 2,並且會使用兩個引數呼叫 C 函式 caml_ml_seek_in。同樣地,

    external seek_in_pair: in_channel * int -> unit = "caml_ml_seek_in_pair"

具有元數 1,而 C 函式 caml_ml_seek_in_pair 接收一個引數(這是 OCaml 值的配對)。

在確定基本函數的元數時,不會展開類型縮寫。例如,

        type int_endo = int -> int
        external f : int_endo -> int_endo = "f"
        external g : (int -> int) -> (int -> int) = "f"

f 具有元數 1,但 g 具有元數 2。這允許基本函數傳回函數值(如上面的 f 範例中所示):只需記得在類型縮寫中命名函數傳回類型即可。

除了 C 函式的名稱之外,語言還接受具有一或兩個旗標字串的外部宣告。這些旗標保留供標準函式庫的實作使用。

1.2 實作基本函數

元數為 n ≤ 5 的使用者基本函數是由 C 函式實作的,這些函式採用 nvalue 類型的引數,並傳回 value 類型的結果。類型 value 是 OCaml 值的表示的類型。它會編碼多個基本類型(整數、浮點數、字串、…)以及 OCaml 資料結構的物件。類型 value 和相關的轉換函式與巨集會在下面詳細說明。例如,以下是實作 In_channel.input 基本函數的 C 函式的宣告,它採用 4 個引數

CAMLprim value input(value channel, value buffer, value offset, value length)
{
  ...
}

當在 OCaml 程式中應用基本函式時,會使用基本函式所應用到的運算式的 值 作為引數呼叫 C 函式。函式傳回的值會作為函式應用程式的結果傳回給 OCaml 程式。

元數大於 5 的使用者基本函數應該由兩個 C 函式實作。第一個函式將與位元組碼編譯器 ocamlc 一起使用,它會接收兩個引數:OCaml 值陣列的指標(引數的值),以及提供的引數數量的整數。另一個函式將與原生碼編譯器 ocamlopt 一起使用,它會直接採用其引數。例如,以下是 7 個引數的基本函數 Nat.add_nat 的兩個 C 函式

CAMLprim value add_nat_native(value nat1, value ofs1, value len1,
                              value nat2, value ofs2, value len2,
                              value carry_in)
{
  ...
}
CAMLprim value add_nat_bytecode(value * argv, int argn)
{
  return add_nat_native(argv[0], argv[1], argv[2], argv[3],
                        argv[4], argv[5], argv[6]);
}

兩個 C 函式的名稱必須在基本函數宣告中給定,如下所示

        external name : type =
                 bytecode-C-function-name native-code-C-function-name

例如,在 add_nat 的情況下,宣告是

        external add_nat: nat -> int -> int -> nat -> int -> int -> int -> int
                        = "add_nat_bytecode" "add_nat_native"

實作使用者基本函數實際上是兩個獨立的任務:一方面,解碼引數以從給定的 OCaml 值中提取 C 值,並將傳回值編碼為 OCaml 值;另一方面,實際根據引數計算結果。除了非常簡單的基本函數之外,通常最好使用兩個不同的 C 函式來實作這兩個任務。第一個函式實際實作基本函數,採用原生 C 值作為引數並傳回原生 C 值。第二個函式通常稱為「存根程式碼」,它是第一個函式的簡單包裝函式,它會將其引數從 OCaml 值轉換為 C 值、呼叫第一個函式,並將傳回的 C 值轉換為 OCaml 值。例如,以下是 Int64.float_of_bits 基本函數的存根程式碼

CAMLprim value caml_int64_float_of_bits(value vi)
{
  return caml_copy_double(caml_int64_float_of_bits_unboxed(Int64_val(vi)));
}

(這裡,caml_copy_doubleInt64_val 是類型 value 的轉換函式和巨集,將在稍後說明。CAMLprim 巨集會展開為必要的編譯器指示,以確保該函式已匯出且可從 OCaml 存取。)繁重的工作是由宣告為的函式 caml_int64_float_of_bits_unboxed 執行

double caml_int64_float_of_bits_unboxed(int64_t i)
{
  ...
}

若要撰寫處理 OCaml 值的 C 程式碼,會提供下列包含檔

包含檔提供
caml/mlvalues.h類型 value 的定義和轉換巨集
caml/alloc.h配置函式(用於建立結構化 OCaml 物件)
caml/memory.h與記憶體相關的雜項函式和巨集(用於 GC 介面、結構的就地修改等等)。
caml/fail.h用於引發例外狀況的函式(請參閱22.4.5節)
caml/callback.h從 C 回呼到 OCaml(請參閱22.7節)。
caml/custom.h自訂區塊的操作(請參閱22.9節)。
caml/intext.h用於為自訂區塊撰寫使用者定義的序列化和還原序列化函式的操作(請參閱22.9節)。
caml/threads.h在存在多個執行緒的情況下進行介接的操作(請參閱22.12節)。

這些檔案位於 OCaml 標準函式庫目錄的 caml/ 子目錄中,該目錄由命令 ocamlc -where 傳回(通常是 /usr/local/lib/ocaml/usr/lib/ocaml)。

OCaml 執行期系統包含三個主要部分:位元組碼直譯器、記憶體管理員,以及一組實作基本操作的 C 函式。提供了一些位元組碼指令來呼叫這些 C 函式,這些函式由它們在函式表(基本函式表)中的偏移量指定。

在預設模式下,OCaml 連接器會產生標準執行時系統的位元組碼,其中包含一組標準的原生函數。若參考到此標準集合中不存在的原生函數,將會導致「無法使用的 C 原生函數」錯誤。(除非支援 C 函式庫的動態載入,請參閱下方第 22.1.4 節。)

在「自訂執行時」模式下,OCaml 連接器會掃描物件檔案,並決定所需的原生函數集合。接著,它會透過呼叫原生碼連接器,使用下列項目建立合適的執行時系統:

這會建立一個包含所需原生函數的執行時系統。OCaml 連接器會為此自訂執行時系統產生位元組碼。位元組碼會附加到自訂執行時系統的末尾,以便在啟動輸出檔案(自訂執行時 + 位元組碼)時自動執行。

若要以「自訂執行時」模式連結,請執行 ocamlc 命令,並帶有:

如果您使用原生碼編譯器 ocamlopt,則不需要 -custom 標記,因為 ocamlopt 的最終連結階段總是會建立獨立的可執行檔。若要建立混合 OCaml/C 可執行檔,請執行 ocamlopt 命令,並帶有:

從 Objective Caml 3.00 開始,可以在 OCaml 函式庫檔案 .cma.cmxa 中記錄 -custom 選項以及 C 函式庫的名稱。例如,考慮一個 OCaml 函式庫 mylib.cma,它是從 OCaml 物件檔案 a.cmob.cmo 建立的,並且參考 libmylib.a 中的 C 程式碼。如果函式庫是如下建立的:

        ocamlc -a -o mylib.cma -custom a.cmo b.cmo -cclib -lmylib

該函式庫的使用者可以簡單地與 mylib.cma 連結

        ocamlc -o myprog mylib.cma ...

系統會自動加入 -custom-cclib -lmylib 選項,達到與

        ocamlc -o myprog -custom a.cmo b.cmo ... -cclib -lmylib

當然,另一種方法是在不使用額外選項的情況下建立函式庫

        ocamlc -a -o mylib.cma a.cmo b.cmo

然後要求使用者在連結時自行提供 -custom-cclib -lmylib 選項

        ocamlc -o myprog -custom mylib.cma ... -cclib -lmylib

然而,前一種方法對於函式庫的最終使用者來說更方便。

從 Objective Caml 3.03 開始,提供了一種替代方案來取代使用 -custom 程式碼靜態連結 C 程式碼。在此模式下,OCaml 連接器會產生純位元組碼可執行檔(沒有內嵌的自訂執行時系統),該執行檔只會記錄包含 C 程式碼的動態載入函式庫的名稱。標準的 OCaml 執行時系統 ocamlrun 接著會動態載入這些函式庫,並在執行位元組碼之前解析對所需原生函數的參考。

此功能目前在 OCaml 支援的所有平台上都可用,Cygwin 64 位元除外。

若要使用 OCaml 程式碼動態連結 C 程式碼,C 程式碼必須先編譯成共享函式庫(在 Unix 下)或 DLL(在 Windows 下)。這包含 1- 使用適當的 C 編譯器標記編譯 C 檔案,以產生與位置無關的程式碼(當作業系統需要時),以及 2- 從產生的物件檔案建立共享函式庫。產生的共享函式庫或 DLL 檔案必須安裝在 ocamlrun 稍後在程式啟動時可以找到的位置(請參閱第 15.3 節)。最後(步驟 3),執行 ocamlc 命令,並帶有:

不要設定 -custom 標記,否則您將回到第 22.1.3 節所述的靜態連結。ocamlmklib 工具(請參閱第 22.14 節)會自動執行步驟 2 和 3。

如同靜態連結的情況,可以在 OCaml .cma 函式庫封存檔中記錄 C 函式庫的名稱(建議這樣做)。再次考慮一個 OCaml 函式庫 mylib.cma,它是從 OCaml 物件檔案 a.cmob.cmo 建立的,並且參考 dllmylib.so 中的 C 程式碼。如果函式庫是如下建立的:

        ocamlc -a -o mylib.cma a.cmo b.cmo -dllib -lmylib

該函式庫的使用者可以簡單地與 mylib.cma 連結

        ocamlc -o myprog mylib.cma ...

系統會自動加入 -dllib -lmylib 選項,達到與

        ocamlc -o myprog a.cmo b.cmo ... -dllib -lmylib

透過這種機制,函式庫 mylib.cma 的使用者不需要知道它參考了 C 程式碼,也不需要知道此 C 程式碼是否必須靜態連結(使用 -custom)或動態連結。

1.5 選擇靜態連結與動態連結

在描述了兩種不同的 C 程式碼與 OCaml 程式碼連結方式後,我們現在回顧一下每種方法的優缺點,以協助混合 OCaml/C 函式庫的開發人員做出決定。

動態連結的主要優點是它保留了位元組碼可執行檔的平台獨立性。也就是說,位元組碼可執行檔不包含任何機器碼,因此可以在平台 A 上編譯,並在其他平台 BC、... 上執行,只要在所有這些平台上都有可用的所需共享函式庫。相反地,由 ocamlc -custom 產生的可執行檔只能在其建立的平台上執行,因為它們包含特定於該平台的客製化執行時系統。此外,動態連結會產生較小的可執行檔。

動態連結的另一個優點是,函式庫的最終使用者不需要在其電腦上安裝 C 編譯器、C 連接器和 C 執行時函式庫。這在 Unix 和 Cygwin 下不是什麼大問題,但許多 Windows 使用者不願意安裝 Microsoft Visual C,只是為了能夠執行 ocamlc -custom

動態連結有兩個缺點。首先,產生的可執行檔不是獨立的:它需要共享函式庫以及 ocamlrun 安裝在執行程式碼的電腦上。如果您希望發佈獨立的可執行檔,最好使用 ocamlc -custom -ccopt -staticocamlopt -ccopt -static 靜態連結它。動態連結也會引發「DLL 地獄」問題:必須小心確保在啟動時找到正確版本的共享函式庫。

動態連結的第二個缺點是它使函式庫的建構更加複雜。編譯為與位置無關的程式碼和建立共享函式庫的 C 編譯器和連接器標記在不同的 Unix 系統之間差異很大。此外,並非所有 Unix 系統都支援動態連結,這需要在函式庫的 Makefile 中使用靜態連結作為備用方案。ocamlmklib 命令(請參閱第 22.14 節)會嘗試隱藏一些這些系統相依性。

總之:在原生 Windows 移植下強烈建議使用動態連結,因為沒有可移植性問題,而且對於最終使用者來說更加方便。在 Unix 下,應該考慮針對成熟、經常使用的函式庫使用動態連結,因為它增強了位元組碼可執行檔的平台獨立性。對於新的或很少使用的函式庫,靜態連結更容易以可移植的方式設定。

1.6 建構獨立的自訂執行時系統

每次將 OCaml 程式碼與 C 函式庫連結時都建立自訂執行時系統(例如 ocamlc -custom 所做的那樣),有時會很不方便。一方面,在某些系統上(具有不良的連接器或慢速的遠端檔案系統)建構執行時系統速度很慢;另一方面,位元組碼檔案的平台獨立性會遺失,因此必須針對每個感興趣的平台執行一個 ocamlc -custom 連結。

取代 ocamlc -custom 的方法是分別建立一個整合所需 C 函式庫的自訂執行時系統,然後產生可以在此自訂執行時上運行的「純」位元組碼可執行檔(不包含它們自己的執行時系統)。這是透過 ocamlc-make-runtime-use-runtime 標記來實現的。例如,若要建立一個整合「Unix」和「Threads」函式庫的 C 部分的自訂執行時系統,請執行:

        ocamlc -make-runtime -o /home/me/ocamlunixrun unix.cma threads.cma

若要產生在此執行時系統上運行的位元組碼可執行檔,請執行:

        ocamlc -use-runtime /home/me/ocamlunixrun -o myprog \
                unix.cma threads.cma your .cmo and .cma files

接著,可以像平常一樣啟動位元組碼可執行檔 myprogmyprog args/home/me/ocamlunixrun myprog args

請注意,位元組碼函式庫 unix.cmathreads.cma 必須給定兩次:在建置執行階段系統時(以便 ocamlc 知道需要哪些 C 原始碼),以及在建置位元組碼可執行檔時(以便實際連結 unix.cmathreads.cma 中的位元組碼)。

2 value 型別

所有 OCaml 物件都由 C 型別 value 表示,此型別定義在包含檔 caml/mlvalues.h 中,同時也定義了操作該型別值的巨集。型別為 value 的物件可以是:

2.1 整數值

整數值編碼 63 位元的帶正負號整數(在 32 位元架構上為 31 位元)。它們是未裝箱的(未配置)。

2.2 區塊

堆積中的區塊是透過垃圾回收機制收集的,因此具有嚴格的結構限制。每個區塊都包含一個標頭,其中包含區塊的大小(以字組為單位)和區塊的標籤。標籤會決定區塊內容的結構方式。小於 No_scan_tag 的標籤表示結構化區塊,其中包含格式正確的值,垃圾回收機制會以遞迴方式走訪這些值。大於或等於 No_scan_tag 的標籤表示原始區塊,垃圾回收機制不會掃描其內容。為了讓諸如相等性和結構化輸入/輸出之類的特設多型原始碼能夠獲益,結構化和原始區塊會根據其標籤進一步分類,如下所示:

標籤區塊的內容
0 到 No_scan_tag − 1結構化區塊(OCaml 物件的陣列)。每個欄位都是一個 value
Closure_tag代表函數值的閉包。第一個字組是指向程式碼片段的指標,其餘的字組則是包含環境的 value
String_tag字元字串或位元組序列。
Double_tag倍精確度浮點數。
Double_array_tag倍精確度浮點數的陣列或記錄。
Abstract_tag代表抽象資料型別的區塊。
Custom_tag代表抽象資料型別的區塊,附加了使用者定義的最終化、比較、雜湊、序列化和還原序列化函式。

2.3 堆積外部的指標

在 OCaml 的早期版本中,可以將指向堆積外部位址的字組對齊指標用作 OCaml 值,只需將指標轉換為 value 型別即可。自 OCaml 5.0 起,不再支援這種用法。

從 OCaml 操作指向堆積外部區塊的指標的正確方式,是將這些指標儲存在標籤為 Abstract_tagCustom_tag 的 OCaml 區塊中,然後將這些區塊用作 OCaml 值。

以下範例示範了在 Abstract_tag 區塊內封裝 C 型別 ty * 的堆積外部指標。章節 22.6 提供了一個更完整的範例,使用了 Custom_tag 區塊。

/* Create an OCaml value encapsulating the pointer p */
static value val_of_typtr(ty * p)
{
  value v = caml_alloc(1, Abstract_tag);
  *((ty **) Data_abstract_val(v)) = p;
  return v;
}

/* Extract the pointer encapsulated in the given OCaml value */
static ty * typtr_of_val(value v)
{
  return *((ty **) Data_abstract_val(v));
}

或者,堆積外部指標可以視為「原生」整數,也就是在 32 位元平台上裝箱的 32 位元整數,以及在 64 位元平台上裝箱的 64 位元整數。

/* Create an OCaml value encapsulating the pointer p */
static value val_of_typtr(ty * p)
{
  return caml_copy_nativeint((intnat) p);
}

/* Extract the pointer encapsulated in the given OCaml value */
static ty * typtr_of_val(value v)
{
  return (ty *) Nativeint_val(v);
}

對於至少 2 對齊的指標(保證最低位元為零),我們還有另一個有效表示法,即作為 OCaml 標籤整數。

/* Create an OCaml value encapsulating the pointer p */
static value val_of_typtr(ty * p)
{
  assert (((uintptr_t) p & 1) == 0);  /* check correct alignment */
  return (value) p | 1;
}

/* Extract the pointer encapsulated in the given OCaml value */
static ty * typtr_of_val(value v)
{
  return (ty *) (v & ~1);
}

3 OCaml 資料型別的表示法

本節說明 OCaml 資料型別如何在 value 型別中進行編碼。

3.1 原子型別

OCaml 型別編碼
int未裝箱的整數值。
char未裝箱的整數值 (ASCII 碼)。
float標籤為 Double_tag 的區塊。
bytes標籤為 String_tag 的區塊。
string標籤為 String_tag 的區塊。
int32標籤為 Custom_tag 的區塊。
int64標籤為 Custom_tag 的區塊。
nativeint標籤為 Custom_tag 的區塊。

3.2 元組和記錄

元組由指向標籤為 0 的區塊的指標表示。

記錄也由標籤為零的區塊表示。記錄型別宣告中標籤的排序決定了記錄欄位的配置:與第一個宣告的標籤相關聯的值會儲存在區塊的欄位 0 中,與第二個標籤相關聯的值則會儲存在欄位 1 中,依此類推。

作為最佳化,欄位都具有靜態型別 float 的記錄會以浮點數陣列的形式表示,其標籤為 Double_array_tag。(請參閱下方關於陣列的章節。)

另一個最佳化方式是,可取消裝箱的記錄型別會以特殊方式表示;可取消裝箱的記錄型別是指只有一個欄位的不可變記錄型別。可取消裝箱的型別會以兩種方式之一表示:裝箱或未裝箱。裝箱的記錄型別如上所述表示(透過標籤為 0 或 Double_array_tag 的區塊)。未裝箱的記錄型別會直接由其欄位的值表示(也就是說,沒有區塊來表示記錄本身)。

表示法的選擇方式如下(優先順序遞減):

3.3 陣列

整數和指標陣列的表示方式與元組和記錄類似,也就是指向標籤為 0 的區塊的指標。它們使用 Field 巨集進行讀取,並使用 caml_modify 函式進行寫入。

型別為 floatarray 的值(由 Float.Array 模組操作),以及其宣告僅包含 float 欄位的記錄,會使用有效率的未裝箱表示法:標籤為 Double_array_tag 的區塊,其內容包含原始 double 值,這些值本身不是有效的 OCaml 值。它們應該使用 Double_flat_fieldStore_double_flat_field 巨集存取。

最後,型別為 float array 的陣列可以使用裝箱或未裝箱表示法,具體取決於編譯器的設定方式。它們目前預設使用未裝箱表示法,但可以透過將 --disable-flat-float-array 旗標傳遞給 'configure' 指令碼來讓它們使用裝箱表示法。它們應該使用 Double_array_fieldStore_double_array_field 巨集存取,這兩種巨集在兩種模式下都能正確運作。

3.4 具體資料型別

建構的詞彙以未裝箱的整數(針對常數建構函式)或標籤編碼建構函式的區塊(針對非常數建構函式)表示。針對給定具體型別的常數建構函式和非常數建構函式會分別編號,從 0 開始,按照它們在具體型別宣告中出現的順序排列。常數建構函式由等於其建構函式編號的未裝箱整數表示。使用 n 個引數宣告的非常數建構函式由大小為 n 的區塊表示,並標記建構函式編號;n 個欄位包含其引數。範例

建構的詞彙表示法
()Val_int(0)
falseVal_int(0)
trueVal_int(1)
[]Val_int(0)
h::t大小為 2 且標籤為 0 的區塊;第一個欄位包含 h,第二個欄位包含 t

為方便起見,caml/mlvalues.h 定義了巨集 Val_unitVal_falseVal_trueVal_emptylist,以分別參照 ()falsetrue[]

下列範例說明如何將整數和區塊標籤指派給建構函式

type t =
  | A             (* First constant constructor -> integer "Val_int(0)" *)
  | B of string   (* First non-constant constructor -> block with tag 0 *)
  | C             (* Second constant constructor -> integer "Val_int(1)" *)
  | D of bool     (* Second non-constant constructor -> block with tag 1 *)
  | E of t * t    (* Third non-constant constructor -> block with tag 2 *)

作為最佳化,可取消裝箱的具體資料型別會以特殊方式表示;如果具體資料型別只有一個建構函式,且此建構函式只有一個引數,則此具體資料型別為可取消裝箱的。可取消裝箱的具體資料型別的表示方式與可取消裝箱的記錄型別相同:請參閱章節 22.3.2 中的說明。

3.5 物件

物件以帶有標籤 Object_tag 的區塊表示。區塊的第一個欄位指向物件的類別和相關的方法套件,其格式無法輕易地從 C 語言中利用。第二個欄位包含一個唯一的物件 ID,用於比較。物件的其餘欄位包含物件實例變數的值。直接存取實例變數是不安全的,因為型別系統不保證物件包含哪些實例變數。

可以使用 C 函數 caml_get_public_method(宣告於 <caml/mlvalues.h> 中)從物件中提取公共方法。由於公共方法標籤的雜湊方式與變體標籤相同,並且方法是將自身作為第一個參數的函數,如果您想從 C 端執行方法呼叫 foo#bar,您應該呼叫

  callback(caml_get_public_method(foo, hash_variant("bar")), foo);

3.6 多型變體

與建構的項類似,多型變體值可以表示為整數(對於不帶參數的多型變體)或區塊(對於帶參數的多型變體)。與建構的項不同,變體建構子不是從 0 開始編號,而是由雜湊值(一個 OCaml 整數)識別,該雜湊值由 C 函數 hash_variant 計算(宣告於 <caml/mlvalues.h> 中):例如,名為 VConstr 的變體建構子的雜湊值為 hash_variant("VConstr")

變體值 `VConstrhash_variant("VConstr") 表示。變體值 `VConstr(v) 由大小為 2 且標籤為 0 的區塊表示,其中欄位編號 0 包含 hash_variant("VConstr"),欄位編號 1 包含 v

與建構的值不同,採用多個參數的多型變體值不會被展平。也就是說,`VConstr(v, w) 由大小為 2 的區塊表示,其欄位編號 1 包含對 (v, w) 的配對的表示,而不是大小為 3 的區塊,其中欄位 1 和 2 包含 vw

4 值的操作

4.1 型別測試

4.2 整數的操作

4.3 存取區塊

表達式 Field(v, n)Byte(v, n)Byte_u(v, n) 是有效的左值。因此,它們可以被賦值,導致值 v 的就地修改。直接賦值給 Field(v, n) 必須謹慎,以避免混淆垃圾收集器(見下文)。

4.4 配置區塊

簡單介面

低階介面

以下函數比 caml_alloc 稍微有效率,但也更難以使用。

從配置函數的角度來看,區塊根據其大小分為零大小區塊、小區塊(大小小於或等於 Max_young_wosize)和大區塊(大小大於 Max_young_wosize)。常數 Max_young_wosize 在 include 檔案 mlvalues.h 中宣告。它保證至少為 64(字組),因此可以假設任何大小小於或等於 64 的常數大小區塊都是小的。對於在執行時計算大小的區塊,必須將大小與 Max_young_wosize 進行比較,以確定正確的配置程序。

4.5 引發例外

提供兩個函數來引發兩個標準例外

從 C 引發任意例外更為微妙:例外識別碼由 OCaml 程式動態配置,因此必須使用以下章節 22.7.3 中描述的註冊機制將其傳達給 C 函數。一旦在 C 中恢復了例外識別碼,以下函數實際上會引發例外

5 與垃圾回收器和諧共處

堆積中的未使用區塊會由垃圾回收器自動回收。這需要操作堆積配置區塊的 C 程式碼進行一些合作。

5.1 簡單介面

本節中描述的所有巨集都宣告在 memory.h 標頭檔中。

規則 1 具有 value 類型參數或區域變數的函式,必須以呼叫 CAMLparam 巨集之一開始,並以 CAMLreturnCAMLreturn0CAMLreturnT 返回。

有六個 CAMLparam 巨集:CAMLparam0CAMLparam5,它們分別接受零到五個參數。如果您的函式具有不超過 5 個 value 類型的參數,請使用相應的巨集,並將這些參數作為引數。如果您的函式具有超過 5 個 value 類型的參數,請使用 CAMLparam5 以及其中的五個參數,並對其餘參數使用一個或多個對 CAMLxparam 巨集(CAMLxparam1CAMLxparam5)的呼叫。

巨集 CAMLreturnCAMLreturn0CAMLreturnT 用於取代 C 關鍵字 return;任何在入口點使用 CAMLparam 巨集的函式,都應在所有出口點使用 CAMLreturn。匯出為 OCaml 外部函式的 C 函式必須回傳一個 value,並且應使用 CAMLreturn (x) 而不是 return x。某些輔助函式可能會操作 OCaml 值,但回傳 void 或其他資料類型。回傳 void 的程序應明確使用 CAMLreturn0,而不應有任何隱含的回傳。回傳某類型 t 的 C 資料的輔助函式應使用 CAMLreturnT (t, x) 而不是 return x

注意

某些 C 編譯器會在每次使用 CAMLparamCAMLlocal 時,針對未使用的變數 caml__dummy_xxx 發出假的警告。您應忽略這些警告。


範例

CAMLprim value my_external (value v1, value v2, value v3)
{
  CAMLparam3 (v1, v2, v3);
  ...
  CAMLreturn (Val_unit);
}


static void helper_procedure (value v1, value v2)
{
  CAMLparam2 (v1, v2);
  ...
  CAMLreturn0;
}

static int helper_function (value v1, value v2)
{
  CAMLparam2 (v1, v2);
  ...
  CAMLreturnT (int, 0);
}
注意

如果您的函式是一個具有超過 5 個參數的原語,用於位元組碼執行階段,則其參數不是 value 類型,並且不得宣告(它們具有 value *int 類型)。

警告

CAMLreturn0 僅應使用於回傳 void 的內部程序。CAMLreturn(Val_unit) 應用於回傳 OCaml 單元值的函式。原語(可以從 OCaml 呼叫的 C 函式)永遠不應回傳 void。

規則 2 value 類型的區域變數必須使用 CAMLlocal 巨集之一宣告。value 的陣列使用 CAMLlocalN 宣告。這些巨集必須在函式的開頭使用,而不是在巢狀區塊中使用。

巨集 CAMLlocal1CAMLlocal5 宣告並初始化一個到五個 value 類型的區域變數。變數名稱會作為巨集的引數給定。CAMLlocalN(x, n) 會宣告並初始化一個 value [n] 類型的區域變數。如果您的區域變數超過 5 個,您可以使用多次對這些巨集的呼叫。

範例

CAMLprim value bar (value v1, value v2, value v3)
{
  CAMLparam3 (v1, v2, v3);
  CAMLlocal1 (result);
  result = caml_alloc (3, 0);
  ...
  CAMLreturn (result);
}
警告

CAMLlocal(和 CAMLxparam)只能在 CAMLparam之後呼叫。如果函式宣告區域值,但不帶任何值引數,則它應以 CAMLparam0 () 開始。

static value foo (int n)
{
  CAMLparam0 ();;
  CAMLlocal (result);
  ...
  CAMLreturn (result);
}
規則 3 對於結構化區塊的欄位,必須使用 Store_field 巨集(對於一般區塊)、Store_double_array_field 巨集(對於 float array 值)或 Store_double_flat_field(對於 floatarray 值和浮點數記錄)進行賦值。其他賦值不得使用 Store_fieldStore_double_array_fieldStore_double_flat_field

Store_field (b, n, v) 會將值 v 儲存在值 b 的欄位編號 n 中,其中 b 必須是一個區塊(即,Is_block(b) 必須為 true)。

範例

CAMLprim value bar (value v1, value v2, value v3)
{
  CAMLparam3 (v1, v2, v3);
  CAMLlocal1 (result);
  result = caml_alloc (3, 0);
  Store_field (result, 0, v1);
  Store_field (result, 1, v2);
  Store_field (result, 2, v3);
  CAMLreturn (result);
}
警告

Store_fieldStore_double_field 的第一個引數必須是由 CAMLparam* 宣告的變數,或由 CAMLlocal* 宣告的參數,以確保由其他引數的評估觸發的垃圾回收,不會在計算第一個引數後使其失效。

與 CAMLlocalN 一起使用

使用 CAMLlocalN 宣告的值陣列,不得使用 Store_field 寫入。請改用一般的 C 陣列語法。

規則 4 包含值的全域變數必須使用 caml_register_global_root 函式向垃圾回收器註冊,但全域變數和位置僅會包含 OCaml 整數(永遠不會是指標),則無需註冊。

對於任何在 OCaml 堆積外部,且包含值且無法保證從另一個已註冊的全域變數或位置、以 CAMLlocal 宣告的區域變數或以 CAMLparam 宣告的函式參數可存取到的記憶體位置,也是如此(只要它包含該值)。

全域變數 v 的註冊,是藉由在第一次將有效值儲存在 v 之前或之後立即呼叫 caml_register_global_root(&v) 來完成;同樣地,任意位置 p 的註冊,是藉由呼叫 caml_register_global_root(p) 來完成。

您不得在註冊和儲存值之間呼叫任何 OCaml 執行階段函式或巨集。您也不得在變數 v(同樣地,位置 p)中儲存任何不是有效值的東西。

當此類變數或位置中的值在 OCaml 堆積中移動時,註冊會導致垃圾回收器更新變數或記憶體位置的內容。在執行緒的情況下,必須謹慎確保與 OCaml 執行階段進行適當的同步,以避免在讀取或寫入值時發生與垃圾回收器競爭的情況。(請參閱第 22.12.2 節。)

已註冊的全域變數 v 可以藉由呼叫 caml_remove_global_root(&v) 來取消註冊。

如果在註冊後很少修改全域變數 v 的內容,則可以藉由呼叫 caml_register_generational_global_root(&v) 來註冊 v(在使用有效的 value 初始化後,但在任何配置或呼叫 GC 函式之前),並呼叫 caml_remove_generational_global_root(&v) 來取消註冊,以獲得更好的效能。在這種情況下,您不得直接修改 v 的值,而必須使用 caml_modify_generational_global_root(&v,x) 將其設定為 x。垃圾回收器利用 v 在呼叫 caml_modify_generational_global_root 之間不會被修改的保證,來減少掃描它的頻率。如果 v 的修改次數少於次要回收次數,這可以提高效能。

注意

CAML 巨集使用以 caml__ 開頭的識別符號(區域變數、類型識別符號、結構標籤)。請勿在您的程式中使用任何以 caml__ 開頭的識別符號。

5.2 低階介面

我們現在給出對應於低階配置函式 caml_alloc_smallcaml_alloc_shr 的 GC 規則。如果您堅持使用簡化的配置函式 caml_alloc,則可以忽略這些規則。

規則 5 在使用低階函式配置結構化區塊(標籤小於 No_scan_tag 的區塊)後,此區塊的所有欄位必須在下一個配置操作之前填入格式正確的值。如果該區塊是使用 caml_alloc_small 配置的,則透過直接賦值給區塊的欄位來執行填寫:

        Field(v, n) = vn;
如果該區塊是使用 caml_alloc_shr 配置的,則透過 caml_initialize 函式執行填寫

        caml_initialize(&Field(v, n), vn);

下一個配置可能會觸發垃圾回收。垃圾回收器假定所有結構化區塊都包含格式正確的值。新建立的區塊包含隨機資料,這些資料通常不代表格式正確的值。

如果您真的需要在欄位接收最終值之前進行配置,請先使用常數值(例如 Val_unit)初始化,然後配置,然後使用正確的值修改欄位(請參閱規則 6)。

規則 6 直接將值賦予區塊的欄位,如同

        Field(v, n) = w;
只有在 v 是由 caml_alloc_small 新配置的區塊時才是安全的;也就是說,在 v 配置之後到欄位賦值之間沒有發生任何配置。在所有其他情況下,絕對不要直接賦值。如果區塊剛由 caml_alloc_shr 配置,請使用 caml_initialize 來第一次為欄位賦值

        caml_initialize(&Field(v, n), w);
否則,您正在更新先前包含格式正確的值的欄位;然後,呼叫 caml_modify 函式

        caml_modify(&Field(v, n), w);

為了說明以上規則,以下是一個 C 函式,它會建構並傳回一個包含作為參數給定的兩個整數的列表。首先,我們使用簡化的配置函式來編寫它

value alloc_list_int(int i1, int i2)
{
  CAMLparam0 ();
  CAMLlocal2 (result, r);

  r = caml_alloc(2, 0);                   /* Allocate a cons cell */
  Store_field(r, 0, Val_int(i2));         /* car = the integer i2 */
  Store_field(r, 1, Val_emptylist);       /* cdr = the empty list [] */
  result = caml_alloc(2, 0);              /* Allocate the other cons cell */
  Store_field(result, 0, Val_int(i1));    /* car = the integer i1 */
  Store_field(result, 1, r);              /* cdr = the first cons cell */
  CAMLreturn (result);
}

在這裡,result 的註冊並非絕對必要,因為在它取得其值之後沒有發生任何配置,但是簡單地註冊所有類型為 value 的區域變數會更容易且更安全。

這是使用低階配置函式編寫的相同函式。我們注意到 cons 儲存格是小的區塊,可以使用 caml_alloc_small 進行配置,並透過對其欄位直接賦值來填入。

value alloc_list_int(int i1, int i2)
{
  CAMLparam0 ();
  CAMLlocal2 (result, r);

  r = caml_alloc_small(2, 0);             /* Allocate a cons cell */
  Field(r, 0) = Val_int(i2);              /* car = the integer i2 */
  Field(r, 1) = Val_emptylist;            /* cdr = the empty list [] */
  result = caml_alloc_small(2, 0);        /* Allocate the other cons cell */
  Field(result, 0) = Val_int(i1);         /* car = the integer i1 */
  Field(result, 1) = r;                   /* cdr = the first cons cell */
  CAMLreturn (result);
}

在以上兩個範例中,列表是從底部向上建構的。以下是另一種從頂部向下進行的方法。它效率較低,但說明了 caml_modify 的使用。

value alloc_list_int(int i1, int i2)
{
  CAMLparam0 ();
  CAMLlocal2 (tail, r);

  r = caml_alloc_small(2, 0);             /* Allocate a cons cell */
  Field(r, 0) = Val_int(i1);              /* car = the integer i1 */
  Field(r, 1) = Val_int(0);               /* A dummy value
  tail = caml_alloc_small(2, 0);          /* Allocate the other cons cell */
  Field(tail, 0) = Val_int(i2);           /* car = the integer i2 */
  Field(tail, 1) = Val_emptylist;         /* cdr = the empty list [] */
  caml_modify(&Field(r, 1), tail);        /* cdr of the result = tail */
  CAMLreturn (r);
}

直接執行 Field(r, 1) = tail 是不正確的,因為自從 r 被配置以來,tail 的配置已經發生。

5.3 待處理的動作和非同步例外

自 4.10 版以來,配置函式保證不會執行任何 OCaml 程式碼,包括 finaliser、訊號處理程式以及在同一個網域上執行的其他執行緒。相反地,它們的執行會延遲到稍後的安全點。

來自 <caml/signals.h> 的函式 caml_process_pending_actions 會執行任何待處理的訊號處理程式和 finaliser、Memprof 回呼、搶佔式系統執行緒切換,以及要求的小型和大型垃圾收集。特別是,它可以引發非同步例外,並導致來自同一個網域的 OCaml 堆積上的突變。建議在長時間運行的非阻塞 C 程式碼內的安全點定期呼叫它。

提供了變體 caml_process_pending_actions_exn,它會傳回例外而不是直接將其引發到 OCaml 程式碼中。其結果必須使用 Is_exception_result 進行測試,如果適當,則接著使用 Extract_exception。它通常用於在重新引發之前進行清除

    CAMLlocal1(exn);
    ...
    exn = caml_process_pending_actions_exn();
    if(Is_exception_result(exn)) {
      exn = Extract_exception(exn);
      ...cleanup...
      caml_raise(exn);
    }

在垃圾收集存在的情況下,正確使用例外傳回的詳細資訊,請參閱第  ‍22.7.1 節。

6 一個完整的範例

本節概述如何將 Unix curses 程式庫中的函式提供給 OCaml 程式使用。首先,以下是宣告 curses 基本型別和資料型別的介面 curses.ml

(* File curses.ml -- declaration of primitives and data types *)
type window                   (* The type "window" remains abstract *)
external initscr: unit -> window = "caml_curses_initscr"
external endwin: unit -> unit = "caml_curses_endwin"
external refresh: unit -> unit = "caml_curses_refresh"
external wrefresh : window -> unit = "caml_curses_wrefresh"
external newwin: int -> int -> int -> int -> window = "caml_curses_newwin"
external addch: char -> unit = "caml_curses_addch"
external mvwaddch: window -> int -> int -> char -> unit = "caml_curses_mvwaddch"
external addstr: string -> unit = "caml_curses_addstr"
external mvwaddstr: window -> int -> int -> string -> unit
         = "caml_curses_mvwaddstr"
(* lots more omitted *)

若要編譯此介面

        ocamlc -c curses.ml

若要實作這些函式,我們只需要提供 stub 程式碼;核心函式已經在 curses 程式庫中實作。stub 程式碼檔案 curses_stubs.c 看起來像這樣

/* File curses_stubs.c -- stub code for curses */
#include <curses.h>
#include <caml/mlvalues.h>
#include <caml/memory.h>
#include <caml/alloc.h>
#include <caml/custom.h>

/* Encapsulation of opaque window handles (of type WINDOW *)
   as OCaml custom blocks. */

static struct custom_operations curses_window_ops = {
  "fr.inria.caml.curses_windows",
  custom_finalize_default,
  custom_compare_default,
  custom_hash_default,
  custom_serialize_default,
  custom_deserialize_default,
  custom_compare_ext_default,
  custom_fixed_length_default
};

/* Accessing the WINDOW * part of an OCaml custom block */
#define Window_val(v) (*((WINDOW **) Data_custom_val(v)))

/* Allocating an OCaml custom block to hold the given WINDOW * */
static value alloc_window(WINDOW * w)
{
  value v = caml_alloc_custom(&curses_window_ops, sizeof(WINDOW *), 0, 1);
  Window_val(v) = w;
  return v;
}

CAMLprim value caml_curses_initscr(value unit)
{
  CAMLparam1 (unit);
  CAMLreturn (alloc_window(initscr()));
}

CAMLprim value caml_curses_endwin(value unit)
{
  CAMLparam1 (unit);
  endwin();
  CAMLreturn (Val_unit);
}

CAMLprim value caml_curses_refresh(value unit)
{
  CAMLparam1 (unit);
  refresh();
  CAMLreturn (Val_unit);
}

CAMLprim value caml_curses_wrefresh(value win)
{
  CAMLparam1 (win);
  wrefresh(Window_val(win));
  CAMLreturn (Val_unit);
}

CAMLprim value caml_curses_newwin(value nlines, value ncols, value x0, value y0)
{
  CAMLparam4 (nlines, ncols, x0, y0);
  CAMLreturn (alloc_window(newwin(Int_val(nlines), Int_val(ncols),
                                  Int_val(x0), Int_val(y0))));
}

CAMLprim value caml_curses_addch(value c)
{
  CAMLparam1 (c);
  addch(Int_val(c));            /* Characters are encoded like integers */
  CAMLreturn (Val_unit);
}

CAMLprim value caml_curses_mvwaddch(value win, value x, value y, value c)
{
  CAMLparam4 (win, x, y, c);
  mvwaddch(Window_val(win), Int_val(x), Int_val(y), Int_val(c));
  CAMLreturn (Val_unit);
}

CAMLprim value caml_curses_addstr(value s)
{
  CAMLparam1 (s);
  addstr(String_val(s));
  CAMLreturn (Val_unit);
}

CAMLprim value caml_curses_mvwaddstr(value win, value x, value y, value s)
{
  CAMLparam4 (win, x, y, s);
  mvwaddstr(Window_val(win), Int_val(x), Int_val(y), String_val(s));
  CAMLreturn (Val_unit);
}

/* This goes on for pages. */

可以使用以下命令編譯檔案 curses_stubs.c

        cc -c -I`ocamlc -where` curses_stubs.c

或更簡單地說,

        ocamlc -c curses_stubs.c

(當傳遞 .c 檔案時,ocamlc 命令會直接使用正確的 -I 選項,在該檔案上呼叫 C 編譯器。)

現在,以下是一個範例 OCaml 程式 prog.ml,它使用 curses 模組

(* File prog.ml -- main program using curses *)
open Curses;;
let main_window = initscr () in
let small_window = newwin 10 5 20 10 in
  mvwaddstr main_window 10 2 "Hello";
  mvwaddstr small_window 4 3 "world";
  refresh();
  Unix.sleep 5;
  endwin()

若要編譯並連結此程式,請執行

       ocamlc -custom -o prog unix.cma curses.cmo prog.ml curses_stubs.o -cclib -lcurses

(在某些電腦上,您可能需要使用 -cclib -lcurses -cclib -ltermcap-cclib -ltermcap 來取代 -cclib -lcurses。)

7 進階主題:從 C 到 OCaml 的回呼

到目前為止,我們已經描述了如何從 OCaml 呼叫 C 函式。在本節中,我們將展示 C 函式如何呼叫 OCaml 函式,無論是作為回呼(OCaml 呼叫 C,而 C 呼叫 OCaml),還是主要程式是用 C 編寫的。

7.1 從 C 套用 OCaml 閉包

C 函式可以將 OCaml 函式值(閉包)套用到 OCaml 值。提供以下函式來執行套用

如果函式 f 沒有傳回,而是引發逸出套用範圍的例外,則此例外會傳播到下一個封閉的 OCaml 程式碼,並跳過 C 程式碼。也就是說,如果 OCaml 函式 f 呼叫 C 函式 g,而 C 函式 g 又回呼 OCaml 函式 h,而 OCaml 函式 h 又引發 stray 例外,則 g 的執行會被中斷,且例外會傳播回 f

如果 C 程式碼希望捕捉逸出 OCaml 函式的例外,可以使用函式 caml_callback_exncaml_callback2_exncaml_callback3_exncaml_callbackN_exn。這些函式會採用與其非 _exn 對應函式相同的引數,但會捕捉逸出的例外並將其傳回 C 程式碼。必須使用巨集 Is_exception_result(v) 來測試 caml_callback*_exn 函式的傳回值 v。如果巨集傳回「false」,則不會發生任何例外,且 v 是 OCaml 函式傳回的值。如果 Is_exception_result(v) 傳回「true」,則會逸出例外,且可以使用 Extract_exception(v) 來還原其值(例外描述符)。

警告

如果 OCaml 函式傳回例外,在呼叫可能會觸發垃圾收集的函式之前,應將 Extract_exception 套用至例外結果。否則,如果在垃圾收集期間 v 是可到達的,則執行階段可能會當機,因為 v 不包含有效值。

範例

    CAMLprim value call_caml_f_ex(value closure, value arg)
    {
      CAMLparam2(closure, arg);
      CAMLlocal2(res, tmp);
      res = caml_callback_exn(closure, arg);
      if(Is_exception_result(res)) {
        res = Extract_exception(res);
        tmp = caml_alloc(3, 0); /* Safe to allocate: res contains valid value. */
        ...
      }
      CAMLreturn (res);
    }

7.2 取得或註冊 OCaml 閉包,以便在 C 函式中使用

有兩種方法可以取得 OCaml 函式值(閉包),以便傳遞給上述描述的 callback 函式。其中一種方法是將 OCaml 函式作為引數傳遞給基本函式。例如,如果 OCaml 程式碼包含宣告

    external apply : ('a -> 'b) -> 'a -> 'b = "caml_apply"

則對應的 C stub 可以編寫如下

    CAMLprim value caml_apply(value vf, value vx)
    {
      CAMLparam2(vf, vx);
      CAMLlocal1(vy);
      vy = caml_callback(vf, vx);
      CAMLreturn(vy);
    }

另一種可能性是使用 OCaml 提供的註冊機制。此註冊機制可讓 OCaml 程式碼以某些全域名稱註冊 OCaml 函式,並讓 C 程式碼透過此全域名稱擷取對應的閉包。

在 OCaml 端,註冊是透過評估 Callback.register n v 來執行。這裡,n 是全域名稱(任意字串),而 v 是 OCaml 值。例如

    let f x = print_string "f is applied to "; print_int x; print_newline()
    let _ = Callback.register "test function" f

在 C 端,透過呼叫 caml_named_value(n) 來取得以名稱 n 註冊的值的指標。然後必須取消對傳回指標的參照,才能還原實際的 OCaml 值。如果沒有以名稱 n 註冊任何值,則會傳回空指標。例如,以下是一個 C 包裝函式,它會呼叫上面的 OCaml 函式 f

    void call_caml_f(int arg)
    {
        caml_callback(*caml_named_value("test function"), Val_int(arg));
    }

caml_named_value 傳回的指標是常數,可以安全地快取在 C 變數中,以避免重複的名稱查閱。指標指向的值無法從 C 變更。但是,它可能會在垃圾收集期間變更,因此必須始終在使用時重新計算。以下是上面 call_caml_f 的更有效率的變體,它只會呼叫一次 caml_named_value

    void call_caml_f(int arg)
    {
        static const value * closure_f = NULL;
        if (closure_f == NULL) {
            /* First time around, look up by name */
            closure_f = caml_named_value("test function");
        }
        caml_callback(*closure_f, Val_int(arg));
    }

7.3 註冊 OCaml 例外,以便在 C 函式中使用

上述描述的註冊機制也可以用於將例外識別碼從 OCaml 傳達給 C。OCaml 程式碼會透過評估 Callback.register_exception n exn 來註冊例外,其中 n 是任意名稱,而 exn 是要註冊的例外的例外值。例如

    exception Error of string
    let _ = Callback.register_exception "test exception" (Error "any string")

然後,C 程式碼可以使用 caml_named_value 還原例外識別碼,並將其作為第一個引數傳遞給函式 raise_constantraise_with_argraise_with_string (在第  ‍22.4.5 節中描述),以實際引發例外。例如,以下是一個 C 函式,它會使用給定的引數引發 Error 例外

    void raise_error(char * msg)
    {
        caml_raise_with_string(*caml_named_value("test exception"), msg);
    }

7.4 C 中的主要程式

在正常操作中,一個混合 OCaml/C 程式會先執行 OCaml 的初始化程式碼,然後可能會接著呼叫 C 函式。我們說主程式是 OCaml 程式碼。在某些應用程式中,會希望 C 程式碼扮演主程式的角色,在需要時呼叫 OCaml 函式。這可以透過以下方式達成:

7.5 將 OCaml 程式碼嵌入 C 程式碼

自訂執行時期模式中的位元組碼編譯器(ocamlc -custom)通常會將位元組碼附加到包含自訂執行時期的可執行檔。這會導致兩個結果。首先,最終的連結步驟必須由 ocamlc 執行。其次,OCaml 執行時期程式庫必須能夠從命令列參數中找到可執行檔的名稱。當使用如第 22.7.4 節中的 caml_main(argv) 時,這表示 argv[0]argv[1] 必須包含可執行檔的名稱。

另一種方法是將位元組碼嵌入到 C 程式碼中。為此提供了 ocamlc-output-obj-output-complete-obj 選項。它們會使 ocamlc 編譯器輸出一個 C 物件檔案(.o 檔案,在 Windows 下為 .obj),其中包含 OCaml 程式部分的位元組碼,以及一個 caml_startup 函式。由 ocamlc -output-complete-obj 產生的 C 物件檔案也包含執行時期和自動連結程式庫。然後,由 ocamlc -output-objocamlc -output-complete-obj 產生的 C 物件檔案可以使用標準 C 編譯器與 C 程式碼連結,或儲存在 C 程式庫中。

必須從主要的 C 程式呼叫 caml_startup 函式,以初始化 OCaml 執行時期並執行 OCaml 初始化程式碼。就像 caml_main 一樣,它接受一個包含命令列參數的 argv 參數。與 caml_main 不同的是,此 argv 參數僅用於初始化 Sys.argv,而不用於尋找可執行檔的名稱。

如果例外狀況從頂層模組初始化程式中逸出,caml_startup 函式會呼叫未捕獲的例外狀況處理程式(或在 ocamldebug 下執行時進入偵錯工具)。可以使用 caml_startup_exn 函式並使用 Is_exception_result 測試結果(如果適用,則接著使用 Extract_exception)在 C 程式碼中捕獲此類例外狀況。

-output-obj-output-complete-obj 選項也可以用來取得 C 原始碼檔案。更重要的是,這些選項也可以直接產生一個共享程式庫(.so 檔案,在 Windows 下為 .dll),其中包含 OCaml 程式碼、OCaml 執行時期系統和傳遞給 ocamlc 的任何其他靜態 C 程式碼(.o.a,分別為 .obj.lib)。這種 -output-obj-output-complete-obj 的用法非常類似於正常的連結步驟,但它不是產生自動執行 OCaml 程式碼的主程式,而是產生一個可以在需要時執行 OCaml 程式碼的共享程式庫。-output-obj-output-complete-obj 的三種可能行為(產生 C 原始碼 .c、C 物件檔案 .o、共享程式庫 .so)是根據結果檔案的擴展名(使用 -o 給定)來選擇的。

原生程式碼編譯器 ocamlopt 也支援 -output-obj-output-complete-obj 選項,使其輸出一個 C 物件檔案或一個共享程式庫,其中包含命令列上所有 OCaml 模組的原生程式碼,以及 OCaml 啟動程式碼。初始化是透過呼叫 caml_startup(或 caml_startup_exn)來完成的,如同位元組碼編譯器的情況一樣。由 ocamlopt -output-complete-obj 產生的檔案也包含執行時期和自動連結程式庫。

對於最終連結階段,除了 -output-obj 產生的物件檔案之外,您還必須提供 OCaml 執行時期程式庫(位元組碼為 libcamlrun.a,原生程式碼為 libasmrun.a),以及所使用 OCaml 程式庫所需的所有 C 程式庫。例如,假設您程式的 OCaml 部分使用了 Unix 程式庫。使用 ocamlc,您應該執行

        ocamlc -output-obj -o camlcode.o unix.cma other .cmo and .cma files
        cc -o myprog C objects and libraries \
           camlcode.o -L‘ocamlc -where‘ -lunix -lcamlrun

使用 ocamlopt,您應該執行

        ocamlopt -output-obj -o camlcode.o unix.cmxa other .cmx and .cmxa files
        cc -o myprog C objects and libraries \
           camlcode.o -L‘ocamlc -where‘ -lunix -lasmrun

對於最終連結階段,除了 -output-complete-obj 產生的物件檔案之外,您只需要提供 OCaml 執行時期所需的 C 程式庫即可。

例如,假設您程式的 OCaml 部分使用了 Unix 程式庫。使用 ocamlc,您應該執行

        ocamlc -output-complete-obj -o camlcode.o unix.cma other .cmo and .cma files
        cc -o myprog C objects and libraries \
           camlcode.o C libraries required by the runtime, eg -lm  -ldl -lcurses -lpthread

使用 ocamlopt,您應該執行

        ocamlopt -output-complete-obj -o camlcode.o unix.cmxa other .cmx and .cmxa files
        cc -o myprog C objects and libraries \
           camlcode.o C libraries required by the runtime, eg -lm -ldl
警告

在某些埠上,在最終連結階段需要特殊的選項,這些選項將 -output-obj-output-complete-obj 選項產生的物件檔案與程式的其餘部分連結在一起。這些選項會在 OCaml 編譯期間產生的組態檔 Makefile.config 中顯示為變數 OC_LDFLAGS

堆疊回溯。

當由 ocamlc -g 產生的 OCaml 位元組碼嵌入到 C 程式中時,不會包含任何除錯資訊,因此無法在未捕獲的例外狀況上列印堆疊回溯。當由 ocamlopt -g 產生的原生程式碼嵌入到 C 程式中時,情況並非如此:堆疊回溯資訊可用,但必須以程式方式開啟回溯機制。這可以透過在其中一個 OCaml 模組的初始化中呼叫 Printexc.record_backtrace true 從 OCaml 端達成。也可以透過在 OCaml-C 膠合程式碼中呼叫 caml_record_backtraces(1); 從 C 端達成。(caml_record_backtracesbacktrace.h 中宣告)

卸載執行時期。

如果使用 -output-obj 產生的共享程式庫要由單一程序重複載入和卸載,則必須注意明確卸載 OCaml 執行時期,以避免各種系統資源洩漏。

自 4.05 起,可以使用 caml_shutdown 函式來優雅地關閉執行時期,這等於以下內容

由於一個共享函式庫可能同時有多個客戶端,因此為了方便起見,可以多次呼叫 caml_startup(以及 caml_startup_pooled),前提是每次呼叫都與對應的 caml_shutdown 呼叫配對(以巢狀方式)。一旦沒有未完成的 caml_startup 呼叫,執行時期就會被卸載。

一旦執行時期被卸載,在重新載入共享函式庫並重新初始化其靜態資料之前,無法再次啟動。因此,目前此功能僅適用於建構可重新載入的共享函式庫。

Unix 信號處理。

根據目標平台和作業系統,當呼叫 caml_startup 時,原生碼執行時期系統可能會為 SIGSEGVSIGTRAPSIGFPE 信號中的一個或多個安裝信號處理常式,並在呼叫 caml_shutdown 時將這些信號重設為其預設行為。用 C 編寫的主程式不應嘗試自行處理這些信號。

8 使用回呼的進階範例

本節說明第 22.7 節中描述的回呼功能。我們將以一種方式封裝一些 OCaml 函式,使其可以與 C 程式碼連結,並像任何 C 函式一樣從 C 呼叫。OCaml 函式在以下 mod.ml OCaml 原始碼中定義

(* File mod.ml -- some "useful" OCaml functions *)

let rec fib n = if n < 2 then 1 else fib(n-1) + fib(n-2)

let format_result n = Printf.sprintf "Result is: %d\n" n

(* Export those two functions to C *)

let _ = Callback.register "fib" fib
let _ = Callback.register "format_result" format_result

以下是從 C 呼叫這些函式的 C Stub 程式碼

/* File modwrap.c -- wrappers around the OCaml functions */

#include <stdio.h>
#include <string.h>
#include <caml/mlvalues.h>
#include <caml/callback.h>

int fib(int n)
{
  static const value * fib_closure = NULL;
  if (fib_closure == NULL) fib_closure = caml_named_value("fib");
  return Int_val(caml_callback(*fib_closure, Val_int(n)));
}

char * format_result(int n)
{
  static const value * format_result_closure = NULL;
  if (format_result_closure == NULL)
    format_result_closure = caml_named_value("format_result");
  return strdup(String_val(caml_callback(*format_result_closure, Val_int(n))));
  /* We copy the C string returned by String_val to the C heap
     so that it remains valid after garbage collection. */
}

我們現在將 OCaml 程式碼編譯為 C 物件檔案,並將其與 modwrap.c 中的 Stub 程式碼和 OCaml 執行時期系統一起放入 C 函式庫中

        ocamlc -custom -output-obj -o modcaml.o mod.ml
        ocamlc -c modwrap.c
        cp `ocamlc -where`/libcamlrun.a mod.a && chmod +w mod.a
        ar r mod.a modcaml.o modwrap.o

(也可以使用 ocamlopt -output-obj 而不是 ocamlc -custom -output-obj。在這種情況下,請將 libcamlrun.a(位元組碼執行時期函式庫)替換為 libasmrun.a(原生碼執行時期函式庫)。)

現在,我們可以在任何 C 程式中使用兩個函式 fibformat_result,就像常規的 C 函式一樣。只需記住先呼叫一次 caml_startup(或 caml_startup_exn)。

/* File main.c -- a sample client for the OCaml functions */

#include <stdio.h>
#include <caml/callback.h>

extern int fib(int n);
extern char * format_result(int n);

int main(int argc, char ** argv)
{
  int result;

  /* Initialize OCaml code */
  caml_startup(argv);
  /* Do some computation */
  result = fib(10);
  printf("fib(10) = %s\n", format_result(result));
  return 0;
}

若要建置整個程式,只需如下呼叫 C 編譯器

        cc -o prog -I `ocamlc -where` main.c mod.a -lcurses

(在某些機器上,您可能需要放置 -ltermcap-lcurses -ltermcap 而不是 -lcurses。)

9 進階主題:自訂區塊

標籤為 Custom_tag 的區塊包含任意的使用者資料和指向 C 結構的指標,類型為 struct custom_operations,它將使用者提供的最終處理、比較、雜湊、序列化和反序列化函式與此區塊相關聯。

9.1 struct custom_operations

struct custom_operations<caml/custom.h> 中定義,包含以下欄位

注意:附加到自訂區塊描述符的 finalizecomparehashserializedeserialize 函式,僅允許與 OCaml 執行時期進行有限的互動。在這些函式中,請勿呼叫任何 OCaml 配置函式,也不要執行任何回呼到 OCaml 程式碼的操作。請勿使用 CAMLparam 來註冊這些函式的參數,也不要使用 CAMLreturn 來傳回結果。請勿引發例外 (若要在反序列化期間發出錯誤訊號,請使用 caml_deserialize_error)。請勿移除全域根。如有疑問,請謹慎行事。在 serializedeserialize 函式中,允許(甚至建議)使用章節 22.9.4 中的對應函式。

9.2 配置自訂區塊

自訂區塊必須透過 caml_alloc_customcaml_alloc_custom_mem 配置。

caml_alloc_custom(ops, size, used, max)

傳回一個新的自訂區塊,其中包含 size 位元組的使用者資料空間,且其相關操作由 ops 給定(指向 struct custom_operations 的指標,通常以 C 全域變數靜態配置)。

當最終化的物件包含指向堆外資源的指標時,兩個參數 usedmax 用於控制垃圾回收的速度。一般來說,OCaml 增量式主要收集器會根據程式的配置率調整其速度。程式配置的速度越快,GC 就會越努力地回收無法存取的區塊,並避免產生大量的「浮動垃圾」(GC 尚未收集的未參考物件)。

通常,配置率是透過計算已配置區塊的堆內大小來測量。然而,最終化的物件經常包含指向堆外記憶體區塊和其他資源(例如檔案描述符、X Windows 點陣圖等)的指標。對於這些區塊,區塊的堆內大小並不是衡量程式配置資源量的良好指標。

兩個參數 usedmax 讓 GC 了解所配置的最終化區塊消耗了多少堆外資源:您將配置給此物件的資源量指定為參數 used,以及您希望在浮動垃圾中看到的最大資源量指定為參數 max。單位是任意的:GC 只關心比率 used / max

例如,如果您要配置一個保存 w 乘以 h 像素的 X Windows 點陣圖的最終化區塊,並且您不希望有超過 1 百萬像素的未回收點陣圖,請指定 used = w * hmax = 1000000。

另一種描述 usedmax 參數效果的方式是使用完整的 GC 週期。如果您配置了許多 used / max = 1 / N 的自訂區塊,則 GC 將在每次配置 N 次時執行一個完整的週期(檢查堆中的每個物件,並對那些無法存取的物件呼叫最終化函式)。例如,如果 used = 1 和 max = 1000,則 GC 將在每次配置自訂區塊至少 1000 次時執行一個完整的週期。

如果您的最終化區塊不包含指向堆外資源的指標,或者如果先前的討論對您來說沒有什麼意義,請直接取 used = 0 和 max = 1。但是,如果您稍後發現最終化函式沒有被「經常」呼叫,請考慮提高 used / max 比率。

caml_alloc_custom_mem(ops, size, used)

當您的自訂區塊僅保存堆外記憶體(使用 malloccaml_stat_alloc 配置的記憶體)且沒有其他資源時,請使用此函式。used 應該是您的自訂區塊所保存的堆外記憶體位元組數。此函式的作用類似於 caml_alloc_custom,只是 max 參數受使用者控制(透過 custom_major_ratiocustom_minor_ratiocustom_minor_max_size 參數),並且與堆大小成正比。自 OCaml 4.08.0 版起可用。

9.3 存取自訂區塊

自訂區塊 v 的資料部分可以透過指標 Data_custom_val(v) 存取。此指標的類型為 void *,應轉換為儲存在自訂區塊中的資料的實際類型。

自訂區塊的內容不會被垃圾回收器掃描,因此不得包含 OCaml 堆中的任何指標。換句話說,永遠不要在自訂區塊中儲存 OCaml value,也不要使用 FieldStore_fieldcaml_modify 來存取自訂區塊的資料部分。相反地,任何 C 資料結構(不包含堆指標)都可以儲存在自訂區塊中。

9.4 撰寫自訂序列化和反序列化函式

以下在 <caml/intext.h> 中定義的函式,用於以可移植的方式寫入和讀回自訂區塊的內容。當資料在小端機器上寫入,並在大端機器上讀回時,這些函式會處理位元組順序轉換。

函式動作
caml_serialize_int_1寫入 1 位元組整數
caml_serialize_int_2寫入 2 位元組整數
caml_serialize_int_4寫入 4 位元組整數
caml_serialize_int_8寫入 8 位元組整數
caml_serialize_float_4寫入 4 位元組浮點數
caml_serialize_float_8寫入 8 位元組浮點數
caml_serialize_block_1寫入 1 位元組數量陣列
caml_serialize_block_2寫入 2 位元組數量陣列
caml_serialize_block_4寫入 4 位元組數量陣列
caml_serialize_block_8寫入 8 位元組數量陣列
caml_deserialize_uint_1讀取不帶正負號的 1 位元組整數
caml_deserialize_sint_1讀取帶正負號的 1 位元組整數
caml_deserialize_uint_2讀取不帶正負號的 2 位元組整數
caml_deserialize_sint_2讀取帶正負號的 2 位元組整數
caml_deserialize_uint_4讀取不帶正負號的 4 位元組整數
caml_deserialize_sint_4讀取帶正負號的 4 位元組整數
caml_deserialize_uint_8讀取不帶正負號的 8 位元組整數
caml_deserialize_sint_8讀取帶正負號的 8 位元組整數
caml_deserialize_float_4讀取 4 位元組浮點數
caml_deserialize_float_8讀取 8 位元組浮點數
caml_deserialize_block_1讀取 1 位元組數量陣列
caml_deserialize_block_2讀取 2 位元組數量陣列
caml_deserialize_block_4讀取 4 位元組數量陣列
caml_deserialize_block_8讀取 8 位元組數量陣列
caml_deserialize_error發出反序列化期間發生錯誤的訊號;input_valueMarshal.from_... 會在清除其內部資料結構後引發 Failure 例外

序列化函式會附加到它們所套用的自訂區塊。顯然,反序列化函式無法以這種方式附加,因為在反序列化開始時,自訂區塊還不存在!因此,包含反序列化函式的 struct custom_operations 必須預先使用在 <caml/custom.h> 中宣告的 register_custom_operations 函式向反序列化器註冊。反序列化會透過從輸入串流讀取識別碼、配置輸入串流中指定大小的自訂區塊、搜尋已註冊的 struct custom_operation 區塊以尋找具有相同識別碼的區塊,並呼叫其 deserialize 函式來填入自訂區塊的資料部分。

9.5 選擇識別碼

struct custom_operations 中的識別碼必須謹慎選擇,因為它們必須唯一識別用於序列化和反序列化操作的資料結構。特別是,請考慮在識別碼中包含版本號碼;這樣一來,稍後可以變更資料格式,但仍可以提供回溯相容的反序列化函式。

_(底線字元)開頭的識別碼保留給 OCaml 執行時期系統;請勿將它們用於您的自訂資料。我們建議使用 URL(http://mymachine.mydomain.com/mylibrary/version-number)或 Java 風格的套件名稱(com.mydomain.mymachine.mylibrary.version-number)作為識別碼,以最大程度地降低識別碼衝突的風險。

9.6 最終化區塊

自訂區塊泛化了在 OCaml 3.00 版之前存在的最終區塊。為了向後相容,自訂區塊的格式與最終區塊的格式相容,並且 caml_alloc_final 函式仍然可用於分配具有給定最終化函式的自訂區塊,但預設的比較、雜湊和序列化函式。(特別是,最終化函式不得存取 OCaml 執行期。)

caml_alloc_final(n, f, used, max) 會傳回一個大小為 n+1 個字組的新自訂區塊,其最終化函式為 f。第一個字組保留用於儲存自訂操作;其他 n 個字組可用於您的資料。兩個參數 usedmax 用於控制垃圾收集的速度,如 caml_alloc_custom 所述。

10 進階主題:Bigarrays 和 OCaml-C 介面

本節說明了將 C 或 Fortran 程式碼與 OCaml 程式碼介面的 C stub 程式碼如何使用 Bigarrays。

10.1 包含檔案

包含檔案 <caml/bigarray.h> 必須包含在 C stub 檔案中。它宣告了下面討論的函式、常數和巨集。

10.2 從 C 或 Fortran 存取 OCaml Bigarray

如果 v 是表示 Bigarray 的 OCaml value,則運算式 Caml_ba_data_val(v) 會傳回指向陣列資料部分的指標。此指標的類型為 void *,並且可以轉換為陣列的適當 C 類型(例如 double []char [][10] 等)。

OCaml Bigarray 的各種特性可以從 C 中查詢,如下所示

C 運算式傳回值
Caml_ba_array_val(v)->num_dims維度數
Caml_ba_array_val(v)->dim[i]i 個維度
Caml_ba_array_val(v)->flags & CAML_BA_KIND_MASK陣列元素的種類

陣列元素的種類是以下常數之一

常數元素種類
CAML_BA_FLOAT1616 位元半精度浮點數
CAML_BA_FLOAT3232 位元單精度浮點數
CAML_BA_FLOAT6464 位元雙精度浮點數
CAML_BA_SINT88 位元有號整數
CAML_BA_UINT88 位元無號整數
CAML_BA_SINT1616 位元有號整數
CAML_BA_UINT1616 位元無號整數
CAML_BA_INT3232 位元有號整數
CAML_BA_INT6464 位元有號整數
CAML_BA_CAML_INT31 位元或 63 位元有號整數
CAML_BA_NATIVE_INT32 位元或 64 位元(平台原生)整數
CAML_BA_COMPLEX3232 位元單精度複數
CAML_BA_COMPLEX6464 位元雙精度複數
CAML_BA_CHAR8 位元字元
警告

Caml_ba_array_val(v) 必須始終立即解引用,且不得儲存在任何地方,包括區域變數。它會解析為衍生指標:它不是有效的 OCaml 值,而是指向由 GC 管理的記憶體區域。因此,此值不得儲存在任何可能會在 GC 中存活的記憶體位置中。

以下範例顯示將二維 Bigarray 傳遞給 C 函式和 Fortran 函式。

    extern void my_c_function(double * data, int dimx, int dimy);
    extern void my_fortran_function_(double * data, int * dimx, int * dimy);

    CAMLprim value caml_stub(value bigarray)
    {
      int dimx = Caml_ba_array_val(bigarray)->dim[0];
      int dimy = Caml_ba_array_val(bigarray)->dim[1];
      /* C passes scalar parameters by value */
      my_c_function(Caml_ba_data_val(bigarray), dimx, dimy);
      /* Fortran passes all parameters by reference */
      my_fortran_function_(Caml_ba_data_val(bigarray), &dimx, &dimy);
      return Val_unit;
    }

10.3 將 C 或 Fortran 陣列包裝為 OCaml Bigarray

可以使用 caml_ba_alloccaml_ba_alloc_dims 函式來包裝指向已配置的 C 或 Fortran 陣列的指標 p,並將其作為 Bigarray 傳回給 OCaml。

以下範例說明如何讓靜態配置的 C 和 Fortran 陣列可供 OCaml 使用。

    extern long my_c_array[100][200];
    extern float my_fortran_array_[300][400];

    CAMLprim value caml_get_c_array(value unit)
    {
      long dims[2];
      dims[0] = 100; dims[1] = 200;
      return caml_ba_alloc(CAML_BA_NATIVE_INT | CAML_BA_C_LAYOUT,
                           2, my_c_array, dims);
    }

    CAMLprim value caml_get_fortran_array(value unit)
    {
      return caml_ba_alloc_dims(CAML_BA_FLOAT32 | CAML_BA_FORTRAN_LAYOUT,
                                2, my_fortran_array_, 300L, 400L);
    }

11 進階主題:更便宜的 C 呼叫

本節說明如何讓 C 函式呼叫更便宜。

注意:這僅適用於原生編譯器。因此,無論何時您使用這些方法中的任何一種,都必須提供一個替代的位元組碼 stub,該 stub 會忽略所有特殊註解。

11.1 傳遞未封箱的值

我們之前說過,所有 OCaml 物件都由 C 類型 value 表示,並且必須使用 Int_val 等巨集從 value 類型解碼資料。但是,可以告訴 OCaml 原生碼編譯器為我們執行此操作,並將未封箱的引數傳遞給 C 函式。同樣,也可以告訴 OCaml 預期未封箱的結果並為我們封箱。

這樣做的動機是,透過讓 ‘ocamlopt‘ 處理封箱,它通常可以決定完全取消封箱。

例如,讓我們考慮這個範例

external foo : float -> float -> float = "foo"

let f a b =
  let len = Array.length a in
  assert (Array.length b = len);
  let res = Array.make len 0. in
  for i = 0 to len - 1 do
    res.(i) <- foo a.(i) b.(i)
  done

浮點數陣列在 OCaml 中是未封箱的,但是 C 函式 foo 預期其引數為封箱的浮點數,並傳回封箱的浮點數。因此,OCaml 編譯器別無選擇,只能封箱 a.(i)b.(i),並解封箱 foo 的結果。這會導致配置 3 * len 個暫時浮點數值。

現在,如果我們使用 [@unboxed] 註解引數和結果,則原生碼編譯器將能夠避免所有這些配置

external foo
  :  (float [@unboxed])
  -> (float [@unboxed])
  -> (float [@unboxed])
  = "foo_byte" "foo"

在這種情況下,C 函式必須如下所示

CAMLprim double foo(double a, double b)
{
  ...
}

CAMLprim value foo_byte(value a, value b)
{
  return caml_copy_double(foo(Double_val(a), Double_val(b)))
}

為了方便起見,當所有引數和結果都使用 [@unboxed] 註解時,可以只在宣告本身上放置一次屬性。因此,我們也可以改寫

external foo : float -> float -> float = "foo_byte" "foo" [@@unboxed]

下表總結了哪些 OCaml 類型可以未封箱,以及應該使用哪些 C 類型對應

OCaml 型別C 類型
floatdouble
int32int32_t
int64int64_t
nativeintintnat

同樣,可以在 OCaml 和 C 之間傳遞未標記的 OCaml 整數。這可以透過使用 [@untagged] 註解引數和/或結果來完成

external f : string -> (int [@untagged]) = "f_byte" "f"

對應的 C 類型必須是 intnat

注意:請勿使用 C int 類型來對應 (int [@untagged])。這是因為它們的大小通常不同。

可以使用 [@untagged] 註解任何立即類型,即表示方式類似於 int 的類型。這包括 boolchar、任何只有常數建構子的變體類型。注意:這不包括 Unix.file_descr,它並非在所有平台上都表示為整數。

11.2 直接 C 呼叫

為了能夠在 C 函式中間執行垃圾收集器,OCaml 原生碼編譯器會在 C 呼叫周圍產生一些簿記程式碼。從技術上講,它使用 OCaml 執行期的 C 函式 caml_c_call 包裝每個 C 呼叫。

對於重複呼叫的小函式,此間接呼叫可能會對效能產生重大影響。但是,如果我們知道 C 函式不會配置、不會引發例外狀況,並且不會釋放網域鎖定(請參閱第 22.12.2 節),則不需要這樣做。我們可以透過使用屬性 [@@noalloc] 註解外部宣告,來告知 OCaml 原生碼編譯器此事實

external bar : int -> int -> int = "foo" [@@noalloc]

在這種情況下,從 OCaml 呼叫 bar 與呼叫任何其他 OCaml 函式一樣便宜,除了 OCaml 編譯器無法內聯 C 函式之外...

11.3 範例:直接呼叫 C 函式庫函式

使用這些屬性,可以不經由間接方式呼叫 C 函式庫函式。例如,許多數學函式在 OCaml 標準函式庫中就是這樣定義的

external sqrt : float -> float = "caml_sqrt_float" "sqrt"
  [@@unboxed] [@@noalloc]
(** Square root. *)

external exp : float -> float = "caml_exp_float" "exp" [@@unboxed] [@@noalloc]
(** Exponential. *)

external log : float -> float = "caml_log_float" "log" [@@unboxed] [@@noalloc]
(** Natural logarithm. *)

12 進階主題:多執行緒

在混合 OCaml/C 應用程式中使用多個執行緒(共享記憶體並行)需要特別的預防措施,這些措施將在本節中說明。

12.1 註冊從 C 建立的執行緒

只有當呼叫執行緒為 OCaml 執行時系統所知時,才可能從 C 回調到 OCaml。 從 OCaml 建立的執行緒(透過系統執行緒函式庫的 Thread.create 函式)會自動為執行時系統所知。 如果應用程式從 C 建立額外的執行緒,並希望從這些執行緒回調到 OCaml 程式碼,則必須先向執行時系統註冊它們。 以下函式在標頭檔 <caml/threads.h> 中宣告。

12.2 使用系統執行緒並行執行長時間運行的 C 程式碼

網域是 OCaml 程式的並行單位。 當使用系統執行緒函式庫時,多個執行緒可能會附加到同一個網域。 但是,在任何時間,最多只有一個執行緒可以執行 OCaml 程式碼或使用網域的 OCaml 執行時系統的 C 程式碼。 從技術上講,這是通過一個「網域鎖」強制執行的,任何執行緒在網域中執行此類程式碼時都必須持有該鎖。

當 OCaml 呼叫實作基本功能的 C 程式碼時,網域鎖會被持有,因此 C 程式碼可以完全存取執行時系統的功能。 但是,同一網域中的其他執行緒無法與基本功能的 C 程式碼同時執行 OCaml 程式碼。 有關多網域的行為,另請參閱第 9.6 章 ‍

如果 C 基本功能執行很長時間或執行可能會阻塞的輸入輸出操作,它可以明確釋放網域鎖,使同一網域中的其他 OCaml 執行緒可以與其操作並行執行。 C 程式碼必須在返回 OCaml 之前重新取得網域鎖。 這可以透過以下函式實現,這些函式在標頭檔 <caml/threads.h> 中宣告。

這些函式透過在釋放鎖定之前和取得鎖定之後呼叫非同步回調(第 22.5.3 節 ‍)來輪詢待處理的訊號。因此,它們可以執行任意的 OCaml 程式碼,包括引發非同步例外。

在呼叫 caml_release_runtime_system() 之後以及在呼叫 caml_acquire_runtime_system() 之前,C 程式碼不得存取任何 OCaml 資料,也不得呼叫執行時系統的任何函式,也不得回調到 OCaml 程式碼。因此,OCaml 提供給 C 基本功能的參數必須在呼叫 caml_release_runtime_system() 之前複製到 C 資料結構中,並且在 caml_acquire_runtime_system() 返回後,要返回給 OCaml 的結果必須編碼為 OCaml 值。

範例:以下 C 基本功能會呼叫 gethostbyname 來尋找主機名稱的 IP 位址。 gethostbyname 函式可能會阻塞很長時間,因此我們選擇在它運行時釋放 OCaml 執行時系統。

CAMLprim stub_gethostbyname(value vname)
{
  CAMLparam1 (vname);
  CAMLlocal1 (vres);
  struct hostent * h;
  char * name;

  /* Copy the string argument to a C string, allocated outside the
     OCaml heap. */
  name = caml_stat_strdup(String_val(vname));
  /* Release the OCaml run-time system */
  caml_release_runtime_system();
  /* Resolve the name */
  h = gethostbyname(name);
  /* Free the copy of the string, which we might as well do before
     acquiring the runtime system to benefit from parallelism. */
  caml_stat_free(name);
  /* Re-acquire the OCaml run-time system */
  caml_acquire_runtime_system();
  /* Encode the relevant fields of h as the OCaml value vres */
  ... /* Omitted */
  /* Return to OCaml */
  CAMLreturn (vres);
}

巨集 Caml_state 會評估為網域狀態變數,並在偵錯模式下檢查是否持有網域鎖定。 這種檢查也放置在 C API 的主要入口點中的正常模式中; 這就是為什麼在沒有正確擁有網域鎖定的情況下呼叫某些執行時函式和巨集可能會導致致命錯誤的原因:沒有持有網域鎖定。變體 Caml_state_opt 不執行任何檢查,但當未持有網域鎖定時,會評估為 NULL。 這可讓您確定屬於網域的執行緒目前是否持有其網域鎖定,以達到各種目的。

從 C 到 OCaml 的回調必須在持有 OCaml 執行時系統的網域鎖定時執行。如果回調是由未釋放執行時系統的 C 基本功能執行,則自然是這種情況。 如果 C 基本功能先前釋放了執行時系統,或者回調是從未從 OCaml 呼叫的其他 C 程式碼(例如 GUI 應用程式中的事件迴圈)執行的,則必須在回調之前取得執行時系統,並在之後釋放

  caml_acquire_runtime_system();
  /* Resolve OCaml function vfun to be invoked */
  /* Build OCaml argument varg to the callback */
  vres = callback(vfun, varg);
  /* Copy relevant parts of result vres to C data structures */
  caml_release_runtime_system();

注意:上述的 acquirerelease 函式是在 OCaml 3.12 中引入的。 較舊的程式碼使用以下歷史名稱,這些名稱在 <caml/signals.h> 中宣告

直觀概念:「阻塞區段」是不使用 OCaml 執行時系統的一段 C 程式碼,通常是阻塞的輸入/輸出操作。

13 進階主題:與 Windows Unicode API 介接

本節包含一些撰寫使用 Windows Unicode API 的 C 存根的一般指導方針。

Windows 下的 OCaml 系統可以在建置時配置為兩種模式之一

在以下內容中,如果字串在 Unicode 模式下以 UTF-8 編碼、在舊版模式下以目前的程式碼頁編碼,或是在 Unix 下為任意字串,則我們說該字串具有 *OCaml 編碼*。 如果字串在 Windows 下以 UTF-16 編碼,或是在 Unix 下為任意字串,則該字串具有 *平台編碼*。

從 C 存根撰寫者的角度來看,與 Windows Unicode API 互動的挑戰有兩方面

Windows 下的原生 C 字元類型為 WCHAR,寬度為兩個位元組,而 Unix 下則為 char,寬度為一個位元組。 在 <caml/misc.h> 中定義了一個類型 char_os,代表每個平台的具體 C 字元類型。 平台編碼中的字串類型為 char_os *

以下函式公開用於協助撰寫相容的 C 存根。 若要使用它們,您需要同時包含 <caml/misc.h><caml/osdeps.h>

注意: caml_stat_strdup_to_oscaml_stat_strdup_of_os 返回的字串是使用 caml_stat_alloc 配置的,因此當不再需要時,需要使用 caml_stat_free 來釋放它們。

範例

我們希望以一種在 Unix 和 Windows 下都能運作的方式綁定 getenv 函數。在 Unix 下,這個函數的原型為

    char *getenv(const char *);

而在 Windows 下的 Unicode 版本原型為

    WCHAR *_wgetenv(const WCHAR *);

char_os 而言,兩個函數都接受 char_os * 類型的參數,並返回相同類型的結果。我們首先選擇要綁定的正確函數實作

#ifdef _WIN32
#define getenv_os _wgetenv
#else
#define getenv_os getenv
#endif

其餘的綁定在兩個平台上都是相同的

#include <caml/mlvalues.h>
#include <caml/misc.h>
#include <caml/alloc.h>
#include <caml/fail.h>
#include <caml/osdeps.h>
#include <stdlib.h>

CAMLprim value stub_getenv(value var_name)
{
  CAMLparam1(var_name);
  CAMLlocal1(var_value);
  char_os *var_name_os, *var_value_os;

  var_name_os = caml_stat_strdup_to_os(String_val(var_name));
  var_value_os = getenv_os(var_name_os);
  caml_stat_free(var_name_os);

  if (var_value_os == NULL)
    caml_raise_not_found();

  var_value = caml_copy_string_of_os(var_value_os);

  CAMLreturn(var_value);
}

14 建立混合 C/OCaml 函式庫:ocamlmklib

ocamlmklib 命令有助於建構同時包含 OCaml 程式碼和 C 程式碼的函式庫,並且可用於靜態連結和動態連結模式。此命令在 Objective Caml 3.11 版本後於 Windows 上可用,而在 Objective Caml 3.03 版本後於其他作業系統上可用。

ocamlmklib 命令接受三種參數

它會產生以下輸出

此外,還會識別下列選項

-cclib-ccopt-I-linkall
這些選項會原封不動地傳遞給 ocamlcocamlopt。請參閱這些命令的文件。
-rpath-R-Wl,-rpath-Wl,-R
這些選項會原封不動地傳遞給 C 編譯器。請參閱 C 編譯器的文件。
-custom
強制僅建構靜態連結的函式庫,即使支援動態連結也是如此。
-failsafe
如果在建構共用函式庫時發生問題(例如,某些支援函式庫無法作為共用函式庫使用),則回復為建構靜態連結的函式庫。
-Ldir
dir 新增至支援函式庫 (-llib) 的搜尋路徑。
-ocamlc cmd
使用 cmd 而非 ocamlc 來呼叫位元組碼編譯器。
-ocamlopt cmd
使用 cmd 而非 ocamlopt 來呼叫原生碼編譯器。
-o output
設定產生的 OCaml 函式庫名稱。ocamlmklib 將產生 output.cma 和/或 output.cmxa。如果未指定,則預設為 a
-oc outputc
設定產生的 C 函式庫名稱。ocamlmklib 將產生 liboutputc.so (如果支援共用函式庫) 和 liboutputc.a。如果未指定,則預設為使用 -o 指定的輸出名稱。
範例

考慮一個標準 libz C 函式庫的 OCaml 介面,用於讀寫壓縮檔案。假設此函式庫位於 /usr/local/zlib 中。此介面由 OCaml 部分 zip.cmo/zip.cmx 和 C 部分 zipstubs.o 組成,其中包含圍繞 libz 進入點的存根程式碼。下列命令會建構 OCaml 函式庫 zip.cmazip.cmxa,以及配套的 C 函式庫 dllzip.solibzip.a

ocamlmklib -o zip zip.cmo zip.cmx zipstubs.o -lz -L/usr/local/zlib

如果支援共用函式庫,這會執行下列命令

ocamlc -a -o zip.cma zip.cmo -dllib -lzip \
        -cclib -lzip -cclib -lz -ccopt -L/usr/local/zlib
ocamlopt -a -o zip.cmxa zip.cmx -cclib -lzip \
        -cclib -lzip -cclib -lz -ccopt -L/usr/local/zlib
gcc -shared -o dllzip.so zipstubs.o -lz -L/usr/local/zlib
ar rc libzip.a zipstubs.o

注意:此範例位於 Unix 系統上。在其他系統上,確切的命令列可能會有所不同。

如果不支持共用函式庫,則會改為執行下列命令

ocamlc -a -custom -o zip.cma zip.cmo -cclib -lzip \
        -cclib -lz -ccopt -L/usr/local/zlib
ocamlopt -a -o zip.cmxa zip.cmx -lzip \
        -cclib -lz -ccopt -L/usr/local/zlib
ar rc libzip.a zipstubs.o

除了同時建構位元組碼函式庫、原生碼函式庫和 C 函式庫之外,可以呼叫 ocamlmklib 三次來分別建構每個函式庫。因此,

ocamlmklib -o zip zip.cmo -lz -L/usr/local/zlib

建構位元組碼函式庫 zip.cma,而

ocamlmklib -o zip zip.cmx -lz -L/usr/local/zlib

建構原生碼函式庫 zip.cmxa,而

ocamlmklib -o zip zipstubs.o -lz -L/usr/local/zlib

建構 C 函式庫 dllzip.solibzip.a。請注意,支援函式庫 (-lz) 和對應的選項 (-L/usr/local/zlib) 必須在所有三次 ocamlmklib 的調用中給出,因為它們在不同的時間點是需要的,具體取決於是否支援共用函式庫。

15 警語:內部執行階段 API

並非 caml/ 目錄中提供的所有標頭都在先前的章節中描述過。所有未提及的標頭都屬於內部執行階段 API 的一部分,對於此 API,沒有穩定性保證。如果您真的需要存取此內部執行階段 API,本節提供一些指南,可能會幫助您編寫在每個新版本的 OCaml 上都不會中斷的程式碼。

注意

如果程式設計師發現實際且有用的使用案例需要依賴內部 API,則鼓勵他們在錯誤追蹤器上開啟改進請求。

15.1 內部變數和 CAML_INTERNALS

自 OCaml 4.04 起,可以藉由在載入 caml 標頭檔案之前定義 CAML_INTERNALS 巨集來存取內部執行階段 API 的每個部分。如果未定義此巨集,則會隱藏內部執行階段 API 的部分內容。

如果您正在使用內部 C 變數,請勿手動重新定義它們。您應該包含對應的標頭檔案來匯入這些變數。這些變數的表示形式已在 OCaml 4.10 中變更過一次,並且仍在演變中。如果您的程式碼依賴這些內部且脆弱的屬性,則它會在某個時間點中斷。

例如,與其重新定義 caml_young_limit

extern int caml_young_limit;

這會在 OCaml ≥ 4.10 中中斷,不如包含 minor_gc 標頭

#include <caml/minor_gc.h>

15.2 OCaml 版本巨集

最後,如果包含正確的標頭不夠,或者如果您需要支援早於 OCaml 4.04 的版本,則標頭檔案 caml/version.h 應該可以幫助您定義自己的相容性層。此檔案提供一些定義目前 OCaml 版本的巨集。特別是,OCAML_VERSION 巨集描述了目前版本,其格式為 MmmPP。例如,如果您需要針對早於 4.10.0 的版本進行一些特定的處理,您可以撰寫

#include <caml/version.h>
#if OCAML_VERSION >= 41000
...
#else
...
#endif