summaryrefslogtreecommitdiff
path: root/source/_posts/在-OCaml-中通过-CTypes-绑定-libseccomp.md
diff options
context:
space:
mode:
Diffstat (limited to 'source/_posts/在-OCaml-中通过-CTypes-绑定-libseccomp.md')
-rw-r--r--source/_posts/在-OCaml-中通过-CTypes-绑定-libseccomp.md595
1 files changed, 595 insertions, 0 deletions
diff --git a/source/_posts/在-OCaml-中通过-CTypes-绑定-libseccomp.md b/source/_posts/在-OCaml-中通过-CTypes-绑定-libseccomp.md
new file mode 100644
index 00000000..086044e9
--- /dev/null
+++ b/source/_posts/在-OCaml-中通过-CTypes-绑定-libseccomp.md
@@ -0,0 +1,595 @@
+---
+title: 在 OCaml 中通过 CTypes 绑定 libseccomp
+date: 2026-03-22 23:13:33
+tags: [Technique]
+---
+
+CTypes 允许我们用纯 OCaml 代码绑定 C lib 而不用编写任何 C 代码。这篇文章是以 [bonding](https://github.com/muqiuhan/bonding) 项目中的 `libseccomp` 绑定为例写的。
+
+CTypes 的核心概念是建立 OCaml 类型与 C 类型的双向映射,ctypes 中有如下基本类型:
+
+```ocaml
+open Ctypes
+
+(* C 类型与 OCaml 类型的对应关系 *)
+let c_int : int typ = int (* int *)
+let c_uint : Unsigned.UInt.t typ = uint (* unsigned int *)
+let c_long : Signed.Long.t typ = long (* long *)
+let c_int64 : Unsigned.UInt64.t typ = uint64_t (* int64_t / uint64_t *)
+let c_float : float typ = float (* float *)
+let c_double : float typ = double (* double *)
+let c_char : char typ = char (* char *)
+let c_void : unit typ = void (* void *)
+```
+
+## 指针
+
+对于 C 的指针,CTypes 提供了多种指针表示:
+
+```ocaml
+(* 通用指针:void* *)
+let void_ptr : unit ptr typ = ptr void
+
+(* 类型化指针:int* *)
+let int_ptr : int ptr typ = ptr int
+
+(* 不透明指针:用于表示我们不知道内部结构的 C 结构体 *)
+let scmp_filter_ctx : unit ptr typ = ptr void
+```
+
+bonding 中的 seccomp_stubs.ml 中是这么写的:
+
+```ocaml
+type scmp_filter_ctx = unit ptr
+let scmp_filter_ctx : scmp_filter_ctx typ = ptr void
+```
+
+这里 `scmp_filter_ctx` 是 libseccomp 的核心类型,它是一个不透明指针。C 库不暴露其内部结构,我们只能通过库函数操作它。使用 `ptr void` 是表示不透明指针的标准做法。
+
+## 结构体
+
+对于结构体,ctypes 通过 `structure` 和 `field` 函数定义:
+
+```ocaml
+(* 定义 C 结构体:
+ struct scmp_arg_cmp {
+ unsigned int arg;
+ unsigned int op;
+ uint64_t datum_a;
+ uint64_t datum_b;
+ };
+*)
+
+let scmp_arg_cmp : unit structure typ =
+ let s = structure "scmp_arg_cmp" in (* 1. 创建结构体模板 *)
+ let _arg = field s "arg" uint in (* 2. 添加字段 *)
+ let _op = field s "op" uint in
+ let _datum_a = field s "datum_a" uint64_t in
+ let _datum_b = field s "datum_b" uint64_t in
+ seal s; (* 3. 封印结构体 *)
+ s (* 4. 返回类型描述符 *)
+```
+
+注意:
+
+- `structure "name"` 创建一个未完成的类型描述符
+- `field s "fieldname" typ` 添加字段,返回字段访问器
+- `seal s` 完成结构体定义,之后不可再添加字段
+- 返回的 `s` 可以作为类型使用
+
+例如:
+
+```ocaml
+let scmp_arg_cmp : unit structure typ =
+ let s = structure "scmp_arg_cmp" in
+ let _arg = field s "arg" uint in
+ let _op = field s "op" uint in
+ let _datum_a = field s "datum_a" uint64_t in
+ let _datum_b = field s "datum_b" uint64_t in
+ seal s;
+ s
+```
+
+定义结构体后,可以通过字段访问器读写值:
+
+```ocaml
+(* 写入字段值 *)
+let set_arg_cmp_values struct_ptr arg_val op_val datum_a_val datum_b_val =
+ let s = structure "scmp_arg_cmp" in
+ let arg = field s "arg" uint in
+ let op = field s "op" uint in
+ let datum_a = field s "datum_a" uint64_t in
+ let datum_b = field s "datum_b" uint64_t in
+ seal s;
+
+ (* 使用 setf 设置字段值 *)
+ setf struct_ptr arg (Unsigned.UInt.of_int arg_val);
+ setf struct_ptr op (Unsigned.UInt.of_int op_val);
+ setf struct_ptr datum_a (Unsigned.UInt64.of_int64 datum_a_val);
+ setf struct_ptr datum_b (Unsigned.UInt64.of_int64 datum_b_val)
+```
+
+这里展示了如何创建辅助函数来操作结构体。注意:
+- 每次都需要重新定义结构体和字段访问器
+- `setf` 的参数顺序:结构体指针、字段访问器、值
+- OCaml 的无符号类型需要显式转换
+
+## 动态库
+
+使用 CTypes 绑定 C 库只需要:加载库、然后绑定函数。
+
+首先,加载共享库:
+
+```ocaml
+let libseccomp =
+ let paths = [ "libseccomp.so.2"; "libseccomp.so.1"; "libseccomp.so" ] in
+ let rec try_paths = function
+ | [] ->
+ Dl.dlopen ~filename:"libseccomp.so.2" ~flags:[ Dl.RTLD_NOW ]
+ | path :: rest -> (
+ try Dl.dlopen ~filename:path ~flags:[ Dl.RTLD_NOW ] with
+ | _ ->
+ try_paths rest)
+ in
+ try_paths paths
+```
+
+这段代码用了一个提升动态库加载稳定性的小技巧:
+
+- 尝试多个可能的库文件名(不同系统可能版本不同)
+- 依次尝试,直到成功
+- 如果全部失败,使用默认路径(触发异常)
+
+注意 `Dl.dlopen` 的参数:
+- `filename`: 库文件名(可以是绝对路径或库文件名)
+- `flags`: 加载标志
+ - `RTLD_NOW`: 立即解析所有符号
+ - `RTLD_LAZY`: 延迟解析符号
+
+加载完成后使用 `Foreign.foreign` 绑定到函数,基本语法:
+
+```ocaml
+let function_name =
+ Foreign.foreign
+ ~from:library
+ "c_function_name"
+ (arg1_type @-> arg2_type @-> ... @-> returning return_type)
+```
+
+单参数函数:
+
+```ocaml
+let seccomp_init =
+ Foreign.foreign
+ ~from:libseccomp
+ "seccomp_init"
+ (uint32_t @-> returning scmp_filter_ctx)
+```
+
+对应的 C 函数原型:
+
+```c
+scmp_filter_ctx seccomp_init(uint32_t def_action);
+```
+
+`Foreign.foreign` 的参数:
+- `~from:libseccomp`: 指定函数所在的库
+- `"seccomp_init"`: C 函数名
+- `uint32_t @-> returning scmp_filter_ctx`: 函数签名
+ - `@->` 分隔参数
+ - `returning` 标记返回类型
+
+对于多参数函数:
+
+```ocaml
+let seccomp_rule_add =
+ Foreign.foreign
+ ~from:libseccomp
+ "seccomp_rule_add"
+ (scmp_filter_ctx @-> uint32_t @-> int @-> uint @-> returning int)
+```
+
+**C 函数原型**:
+```c
+int seccomp_rule_add(scmp_filter_ctx ctx, uint32_t action,
+ int syscall, unsigned int arg_cnt);
+```
+
+对于无参数函数:
+
+```ocaml
+let seccomp_arch_native =
+ Foreign.foreign
+ ~from:libseccomp
+ "seccomp_arch_native"
+ (void @-> returning uint32_t)
+```
+
+对于指针参数:
+
+```ocaml
+let seccomp_load =
+ Foreign.foreign
+ ~from:libseccomp
+ "seccomp_load"
+ (scmp_filter_ctx @-> returning int)
+```
+
+**C 函数原型**:
+```c
+int seccomp_load(scmp_filter_ctx ctx);
+```
+
+对于字符串参数:
+
+```ocaml
+let seccomp_syscall_resolve_name =
+ Foreign.foreign
+ ~from:libseccomp
+ "seccomp_syscall_resolve_name"
+ (string @-> returning int)
+```
+
+这里 CTypes 会自动处理 OCaml 字符串与 C 字符串的转换。
+
+对于返回 NULL 的情况:
+
+```ocaml
+let seccomp_syscall_resolve_num_arch =
+ Foreign.foreign
+ ~from:libseccomp
+ "seccomp_syscall_resolve_num_arch"
+ (uint32_t @-> int @-> returning string_opt)
+```
+
+`string_opt` 处理可能返回 NULL 的字符串指针。
+
+## 内存管理
+
+C 需要手动管理内存,CTypes 提供了相应的工具。
+
+分配单个值:
+
+```ocaml
+(* 分配并初始化单个值 *)
+let ptr = allocate int 42
+
+(* 读取值 *)
+let value = !@ ptr
+
+(* 写入值 *)
+ptr <-@ 100
+```
+
+分配数组:
+
+```ocaml
+(* 分配数组(未初始化) *)
+let arr = allocate_n int ~count:10
+
+(* 分配并初始化数组 *)
+let arr = allocate_n int ~count:5 ~initial:0
+```
+
+例如:
+
+```ocaml
+let cmp_array = Ctypes.allocate_n Stubs.Types.scmp_arg_cmp ~count:n in
+```
+
+### 指针算术
+
+```ocaml
+let arr = allocate_n int ~count:10
+
+(* 访问第 i 个元素 *)
+let elem_ptr = arr +@ i
+
+(* 读写元素 *)
+let value = !@ elem_ptr
+elem_ptr <-@ 42
+```
+
+例如:
+
+```ocaml
+Stubs.Types.set_arg_cmp_values
+ !@(cmp_array +@ i) (* 第 i 个结构体的指针 *)
+ cmp.arg
+ (cmp_op_to_int cmp.op)
+ cmp.datum_a
+ cmp.datum_b
+```
+
+`!@` 运算符解引用指针,`+@` 进行指针算术。
+
+### 指针转换
+
+```ocaml
+(* 类型化指针转 void 指针 *)
+let void_p = to_voidp int_ptr
+
+(* void 指针转类型化指针 *)
+let int_p = from_voidp int void_p
+
+(* 获取空指针 *)
+let null_ptr : unit ptr = null
+
+(* 检查是否为空指针 *)
+if is_null ptr then ...
+```
+
+**实例解析(seccomp.ml:96)**:
+
+```ocaml
+if Ctypes.ptr_compare ctx Ctypes.null = 0 then
+ Error Init_failed
+```
+
+检查 `seccomp_init` 是否返回了 NULL(失败)。
+
+## 一些模式
+
+### 类型视图(Type Views)
+
+当 C 类型与 OCaml 类型不能直接对应时,使用 `view` 创建自定义转换:
+
+```ocaml
+(* 将 C int 转换为 OCaml bool *)
+let c_bool = view
+ ~read:(fun i -> i <> 0)
+ ~write:(fun b -> if b then 1 else 0)
+ int
+```
+
+### 枚举类型映射
+
+在 seccomp 实现中,使用 OCaml 变体和转换函数映射 C 枚举:
+
+```ocaml
+(* OCaml 端的类型定义(seccomp.ml:18-25) *)
+type action =
+ | Allow
+ | Errno of int
+ | KillProcess
+ | KillThread
+ | Log
+ | Trace of int
+ | Trap
+
+(* C 端的常量定义(seccomp_stubs.ml:58-73) *)
+module Constants = struct
+ let scmp_act_allow = 0x7fff0000l
+ let scmp_act_kill_thread = 0x00000000l
+ let scmp_act_kill_process = 0x00000000l
+ let scmp_act_trap = 0x00030000l
+ let scmp_act_log = 0x7ffc0000l
+ let scmp_act_errno code =
+ Int32.(logor 0x00050000l (logand (of_int code) 0x0000ffffl))
+end
+
+(* 转换函数(seccomp.ml:60-75) *)
+let action_to_uint32 = function
+ | Allow -> int32_to_uint32 Stubs.Constants.scmp_act_allow
+ | KillThread -> int32_to_uint32 Stubs.Constants.scmp_act_kill_thread
+ | KillProcess -> int32_to_uint32 Stubs.Constants.scmp_act_kill_process
+ | Trap -> int32_to_uint32 Stubs.Constants.scmp_act_trap
+ | Log -> int32_to_uint32 Stubs.Constants.scmp_act_log
+ | Errno code -> int32_to_uint32 (Stubs.Constants.scmp_act_errno code)
+ | Trace code -> int32_to_uint32 (Stubs.Constants.scmp_act_trace code)
+```
+
+这种模式的优势:
+
+- OCaml 端使用类型安全的变体
+- C 端使用整数常量
+- 转换函数处理映射
+- 编译器帮助捕获类型错误
+
+### 错误处理模式
+
+C 那边通常用返回值表示错误。OCaml 使用 result 类型,例如在 seccomp 的 binding 中我这么做:
+
+```ocaml
+type error =
+ | Init_failed
+ | Rule_add_failed of int
+ | Load_failed of int
+ | Unknown_syscall of string
+
+let init default_action =
+ let ctx = Stubs.Functions.seccomp_init (action_to_uint32 default_action) in
+ if Ctypes.ptr_compare ctx Ctypes.null = 0 then
+ Error Init_failed
+ else
+ Ok ctx
+```
+
+## 完整示例
+
+让我们来看看一个完整的 `rule_add_conditional` 函数的实现:
+
+```ocaml
+let rule_add_conditional ctx action syscall cmps =
+ let n = Array.length cmps in
+ if n = 0 then
+ rule_add ctx action syscall
+ else (
+ (* 1. 分配 C 数组 *)
+ let cmp_array = Ctypes.allocate_n Stubs.Types.scmp_arg_cmp ~count:n in
+
+ (* 2. 填充每个结构体 *)
+ Array.iteri
+ (fun i cmp ->
+ Stubs.Types.set_arg_cmp_values
+ !@(cmp_array +@ i) (* 解引用第 i 个元素 *)
+ cmp.arg
+ (cmp_op_to_int cmp.op)
+ cmp.datum_a
+ cmp.datum_b)
+ cmps;
+
+ (* 3. 调用 C 函数 *)
+ let result =
+ Stubs.Functions.seccomp_rule_add_array
+ ctx
+ (action_to_uint32 action)
+ syscall
+ (Unsigned.UInt.of_int n)
+ cmp_array
+ in
+
+ (* 4. 检查结果 *)
+ if result < 0 then
+ Error (Rule_add_failed result)
+ else
+ Ok ()
+ )
+```
+
+- 数组分配: `allocate_n` 分配 `n` 个 `scmp_arg_cmp` 结构体的空间
+- 结构体填充:
+ - `cmp_array +@ i` 获取第 i 个元素的指针
+ - `!@` 解引用得到结构体
+ - `set_arg_cmp_values` 填充字段
+- 函数调用: 传递数组和长度给 C 函数
+- 返回值: C 函数返回负值表示错误
+
+对应的 C API:
+```c
+int seccomp_rule_add_array(scmp_filter_ctx ctx, uint32_t action,
+ int syscall, unsigned int arg_cnt,
+ const struct scmp_arg_cmp *arg_array);
+```
+
+### 双层架构 Binding
+
+seccomp 绑定用了清晰的三层架构:
+- seccomp.ml / seccomp.mli 是顶层 API 纯粹的OCaml 风格的类型和函数
+- seccomp_stubs.ml 绑定层,放 CTypes 绑定,类型和函数定义
+
+### 模块组织
+
+```ocaml
+(* seccomp_stubs.ml 的模块组织 *)
+
+module Types = struct (* C 类型定义 *)
+ type scmp_filter_ctx = unit Ctypes.ptr
+ val scmp_filter_ctx : ...
+ val scmp_arg_cmp : ...
+end
+
+module Constants = struct (* C 常量 *)
+ let scmp_act_allow = ...
+ let scmp_cmp_ne = ...
+end
+
+module Functions = struct (* C 函数绑定 *)
+ let seccomp_init = ...
+ let seccomp_load = ...
+end
+```
+
+类型,常量,函数分离。
+
+
+## 常见问题
+
+### 1. 内存泄漏
+
+C 分配的内存需要手动释放:
+```ocaml
+(* 始终提供 release 函数 *)
+let release ctx = Stubs.Functions.seccomp_release ctx
+
+(* 使用 Fun.protect 确保清理 *)
+let with_filter default_action f =
+ match init default_action with
+ | Error e -> Error e
+ | Ok ctx ->
+ try
+ let result = f ctx in
+ release ctx;
+ result
+ with e ->
+ release ctx;
+ raise e
+```
+
+### 2. 类型不匹配
+
+OCaml 和 C 的类型表示不同:
+```ocaml
+(* 使用 Unsigned 模块的转换函数 *)
+let int32_to_uint32 n = Unsigned.UInt32.of_int32 n
+
+(* 显式类型转换 *)
+let c_uint = Unsigned.UInt.of_int 42
+```
+
+### 3. 空指针检查
+
+C 函数可能返回 NULL:
+```ocaml
+(* 始终检查返回的指针 *)
+if Ctypes.ptr_compare ctx Ctypes.null = 0 then
+ Error Init_failed
+else
+ Ok ctx
+```
+
+### 4. 字符串所有权
+
+C 返回的字符串生命周期不确定:
+```ocaml
+(* 如果 C 返回需要释放的字符串,使用 string 并复制 *)
+let result = Bytes.of_string c_string in
+(* 释放 C 字符串 *)
+C.free c_string_ptr;
+Bytes.to_string result
+```
+
+## 调试技巧
+
+### 1. 检查库加载
+
+```ocaml
+let () =
+ try
+ let _ = Dl.dlopen ~filename:"libseccomp.so.2" ~flags:[ Dl.RTLD_NOW ] in
+ print_endline "Library loaded successfully"
+ with
+ | Dl.DL_error msg ->
+ Printf.eprintf "Failed to load library: %s\n" msg
+```
+
+### 2. 检查函数绑定
+
+```ocaml
+let () =
+ try
+ let _ = Foreign.foreign ~from:libseccomp "seccomp_init"
+ (Ctypes.uint32_t @-> returning Ctypes.ptr Ctypes.void) in
+ print_endline "Function bound successfully"
+ with
+ | _ ->
+ print_endline "Function binding failed"
+```
+
+### 3. 检查结构体大小
+
+```ocaml
+let () =
+ let s = structure "scmp_arg_cmp" in
+ let _ = field s "arg" uint in
+ seal s;
+ Printf.printf "Struct size: %d\n" (sizeof s)
+```
+
+## 参考资源
+
+- CTypes 官方文档: https://github.com/yallop/ocaml-ctypes
+- CTypes 教程: https://github.com/yallop/ocaml-ctypes/wiki/ctypes-tutorial
+- CTypes Wiki: https://github.com/yallop/ocaml-ctypes/wiki
+- libseccomp 文档: https://man7.org/linux/man-pages/man3/seccomp.3.html
+- bonding 项目: https://github.com/muqiuhan/bonding