C++的注册机制和插件系统

文章目录

  • 一、注册机制
    • [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 就自动按地址跳转执行

完整的流程就是:

  1. 【编译时】编译器看到 f()
  2. 【编译时】知道 f 是函数指针,生成好调用函数的汇编指令:
asm 复制代码
mov rax, [f]     ; 从内存变量 f 里读取地址,把 f 里存的"函数地址"读到 rax 寄存器。rax :x64 CPU 内置通用寄存器,
call rax         ; 调用 rax 里的地址

编译结束后:代码、f、f () 全部消失!只剩下二进制指令!上面两行汇编语言就对应下面这两步运行时CPU做的事:

  1. 【运行时】拿出 f 里存的函数入口地址
  2. 【运行时】CPU 跳转到这个地址,开始执行函数机器码
  3. 【运行时】执行完 new MyPlugin(),返回值给 obj

2.1.3 为什么主程序需要「无知」?

  1. 逻辑稳定:只关心插件的加载和卸载,不关心插件内部逻辑,主程序逻辑越简单,越容易判断插件是不是「疯了」,比如插件如果超时就直接杀死,保证主流程不卡死
  2. 架构稳定:主程序和插件在不同进程中,使用进程隔离,主程序作为宿主是独立的进程,和插件进程之间可以使用RPC通信。比如插件进程崩溃,操作系统杀死插件进程,主进程可以重启一个子进程来加载插件
  3. 开发稳定:不管插件怎么变,只负责调用插件的加载函数

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);

这三者相当于:总公司、劳动合同 和 外包。

相关推荐
兩尛1 小时前
c++知识点4
开发语言·c++
墨染千千秋1 小时前
C++输入输出全解
c++
云qq1 小时前
C++ 原子操作
开发语言·c++·算法
A charmer1 小时前
第一章:基础语法破冰|从 C++ 无缝切换 OC 语法
c++·objective-c
Try,多训练2 小时前
软件设计师备考第一性原理分析
java·经验分享·学习方法
fffzd2 小时前
C++入门(一)
开发语言·c++·命名空间·输入输出·缺省参数
Seven972 小时前
Tomcat Container容器之Engine:StandardEngine
java
m0_738120722 小时前
Webshell流量分析——常见扫描器AWVS,goby,xray流量特征分析
服务器·前端·安全·web安全·网络安全
jinanwuhuaguo2 小时前
(第三十六篇)OpenClaw 去中心化的秩序——从“中心调度”到“网格自治”的治理革命
java·大数据·开发语言·网络·docker·去中心化·github