模块通过 C++ 接口提供应用服务,插件负责创建和注册实现,插件管理器调用生命周期回调。实体访问、定时器和业务规则都可以作为模块;插件用于组合并安装一组模块。
下面将计数器接口与实现分开,并由另一个模块调用。配合创建插件中的示例即可构建动态库。
定义接口与实现
将下面的代码保存为 sample_module.hpp:
#pragma once
#include <cstdint>
#include <ark/module.hpp>
#include <ark/plugin_manager.hpp>
namespace ark {
class counter_module : public module
{
public:
virtual void increment() = 0;
virtual std::uint64_t value() const = 0;
};
class counter_module_impl final : public counter_module
{
ARK_DECLARE_MODULE_FUNCTIONS
public:
void increment() override {
++value_;
}
std::uint64_t value() const override {
return value_;
}
private:
std::uint64_t value_ = 0;
};
class sample_module final : public module
{
ARK_DECLARE_MODULE_FUNCTIONS
public:
bool post_init() override {
counter_module_ = find_module<counter_module>();
return counter_module_ != nullptr;
}
bool update() override {
if (counter_module_ == nullptr)
{
return false;
}
counter_module_->increment();
return true;
}
bool pre_shut() override {
counter_module_ = nullptr;
return true;
}
private:
counter_module* counter_module_ = nullptr;
};
} // namespace ark示例把三个类型放在同一个文件中,便于说明关系。应用项目应向调用方提供接口头文件,将实现细节保留在内部。两个实现类都使用 ARK_DECLARE_MODULE_FUNCTIONS;这个宏保存管理器指针和注册名称,并提供受保护的 find_module<T>() 方法。
使用同一个接口注册和查找
以 counter_module 为接口注册 counter_module_impl,再通过 find_module<counter_module>() 查找。注册键来自接口类型的 RTTI 名称;实现类名称和动态库文件名都是不同的键。同一个管理器中,每个接口只注册一个实现。注册宏不会把重复注册转换成受支持的多实现模型。
返回的指针由插件持有,调用方不拥有对象。不要删除这个指针,也不要在插件注销模块后继续使用。接口不存在或动态类型转换失败时,启用断言的构建会触发断言;关闭断言时,find_module<T>() 返回 nullptr。使用前应检查返回值,不要在 Debug 中通过失败查找探测可选服务。应用和插件需要使用兼容的头文件、编译器与 ABI 设置,并启用 RTTI。
示例中的两个模块在应用更新线程中调用。计数器没有使用原子变量,不支持任意工作线程直接访问。工作线程的完成结果应投递到模块所属线程,或使用服务明确提供的异步 API。网络回调的分发方式见网络接口。
选择生命周期阶段
| 回调 | 适合处理的工作 | 当前约束 |
|---|---|---|
init() |
初始化本模块资源 | 其他已注册模块可能尚未执行 init() |
post_init() |
查找接口,使用已在 init() 中初始化的依赖 |
其他模块的 post_init() 不按依赖顺序执行 |
check_config() |
检查配置和依赖 | 管理器目前不汇总回调的布尔返回值 |
pre_update() |
准备进入应用循环 | 避免在这个阶段执行无期限的工作 |
update() |
处理当前帧的队列任务 | 只调用实现类覆盖的 update() |
pre_shut() |
停止接收新任务,开始解除订阅 | 所有模块先完成这个阶段,再进入 shut() |
shut() |
释放模块持有的资源 | 完成关闭回调后才卸载动态库 |
管理器先安装所有模块,再执行初始化阶段。每个阶段遍历已注册模块;当前容器不提供依赖排序,也不保证按照清单顺序执行。基类回调默认返回 true,只需覆盖使用到的阶段。注册宏检测到实现类覆盖 update() 时,才把它加入更新列表。因此计数器实现不参与逐帧更新,sample_module 则在每次管理器更新时递增一次计数器。
保证回调不超出对象生命周期
示例只保存其他模块的指针,并在 pre_shut() 中清空。实际模块还可能注册定时器、事件处理器或捕获 this 的异步操作。保存这些操作的标识符或取消句柄,先停止接收新任务,再按所属服务的 API 注销或取消,最后才销毁模块。不能仅凭取消操作推断所有完成回调都已执行。
管理器不会追踪任意回调,也不会证明所有使用插件的任务都已结束。不能根据 pre_shut()、shut() 或 stop() 返回成功,推断动态库可以安全替换,或所有回调都已排空。热更新不在本文范围内。