历史快照
本专题页写于包拆分与 FAST-LIVO2 / CustomMsg / 图像输入落地之前,反映当时的单包结构与
五前端状态。已知过时点:目录布局(apps/launch 现属 asuka_ros 包)、共享的
config_sensor.json(外参已改为各前端 odometry 配置的 sensor 块)、ROS 层 acc_scale
等描述、前端数量(现六种,新增 fastlivo)、CustomMsg 与图像输入链路。2026-09 下旬以来:lightning / superodom 前端与 optimization_miao 后端已移除
(现五前端:fastlio / batchlio / superlio / fastlivo / srlivo;smallpointlio 已于 2026-10-23 移除,odometry/10-11 为其历史快照);
freedom 扩展已整体迁出 asuka(移入 nagisa 包);KeyFrame::stamp 统一为扫描
起始时刻;执行器新增连续失败熔断与 wait_for() 排空检测、扩展与执行器有
ok() 健康查询。
现行事实以主文档 (index / architecture / parameters / odometry 等)与源码为准。
title: "扩展模块机制 ExtensionModule" description: >- 与 OdometryEstimation 同构的 dlopen 插件基类;config_extensions 的 loading_extension_modules 列表控制加载集合,AsukaROS 在退出时统一 stop 并调用 at_exit(map_saving_path)。
扩展模块机制 ExtensionModule
职责定位
ExtensionModule 是 asuka 中"扩展插件"的抽象基类,与里程计插件基类 OdometryEstimation 完全同构:同样以 static load_module 为唯一装载入口,同样通过 dlopen 打开 so 并调用其导出的 extern C 工厂函数。差别在契约面——里程计基类定义数据处理接口(insert_imu / process_once / save_map 等),而 ExtensionModule 只约定两个生命周期钩子:stop() 与 at_exit(dump_path)。扩展模块的具体功能(点面优化、图优化、可视化等)不直接进入数据主链路,而是通过全局回调槽(CallbackSlot)被动接入。
加载集合由配置驱动:AsukaROS 构造时读取 config_extensions 的 loading_extension_modules 列表,对其中每个 so 名调用一次 load_module;仓库内已知实现位于 src/asuka/src/asuka/modules/(imu_prediction、optimization_loam、optimization_miao、rviz_viewer)。
契约面:两个虚函数 + 一个静态工厂
extension_module.hpp 全文仅 26 行,包含三件事:
virtual void at_exit(const std::string& dump_path) {}— 进程退出钩子,默认空实现。AsukaROS 退出时把 config_ros 的map_saving_path传入,模块据此把各自的产物(轨迹、位姿图等)落到与地图相同的目录。virtual void stop() {}— 停机钩子,默认空实现。头文件注释给出明确契约:join 模块拥有的所有线程、停止其回调槽发射;必须幂等,且允许从析构函数中被再次调用。static std::shared_ptr<ExtensionModule> load_module(const std::string& so_name)— 工厂入口。
注释同时揭示了一个跨模块停机协议:AsukaROS 在销毁任何模块之前,先对所有已加载模块调用 stop()。stop() 返回后不再有 in-flight 的槽发射,此时模块从回调槽退订并析构才是安全的——这是与 CallbackSlot 解耦机制配套的竞态防护设计。
加载调用链
extension_module.cpp 全文 308 字节,核心只有一行:
完整链路落在共享的 load_module.hpp/cpp 中:
flowchart TD
A["AsukaROS 构造"] --> B["读 config_extensions<br/>loading_extension_modules 列表"]
B --> C["ExtensionModule::load_module(so_name)"]
C --> D["load_module_from_so<ExtensionModule><br/>查找符号 create_extension_module"]
D --> E["load_symbol(so, symbol)"]
E --> F["load_so(so)"]
F --> G{"open_handles<br/>注册表命中?"}
G -- "命中" --> I["dlsym 查工厂符号"]
G -- "未命中" --> H["dlopen(RTLD_LAZY)<br/>句柄登记进注册表"]
H --> I
I -- "成功" --> J["shared_ptr<ExtensionModule>(func())<br/>对象交给 AsukaROS 持有"]
I -- "失败" --> K["spdlog::warn + nullptr 逐层上浮"]
关键节点:
- open_handles 命中判断:同一 so 若被里程计插件与扩展模块同时依赖,只会 dlopen 一次,注册表按库名进程级去重。
- create_extension_module:扩展插件必须导出的 extern C 工厂符号,与里程计侧的
create_odometry_estimation平行。 - 失败路径:dlopen/dlsym 失败均只
spdlog::warn(附 dlerror)并返回 nullptr,不抛异常;是否容忍某个模块缺失由 AsukaROS 决策。
退出与停机时序
sequenceDiagram
participant App as AsukaROS(退出/析构)
participant Odo as AsyncOdometryEstimation
participant Ext as 各扩展模块
participant Reg as open_handles 注册表
App->>Odo: stop()(先停 worker,join 消费线程)
App->>Ext: 对每个已加载模块调用 stop()
Note over Ext: 幂等:join 自有线程、<br/>停止回调槽发射
App->>Ext: at_exit(map_saving_path)
Note over Ext: 模块各自落盘产物
App->>Ext: 依次析构(此时退订回调槽安全)
Note over Reg: 句柄永不 dlclose,<br/>插件代码常驻进程
顺序依据(头文件注释与仓库停机约定一致):先停 odometry worker,再停扩展模块,最后 at_exit,之后才析构模块对象。load_module.hpp 注释还描述了尚未实现的完整卸载三步:① stop 所有可能发射回调槽的线程;② 析构模块使其退订回调槽;③ 从注册表取句柄、dlclose() 并删除条目。当前实现只做到前两步,第三步被刻意搁置。
关键状态:句柄注册表
load_module.cpp 匿名命名空间中的两个静态变量是本机制的核心状态:
std::mutex handle_mutex+std::map<std::string, void*> open_handles— 进程级、按 so 名索引的 dlopen 句柄表。- 句柄"intentionally never closed":插件代码(vtable、静态数据)必须常驻进程——若析构模块后 dlclose,悬垂的虚表指针会立刻致命。注释明确该注册表是未来 unload/reload 实现的接缝,属于稳定性风险边界。
一个实现细节:load_so 在"查表 → dlopen → 登记"之间存在无锁窗口,两线程可能并发打开同一库;由于 dlopen 对同一库本身是引用计数、幂等的(POSIX 语义),该窗口是良性的。
边界条件
- so 缺失 / 依赖缺失:dlopen 返回 nullptr,warn 输出 so 名与 dlerror(),链路上浮 nullptr。
- 工厂符号缺失:dlsym 返回 nullptr,warn 后同样上浮 nullptr。
- RTLD_LAZY 的副作用:函数符号延迟到首次调用才解析,部分符号问题不会在加载期暴露,排查插件故障时需注意。
- 钩子可选:
at_exit/stop均有空默认实现,模块可按需只覆写其一;虚析构保证经shared_ptr<ExtensionModule>销毁时行为正确。 - stop 幂等的现实来源:正常退出路径调用一次,析构兜底路径可能再调用一次,契约要求两次调用都安全。
扩展点:新增一个扩展模块
- 编写继承
asuka::ExtensionModule的类,按需覆写stop()/at_exit(); - 导出
extern "C"工厂create_extension_module(与create_odometry_estimation同构); - 编译为 .so,将库名加入 config_extensions.json 的
loading_extension_modules; - 通过全局回调槽订阅/发布事件接入主链路,避免对 AsukaROS 或具体算法的反向依赖。
与 OdometryEstimation 的同构与差异
| 维度 | OdometryEstimation | ExtensionModule |
|---|---|---|
| 装载入口 | static load_module |
static load_module |
| 底层设施 | load_module_from_so 模板 |
同一模板 |
| 工厂符号 | create_odometry_estimation |
create_extension_module |
| 契约面 | 数据处理(insert / process_once / save_map…) | 生命周期(stop / at_exit) |
| 与主链路关系 | 被 AsyncOdometryEstimation 驱动 | 经回调槽被动接入 |
两者共享 load_so/load_symbol 与句柄注册表,因此"句柄永不关闭、插件代码常驻进程"的约束同时覆盖算法插件与扩展插件;未来任何卸载/重载改造也必须同时处理两类模块。
Sources:
- src/asuka/include/asuka/utility/extension_module.hpp#L1-L26
- src/asuka/src/asuka/utility/extension_module.cpp#L1-L12
- src/asuka/include/asuka/utility/load_module.hpp#L1-L31
- src/asuka/src/asuka/utility/load_module.cpp#L1-L61
关联源码
src/asuka/src/asuka/utility/extension_module.cppsrc/asuka/include/asuka/utility/extension_module.hpp