Skip to content
Gains Summary
Main Navigation 首页 / Home
C++ 编程 / C++ Programming
系统与高性能 / Systems & Performance
Web 开发 / Web Development
人工智能 / Artificial Intelligence
工业软件 / Industrial Software
其他内容 / Other Topics
C++ 编程 / C++系统与性能 / SystemsWeb 开发 / Web人工智能 / AI工业软件 / Industrial

外观

Sidebar Navigation

← C++ 编程 / C++ Programming

插件系统 / Plugin Systems

1. 插件工程:可演进的模块边界 / Plugin Engineering and Evolvable Module Boundaries

2. 运行时动态加载:LoadLibrary 与 dlopen —— 打开插件的大门 / Runtime Dynamic Loading with LoadLibrary and Dlopen

3. 插件接口设计:C ABI、函数表与受控 C++ 接口 / Plugin Interface Design with C ABI, Function Tables, and Controlled C++ Interfaces

4. CMake 构建完整插件项目 / Building a Complete Plugin Project with CMake

5. 插件架构设计模式:发现、生命周期、依赖与通信 / Plugin Architecture Patterns for Discovery, Lifecycle, Dependencies, and Communication

6. 跨平台插件开发:Windows / Linux / macOS 统一策略

7. 插件热加载与版本管理 / Plugin Hot Reloading and Version Management

8. 插件安全:沙箱、进程隔离与权限控制 / Plugin Security with Sandboxing, Process Isolation, and Permissions

9. 工业软件插件系统案例分析 / Industrial Software Plugin System Case Study

10. 综合实战:版本化 C ABI 插件系统 / Full Project: Versioned C ABI Plugin System

本页目录

插件热加载与版本管理 / Plugin Hot Reloading and Version Management ​

📅 创建时间:2026-07-13 🏷️ 标签:#热加载 #live-reload #版本管理 #SemVer #ABI兼容 📚 前置知识:runtime dynamic loading, plugin architecture patterns


📋 本章目标 ​

  • 理解热加载的完整流程:检测变化 → 卸载旧版 → 加载新版 → 恢复状态
  • 掌握文件变化检测方案:inotify(Linux)和 ReadDirectoryChangesW(Windows)
  • 理解热加载中的状态保持问题和解决方案
  • 掌握 SemVer 在插件版本管理中的运用
  • 学会接口版本协商与向后兼容的接口演化策略

第1部分:热加载的原理与流程 ​

1.1 完整流程 ​

┌─────────────────────────────────────────────────────────────────────────────┐
│              热加载完整流程                                                   │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  1. 检测变化                                                                 │
│  ┌───────────────────────────────────────────────────────────────┐         │
│  │ inotify / 轮询 / signal → 发现 plugin.so 文件被修改            │         │
│  └───────────────────────────────────────────────────────────────┘         │
│                     ↓                                                       │
│  2. 保存状态                                                                 │
│  ┌───────────────────────────────────────────────────────────────┐         │
│  │ 从旧插件实例中提取当前状态 → 序列化为 JSON / 二进制 blob       │         │
│  │ (如果插件是"无状态"的,这步可以跳过)                          │         │
│  └───────────────────────────────────────────────────────────────┘         │
│                     ↓                                                       │
│  3. 卸载旧版                                                                 │
│  ┌───────────────────────────────────────────────────────────────┐         │
│  │ plugin->shutdown() → destroy(plugin) → dlclose(handle)        │         │
│  │ ⚠️ 确保没有任何线程还在使用旧的插件实例!                      │         │
│  └───────────────────────────────────────────────────────────────┘         │
│                     ↓                                                       │
│  4. 加载新版                                                                 │
│  ┌───────────────────────────────────────────────────────────────┐         │
│  │ dlopen(new_path) → dlsym create → plugin->initialize()        │         │
│  └───────────────────────────────────────────────────────────────┘         │
│                     ↓                                                       │
│  5. 恢复状态                                                                 │
│  ┌───────────────────────────────────────────────────────────────┐         │
│  │ 把保存的状态注入新插件实例 → 新插件恢复工作状态                │         │
│  └───────────────────────────────────────────────────────────────┘         │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘
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

1.2 文件变化检测 ​

cpp
// ====== Linux: inotify 方案 ======
#include <sys/inotify.h>
#include <unistd.h>
#include <thread>

class FileWatcher {
    int fd;
    std::unordered_map<int, std::string> wd_to_path;
    std::function<void(const std::string&)> callback;

public:
    void watch(const std::string& path) {
        fd = inotify_init();
        int wd = inotify_add_watch(fd, path.c_str(),
            IN_CLOSE_WRITE | IN_MOVED_TO);  // 文件写完或被移入
        wd_to_path[wd] = path;
    }

    void start() {
        std::thread([this] {
            char buf[4096];
            while (true) {
                int len = read(fd, buf, sizeof(buf));
                auto* event = (inotify_event*)buf;
                if (event->mask & IN_CLOSE_WRITE) {
                    callback(wd_to_path[event->wd] + "/" + event->name);
                }
            }
        }).detach();
    }
};
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
cpp
// ====== 通用方案:轮询文件 mtime ======
class PollWatcher {
    std::unordered_map<std::string, time_t> last_mtime;
    std::chrono::seconds interval{1};

public:
    bool check(const std::string& path) {
        auto ftime = std::filesystem::last_write_time(path);
        auto t = decltype(ftime)::clock::to_time_t(ftime);
        if (last_mtime[path] != t) {
            last_mtime[path] = t;
            return true;
        }
        return false;
    }
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

1.3 状态保持 —— 热加载最棘手的问题 ​

┌─────────────────────────────────────────────────────────────────────────────┐
│              热加载中的状态问题                                               │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  问题:旧插件里有运行时状态(计算结果缓存、打开的连接、用户偏好等)            │
│        卸载旧插件 → 状态丢失 → 新插件从空白开始 → 用户体验断裂               │
│                                                                             │
│  解决方案:                                                                  │
│                                                                             │
│  方案A:序列化/反序列化                                                      │
│  ┌───────────────────────────────────────────────────────────────┐         │
│  │ IPlugin 接口增加:                                              │         │
│  │ virtual const char* saveState() = 0;    // 旧插件 → JSON      │         │
│  │ virtual bool loadState(const char*) = 0; // JSON → 新插件     │         │
│  │                                                               │         │
│  │ 流程:saved = old->saveState() → 卸载旧 → 加载新              │         │
│  │      → new->loadState(saved)                                  │         │
│  └───────────────────────────────────────────────────────────────┘         │
│                                                                             │
│  方案B:外部状态存储(推荐)                                                  │
│  ┌───────────────────────────────────────────────────────────────┐         │
│  │ 插件不持有状态 → 状态由宿主统一管理                            │         │
│  │ 插件是无状态的纯函数 → 热加载时不需要迁移状态                   │         │
│  │ 这是函数式编程思想在插件系统中的体现                            │         │
│  └───────────────────────────────────────────────────────────────┘         │
│                                                                             │
│  方案C:双缓冲 + 原子切换                                                   │
│  ┌───────────────────────────────────────────────────────────────┐         │
│  │ 1. 单独加载新版插件(不卸载旧版)                                │         │
│  │ 2. 用新版处理新请求,旧版继续服务已有连接                       │         │
│  │ 3. 旧版连接全部结束 → 卸载旧版                                  │         │
│  │ 类似 Nginx 的"优雅重载"                                         │         │
│  └───────────────────────────────────────────────────────────────┘         │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘
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

1.4 原子替换 —— 防止读到半写入的文件 ​

bash
# ❌ 危险做法:直接覆盖
cp new_plugin.so plugins/plugin.so   # 如果程序在这时加载 → 读到半截文件

# ✅ 安全做法:写入临时文件 + rename
cp new_plugin.so plugins/.plugin.so.tmp
mv plugins/.plugin.so.tmp plugins/plugin.so   # rename 是原子操作!
1
2
3
4
5
6

第2部分:版本管理 ​

2.1 SemVer 在插件中的运用 ​

┌─────────────────────────────────────────────────────────────────────────────┐
│              Semantic Versioning 在插件系统中的意义                           │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  MAJOR.MINOR.PATCH (如 2.1.3)                                               │
│                                                                             │
│  MAJOR (主版本) —— 不兼容的 API 变更                                        │
│  ┌───────────────────────────────────────────────────────────────┐         │
│  │ 1.x.x → 2.0.0: 接口签名变了、删除了旧函数、行为不兼容          │         │
│  │ 依赖该插件的其他插件可能需要修改                                │         │
│  │ Soname: libfoo.so.1 → libfoo.so.2(Linux 侧)                  │         │
│  └───────────────────────────────────────────────────────────────┘         │
│                                                                             │
│  MINOR (次版本) —— 向后兼容的新功能                                          │
│  ┌───────────────────────────────────────────────────────────────┐         │
│  │ 1.0.x → 1.1.0: 增加了新函数、旧的函数行为不变                  │         │
│  │ 依赖方不需要修改,自动受益                                      │         │
│  │ Soname 不变:libfoo.so.1(Linux 侧)                            │         │
│  └───────────────────────────────────────────────────────────────┘         │
│                                                                             │
│  PATCH (补丁版本) —— 向后兼容的 Bug 修复                                     │
│  ┌───────────────────────────────────────────────────────────────┐         │
│  │ 1.1.0 → 1.1.1: 只修 bug,不改变行为                            │         │
│  │ 通常可以安全热加载                                              │         │
│  └───────────────────────────────────────────────────────────────┘         │
│                                                                             │
│  PATCH 升级 → 可以直接热加载(安全)                                         │
│  MINOR 升级 → 可以热加载(向后兼容)                                          │
│  MAJOR 升级 → 不能热加载(接口不兼容,需要走完整的 version migration)        │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘
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

2.2 接口版本协商 ​

cpp
// 插件声明自己支持哪些 API 版本
class IPlugin {
public:
    // 返回此插件支持的最高 API 版本
    virtual int getApiVersion() { return 1; }

    // 宿主可以据此决定如何与插件交互
};

// 宿主中:
int negotiate_version(IPlugin* plugin) {
    int plugin_version = plugin->getApiVersion();
    int host_version = HOST_API_VERSION;

    // 取交集的最低版本
    return std::min(plugin_version, host_version);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

2.3 COM 风格的接口查询 ​

cpp
// IPlugin 基类增加 queryInterface 方法
class IPlugin {
public:
    // 允许宿主查询"你是否支持 ICalculatorV2 接口?"
    virtual void* queryInterface(const char* interface_name) {
        return nullptr;  // 默认不支持任何扩展接口
    }
};

// 宿主中:
auto* calc = create_calculator();
auto* v2 = static_cast<ICalculatorV2*>(calc->queryInterface("ICalculatorV2"));
if (v2) {
    v2->advancedFeature();   // 使用 v2 新功能
} else {
    calc->basicFeature();    // fallback 到基本功能
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

核心总结 ​

┌─────────────────────────────────────────────────────────────────────────────┐
│              热加载与版本管理核心速查                                          │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  热加载五步:检测 → 保存状态 → 卸载 → 加载 → 恢复状态                         │
│                                                                             │
│  文件检测:Linux inotify / 通用轮询 mtime                                     │
│                                                                             │
│  原子替换:先写 temp 再 rename(原子操作!)                                  │
│                                                                             │
│  SemVer:MAJOR.MINOR.PATCH                                                  │
│    PATCH → 直接热加载                                                        │
│    MINOR → 可以热加载(增功能)                                               │
│    MAJOR → 不能热加载(API 不兼容,需要完整迁移)                             │
│                                                                             │
│  接口演化策略:                                                               │
│    1. 版本号工厂(create(int version))                                       │
│    2. queryInterface(COM 风格)                                              │
│    3. 只增虚函数到末尾 + 不改签名                                              │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

章节测试 ​

测试1:热加载顺序 ​

为什么热加载时要按 shutdown → destroy → dlclose 的顺序,不能直接 dlclose?

测试2:原子替换 ​

为什么热加载场景下,覆盖插件文件要用"写 temp + rename"而不是直接覆盖?

测试3:SemVer ​

一个插件从 1.2.3 升级到 1.3.0。按 SemVer,接口是否兼容?可以热加载吗?从 1.3.0 升级到 2.0.0 呢?


参考答案 ​

测试1答案 ​

答案:直接 dlclose 会导致资源泄漏和潜在崩溃:(1) 插件可能在 initialize 时分配了内存、打开了文件、注册了回调——不先 shutdown 就直接卸载,这些资源就泄漏了;(2) 如果其他线程正在使用插件实例 → dlclose 后虚函数表可能无效 → 崩溃;(3) 正确的关闭顺序是 shutdown(清理资源 + 通知其他组件)→ destroy(delete 实例)→ dlclose(卸载库文件)。

测试2答案 ​

答案:直接覆盖不是原子操作,如果文件的写入速度慢于程序读取的速度(或写入一半时崩溃),程序可能加载到一个不完整的 .so 文件。而 rename() 系统调用是原子的——它只修改目录项中的文件名→inode 映射,不修改文件数据。要么指向旧文件,要么指向新文件,不存在"半新旧"的中间状态。

测试3答案 ​

答案:1.2.3 → 1.3.0:是 MINOR 升级(次版本号变了,主版本号不变),按 SemVer 是向后兼容的——旧的 API 行为不变,只增加了新功能。可以安全热加载。1.3.0 → 2.0.0:是 MAJOR 升级,API 不兼容(删除了旧函数、改了签名/行为),不能直接热加载,需要走完整的版本迁移流程。


相关笔记 ​

  • runtime dynamic loading - 运行时加载基础
  • plugin architecture patterns - 插件生命周期管理
  • plugin security - 热加载的安全考虑

下一步学习 ​

  • [ ] 阅读 12 - 插件安全与沙箱

学习状态:🟡 开始学习

工程深化:把本篇知识落到项目里 ​

这一节不是为了凑篇幅,而是把《插件热加载与版本管理 / Plugin Hot Reloading and Version Management》从“知道概念”推进到“能在项目里稳定使用”。 阅读时可以把每个知识点都追问成四件事:它保护什么边界,失败时有什么现象,怎样最小复现,怎样写进团队流程。

text
concept -> boundary -> failure signal -> minimal proof -> project rule
1

工程切片 1:最小可复现样例 ​

**场景。**围绕 load/unload 建一个很小的案例,不要一开始就放进完整业务系统。把问题压缩到一个可以提交给同事的目录,保留源码、构建命令、版本输出和预期现象。

**边界。**先判断这里讨论的是 load/unload、句柄 还是 入口函数 的责任;如果三者混在一起,先把输入、输出和所有权拆开。

text
input -> load/unload -> 句柄 -> 入口函数 -> observable result
1

**验证。**记录一条能重复运行的命令或测试,并写明成功输出和失败输出。只说“本机能跑”不够,至少要记录工具版本、构建类型和关键参数。

**常见错误。**把偶然通过当成规则、把私有实现当成公开契约、或者让错误路径绕开清理逻辑,都会让本篇主题在真实项目里变得不可维护。

**复盘问题。**如果明天换一个编译器、Qt 版本、插件版本、输入规模或发布目录,这个结论是否仍成立?不成立时,应该由文档、测试还是 CI 给出提示?

工程切片 2:接口边界 ​

**场景。**围绕 句柄 建一个很小的案例,不要一开始就放进完整业务系统。写清调用方能依赖什么、不能依赖什么,并把隐式假设转换成命名函数、配置项或测试。

**边界。**先判断这里讨论的是 句柄、入口函数 还是 引用计数 的责任;如果三者混在一起,先把输入、输出和所有权拆开。

text
input -> 句柄 -> 入口函数 -> 引用计数 -> observable result
1

**验证。**记录一条能重复运行的命令或测试,并写明成功输出和失败输出。只说“本机能跑”不够,至少要记录工具版本、构建类型和关键参数。

**常见错误。**把偶然通过当成规则、把私有实现当成公开契约、或者让错误路径绕开清理逻辑,都会让本篇主题在真实项目里变得不可维护。

**复盘问题。**如果明天换一个编译器、Qt 版本、插件版本、输入规模或发布目录,这个结论是否仍成立?不成立时,应该由文档、测试还是 CI 给出提示?

工程切片 3:失败注入 ​

**场景。**围绕 入口函数 建一个很小的案例,不要一开始就放进完整业务系统。故意制造一个常见错误,让日志、断言或测试先失败,再用修复后的证据说明规则生效。

**边界。**先判断这里讨论的是 入口函数、引用计数 还是 错误码 的责任;如果三者混在一起,先把输入、输出和所有权拆开。

text
input -> 入口函数 -> 引用计数 -> 错误码 -> observable result
1

**验证。**记录一条能重复运行的命令或测试,并写明成功输出和失败输出。只说“本机能跑”不够,至少要记录工具版本、构建类型和关键参数。

**常见错误。**把偶然通过当成规则、把私有实现当成公开契约、或者让错误路径绕开清理逻辑,都会让本篇主题在真实项目里变得不可维护。

**复盘问题。**如果明天换一个编译器、Qt 版本、插件版本、输入规模或发布目录,这个结论是否仍成立?不成立时,应该由文档、测试还是 CI 给出提示?

工程切片 4:跨平台差异 ​

**场景。**围绕 引用计数 建一个很小的案例,不要一开始就放进完整业务系统。至少比较 Windows、Linux 或不同编译器下的一个差异,记录差异属于标准、实现还是平台约定。

**边界。**先判断这里讨论的是 引用计数、错误码 还是 生命周期 的责任;如果三者混在一起,先把输入、输出和所有权拆开。

text
input -> 引用计数 -> 错误码 -> 生命周期 -> observable result
1

**验证。**记录一条能重复运行的命令或测试,并写明成功输出和失败输出。只说“本机能跑”不够,至少要记录工具版本、构建类型和关键参数。

**常见错误。**把偶然通过当成规则、把私有实现当成公开契约、或者让错误路径绕开清理逻辑,都会让本篇主题在真实项目里变得不可维护。

**复盘问题。**如果明天换一个编译器、Qt 版本、插件版本、输入规模或发布目录,这个结论是否仍成立?不成立时,应该由文档、测试还是 CI 给出提示?

工程切片 5:性能观察 ​

**场景。**围绕 错误码 建一个很小的案例,不要一开始就放进完整业务系统。用小规模和压力规模各跑一次,区分算法成本、同步成本、I/O 成本和工具链配置成本。

**边界。**先判断这里讨论的是 错误码、生命周期 还是 load/unload 的责任;如果三者混在一起,先把输入、输出和所有权拆开。

text
input -> 错误码 -> 生命周期 -> load/unload -> observable result
1

**验证。**记录一条能重复运行的命令或测试,并写明成功输出和失败输出。只说“本机能跑”不够,至少要记录工具版本、构建类型和关键参数。

**常见错误。**把偶然通过当成规则、把私有实现当成公开契约、或者让错误路径绕开清理逻辑,都会让本篇主题在真实项目里变得不可维护。

**复盘问题。**如果明天换一个编译器、Qt 版本、插件版本、输入规模或发布目录,这个结论是否仍成立?不成立时,应该由文档、测试还是 CI 给出提示?

工程切片 6:生命周期 ​

**场景。**围绕 生命周期 建一个很小的案例,不要一开始就放进完整业务系统。标出资源创建、转移、共享、停止和销毁的顺序,特别关注错误返回和提前退出路径。

**边界。**先判断这里讨论的是 生命周期、load/unload 还是 句柄 的责任;如果三者混在一起,先把输入、输出和所有权拆开。

text
input -> 生命周期 -> load/unload -> 句柄 -> observable result
1

**验证。**记录一条能重复运行的命令或测试,并写明成功输出和失败输出。只说“本机能跑”不够,至少要记录工具版本、构建类型和关键参数。

**常见错误。**把偶然通过当成规则、把私有实现当成公开契约、或者让错误路径绕开清理逻辑,都会让本篇主题在真实项目里变得不可维护。

**复盘问题。**如果明天换一个编译器、Qt 版本、插件版本、输入规模或发布目录,这个结论是否仍成立?不成立时,应该由文档、测试还是 CI 给出提示?

工程切片 7:诊断证据 ​

**场景。**围绕 load/unload 建一个很小的案例,不要一开始就放进完整业务系统。保留命令、日志、符号、栈、测试输出或截图,让结论可以被另一个环境重新验证。

**边界。**先判断这里讨论的是 load/unload、句柄 还是 入口函数 的责任;如果三者混在一起,先把输入、输出和所有权拆开。

text
input -> load/unload -> 句柄 -> 入口函数 -> observable result
1

**验证。**记录一条能重复运行的命令或测试,并写明成功输出和失败输出。只说“本机能跑”不够,至少要记录工具版本、构建类型和关键参数。

**常见错误。**把偶然通过当成规则、把私有实现当成公开契约、或者让错误路径绕开清理逻辑,都会让本篇主题在真实项目里变得不可维护。

**复盘问题。**如果明天换一个编译器、Qt 版本、插件版本、输入规模或发布目录,这个结论是否仍成立?不成立时,应该由文档、测试还是 CI 给出提示?

工程切片 8:维护策略 ​

**场景。**围绕 句柄 建一个很小的案例,不要一开始就放进完整业务系统。把一次性经验沉淀到 README、CI、脚本、示例工程或检查表里,避免只存在个人记忆中。

**边界。**先判断这里讨论的是 句柄、入口函数 还是 引用计数 的责任;如果三者混在一起,先把输入、输出和所有权拆开。

text
input -> 句柄 -> 入口函数 -> 引用计数 -> observable result
1

**验证。**记录一条能重复运行的命令或测试,并写明成功输出和失败输出。只说“本机能跑”不够,至少要记录工具版本、构建类型和关键参数。

**常见错误。**把偶然通过当成规则、把私有实现当成公开契约、或者让错误路径绕开清理逻辑,都会让本篇主题在真实项目里变得不可维护。

**复盘问题。**如果明天换一个编译器、Qt 版本、插件版本、输入规模或发布目录,这个结论是否仍成立?不成立时,应该由文档、测试还是 CI 给出提示?

工程切片 9:版本演进 ​

**场景。**围绕 入口函数 建一个很小的案例,不要一开始就放进完整业务系统。为未来变更预留兼容策略:版本号、特性位、弃用窗口、迁移脚本和回滚入口。

**边界。**先判断这里讨论的是 入口函数、引用计数 还是 错误码 的责任;如果三者混在一起,先把输入、输出和所有权拆开。

text
input -> 入口函数 -> 引用计数 -> 错误码 -> observable result
1

**验证。**记录一条能重复运行的命令或测试,并写明成功输出和失败输出。只说“本机能跑”不够,至少要记录工具版本、构建类型和关键参数。

**常见错误。**把偶然通过当成规则、把私有实现当成公开契约、或者让错误路径绕开清理逻辑,都会让本篇主题在真实项目里变得不可维护。

**复盘问题。**如果明天换一个编译器、Qt 版本、插件版本、输入规模或发布目录,这个结论是否仍成立?不成立时,应该由文档、测试还是 CI 给出提示?

工程切片 10:最小消费者 ​

**场景。**围绕 引用计数 建一个很小的案例,不要一开始就放进完整业务系统。从使用者角度写一个小程序或小工程,只依赖公开接口,验证安装包、头文件和运行时行为。

**边界。**先判断这里讨论的是 引用计数、错误码 还是 生命周期 的责任;如果三者混在一起,先把输入、输出和所有权拆开。

text
input -> 引用计数 -> 错误码 -> 生命周期 -> observable result
1

**验证。**记录一条能重复运行的命令或测试,并写明成功输出和失败输出。只说“本机能跑”不够,至少要记录工具版本、构建类型和关键参数。

**常见错误。**把偶然通过当成规则、把私有实现当成公开契约、或者让错误路径绕开清理逻辑,都会让本篇主题在真实项目里变得不可维护。

**复盘问题。**如果明天换一个编译器、Qt 版本、插件版本、输入规模或发布目录,这个结论是否仍成立?不成立时,应该由文档、测试还是 CI 给出提示?

工程切片 11:边界复盘 ​

**场景。**围绕 错误码 建一个很小的案例,不要一开始就放进完整业务系统。用一张表说明问题发生在源码、编译、链接、加载、运行、关闭或发布中的哪一层。

**边界。**先判断这里讨论的是 错误码、生命周期 还是 load/unload 的责任;如果三者混在一起,先把输入、输出和所有权拆开。

text
input -> 错误码 -> 生命周期 -> load/unload -> observable result
1

**验证。**记录一条能重复运行的命令或测试,并写明成功输出和失败输出。只说“本机能跑”不够,至少要记录工具版本、构建类型和关键参数。

**常见错误。**把偶然通过当成规则、把私有实现当成公开契约、或者让错误路径绕开清理逻辑,都会让本篇主题在真实项目里变得不可维护。

**复盘问题。**如果明天换一个编译器、Qt 版本、插件版本、输入规模或发布目录,这个结论是否仍成立?不成立时,应该由文档、测试还是 CI 给出提示?

工程切片 12:反例训练 ​

**场景。**围绕 生命周期 建一个很小的案例,不要一开始就放进完整业务系统。写出一个看似能工作但不可维护的方案,再说明它在规模、团队协作或发布时会怎样失败。

**边界。**先判断这里讨论的是 生命周期、load/unload 还是 句柄 的责任;如果三者混在一起,先把输入、输出和所有权拆开。

text
input -> 生命周期 -> load/unload -> 句柄 -> observable result
1

**验证。**记录一条能重复运行的命令或测试,并写明成功输出和失败输出。只说“本机能跑”不够,至少要记录工具版本、构建类型和关键参数。

**常见错误。**把偶然通过当成规则、把私有实现当成公开契约、或者让错误路径绕开清理逻辑,都会让本篇主题在真实项目里变得不可维护。

**复盘问题。**如果明天换一个编译器、Qt 版本、插件版本、输入规模或发布目录,这个结论是否仍成立?不成立时,应该由文档、测试还是 CI 给出提示?

工程切片 13:最小可复现样例 ​

**场景。**围绕 load/unload 建一个很小的案例,不要一开始就放进完整业务系统。把问题压缩到一个可以提交给同事的目录,保留源码、构建命令、版本输出和预期现象。

**边界。**先判断这里讨论的是 load/unload、句柄 还是 入口函数 的责任;如果三者混在一起,先把输入、输出和所有权拆开。

text
input -> load/unload -> 句柄 -> 入口函数 -> observable result
1

**验证。**记录一条能重复运行的命令或测试,并写明成功输出和失败输出。只说“本机能跑”不够,至少要记录工具版本、构建类型和关键参数。

**常见错误。**把偶然通过当成规则、把私有实现当成公开契约、或者让错误路径绕开清理逻辑,都会让本篇主题在真实项目里变得不可维护。

**复盘问题。**如果明天换一个编译器、Qt 版本、插件版本、输入规模或发布目录,这个结论是否仍成立?不成立时,应该由文档、测试还是 CI 给出提示?

工程切片 14:接口边界 ​

**场景。**围绕 句柄 建一个很小的案例,不要一开始就放进完整业务系统。写清调用方能依赖什么、不能依赖什么,并把隐式假设转换成命名函数、配置项或测试。

**边界。**先判断这里讨论的是 句柄、入口函数 还是 引用计数 的责任;如果三者混在一起,先把输入、输出和所有权拆开。

text
input -> 句柄 -> 入口函数 -> 引用计数 -> observable result
1

**验证。**记录一条能重复运行的命令或测试,并写明成功输出和失败输出。只说“本机能跑”不够,至少要记录工具版本、构建类型和关键参数。

**常见错误。**把偶然通过当成规则、把私有实现当成公开契约、或者让错误路径绕开清理逻辑,都会让本篇主题在真实项目里变得不可维护。

**复盘问题。**如果明天换一个编译器、Qt 版本、插件版本、输入规模或发布目录,这个结论是否仍成立?不成立时,应该由文档、测试还是 CI 给出提示?

本篇收束 ​

掌握《插件热加载与版本管理 / Plugin Hot Reloading and Version Management》的标志,不是记住所有名词,而是能把 load/unload、句柄、入口函数、引用计数、错误码、生命周期 放进一条可验证的工程链路。 先用最小样例建立判断,再用测试和脚本固定判断,最后把失败证据留给未来的自己和团队。

最后更新于:

Pager
上一篇6. 跨平台插件开发:Windows / Linux / macOS 统一策略
下一篇8. 插件安全:沙箱、进程隔离与权限控制 / Plugin Security with Sandboxing, Process Isolation, and Permissions

持续记录,持续成长

Copyright © Tidenflow