diff options
Diffstat (limited to 'source/_posts/在-OCaml-中通过-CTypes-绑定-libseccomp.md')
| -rw-r--r-- | source/_posts/在-OCaml-中通过-CTypes-绑定-libseccomp.md | 595 |
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 |
