blob: 086044e9bcab226a739cea9b766ec69d257a608d (
plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
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
|