我们都知道 C/C++ 是一门静态原生代码编译型高级语言,要实现插件化从开发语言层面上来说挑战性比较大。
例如:
1、像类似 C#、VB.NET、F#、C++.NET 语言编译后的二进制MSIL中间语言程序运行在 .NET CLR 基础上面,人们可以利用其 AppDomain 的特性动态从 “网络”、“本地文件”、“内存” 上面载入 .NET Assembly,并引导其注入入口执行。
2、类似如 .NET Assembly 易于实现的 .NET MSIL 静态注入,也是 C/C++ 编译程序所不能的。
那么,本文就着重探讨 C/C++ 程序插件化的一些想法,根究 C/C++ 编程语言的特性,我们得知如果要实现插件化,即不关闭程序的情况下,改变程序原有执行行为有以下几大类:
1、基于 ShellCode 动态编程
2、基于 C/C++ 程序内嵌 .NET 虚拟机、Mono 虚拟机、JVM 虚拟机、Lua 虚拟机、ECMAscript/JS V8 虚拟机
3、基于动态链接库,适用于 “Windows、Linux”,Android 操作系统存在一些明确限制
第一类:
ShellCode 动态编程,目前来说人们可以尝试利用一些开源的动态即时ASM库进行封装,对用户编程接口提供为:Expression Code Tree(表达式代码树)
可以显著的减少手动拽写ASM的复杂性,人们把 Expression Code Tree 编译为特定的源文件,当需要更新代码函数实现时,替换掉当前的函数。
注意:Expression Code Tree 需要编译为目标计算机平台CPU的 Instruction Set ASM,如果不编译效率不是很好。
第二类:
内嵌其它语言及开发平台虚拟机,推荐使用 “.NET/JVM” 官方虚拟机,Lua/V8虚拟机需要为每个工作线程单独分配虚拟机执行堆栈,C/C++ 层打通多个虚拟机之间共同配合执行的隔阂,但若不对虚拟机实现进行修改,那么两个虚拟机之间不能在同个进程内共享对象、而且每个虚拟机都需要单独读入并解析源代码,这会造成不小的一个内存资源浪费。
注:skynet(云风大神)提出的框架解决了上述提到的两个问题,应用此框架人们对于 C/C++ 扩展实现非常少,绝大部分工作量均为实现脚本文件的代码内容。
第三类:
基于动态链接库的类型,优点:可以全由 C/C++ 编程语言实现,但需要注意一点,基于动态链接库的类型也有一些缺点。
1、不要导出 C/C++ 函数及类型
原因一:导出类型优化编译后会出现内存问题,这是因为 C/C++ 优化编译会进行代码及类型的裁剪。
原因二:C/C++ 导出函数命名风格跨编译器及连接器版本存在兼容性问题,这要求工具链环境不发生任何的改变。
2、人们需按 C 风格函数风格导出
原因零:确保动态链接库对外提供公共接口的通用性。
3、动态链接库被载入,则很难从内存移除
原因一:动态链接库代码或被线程正执行中(多线程)
原因二:内存资源回收上的一些技术复杂性(多线程)
如果人们采用 C/C++ 动态链接库来实现热更新,那么从整体工程架构上就需要进行相对应更多的考量。
1、公有数据应划分到单独的动态库
2、每个执行模块单元尽量单独分库
3、主控程序与动态链接库适用C风格类型及函数传递
3.1、导出函数
3.2、导出类型
4、每个动态链接库实现都应提供C风格接口类型及获取导出信息函数
例如:账户管理模块(逻辑)
struct Module {
int (*ModuleId)();
void (*Finalize)();
void (*Constructor)(...); # 三个点省略:为主控进程,构造注入参数
void (*Clear)();
void (*Tick)();
};
struct AccountManager : Module {
bool (*AddAccount)(...);
bool (*CloseAccount)(...);
};
extern "C" Module* libmodule_getinfo();
我们为每个动态链接库都定义其获取该模块的信息的接口,主控进程载入并根据 ModuleId 注册其模块的动态映射,模块与模块之间调用都应从主控进程检索获得,如果在单线程环境下面,只有两种状态:
1、获取到对应模块引用
2、无法获取其模块引用
主控进程应提供那些机制?
1、动态载入动态链接库模块
2、单线程驱动模型可以提供动态模块资源的卸载及资源释放
3、服务器可以提供网络、文件等等,这种不会发生改变的固态实现
4、序列化及反序列化不应该提供在主控进程,可以单独划分
5、主控进程驱动模块执行的顺序,可以在模块信息上标记,主控进程根据标记来决定驱动那个模块作为入口点执行。
6、公有数据需要单独存放在一个模块上面,这是因为逻辑代码模块改变不应该影响到公有数据,如果公有数据模块发生改变,则尝试数据的兼容,如果不能兼容那么可以重读数据。(如果差异不大的情况下,序列化+反序列化或许会是个好主意)
7、私有数据指什么呢?这部分主要是说那种不重要的数据或临时数据,丢失了对程序并不会产生什么影响的类型数据。
8、集合类数据要怎么做?例如该数据操作了STL怎么办?这种的确不好处理,通常程序上管理的数据大约为以下几种:
Set、List、Map,LinkedList,人们都可以按照数据的情况自行封装一下,实现难度不是很大的,STL这块用到临时数据处理就可以了,字符串本质来说就是一段 Buffers/Chunks 而已。
而且设一段数组长度只有10~50个元素,要模拟Set,人们根本不需要用Set,直接循环查找就可以效率很高的,Set 适用于元素非常多的情况下,但大多数程序根本没有那么多的元素数据要处理,具体还是看场景把,如果是服务器程序也并非所有的情况都要用重量的各种集合类型的。
9、内存池及分配器等等
单线程上面为什么可以实现动态链接库模块的卸载呢?
原因:
人们可以采用主线程循环的方式,主线程每次循环都尝试执行 Tick,但并非所有的模块都需要 Tick,而且 Tick 之间存在顺序关联性,那么人们可以采用标记或由模块自行在主控进程注册 Tick 及顺序,那么则可确保每个模块之间 Tick 执行顺序。
而且正是因为采用了循环执行的架构,那么在每个循环尾部处理动态链接库模块的载入及卸载/回收就不存在技术的难点了,但这个前提是工作在单线程驱动的模型下,多线程下就很难做到了,如果大量上锁效率可能还不如单线程跑的快。