cordis-cj:静态语言怎么做出能卸载的插件
内容提要
cordis-cj 针对静态语言无法真正卸载模块的限制,将插件运行在独立进程中,卸载时直接终止进程,由操作系统回收资源。代价是握手、序列化和依赖排序,分别通过宏、泛型 API 和 inject/provide 声明式依赖解决。调和器每 5 秒对比配置并自愈,探活每 30 秒一次。此外还支持 xisp 脚本插件,并记录了宏包跨模块的工具链缺陷。
延伸解读
进程隔离的代价与补偿
将插件放入独立进程,用终止进程实现卸载,确实绕开了静态语言无法重置全局状态的难题。但跨进程调用必然引入握手、序列化和依赖排序三处开销。cordis-cj 的应对不是消除代价,而是用宏、泛型 API 和声明式依赖把代价藏到框架层,让插件作者只写普通类。读者需注意:这些补偿依赖工具链支持,宏的跨模块缺陷就是例证。
调和器与探活的分工
调和器每 5 秒对比配置并自愈,探活每 30 秒一次,两者周期不同并非随意。前者响应配置变化,后者发现假死。先加后删的切换策略能减少服务中断,但前提是服务不冲突。这套设计让人只维护期望状态,机器负责达成,但配置漂移会被自动纠正,手动干预可能被覆盖,运维需理解这一行为边界。
工具链缺陷如何塑造 API
宏包跨模块不链接的问题,报错信息完全不指向真实原因,排查后结论是宏包必须与调用方同模块。项目没有等待工具链修复,而是保留宏写法给内置插件,外部插件走显式 API。这提醒读者:当底层工具链有缺陷时,API 形状可能被迫妥协,两套写法并存是现实选择,而非设计冗余。
可卸载不等于热更新
进程隔离让停止插件变得安全,但新版本生效仍需换二进制并重启该插件,不是运行时热替换类定义。文章明确列出边界:不做跨机、插件只增不热改、序列化有成本、微内核不卸载。读者应区分架构上的可卸载与动态语言的热替换,后者在静态语言中并未实现,cordis-cj 只是换了承载物。
Q&A
为什么静态语言(比如仓颉)做不了能卸载的插件?
静态语言编译成静态机器码后,模块的全局变量、静态初始化和注册表都落在进程内存里,即使卸掉代码段,这些数据也无法安全重置回初始状态。原生程序的 dlclose 本身也只是建议而非承诺,所以静态语言无法像动态语言那样真正卸载模块。
cordis-cj 是怎么实现插件卸载的?
cordis-cj 让插件运行在独立进程中,卸载时直接终止(kill)该进程,由操作系统回收其全部内存、文件描述符和线程,不依赖插件自己写 dispose。宿主进程作为微内核永不卸载,负责调和与进程管理。
cordis-cj 把插件拆到独立进程后,付出了哪些代价?
主要代价有三处:一是握手,插件启动后需先向宿主声明身份和提供的服务;二是序列化,跨进程调用只能传字节,需通过 JsonValue 转换;三是启动顺序,有依赖的服务必须按 inject/provide 声明做拓扑排序。
cordis-cj 如何解决插件之间的依赖启动顺序问题?
调和器根据插件配置中的 inject 和 provide 声明做拓扑排序,声明了 inject 的插件会被自动排在提供者之后启动,配置里不需要手写顺序,只写依赖关系。
cordis-cj 的调和器是怎么工作的?
宿主每 5 秒读取配置、计算 diff、拓扑排序、启动新插件,并遵循“先加后删”实现平滑切换;探活每 30 秒 ping 一次,超时则重启,崩溃自愈无需人工介入。配置漂移会被自动纠正。
cordis-cj 支持用脚本写插件吗?有什么优缺点?
支持,cordis-cj 允许用 xisp 脚本插件,插件是一个 Lisp 脚本,运行时把宿主能力注入脚本环境(cordis::* 命名空间)。代价是解释执行性能较低,好处是改插件不用重新编译,适合热更新场景。
cordis-cj 的宏 @Plugin 有什么工具链限制?
带 organization 的宏包在跨模块依赖时,宏不会链接到调用方,导致编译报错 undeclared identifier,且报错信息不指向真实原因。去掉 cjpm.toml 里的 organization 可解决,因此宏包必须与调用方同模块,外部插件只能走显式 API。
cordis-cj 这套方案有哪些做不到的边界?
不做跨机(进程隔离仅限单机);插件只增不热改,改代码需换二进制并重启该插件;序列化有成本,大对象跨进程传会慢;宿主微内核本身不卸载,崩了全崩;宏有跨模块限制,外部插件只能走显式 API。