我们都知道 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 执行顺序。

而且正是因为采用了循环执行的架构,那么在每个循环尾部处理动态链接库模块的载入及卸载/回收就不存在技术的难点了,但这个前提是工作在单线程驱动的模型下,多线程下就很难做到了,如果大量上锁效率可能还不如单线程跑的快。

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐