强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

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 值类型
intintint
doubledoublefloat
charcharchar
char*stringstring
voidvoidunit
int*ptr intint ptr
size_tsize_tUnsigned.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 charCArray


绑定 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 移动会导致指针失效。

解决方案

  1. 使用 ctypes 的 CArray:ctypes 自动管理 C 侧内存
(* CArray 分配在 C 堆上,不受 GC 影响 *)
let arr = CArray.make int 100
CArray.set arr 0 42
  1. 使用 Ctypes.Root:防止 OCaml 值被 GC 回收
open Ctypes

let keep_alive value =
  let root = Root.create value in
  (* value 不会被 GC 回收 *)
  (* ... 在 C 代码中使用 value ... *)
  Root.release root  (* 使用完毕后释放 *)
  1. 使用 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 移动对象段错误、数据损坏使用 CArrayRoot
悬垂指针不可预测行为确保 C 指针生命周期
类型大小不匹配数据错误使用 sizeof 验证
回调中分配GC 崩溃避免在回调中分配

⚠️ 注意:ctypes 的 string 参数会复制数据。如果 C 函数需要长期持有指针,使用 CArray 分配并传递 ptr char

💡 提示:开发阶段用 OCAMLRUNPARAM=v=0x400 开启 GC 详细日志,观察 GC 是否干扰了 FFI 操作。


扩展阅读