开源之夏-5

这周见老师了。驻扎这里学习。
外边世界真的好大好大!

今天是周一,总结一下上周。

我准备把sdf与tsapi部分移出来,新建一个项目。
软实现调通之后,实现命令行应用,之后进行69个接口的再完善。

(自己软实现SDF)我去用Evp接口,手动实现SDF,并在编译/链接时就绑到程序里,那这套 SDF 接口就变成了 “编译期静态绑定” 的多态实现。
(已有软实现SDF)我拿到GmSSL的子项目,学习cmake工具,学习build.info,(Tongsuo和GmSSL构建系统的工具)

把SoftSDF软实现尝试动态加载到Tongsuo,跑通SDF后端实现,完善SDF标准,完成接口解析,完成命令行应用。

这个位置基本处于Tongsuo库的腰部,上边去打通命令行应用,下边完成接口标准适配与动态库解析。
现在是8月18日,好的。时限过半,明确了整个实现思路~。

C的面向对象基础

这个项目很典型的使用了C的面向对象写法。
函数指针结构体是这里的一种实现思路。其它的思路方法老师还提到有泛指针……

需要的能力 C 原生缺失 函数指针结构体如何补
封装 class struct { data..., fn_pointers... }
回调 无闭包/委托 成员放 void (*cb)(void *)
多态 无虚函数 手动 vtable(结构体里放函数指针表)
插件 无接口/反射 结构体当接口,动态库填充后传递

函数指针类型的typedef定义

为一个函数指针类型定义一个别名,方便后续使用:

1
2
3
4
typedef int (*SDF_ExportSignPublicKey_ECC_fn)(void *hSessionHandle,
unsigned int uiKeyIndex, OSSL_ECCrefPublicKey *pucPublicKey);
// 怀疑人生prompt
// 这种写法叫什么?这还是C吗,怎么跟我学到的typedef定义方式不一样?还有什么这样的基础语法点我不知道?你是怎么知道的?

效果上:

1
2
3
SDF_ExportSignPublicKey_ECC_fn myFuncPtr;
// 相当于:
// int (*myFuncPtr)(void*, unsigned int, OSSL_ECCrefPublicKey*);

语法上:1970 年代就合法,最早出现在 Unix 内核 里,用来在 纯 C 里“山寨”面向对象、回调、虚函数表
工程上:凡是“需要把数据 + 操作打包在一起”的 C 工程,都会用这一招做“面向对象、回调、多态”。

C 语言没有“类、虚函数、接口、插件机制”这些高级概念,但“数据结构 + 函数指针表”恰好能一次性补齐这些能力,而且代价极低——不依赖语言特性,只靠最普通的指针和结构体。

1
2
3
4
5
6
7
8
9
struct sdf_method_st {
SDF_OpenDevice_fn OpenDevice;
//等价于 int (*OpenDevice)(void); // 真正的指针成员
……
};

extern SDF_METHOD ts_sdf_meth;
//声明一个外部全局变量,告诉编译器:“变量 ts_sdf_meth 的类型是 SDF_METHOD(即 struct sdf_method_st 的别名),但它的实际定义在别的源文件里,链接器阶段再找”。
//加了 extern 就只是“声明”,不会分配空间,避免重复定义。

extern语句

因为在 C 里,不带存储类说明符的“顶层”变量声明就是“定义”,定义就要给它真正分配内存;
eg: 语法规则(C17 §6.9.2/1)
位于文件作用域(translation unit)且不带 extern、static、thread_local 等存储类说明符的变量声明,如果没有初始化器,视为“tentative definition”,最终会被当成“真正的定义”并产生一块对象。

1
2
int g;               // 定义(tentative definition -> 真正定义)→ 占用空间 
// 等价于 int g = 0; 显式初始化,也是定义 → 占用空间

加了 extern 后

1
2
3
/* file2.c */
extern int g; /* 只是声明 → 不占用空间 */
//链接阶段由链接器把 file2.o 里对 g 的引用解析到 file1.o 中真正分配的那一块内存。
1
2
SDF_METHOD ts_sdf_meth;      /* 定义 → 生成符号并为整个结构体分配空间 */
extern SDF_METHOD ts_sdf_meth; /* 声明 → 只生成符号引用,不分配空间 */

这个定义在sdf.h中完成。
extern 之所以在这些场景里“无孔不入”,并不是因为它本身和“面向对象 / 回调 / 多态 / 插件”有什么直接语义关系,而是因为它解决了 “跨文件共享唯一实例” 这一 C 语言在构建大型模块化系统时的刚需。

多态

统一接口,不同实现

在 C++ 里用虚函数表;在 C 里手动造一张表:

1
2
3
4
5
6
7
8
9
10
11
12
struct AnimalVTable {
void (*speak)(Animal *a);
void (*walk)(Animal *a);
};

struct Animal {
struct AnimalVTable *vtbl;
/* 数据成员 */
};
// 运行时把 vtbl 指向 DogVTable、CatVTable,就达到“同一接口,不同表现”的虚函数效果。
// Linux 的 file_operations、你看到的 sdf_method_st 都是这种思路。

这个方法就是,sdf_lib.c里边对软实现/硬实现/插装降级选择机制的实现方法。

多态接口表(Linux file_operations、SDF_METHOD 等)

1
2
/* sdf_core.h */
extern const SDF_METHOD ts_sdf_meth; /* 声明 */

原因:
每个驱动或后端都会提供一个 const 全局对象(只读 vtable),内核或上层库需要在别的 .c 里统一调用。
不把 extern 写好,就等于把“接口”做成“副本”,既浪费内存又导致符号冲突。

插件就是“用动态链接库做实现载体”的运行时多态。

  • 多态的核心 = 同一接口(函数签名 / 虚表 / 协议)+ 不同实现。
  • 实现方式 可以是:
    1. 编译期静态绑定(C++ 虚函数表、内核里的 file_operations 常量表)。
    2. 运行期动态绑定(插件:把接口表里的函数指针换成 dlopen/dlsym 解析到的地址)。

因此,插件只是 “多态 + 动态加载” 的组合,本质上仍是多态。

在这个项目中,x_OpenDevice类函数插桩是一种使用多态技术进行降级实现方法。
而我要实现的去适配厂商库,是使用多态技术进行运行期动态绑定 => 插件

而我的待定任务,软实现SDF,可以选择:

  • 编译期静态绑定(C++ 虚函数表、内核里的 file_operations 常量表)。
  • 运行期动态绑定(插件:把接口表里的函数指针换成 dlopen/dlsym 解析到的地址)。

对应我现在的解决思路:
我准备把sdf与tsapi部分移出来,新建一个项目。
软实现调通之后,实现命令行应用,之后进行69个接口的再完善。

(自己软实现SDF)我去用Evp接口,手动实现SDF,并在编译/链接时就绑到程序里,那这套 SDF 接口就变成了 “编译期静态绑定” 的多态实现。
(已有软实现SDF)我拿到GmSSL的子项目,学习cmake工具,学习build.info,(Tongsuo和GmSSL构建系统的工具)

把SoftSDF软实现尝试动态加载到Tongsuo,跑通SDF后端实现,完善SDF标准,完成接口解析,完成命令行应用。

这个位置基本处于Tongsuo库的腰部,上边去打通命令行应用,下边完成接口标准适配与动态库解析。
现在是8月18日,好的。时限过半,明确了整个实现思路~。

插件

上文中说 运行时把 vtbl 指向 DogVTable、CatVTable,就达到“同一接口,不同表现”的虚函数效果。
动态替换实现,就是这里“指向”的含义。

插件的本质:运行时把符号解析到新的函数地址;

函数指针结构体正好充当“接口契约”:

  • 主程序声明一个 struct plugin_ops
  • 插件 .so 里定义并填充该结构体;
  • dlopen/dlsym 后把结构体地址交给主程序,主程序无需重新编译即可调用新功能。
    1
    2
    struct plugin_ops *ops = dlsym(handle, "plugin_entry");
    ops->init();

DSO机制

这是用 C 写的插件机制”的完整执行流程里最关键的两步——加载动态库(.so/.dll) 和 把库里实现的函数挂到结构体函数指针表上。

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
//sdf_lib.c
#ifdef SDF_LIB
# ifdef SDF_LIB_SHARED
static DSO *sdf_dso = NULL;
# else
extern int SDF_OpenDevice(void **phDeviceHandle) __attribute__((weak));
……
# endif

static CRYPTO_ONCE sdf_lib_once = CRYPTO_ONCE_STATIC_INIT;
static SDF_METHOD sdfm;

DEFINE_RUN_ONCE_STATIC(ossl_sdf_lib_init)
{
# ifdef SDF_LIB_SHARED
# ifndef LIBSDF
# define LIBSDF "sdf"
sdf_dso = DSO_load(NULL, LIBSDF, NULL, 0);
if (sdf_dso != NULL) {
sdfm.OpenDevice = DSO_bind_func(sdf_dso, "SDF_OpenDevice");
}
# else
sdfm.OpenDevice = SDF_OpenDevice;
# endif
return 1;
}
1
sdf_dso = DSO_load(NULL, LIBSDF, NULL ,0);
  • DSO_load ≈ POSIX 的 dlopen / Windows 的 LoadLibrary:
    把磁盘上的 libsdf.so(或 sdf.dll)映射进当前进程地址空间。
1
sdfm.OpenDevice = DSO_bind_func(sdf_dso, "SDF_OpenDevice");
  • DSO_bind_func ≈ dlsym / GetProcAddress:
    在刚才加载的库里查找符号 SDF_OpenDevice 的地址,并填入全局结构体 sdfm 的对应成员(函数指针)。

填完后,sdfm 就成了一张完整的虚函数表,主程序以后通过 sdfm.OpenDevice(…) 就能调用 插件里的实现,而无需在编译期链接到任何具体实现。

  1. 插件系统概念你的代码片段体现
    插件文件libsdf.so
    插件入口/接口表struct sdf_method_st sdfm;
    运行时加载DSO_load
    符号解析DSO_bind_func
    统一调用接口通过 sdfm.xxx(...)
  2. 小结
    因此,这段代码就是“插件机制在 C 中的落地实现”

  • 编译期只面对 struct sdf_method_st 这张接口表;
  • 运行期才把真正的实现从动态库里“插进来”,实现了解耦、可替换、可扩展。

开源之夏-5
https://43.242.201.154/2025/08/15/开源之夏-5/
Author
Dong
Posted on
August 15, 2025
Licensed under