开源之夏03:C语言工程实践——多态、插件与构建系统
这个项目典型地使用了 C 语言的”面向对象”写法。函数指针结构体是实现可插拔后端+统一调用接口的核心手段,在 Linux 内核、OpenSSL、GmSSL 中都大量出现。
C 语言如何实现”面向对象”
C 没有 class、虚函数、接口、反射,但”数据结构 + 函数指针表”恰好能一次性补齐这些能力,代价极低——只靠最普通的指针和结构体。
| 需要的能力 | C 原生缺失 | 函数指针结构体如何补 |
|---|---|---|
| 封装 | 无 class |
struct { data..., fn_pointers... } |
| 回调 | 无闭包/委托 | 成员放 void (*cb)(void *) |
| 多态 | 无虚函数 | 手动 vtable(结构体里放函数指针表) |
| 插件 | 无接口/反射 | 结构体当接口,动态库填充后传递 |
函数指针 typedef
1 | |
这种语法 1970 年代出现在 Unix 内核中,用来在纯 C 里”山寨”面向对象。凡是需要把”数据 + 操作打包在一起”的 C 工程都会用这招。
多态:同一接口,不同实现
1 | |
在这个项目中,多态有两种使用方式:
- 编译期静态绑定:软实现 SDF 编译进库,函数指针直接指向本地函数。
- 运行期动态绑定(插件):通过
dlopen/dlsym在运行时解析.so中的符号,填入函数指针表。
extern 声明 vs 定义
这是 C 语言模块化编程中一个常被忽视但至关重要的细节。在文件作用域,不带 extern 的变量声明就是”定义”(分配空间),加了 extern 才只是”声明”(生成符号引用,不分配空间)。
1 | |
在 SDF 模块中,extern SDF_METHOD ts_sdf_meth; 声明一个外部全局变量,告诉编译器实际定义在别的源文件(或动态库)里,链接器阶段再找。这保证了多个源文件共享同一个 vtable 实例。
Linux file_operations 与 SDF_METHOD
无论是内核的 file_operations 还是用户态的 SDF_METHOD,本质都是”把一组函数指针收进结构体”,靠运行时填表实现 C 语言下的多态接口。
1 | |
| 维度 | Linux file_operations | SDF_METHOD |
|---|---|---|
| 所在空间 | 内核空间 | 用户空间 |
| 代表对象 | 文件/设备 | 密码设备 |
| 被谁调度 | VFS 层 | 上层安全中间件 |
| 多态方式 | 函数指针表 | 函数指针表 |
| 动态替换 | insmod 驱动 | 换 .so 插件 |
DSO 动态加载与安全性
OpenSSL 的 DSO(Dynamic Shared Object) 是跨平台”动态库加载”的封装,本质等价于 Unix 的 dlopen/dlsym + Windows 的 LoadLibrary/GetProcAddress。
1 | |
相比系统原生 API,OpenSSL DSO 额外做了一层安全加固:
| 加固点 | 说明 |
|---|---|
| 受控搜索路径 | 默认不搜当前目录,优先 OPENSSL_DIR |
| 文件名白名单 | 只允许加载约定好的名字 |
| 符号前缀过滤 | 部分 engine 在 bind 前做 strncmp(symbol, "SDF_", 4) |
| 只读映射 | Unix 使用 `RTLD_NOW |
系统 API 只管”把文件映射进来”;OpenSSL DSO 在前后加了”白名单 + 路径限制”,相当于给 dlopen 套了条安全带。
构建系统:GmSSL vs Tongsuo
GmSSL 和 Tongsuo 同为 Best Practice,但构建路径不同:
- GmSSL:使用 CMake,模块开关和源码裁剪在 CMake 脚本中完成,条件编译宏主要用于源码组织,构建流程现代、自动化程度高。
- Tongsuo:沿用 OpenSSL 的构建体系——自研 Configure 脚本 + Perl 生成 Makefile + build.info 依赖描述。因为 Makefile 生成和链接阶段无法像 CMake 那么灵活,必须通过
#ifdef宏在源码层面处理模块裁剪。
这种 Old School 方式叫”单一源码树 + 条件配置脚本”(single-source tree with conditional configuration script),本质是 Autotools 之前的手写配置脚本,根据传入参数生成静态或动态库。
条件编译的三种姿势
Tongsuo 用 #ifdef 在同一 .c 文件里同时容纳静态/动态两套实现,这并非现代 Best Practice:
- 双源文件 + 链接器选择(推荐):
sdf_static.c+sdf_shared.c,构建脚本决定编哪个,源文件零#ifdef。 - 统一接口 + 运行时动态分派:启动时根据配置
dlopen选择.so,所有 if 留在运行时而非预处理器。 - 条件编译 + 独立头文件(最小侵入):把条件编译压到头文件,源文件只
#include。
为什么链接器能替代
#ifdef? 不是链接器升级了,而是”把问题挪到链接阶段”。静态库 vs 动态库的符号优先级规则一直存在:主程序 .o → 静态库 (.a) → 动态库 (.so)。只要通过构建脚本决定是否把某个 .o 编进来,链接器自动处理优先级,不需要在源码里用#ifdef做二选一。
弱符号与空桩
在已有空桩实现时,弱符号是可选的保险丝:
1 | |
构建脚本保证优先级:sdf_meth.c 一参与编译就是强符号,弱符号被忽略;只有无任何实现时弱符号才兜底。
Configure 脚本与 build.info
Configure 脚本 负责全局决策:启用/禁用模块(生成宏定义供 C 程序使用)、动态库路径(-L)、头文件路径(-I)、链接库名(-l)。
build.info 负责模块间依赖指定。每个子目录有自己的 build.info,Perl 脚本自动收集所有分散的 build.info 统一生成最终 Makefile。这种设计牺牲了一些集中性,换来了模块独立、易维护、自动化友好。
在 Configure 中启用 SDF 支持:
1 | |
DSO 机制默认加载名为 "sdf" 的动态库。配置完成后使用 openssl sdf 命令测试。