OCaml 教程 / C 语言互操作(FFI/ctypes)
C 语言互操作(FFI/ctypes)
OCaml 的 FFI(Foreign Function Interface)允许调用 C 库函数和将 OCaml 函数暴露给 C。这是 OCaml 生态系统中对接操作系统、数据库、加密库等底层功能的关键技术。
FFI 概述
OCaml 有两种主要的 FFI 方式:
| 方式 | 特点 | 适用场景 |
|---|---|---|
直接 FFI (external) | 编译器原生支持,需要编写 C 存根 | 高性能绑定、运行时接口 |
| ctypes 库 | 纯 OCaml 描述 C 接口,自动生成 C 存根 | 快速绑定、减少样板代码 |
选择建议
- 使用 ctypes:绑定标准 C 库、快速原型
- 使用 external:需要精细控制内存、GC 交互、性能极致
ctypes 库基础
安装
opam install ctypes ctypes-foreign
基本类型映射
open Ctypes
open Foreign
(* C 类型到 OCaml 类型的映射 *)
let int_t = int (* C: int *)
let double_t = double (* C: double *)
let string_t = string (* C: char* *)
let void_t = void (* C: void *)
let bool_t = bool (* C: _Bool *)
let char_t = char (* C: char *)
let ptr_t = ptr int_t (* C: int* *)
类型对照表
| C 类型 | ctypes 类型 | OCaml 值类型 |
|---|---|---|
int | int | int |
double | double | float |
char | char | char |
char* | string | string |
void | void | unit |
int* | ptr int | int ptr |
size_t | size_t | Unsigned.Size_t.t |
绑定 C 函数
简单函数绑定
open Ctypes
open Foreign
(* 绑定 C 的 abs() 函数 *)
let abs = foreign "abs" (int @-> returning int)
(* 绑定 C 的 strlen() 函数 *)
let strlen = foreign "strlen" (string @-> returning size_t)
(* 使用 *)
let () =
Printf.printf "abs(-42) = %d\n" (abs (-42));
Printf.printf "strlen(\"hello\") = %s\n"
(Unsigned.Size_t.to_string (strlen "hello"))
函数类型构建
ctypes 使用 @-> 和 returning 构建函数类型:
(* C: double pow(double base, double exp) *)
let pow = foreign "pow" (double @-> double @-> returning double)
(* C: int snprintf(char* str, size_t size, const char* format, ...) *)
(* 注意:变参函数需要特殊处理,此处为简化示例 *)
let snprintf = foreign "snprintf"
(string @-> size_t @-> string @-> int @-> returning int)
(* 使用 *)
let () =
let result = pow 2.0 10.0 in
Printf.printf "2^10 = %.0f\n" result
⚠️ 注意:string 参数在 ctypes 中是 const char*(只读)。如果 C 函数会修改字符串,需要使用 ptr char 和 CArray。
绑定 C 结构体
定义结构体类型
open Ctypes
(* C:
struct point {
double x;
double y;
};
*)
type point
let point : point structure typ = structure "point"
let x = field point "x" double
let y = field point "y" double
let () = seal point
(* 创建结构体实例 *)
let make_point x_val y_val =
let p = make point in
setf p x x_val;
setf p y y_val;
p
(* 读取结构体字段 *)
let get_x p = getf p x
let get_y p = getf p y
(* 使用 *)
let () =
let p = make_point 3.0 4.0 in
Printf.printf "Point: (%.1f, %.1f)\n" (get_x p) (get_y p)
嵌套结构体
(* C:
struct rect {
struct point top_left;
struct point bottom_right;
};
*)
type rect
let rect : rect structure typ = structure "rect"
let top_left = field rect "top_left" point
let bottom_right = field rect "bottom_right" point
let () = seal rect
结构体指针
(* 传递结构体指针给 C 函数 *)
(* C: double distance(struct point* p) *)
let distance = foreign "distance"
(ptr point @-> returning double)
let () =
let p = make_point 3.0 4.0 in
let d = distance (addr p) in
Printf.printf "Distance: %.2f\n" d
OCaml 回调 C 函数(函数指针)
将 OCaml 函数传递给 C
open Ctypes
open Foreign
(* C: void qsort(void* base, size_t nmemb, size_t size,
int (*compar)(const void*, const void*)) *)
(* 定义回调类型 *)
let compar_fn = ptr void @-> ptr void @-> returning int
let qsort = foreign "qsort"
(ptr void @-> size_t @-> size_t @-> funptr compar_fn @-> returning void)
(* OCaml 回调函数 *)
let compare_ints a b =
let va = !@(from_voidp int a) in
let vb = !@(from_voidp int b) in
compare va vb
let () =
let arr = CArray.of_list int [5; 3; 1; 4; 2] in
qsort
(to_voidp (CArray.start arr))
(Unsigned.Size_t.of_int 5)
(Unsigned.Size_t.of_int (sizeof int))
compare_ints;
CArray.iter (fun x -> Printf.printf "%d " x) arr;
print_newline ()
(* 输出: 1 2 3 4 5 *)
⚠️ 注意:回调函数中不能触发 OCaml GC(不能分配堆对象)。如果需要在回调中分配,使用 CAMLparam/CAMLreturn 宏(直接 FFI 方式)。
内存管理与 GC 交互
GC 的影响
OCaml 的 GC 可能在任意时刻移动对象。当 C 代码持有 OCaml 堆上的指针时,GC 移动会导致指针失效。
解决方案
- 使用 ctypes 的
CArray:ctypes 自动管理 C 侧内存
(* CArray 分配在 C 堆上,不受 GC 影响 *)
let arr = CArray.make int 100
CArray.set arr 0 42
- 使用
Ctypes.Root:防止 OCaml 值被 GC 回收
open Ctypes
let keep_alive value =
let root = Root.create value in
(* value 不会被 GC 回收 *)
(* ... 在 C 代码中使用 value ... *)
Root.release root (* 使用完毕后释放 *)
- 使用
bigarray做大块内存:分配在 C 堆,GC 不管理
open Bigarray
let ba = Array1.create Float64 C_layout 1000000
(* 可安全传递给 C,不受 GC 移动影响 *)
使用 Dune 构建 FFI 项目
项目结构
my_ffi_project/
├── dune-project
├── lib/
│ ├── dune
│ ├── my_bindings.ml # ctypes 绑定
│ └── stubs/
│ ├── dune # 存根生成规则
│ └── generate.ml # 存根代码生成器
└── bin/
├── dune
└── main.ml
Dune 配置
lib/dune:
(library
(name my_bindings)
(libraries ctypes ctypes.foreign)
(c_library_flags (-lpthread)))
lib/stubs/dune:
(executable
(name generate)
(libraries ctypes))
(rule
(targets my_stubs.c)
(action (run ./generate.exe)))
(library
(name my_stubs)
(ocamlopt_flags (-ccopt -O2))
(c_names my_stubs))
lib/stubs/generate.ml:
let () =
let format = Format.formatter_of_out_channel (open_out "my_stubs.c") in
Cstubs.write_c format ~prefix:"my_" (module My_bindings.C)
实战:绑定 libcurl
open Ctypes
open Foreign
(* 初始化与清理 *)
let curl_easy_init = foreign "curl_easy_init" (void @-> returning (ptr void))
let curl_easy_cleanup = foreign "curl_easy_cleanup"
(ptr void @-> returning void)
(* 设置选项 *)
type curl_option = LONG of int | STRING of string
let curl_easy_setopt_string = foreign "curl_easy_setopt"
(ptr void @-> int @-> string @-> returning int)
(* 执行请求 *)
let curl_easy_perform = foreign "curl_easy_perform"
(ptr void @-> returning int)
(* 简化的 HTTP GET *)
let http_get url =
let handle = curl_easy_init () in
if handle = null then failwith "curl_easy_init failed";
Fun.protect ~finally:(fun () -> curl_easy_cleanup handle) (fun () ->
let _ = curl_easy_setopt_string handle 10002 url in (* CURLOPT_URL *)
let res = curl_easy_perform handle in
if res <> 0 then Printf.eprintf "curl error: %d\n" res
)
⚠️ 注意:此示例简化了错误处理。生产代码需要检查每个 curl 函数的返回码,并绑定 CURLOPT_WRITEFUNCTION 来接收响应数据。
实战:绑定 libsqlite3
open Ctypes
open Foreign
let sqlite3_open = foreign "sqlite3_open"
(string @-> ptr (ptr void) @-> returning int)
let sqlite3_close = foreign "sqlite3_close"
(ptr void @-> returning int)
let sqlite3_exec = foreign "sqlite3_exec"
(ptr void @-> string @-> funptr_opt (ptr void @-> int @-> ptr string
@-> ptr string @-> returning int)
@-> ptr void @-> ptr string @-> returning int)
let with_db path f =
let db = allocate (ptr void) null in
let rc = sqlite3_open path db in
if rc <> 0 then failwith (Printf.sprintf "sqlite3_open failed: %d" rc);
Fun.protect ~finally:(fun () -> ignore (sqlite3_close !@db)) (fun () ->
f !@db)
let exec_sql db sql =
let err = allocate string_opt None in
let rc = sqlite3_exec db sql None null err in
if rc <> 0 then
let msg = match !@err with Some s -> s | None -> "unknown error" in
failwith (Printf.sprintf "SQL error: %s" msg)
let () =
with_db ":memory:" (fun db ->
exec_sql db "CREATE TABLE users (id INTEGER, name TEXT)";
exec_sql db "INSERT INTO users VALUES (1, 'Alice')";
exec_sql db "INSERT INTO users VALUES (2, 'Bob')";
exec_sql db "SELECT * FROM users"
)
调试 FFI 代码
使用 Valgrind
# 检查内存泄漏
valgrind --leak-check=full ./my_program
# 检查非法内存访问
valgrind --track-origins=yes ./my_program
常见问题
| 问题 | 症状 | 解决 |
|---|---|---|
| GC 移动对象 | 段错误、数据损坏 | 使用 CArray 或 Root |
| 悬垂指针 | 不可预测行为 | 确保 C 指针生命周期 |
| 类型大小不匹配 | 数据错误 | 使用 sizeof 验证 |
| 回调中分配 | GC 崩溃 | 避免在回调中分配 |
⚠️ 注意:ctypes 的 string 参数会复制数据。如果 C 函数需要长期持有指针,使用 CArray 分配并传递 ptr char。
💡 提示:开发阶段用 OCAMLRUNPARAM=v=0x400 开启 GC 详细日志,观察 GC 是否干扰了 FFI 操作。