插件接口设计:C ABI、函数表与受控 C++ 接口 / Plugin Interface Design with C ABI, Function Tables, and Controlled C++ Interfaces
1. 为什么接口边界比类设计更重要
插件系统最容易犯的错误,是把“我在同一个工程里能调用一个 C++ 对象”误认为“这个对象可以作为长期插件 ABI”。
宿主和插件不是普通的两个源文件。它们可能由不同编译器、不同标准库、不同运行库、不同优化选项、不同异常模型、不同分配器构建。只要其中一个条件变化,C++ 类布局、vtable、RTTI、异常和 STL 类型都可能变成风险。
所以插件接口设计的第一原则是:
public plugin boundary should be smaller and more stable than internal C++ code插件内部当然可以使用现代 C++、模板、STL、继承、异常、协程、Qt 或第三方库。问题只在边界:哪些数据和函数跨过 DLL/SO 边界。
2. 最简单心智模型
host process
|
| load library
| find exported C symbol
v
plugin_query_api_v1()
|
v
versioned function table
|
+-- init(ctx)
+-- register_capabilities(registry)
+-- process(request, response)
+-- cancel(token)
+-- shutdown()这个模型故意不把 C++ 对象指针作为第一等边界。宿主先拿到一个 C 符号,再通过结构体大小、API 版本和能力位协商,最后调用函数表。函数表背后可以是 C++ 对象,但宿主不需要知道对象布局。
3. 核心术语
C ABI:用 C 语言调用约定、符号名和简单数据布局表达的二进制接口。它不是绝对跨平台魔法,但比 C++ 类 ABI 更容易控制。
函数表:一个结构体,里面放函数指针。它相当于手写 vtable,但字段顺序、结构体大小和版本由我们显式管理。
struct_size:结构体第一个常见字段,表示调用方传入或插件返回的结构体大小。它允许新版本在尾部添加字段,同时老版本仍能识别已知部分。
能力位:插件声明自己支持哪些可选功能,例如 streaming、cancel、hot_reload、gpu_acceleration。能力位比“猜函数是否为空”更利于诊断。
受控 C++ 接口:宿主和插件使用同一 ABI 家族、同一编译器策略、同一运行库约束,并明确禁止跨边界 STL/异常/分配器时,才考虑 C++ 纯虚接口。
4. 三种接口层级
| 层级 | 形式 | 适合场景 | 风险 |
|---|---|---|---|
| 裸函数 | 每个函数单独 dlsym/GetProcAddress | 很小 demo 或单函数扩展 | 无元数据、无版本协商、调用点分散 |
| C ABI 函数表 | 一个导出入口返回版本化函数表 | 默认推荐的跨编译器插件边界 | 需要手写结构和错误码 |
| C++ 纯虚接口 | extern "C" 工厂返回接口指针 | 受控工具链内部生态 | vtable、异常、RTTI、分配器、STL 风险 |
注意:COM 不是“任意 C++ 纯虚类”。COM 依赖固定调用约定、IUnknown、QueryInterface、GUID、AddRef/Release 引用计数和平台 ABI 规范。把普通 C++ 抽象类叫成 COM 风格,会漏掉最关键的契约。
5. 推荐的 C ABI 函数表示例
#pragma once
#include <stddef.h>
#include <stdint.h>
#ifdef _WIN32
#define PLUGIN_EXPORT extern "C" __declspec(dllexport)
#else
#define PLUGIN_EXPORT extern "C" __attribute__((visibility("default")))
#endif
enum plugin_status {
PLUGIN_OK = 0,
PLUGIN_ERROR_INVALID_ARGUMENT = 1,
PLUGIN_ERROR_UNSUPPORTED_VERSION = 2,
PLUGIN_ERROR_INTERNAL = 3
};
struct host_services_v1 {
uint32_t api_version;
size_t struct_size;
void (*log)(int level, const char* message);
void* (*alloc)(size_t bytes);
void (*free)(void* ptr);
};
struct plugin_descriptor_v1 {
uint32_t api_version;
size_t struct_size;
const char* id;
const char* name;
const char* version;
uint64_t capabilities;
};
struct plugin_api_v1 {
uint32_t api_version;
size_t struct_size;
plugin_status (*init)(const host_services_v1* host);
plugin_status (*describe)(plugin_descriptor_v1* out);
plugin_status (*process)(const char* input, char* output, size_t output_size);
plugin_status (*shutdown)();
};
PLUGIN_EXPORT plugin_status plugin_query_api_v1(
const host_services_v1* host,
plugin_api_v1* out_api);这里有几个故意设计:
- 入口函数是 C 符号,宿主能稳定查找;
- 结构体带 api_version 和 struct_size;
- 字符串跨边界只使用 const char* 或宿主提供的缓冲区;
- 分配函数由宿主服务表提供时,释放责任也清楚;
- 所有函数返回错误码,不抛 C++ 异常过边界。
6. 宿主加载顺序
discover candidate file
-> read manifest before loading
-> check signature/hash/path policy
-> load library with local symbols
-> find plugin_query_api_v1
-> pass HostServices
-> check api_version/struct_size/capabilities
-> init
-> register commands or processors
-> run
-> shutdown
-> unload only after references drainmanifest 预校验很重要。宿主不应该先加载任意 DLL/SO 再问“你是谁”。加载本身就可能执行运行时初始化代码,因此安全系统至少要在加载前检查路径、签名、hash、插件 id、版本和权限声明。
7. C++ 纯虚接口什么时候可以用
C++ 纯虚接口可以在下面条件同时满足时作为工程折中:
- 宿主和插件由同一编译器 ABI 家族构建;
- 发布矩阵固定运行库、标准库、异常和 RTTI 策略;
- 接口类没有数据成员,尽量没有多重继承;
- 不在接口参数或返回值中出现 STL、异常对象、智能指针或跨模块分配对象;
- 对象必须由创建它的模块销毁;
- 每次接口变更都进入 ABI 兼容审查和旧插件测试。
这不是“跨编译器安全”,而是“受控环境下可维护”。如果插件生态面向第三方供应商、未知编译器或长期二进制兼容,默认选择 C ABI 函数表。
8. 错误与异常边界
插件内部可以抛异常,但必须在边界内捕获:
static plugin_status process_impl(const char* input, char* output, size_t output_size) {
try {
if (!input || !output || output_size == 0) {
return PLUGIN_ERROR_INVALID_ARGUMENT;
}
// internal C++ code may use std::string, exceptions, libraries
return PLUGIN_OK;
} catch (...) {
return PLUGIN_ERROR_INTERNAL;
}
}不要让异常穿过宿主和插件边界。不同编译器、运行库和优化配置下,异常展开、RTTI 匹配和析构顺序都可能不同。
9. 所有权规则
host allocated memory -> host frees
plugin allocated memory -> plugin frees
shared output buffer -> allocator owner is explicit最简单的做法是宿主提供输出缓冲区,插件写入并返回状态。大对象可以使用句柄:宿主只拿到 opaque handle,再通过 destroy 函数释放。
10. 版本演进策略
函数表最适合尾部扩展:
struct plugin_api_v2 {
uint32_t api_version;
size_t struct_size;
plugin_status (*init)(const host_services_v1*);
plugin_status (*describe)(plugin_descriptor_v1*);
plugin_status (*process)(const char*, char*, size_t);
plugin_status (*shutdown)();
plugin_status (*cancel)(uint64_t request_id); // v2 appended
};老宿主看到 struct_size 只读自己知道的字段;新宿主发现 size 不足,就知道 cancel 不可用。不要在中间插字段,也不要改变已有字段语义。
11. 常见错误
- 把
extern "C"工厂函数返回 C++ 对象指针,说成跨编译器安全; - 接口中传递
std::string、std::vector、异常、智能指针; - 插件
new,宿主delete; - 没有
api_version和struct_size,导致无法平滑演进; - 加载后才检查插件是否可信;
- unload 前没有停止线程、撤销回调、排空引用;
- 把 COM 简化为“纯虚类”,漏掉 GUID、引用计数和调用约定。
12. 练习
- 把一个
IPlugin* create()接口改成plugin_query_api_v1()函数表。 - 给函数表新增
cancel能力,要求老宿主仍能加载老插件。 - 故意让插件返回过大的 struct_size,宿主应拒绝并打印清晰错误。
- 写一个 manifest,加载前检查插件 id、api version、sha256 和 capabilities。
- 在插件内部抛异常,确认边界函数捕获并返回错误码。
13. 总结
插件接口设计的核心,是把边界做小、做显式、做可协商。C ABI 函数表不是最优雅的写法,却是最容易跨编译器、跨供应商、跨版本长期维护的写法。C++ 纯虚接口可以用,但只能在受控 ABI 环境里用,并且必须接受 ABI policy、兼容矩阵和旧插件测试约束。
15. 工程深化:从示例走向可维护插件生态
《插件接口设计》真正要训练的不是记住某个 API 名字,而是能把插件边界写成长期可执行的工程契约。下面这些切片可以作为 code review、实验和发布检查的素材。
design contract -> implementation -> failure injection -> CI evidence -> release rule深化切片 1:ABI policy 模板
**工程问题。**把插件接口分成 stable、experimental、internal 三类。stable 字段只能尾部扩展;experimental 需要能力位保护;internal 不允许第三方依赖。
**最小实验。**建立一个只包含宿主、一个插件、一个 manifest 和一条构建命令的小目录。先让正确路径通过,再故意破坏一个变量,例如版本号、结构体大小、入口符号、权限声明、线程关闭或输出缓冲区。
**观察证据。**记录宿主日志、动态加载错误、返回状态码、插件 id、api version、capabilities 和当前生命周期状态。错误消息应能让维护者知道失败发生在发现、加载、协商、初始化、运行、关闭还是卸载阶段。
candidate plugin
-> check one boundary
-> inject one failure
-> record one reproducible proof**复盘问题。**如果这个插件由第三方供应商构建、两年后升级、或在 Windows/Linux 两个平台发布,这条规则是否仍然能保护宿主?如果不能,应把它升级为 manifest 字段、ABI policy、CI 测试或加载器状态检查。
深化切片 2:HostServices 设计
**工程问题。**宿主服务表只放跨边界必需能力,例如日志、内存、注册、时间、取消检查。不要把整个宿主 C++ 对象指针塞给插件。
**最小实验。**建立一个只包含宿主、一个插件、一个 manifest 和一条构建命令的小目录。先让正确路径通过,再故意破坏一个变量,例如版本号、结构体大小、入口符号、权限声明、线程关闭或输出缓冲区。
**观察证据。**记录宿主日志、动态加载错误、返回状态码、插件 id、api version、capabilities 和当前生命周期状态。错误消息应能让维护者知道失败发生在发现、加载、协商、初始化、运行、关闭还是卸载阶段。
candidate plugin
-> check one boundary
-> inject one failure
-> record one reproducible proof**复盘问题。**如果这个插件由第三方供应商构建、两年后升级、或在 Windows/Linux 两个平台发布,这条规则是否仍然能保护宿主?如果不能,应把它升级为 manifest 字段、ABI policy、CI 测试或加载器状态检查。
深化切片 3:错误码设计
**工程问题。**错误码要稳定、可记录、可翻译。插件内部异常应转换成错误码,并通过 last_error 或诊断回调提供细节。
**最小实验。**建立一个只包含宿主、一个插件、一个 manifest 和一条构建命令的小目录。先让正确路径通过,再故意破坏一个变量,例如版本号、结构体大小、入口符号、权限声明、线程关闭或输出缓冲区。
**观察证据。**记录宿主日志、动态加载错误、返回状态码、插件 id、api version、capabilities 和当前生命周期状态。错误消息应能让维护者知道失败发生在发现、加载、协商、初始化、运行、关闭还是卸载阶段。
candidate plugin
-> check one boundary
-> inject one failure
-> record one reproducible proof**复盘问题。**如果这个插件由第三方供应商构建、两年后升级、或在 Windows/Linux 两个平台发布,这条规则是否仍然能保护宿主?如果不能,应把它升级为 manifest 字段、ABI policy、CI 测试或加载器状态检查。
深化切片 4:字符串与缓冲区
**工程问题。*输入可用 const char 加长度,输出优先由宿主提供缓冲区;需要动态输出时必须明确 allocator owner。
**最小实验。**建立一个只包含宿主、一个插件、一个 manifest 和一条构建命令的小目录。先让正确路径通过,再故意破坏一个变量,例如版本号、结构体大小、入口符号、权限声明、线程关闭或输出缓冲区。
**观察证据。**记录宿主日志、动态加载错误、返回状态码、插件 id、api version、capabilities 和当前生命周期状态。错误消息应能让维护者知道失败发生在发现、加载、协商、初始化、运行、关闭还是卸载阶段。
candidate plugin
-> check one boundary
-> inject one failure
-> record one reproducible proof**复盘问题。**如果这个插件由第三方供应商构建、两年后升级、或在 Windows/Linux 两个平台发布,这条规则是否仍然能保护宿主?如果不能,应把它升级为 manifest 字段、ABI policy、CI 测试或加载器状态检查。
深化切片 5:能力协商
**工程问题。**能力位用于表达可选功能,宿主不能只因为函数指针非空就默认可用;能力、版本和 struct_size 要一起检查。
**最小实验。**建立一个只包含宿主、一个插件、一个 manifest 和一条构建命令的小目录。先让正确路径通过,再故意破坏一个变量,例如版本号、结构体大小、入口符号、权限声明、线程关闭或输出缓冲区。
**观察证据。**记录宿主日志、动态加载错误、返回状态码、插件 id、api version、capabilities 和当前生命周期状态。错误消息应能让维护者知道失败发生在发现、加载、协商、初始化、运行、关闭还是卸载阶段。
candidate plugin
-> check one boundary
-> inject one failure
-> record one reproducible proof**复盘问题。**如果这个插件由第三方供应商构建、两年后升级、或在 Windows/Linux 两个平台发布,这条规则是否仍然能保护宿主?如果不能,应把它升级为 manifest 字段、ABI policy、CI 测试或加载器状态检查。
深化切片 6:COM 对比
**工程问题。**COM 的稳定来自 IUnknown、GUID、QueryInterface、AddRef/Release 和调用约定,不是来自普通 C++ 纯虚类本身。
**最小实验。**建立一个只包含宿主、一个插件、一个 manifest 和一条构建命令的小目录。先让正确路径通过,再故意破坏一个变量,例如版本号、结构体大小、入口符号、权限声明、线程关闭或输出缓冲区。
**观察证据。**记录宿主日志、动态加载错误、返回状态码、插件 id、api version、capabilities 和当前生命周期状态。错误消息应能让维护者知道失败发生在发现、加载、协商、初始化、运行、关闭还是卸载阶段。
candidate plugin
-> check one boundary
-> inject one failure
-> record one reproducible proof**复盘问题。**如果这个插件由第三方供应商构建、两年后升级、或在 Windows/Linux 两个平台发布,这条规则是否仍然能保护宿主?如果不能,应把它升级为 manifest 字段、ABI policy、CI 测试或加载器状态检查。
深化切片 7:C++ 接口适用条件
**工程问题。**同一编译器 ABI、同一运行库、固定异常/RTTI 策略、禁止 STL 过边界、旧插件测试都满足时,才考虑纯虚接口。
**最小实验。**建立一个只包含宿主、一个插件、一个 manifest 和一条构建命令的小目录。先让正确路径通过,再故意破坏一个变量,例如版本号、结构体大小、入口符号、权限声明、线程关闭或输出缓冲区。
**观察证据。**记录宿主日志、动态加载错误、返回状态码、插件 id、api version、capabilities 和当前生命周期状态。错误消息应能让维护者知道失败发生在发现、加载、协商、初始化、运行、关闭还是卸载阶段。
candidate plugin
-> check one boundary
-> inject one failure
-> record one reproducible proof**复盘问题。**如果这个插件由第三方供应商构建、两年后升级、或在 Windows/Linux 两个平台发布,这条规则是否仍然能保护宿主?如果不能,应把它升级为 manifest 字段、ABI policy、CI 测试或加载器状态检查。
深化切片 8:接口扩展策略
**工程问题。**优先尾部新增函数表字段或新增 query_api_v2;不要改变已有字段含义,不要在结构体中间插字段。
**最小实验。**建立一个只包含宿主、一个插件、一个 manifest 和一条构建命令的小目录。先让正确路径通过,再故意破坏一个变量,例如版本号、结构体大小、入口符号、权限声明、线程关闭或输出缓冲区。
**观察证据。**记录宿主日志、动态加载错误、返回状态码、插件 id、api version、capabilities 和当前生命周期状态。错误消息应能让维护者知道失败发生在发现、加载、协商、初始化、运行、关闭还是卸载阶段。
candidate plugin
-> check one boundary
-> inject one failure
-> record one reproducible proof**复盘问题。**如果这个插件由第三方供应商构建、两年后升级、或在 Windows/Linux 两个平台发布,这条规则是否仍然能保护宿主?如果不能,应把它升级为 manifest 字段、ABI policy、CI 测试或加载器状态检查。
深化切片 9:卸载前置条件
**工程问题。**接口设计阶段就要决定 shutdown、cancel、release、callback revoke 的顺序,否则加载器只能靠约定猜。
**最小实验。**建立一个只包含宿主、一个插件、一个 manifest 和一条构建命令的小目录。先让正确路径通过,再故意破坏一个变量,例如版本号、结构体大小、入口符号、权限声明、线程关闭或输出缓冲区。
**观察证据。**记录宿主日志、动态加载错误、返回状态码、插件 id、api version、capabilities 和当前生命周期状态。错误消息应能让维护者知道失败发生在发现、加载、协商、初始化、运行、关闭还是卸载阶段。
candidate plugin
-> check one boundary
-> inject one failure
-> record one reproducible proof**复盘问题。**如果这个插件由第三方供应商构建、两年后升级、或在 Windows/Linux 两个平台发布,这条规则是否仍然能保护宿主?如果不能,应把它升级为 manifest 字段、ABI policy、CI 测试或加载器状态检查。
深化切片 10:最小反例
**工程问题。**写一个用 std::string 做返回值的接口,再用不同运行库构建宿主和插件,观察崩溃或内存破坏风险。
**最小实验。**建立一个只包含宿主、一个插件、一个 manifest 和一条构建命令的小目录。先让正确路径通过,再故意破坏一个变量,例如版本号、结构体大小、入口符号、权限声明、线程关闭或输出缓冲区。
**观察证据。**记录宿主日志、动态加载错误、返回状态码、插件 id、api version、capabilities 和当前生命周期状态。错误消息应能让维护者知道失败发生在发现、加载、协商、初始化、运行、关闭还是卸载阶段。
candidate plugin
-> check one boundary
-> inject one failure
-> record one reproducible proof**复盘问题。**如果这个插件由第三方供应商构建、两年后升级、或在 Windows/Linux 两个平台发布,这条规则是否仍然能保护宿主?如果不能,应把它升级为 manifest 字段、ABI policy、CI 测试或加载器状态检查。
深化切片 11:文档化
**工程问题。**接口头文件旁边应放 ABI policy、版本历史、字段扩展规则、错误码表和所有权表。
**最小实验。**建立一个只包含宿主、一个插件、一个 manifest 和一条构建命令的小目录。先让正确路径通过,再故意破坏一个变量,例如版本号、结构体大小、入口符号、权限声明、线程关闭或输出缓冲区。
**观察证据。**记录宿主日志、动态加载错误、返回状态码、插件 id、api version、capabilities 和当前生命周期状态。错误消息应能让维护者知道失败发生在发现、加载、协商、初始化、运行、关闭还是卸载阶段。
candidate plugin
-> check one boundary
-> inject one failure
-> record one reproducible proof**复盘问题。**如果这个插件由第三方供应商构建、两年后升级、或在 Windows/Linux 两个平台发布,这条规则是否仍然能保护宿主?如果不能,应把它升级为 manifest 字段、ABI policy、CI 测试或加载器状态检查。
深化切片 12:审查口径
**工程问题。**code review 不只看函数好不好用,还要看每个字段能否跨编译器、跨版本、跨进程诊断。
**最小实验。**建立一个只包含宿主、一个插件、一个 manifest 和一条构建命令的小目录。先让正确路径通过,再故意破坏一个变量,例如版本号、结构体大小、入口符号、权限声明、线程关闭或输出缓冲区。
**观察证据。**记录宿主日志、动态加载错误、返回状态码、插件 id、api version、capabilities 和当前生命周期状态。错误消息应能让维护者知道失败发生在发现、加载、协商、初始化、运行、关闭还是卸载阶段。
candidate plugin
-> check one boundary
-> inject one failure
-> record one reproducible proof**复盘问题。**如果这个插件由第三方供应商构建、两年后升级、或在 Windows/Linux 两个平台发布,这条规则是否仍然能保护宿主?如果不能,应把它升级为 manifest 字段、ABI policy、CI 测试或加载器状态检查。
深化切片 13:ABI policy 模板
**工程问题。**把插件接口分成 stable、experimental、internal 三类。stable 字段只能尾部扩展;experimental 需要能力位保护;internal 不允许第三方依赖。
**最小实验。**建立一个只包含宿主、一个插件、一个 manifest 和一条构建命令的小目录。先让正确路径通过,再故意破坏一个变量,例如版本号、结构体大小、入口符号、权限声明、线程关闭或输出缓冲区。
**观察证据。**记录宿主日志、动态加载错误、返回状态码、插件 id、api version、capabilities 和当前生命周期状态。错误消息应能让维护者知道失败发生在发现、加载、协商、初始化、运行、关闭还是卸载阶段。
candidate plugin
-> check one boundary
-> inject one failure
-> record one reproducible proof**复盘问题。**如果这个插件由第三方供应商构建、两年后升级、或在 Windows/Linux 两个平台发布,这条规则是否仍然能保护宿主?如果不能,应把它升级为 manifest 字段、ABI policy、CI 测试或加载器状态检查。
深化切片 14:HostServices 设计
**工程问题。**宿主服务表只放跨边界必需能力,例如日志、内存、注册、时间、取消检查。不要把整个宿主 C++ 对象指针塞给插件。
**最小实验。**建立一个只包含宿主、一个插件、一个 manifest 和一条构建命令的小目录。先让正确路径通过,再故意破坏一个变量,例如版本号、结构体大小、入口符号、权限声明、线程关闭或输出缓冲区。
**观察证据。**记录宿主日志、动态加载错误、返回状态码、插件 id、api version、capabilities 和当前生命周期状态。错误消息应能让维护者知道失败发生在发现、加载、协商、初始化、运行、关闭还是卸载阶段。
candidate plugin
-> check one boundary
-> inject one failure
-> record one reproducible proof**复盘问题。**如果这个插件由第三方供应商构建、两年后升级、或在 Windows/Linux 两个平台发布,这条规则是否仍然能保护宿主?如果不能,应把它升级为 manifest 字段、ABI policy、CI 测试或加载器状态检查。
深化切片 15:错误码设计
**工程问题。**错误码要稳定、可记录、可翻译。插件内部异常应转换成错误码,并通过 last_error 或诊断回调提供细节。
**最小实验。**建立一个只包含宿主、一个插件、一个 manifest 和一条构建命令的小目录。先让正确路径通过,再故意破坏一个变量,例如版本号、结构体大小、入口符号、权限声明、线程关闭或输出缓冲区。
**观察证据。**记录宿主日志、动态加载错误、返回状态码、插件 id、api version、capabilities 和当前生命周期状态。错误消息应能让维护者知道失败发生在发现、加载、协商、初始化、运行、关闭还是卸载阶段。
candidate plugin
-> check one boundary
-> inject one failure
-> record one reproducible proof**复盘问题。**如果这个插件由第三方供应商构建、两年后升级、或在 Windows/Linux 两个平台发布,这条规则是否仍然能保护宿主?如果不能,应把它升级为 manifest 字段、ABI policy、CI 测试或加载器状态检查。
深化切片 16:字符串与缓冲区
**工程问题。*输入可用 const char 加长度,输出优先由宿主提供缓冲区;需要动态输出时必须明确 allocator owner。
**最小实验。**建立一个只包含宿主、一个插件、一个 manifest 和一条构建命令的小目录。先让正确路径通过,再故意破坏一个变量,例如版本号、结构体大小、入口符号、权限声明、线程关闭或输出缓冲区。
**观察证据。**记录宿主日志、动态加载错误、返回状态码、插件 id、api version、capabilities 和当前生命周期状态。错误消息应能让维护者知道失败发生在发现、加载、协商、初始化、运行、关闭还是卸载阶段。
candidate plugin
-> check one boundary
-> inject one failure
-> record one reproducible proof**复盘问题。**如果这个插件由第三方供应商构建、两年后升级、或在 Windows/Linux 两个平台发布,这条规则是否仍然能保护宿主?如果不能,应把它升级为 manifest 字段、ABI policy、CI 测试或加载器状态检查。
深化切片 17:能力协商
**工程问题。**能力位用于表达可选功能,宿主不能只因为函数指针非空就默认可用;能力、版本和 struct_size 要一起检查。
**最小实验。**建立一个只包含宿主、一个插件、一个 manifest 和一条构建命令的小目录。先让正确路径通过,再故意破坏一个变量,例如版本号、结构体大小、入口符号、权限声明、线程关闭或输出缓冲区。
**观察证据。**记录宿主日志、动态加载错误、返回状态码、插件 id、api version、capabilities 和当前生命周期状态。错误消息应能让维护者知道失败发生在发现、加载、协商、初始化、运行、关闭还是卸载阶段。
candidate plugin
-> check one boundary
-> inject one failure
-> record one reproducible proof**复盘问题。**如果这个插件由第三方供应商构建、两年后升级、或在 Windows/Linux 两个平台发布,这条规则是否仍然能保护宿主?如果不能,应把它升级为 manifest 字段、ABI policy、CI 测试或加载器状态检查。
深化切片 18:COM 对比
**工程问题。**COM 的稳定来自 IUnknown、GUID、QueryInterface、AddRef/Release 和调用约定,不是来自普通 C++ 纯虚类本身。
**最小实验。**建立一个只包含宿主、一个插件、一个 manifest 和一条构建命令的小目录。先让正确路径通过,再故意破坏一个变量,例如版本号、结构体大小、入口符号、权限声明、线程关闭或输出缓冲区。
**观察证据。**记录宿主日志、动态加载错误、返回状态码、插件 id、api version、capabilities 和当前生命周期状态。错误消息应能让维护者知道失败发生在发现、加载、协商、初始化、运行、关闭还是卸载阶段。
candidate plugin
-> check one boundary
-> inject one failure
-> record one reproducible proof**复盘问题。**如果这个插件由第三方供应商构建、两年后升级、或在 Windows/Linux 两个平台发布,这条规则是否仍然能保护宿主?如果不能,应把它升级为 manifest 字段、ABI policy、CI 测试或加载器状态检查。
16. 收束检查
- 是否存在一个唯一、稳定、可查找的 C ABI 入口?
- 是否在加载前完成 manifest、路径、版本和信任检查?
- 是否用
api_version、struct_size和 capabilities 完成协商? - 是否明确内存、错误、异常、线程、回调和卸载责任?
- 是否有坏插件、旧插件、新宿主、取消、shutdown 失败和回滚测试?