文章目录
- 一、注册机制
-
- [1.1 为什么需要注册机制?](#1.1 为什么需要注册机制?)
- [1.2 为什么python不需要?](#1.2 为什么python不需要?)
- 二、C++插件系统的标准工业架构:主程序+共享头文件+插件程序
-
- [2.1 主程序](#2.1 主程序)
-
- [2.1.1 什么是加载插件?](#2.1.1 什么是加载插件?)
- [2.1.2 主程序拿到函数地址怎么调用函数?](#2.1.2 主程序拿到函数地址怎么调用函数?)
- [2.1.3 为什么主程序需要「无知」?](#2.1.3 为什么主程序需要「无知」?)
- [2.2 共享头文件(shared interface)](#2.2 共享头文件(shared interface))
- [2.3 插件程序](#2.3 插件程序)
一、注册机制
根据一个字符串key 调用对应的 creator,创建一个类。字符串key 和 value(creator,一个函数指针,封装了创建这个类的动作)保存在注册表中。creator返回指向基类的指针,主程序拿到这个指针后,通过虚函数操作它,不关心具体是什么类以及类的具体实现。
1.1 为什么需要注册机制?
C++是静态编译型语言,C++运行前必须编译,无法在运行时读源码。代码必须经过编译器编译成二进制文件(机器码)交给CPU执行。不能把编译器打包带着走,或者每次运行都重新编译。基于注册机制,C++才能通过注册表创建主程序不知道的对象,注册表的本质就是保存一个全局工厂map,key是类明字符串,value是创建插件类的函数。(工厂:名字+怎么造对象)。每个插件可以往里添加或删除键值对
Creator 把「运行时才确定创建哪一个具体类对象」的行为,封装成了 编译时就固定调用范式、运行时再填真实函数地址 的间接调用。
1.2 为什么python不需要?
Python等解释型语言:python解释器=python编译器+python虚拟机。.py运行时首先编译成字节码,保存在__pycache__下的.pyc文件中,CPU看不懂,必须通过Python虚拟机PVM逐行转换成C函数,然后去执行这些预先编译成0 1 机器码的C函数。这些C函数是python安装包内置的。
python自带反射特性,不需要注册,程序在运行时会把 类、函数、变量名和结构信息都放在内存。通过字符串找到对应的类:SomeClass = globals()["SomeClass"],创建类:my_class = SomeCalss()
二、C++插件系统的标准工业架构:主程序+共享头文件+插件程序
为什么需要这样的设计?看看它们的分工:
2.1 主程序
制定规则,调用最少的基本接口,制定规则和提供资源,不关心具体的插件程序业务细节,只关心系统的生命周期。
职责:
- 启动系统
- 每帧刷新
- 关闭系统
特点:
- 接口最少,主程序越「无知」,系统越稳定。主程序
完整的流程:
1. 主程序启动
2. 主程序加载插件(DLL/so) → 自动触发注册
3. 插件把自己塞进 map,塞进去的是一个 "能创建插件对象" 的creator函数指针(地址)
4. 主程序遍历 map → 调用 creator → 实例化(new 插件)
5. 主程序使用插件
2.1.1 什么是加载插件?
加载插件就是把一个外部编译好的程序模块,读到内存里。主程序运行时才去读,不是编译时写死的。DLL是一个可以在程序运行时被 "读进来" 的代码文件,是外部模块。
Windows → .dll
Linux → .so (shared object)
Mac → .dylib/.bundle
2.1.2 主程序拿到函数地址怎么调用函数?
cpp
using Creator = void* (*)(); // 定义函数地址类型,一个能放函数地址的类型
Creator f = CreatePlugin; // 主程序遍历map,把 CreatePlugin 函数的地址放进变量f,也就是把 creator函数指针赋值给f
void* obj = f(); // 只要函数指针变量名后面加 (),CPU 就自动按地址跳转执行
完整的流程就是:
- 【编译时】编译器看到 f()
- 【编译时】知道 f 是函数指针,生成好调用函数的汇编指令:
asm
mov rax, [f] ; 从内存变量 f 里读取地址,把 f 里存的"函数地址"读到 rax 寄存器。rax :x64 CPU 内置通用寄存器,
call rax ; 调用 rax 里的地址
编译结束后:代码、f、f () 全部消失!只剩下二进制指令!上面两行汇编语言就对应下面这两步运行时CPU做的事:
- 【运行时】拿出 f 里存的函数入口地址
- 【运行时】CPU 跳转到这个地址,开始执行函数机器码
- 【运行时】执行完 new MyPlugin(),返回值给 obj
2.1.3 为什么主程序需要「无知」?
- 逻辑稳定:只关心插件的加载和卸载,不关心插件内部逻辑,主程序逻辑越简单,越容易判断插件是不是「疯了」,比如插件如果超时就直接杀死,保证主流程不卡死
- 架构稳定:主程序和插件在不同进程中,使用进程隔离,主程序作为宿主是独立的进程,和插件进程之间可以使用RPC通信。比如插件进程崩溃,操作系统杀死插件进程,主进程可以重启一个子进程来加载插件
- 开发稳定:不管插件怎么变,只负责调用插件的加载函数
2.2 共享头文件(shared interface)
纯虚接口,可以动态增减,是连接主程序和插件程序的唯一桥梁。是独立的 .h文件,不包含业务实现,只包含纯虚类接口定义
职责:
通过纯虚类规定插件类必须有哪些成员方法,包括类的能力和查询。比如定义注册插件类的宏或者一些模板函数模板类。
- 定义插件类必须提供哪些能力
- 定义插件类必须提供的查询接口
- 可以动态增减,用于新功能相关的逻辑扩展
cpp
// plugin_def.h
#pragma once
// 导出宏、工厂类型、注册宏都放这里
#ifdef _WIN32
#define PLUGIN_API extern "C" __declspec(dllexport)
#else
#define PLUGIN_API extern "C"
#endif
// 注册宏定义
#define REGISTER_PLUGIN(PluginClass) \
PLUGIN_API void RegisterPlugin() { \
PluginMgr::Regist<PluginClass>(); \
}
2.3 插件程序
插件类的具体实现,包含具体的业务逻辑,提供插件能力。包括具体注册代码。
职责:
- 继承共享头文件的接口
- 实现插件逻辑
cpp
// myplugin.cpp
#include "plugin_def.h"
// 业务代码、类实现...
// 就这一行,写在插件 cpp 最末尾
REGISTER_PLUGIN(MyPlugin);
这三者相当于:总公司、劳动合同 和 外包。