MITK 三套注册表数据结构分析
第一部分:BlueBerry 扩展注册表(ExtensionRegistry / RegistryObjectManager)
第二部分:CppMicroServices 注册表(ModuleRegistry / ServiceRegistry)
第三部分:CTK 中的注册表(ctkPlugins / ctkServices)
第四部分:血缘关系 ------ Knopflerfish 与 Equinox
附:CppMicroServices 版本信息
第一部分 BlueBerry 扩展注册表的数据结构
源码:Plugins/org.blueberry.core.runtime/src/internal/,核心是 RegistryObjectManager。
一、总体设计:int ID 扁平对象图 + 多索引
注册表不是一棵指针连接的对象树,而是:
所有对象压平存储,以 int objectId 为主键;父子关系用 int 数组表达;
再配多张字符串索引表加速查询。
┌─────────────────────────────────────────┐
│ RegistryObjectManager │
字符串索引 │ extensionPoints: QHash<QString,int> │──┐
(名字→ID) │ namespacesIndex: KeyedHashSet │ │ 查到 id
│ contributors: QHash<QString,Contrib> │ │
│ orphanExtensions: QHash<QString,QList<int>>│
├─────────────────────────────────────────┤ │
主存储 │ cache: RegistryObjectReferenceMap │◄─┘
(ID→对象) │ (int → RegistryObject, 可软引用) │
│ heldObjects: KeyedHashSet (强引用锚) │
└─────────────────────────────────────────┘
二、对象本体:RegistryObject(berryRegistryObject.h: L26)
三类对象(ExtensionPoint / Extension / ConfigurationElement)共享同一基类:
cpp
class RegistryObject : public KeyedElement {
uint objectId; // 主键(nextId 自增分配)
QList<int> children; // ★ 子对象只存 ID 数组,不存指针
int extraDataOffset; // 位打包:bit31=无额外数据, bit30=持久化标志,
// bit0-29=缓存文件偏移(≤1GB)
};
两个关键决策:
children是QList<int>------树结构完全靠 ID 间接引用。
好处:对象可独立序列化到磁盘缓存、可懒加载、删除时无悬空指针。extraDataOffset位打包------一个 int 同时编码"持久化标志 + 缓存文件偏移"。
ConfigurationElement 的属性存法 (berryConfigurationElement.h: L36-40)极致紧凑:
cpp
// 格式: [p1, v1, p2, v2, ..., configurationElementValue]
// 偶数长度 = 没有元素文本值;属性名/值交替排列
QList<QString> propertiesAndValue; // 不用 QHash!一个扁平字符串数组
每元素通常只有 2~4 个属性,线性扫比哈希省内存。
三、主存储:RegistryObjectReferenceMap(ID → 对象)
berryRegistryObjectManager.h: L208:
cpp
RegistryObjectReferenceMap* cache; // key: object id, value: 对象
KeyedHashSet heldObjects; // 必须常驻内存对象的强引用
RegistryObjectReferenceMap 支持 HARD/SOFT 两种引用模式
(berryRegistryObjectReferenceMap.h: L32-37)------软引用模式下不常用的对象
可被回收,需要时从磁盘缓存按 extraDataOffset 重新加载(Load(id, type))。
Add(object, hold) 的 hold 参数决定是否放进 heldObjects 锚定。
四、索引表:按不同维度加速查询
| 索引 | 类型 | 用途 |
|---|---|---|
extensionPoints |
HashtableOfStringAndInt(QHash<QString,int> 薄封装) |
扩展点全名 → objectId |
namespacesIndex |
KeyedHashSet(内部 QSet<KeyedElement>,元素自带 GetKey) |
命名空间 → 该插件的 extension/extensionPoint 集合 |
contributors |
QHash<QString, RegistryContributor> |
贡献者 ID → 贡献者(哪个插件) |
orphanExtensions |
QHash<QString, QList<int>> |
孤儿表:目标扩展点未注册的 extension 暂存,等扩展点出现再认领 |
newContributions / formerContributions |
KeyedHashSet |
本次会话新增贡献 vs 上次会话缓存贡献(懒加载) |
五、对外暴露:Handle 层(安全间接引用)
消费者从来拿不到 RegistryObject 裸指针,拿到的是 Handle (berryHandle.h: L31):
cpp
class Handle : public virtual Object {
const IObjectManager* const objectManager;
int id; // Handle 只持有 ID!
virtual RegistryObject* GetObject(); // 每次访问按 ID 现查
};
IConfigurationElement / IExtension / IExtensionPoint 的实现全是
"ID + 管理器指针"的轻量句柄。好处:插件卸载、对象移除后,
Handle 访问得到受控失败而非野指针崩溃;Handle 可随意复制,代价只是一个 int。
六、线程安全与懒加载
- 所有公开方法加
QMutex mutex(L105),内部用_unlocked后缀版本避免重入死锁 - 多个
xxxLoaded布尔标志(orphanExtensionsLoaded、contributorsLoaded、
namespacesIndexLoaded)实现分区懒加载------哪部分被查询才加载哪部分 isDirty/fromCache标志支撑"注册表磁盘缓存":
启动时间戳一致则直接映射缓存文件,跳过全部 plugin.xml 解析
七、查询路径示例
GetConfigurationElementsFor("org.blueberry.ui.views")
→ extensionPoints 索引查到扩展点 objectId
→ cache.Get(id) 取 ExtensionPoint 对象(可能触发磁盘 Load)
→ GetRawChildren() 拿到 extension 的 int ID 数组
→ 逐个 GetObjects(ids, EXTENSION) → 再取各自 children(ConfigurationElement ID)
→ 包装成 ConfigurationElementHandle 列表返回
小结
| 设计点 | 实现 |
|---|---|
| 主键化 | 一切对象 = int objectId,nextId 自增 |
| 树 = ID 数组 | children: QList<int>,无指针互连 |
| 属性 = 扁平数组 | [p1,v1,p2,v2,...,value] 交替存放 |
| 内存可伸缩 | ReferenceMap 软引用 + heldObjects 强引用锚 + 磁盘偏移量重加载 |
| 多维索引 | 名字/命名空间/贡献者/孤儿四套哈希索引 |
| 安全暴露 | Handle(ID+管理器)而非裸指针 |
| 并发 | 单 QMutex + _unlocked 内部版本 |
| 启动加速 | 位打包偏移 + 分区懒加载 + 时间戳校验缓存 |
这套结构原样照搬 Eclipse Equinox------为"数千插件、数万扩展"的规模优化:
启动不解析、内存可回收、查询走索引。
第二部分 CppMicroServices 注册表的数据结构
与 BlueBerry 完全不同的设计------纯内存、指针直连、按需求即时索引 的三层哈希结构。
源码:Modules/CppMicroServices/core/src/。
一、三张注册表总览
① ModuleRegistry(模块表,全局静态) QHash: 模块名 → Module*
② ServiceRegistry(服务表,挂在 CoreModuleContext 里)
三个容器互为视角,指向同一批 ServiceRegistrationBase
③ ServiceListeners(监听器表) 按过滤条件分桶的监听器缓存
二、ModuleRegistry ------ 模块表(module/usModuleRegistry.cpp: L39-73)
cpp
typedef US_UNORDERED_MAP_TYPE<std::string, Module*> ModuleMap; // name → Module*
US_GLOBAL_STATIC_WITH_DELETER(ModuleMap, modules, ModuleDeleter) // 进程级静态单例
US_GLOBAL_STATIC(Mutex, modulesLock) // 独立互斥锁
US_GLOBAL_STATIC(Mutex, countLock) // 保护自增 id 计数
一张 unordered_map<string, Module*>,裸指针直连
(对象生命周期与 .so 绑定,进程退出时由 ModuleDeleter 统一清理)。
三、ServiceRegistry ------ 服务表核心(service/usServiceRegistry_p.h: L63-82)
cpp
class ServiceRegistry {
mutable Mutex mutex; // 单锁保护全部三个容器
// 视角1: 服务注册对象 → 它声明的接口名列表
US_UNORDERED_MAP_TYPE<ServiceRegistrationBase, std::vector<std::string>> services;
// 视角2: 接口名 → 按 ranking 升序的注册列表(★ 查询主索引)
US_UNORDERED_MAP_TYPE<std::string, std::vector<ServiceRegistrationBase>> classServices;
// 视角3: 全量平面列表(按模块枚举时用)
std::vector<ServiceRegistrationBase> serviceRegistrations;
};
三个容器存同一批 ServiceRegistrationBase(引用计数句柄),索引维度不同:
classServices是查询热路径:GetServiceReference("IAlgorithm")一次哈希命中,
取back()即最高 ranking(插入时lower_bound保序,usServiceRegistry.cpp: L121-124)- 一个服务注册 N 个接口,就在
classServices出现 N 次
四、服务对象本体:ServiceRegistrationBasePrivate(usServiceRegistrationBasePrivate.h: L40-115)
cpp
class ServiceRegistrationBasePrivate {
AtomicInt ref; // 隐式共享的引用计数(handle/body 惯用法)
InterfaceMap service; // ★ 服务本体: std::map<string接口名, void*实现指针>
// (usServiceInterface.h: L138)
// ---- 使用者记账(与 BlueBerry 最大的不同点)----
ModuleToRefsMap dependents; // QHash<Module*,int>: 谁在用我 + 未配对的 GetService 次数
ModuleToServiceMap moduleServiceInstance; // 模块作用域工厂: 每模块一个实例
ModuleToServicesMap prototypeServiceInstances; // 原型工厂: 每模块多个实例
ModulePrivate* module; // 谁注册的我(裸指针回连)
ServiceReferenceBase reference; // 对外发放的引用对象
ServicePropertiesImpl properties; // 属性表(service.id / objectclass / service.ranking)
volatile bool available; // 服务是否可获取
volatile bool unregistering; // 防递归注销标志
Mutex eventLock, propsLock; // 细粒度锁
};
关键设计:
InterfaceMap = std::map<std::string, void*>------服务本体就是
"接口名 → 对象指针"的 map。C++ 无反射,多接口注册靠模板在编译期
把T*转void*存入,取出时按接口名转回。dependents使用计数 ------每次 GetService +1、UngetService -1;
模块卸载时框架据此知道"谁还欠着我的服务没还",实现自动清理。- handle/body 分离 ------对外的
ServiceRegistrationBase/ServiceReferenceBase
都是指向 Private 的引用计数薄壳;服务注销后 Private 仍可存活
(available=false),旧引用访问得到受控失败。
五、ServiceListeners ------ 监听器表(service/usServiceListeners_p.h: L43-65)
带过滤器优化的两级结构:
cpp
class ServiceListeners {
// 简单过滤器(只按 objectclass 匹配)→ 缓存桶,事件分发 O(1) 定位
US_UNORDERED_MAP_TYPE<std::string, std::list<ServiceListenerEntry>> cache[2];
// 按 objectclass 和 service.id 两个维度各一份
// 复杂 LDAP 过滤器 / 无过滤器 → 只能逐个求值
std::list<ServiceListenerEntry> complicatedListeners;
// 模块级监听器:ModuleContext → 回调列表
US_UNORDERED_MAP_TYPE<ModuleContext*, std::list<std::pair<ModuleListener,void*>>> moduleListenerMap;
};
发 ServiceEvent 时:先按事件服务的 objectclass/service.id 命中缓存桶里的简单监听器,
再对 complicatedListeners 逐个跑 LDAP 表达式------避免每次事件全量遍历。
第三部分 CTK 中的注册表
CTK 源码位置:
D:\MITK-superbuild-2023.12\ep\src\CTK\Libs\PluginFramework\(CTK 是外部依赖,由
CMakeExternals/CTK.cmake从 github.com/MITK/CTK 拉取)
结论先行:
CTK 有两张注册表(插件表 + 服务表),但没有 ExtensionRegistry。
BlueBerry ExtensionRegistry 不是"依赖 CTK 实现"的------它的解析与存储是
BlueBerry 自有代码,CTK 只提供输入源和触发时机(详见下文第三节)。
一、CTK 的两张注册表
1. ctkPlugins ------ 插件注册表(ctkPlugins_p.h: L45-63)
cpp
class ctkPlugins {
QHash<QString, QSharedPointer<ctkPlugin>> plugins; // location(URL) → 插件
mutable QReadWriteLock pluginsLock; // 读写锁(比 us:: 的单 Mutex 更细)
};
2. ctkServices ------ 服务注册表(ctkServices_p.h: L40-71)
cpp
class ctkServices {
QHash<ctkServiceRegistration, QStringList> services; // 注册 → 接口名列表
QHash<QString, QList<ctkServiceRegistration>> classServices; // 接口名 → 按rank排序的注册列表
};
这与第二部分 CppMicroServices 的 ServiceRegistry 几乎逐字段对应
(services / classServices 连名字都一样)------因为两者同源(见第四部分),
只是 CTK 用 Qt 容器 + QSharedPointer,us:: 用 STL + 自制引用计数。
二、CTK 独有的两个特性
1. SQLite 持久化(ctkPluginStorageSQL)
us:: 和 BlueBerry 都没有的能力:
cpp
// ctkPluginStorageSQL.cpp: L38-39
#define PLUGINS_TABLE "Plugins" // 插件元数据表
#define PLUGIN_RESOURCES_TABLE "PluginResources" // ★ 插件资源(含 plugin.xml)存进 SQLite!
CTK 把插件安装状态、以及插件内嵌资源整个存入 SQLite 数据库 。
plugin->getResource("plugin.xml") 实际是从这个数据库读出来的。
2. Qt 信号槽做服务事件(ctkServiceSlotEntry)
监听器不是回调接口而是 Qt slot,配 LDAP 过滤器(ctkLDAPExpr,同样来自 Knopflerfish)。
三、修正:"BlueBerry ExtensionRegistry 依赖 CTK 实现"
准确的关系是分层协作,而非实现依赖:
| 环节 | 谁做的 |
|---|---|
| plugin.xml 字节从哪来 | CTK :plugin->getResource() 从 SQLite 资源表取出 |
| 何时触发解析 | CTK :插件 RESOLVED 事件 → CTKPluginListener::PluginChanged |
| ExtensionRegistry 注册为服务 | CTK :context->registerService<IExtensionRegistry>() |
| XML 解析 | BlueBerry 自己 :ExtensionsParser(SAX 状态机) |
| 存储结构 | BlueBerry 自己 :RegistryObjectManager(第一部分详析) |
| 查询/Handle API | BlueBerry 自己 |
所以:CTK 是 ExtensionRegistry 的"宿主与快递员" (管插件生命周期、
送来 plugin.xml 字节),但注册表本身的解析、存储、查询全部是 BlueBerry
从 Eclipse Equinox 移植的自有代码。CTK 自己压根没有扩展点概念
(Libs/ 下搜不到 ExtensionRegistry,只有无关的 ctkExtensionFactory
------那是 Qt Designer 插件工厂)。
第四部分 血缘关系:Knopflerfish 与 Equinox
Knopflerfish 是什么
Knopflerfish 是一个开源的 Java OSGi 框架实现 ------理解它是什么,
就能理解为什么 CTK 和 CppMicroServices 的代码长得几乎一样。
- 由瑞典公司 Makewave(前身 Gatespace Telematics)开发维护,2003 年开源,BSD 风格许可证
- 是 OSGi 规范最早、最著名的三大实现之一:
| OSGi 实现 | 维护方 | 著名衍生物 |
|---|---|---|
| Equinox | Eclipse 基金会 | Eclipse IDE 的内核 → BlueBerry 移植自它 |
| Felix | Apache | 众多 Java 服务器 |
| Knopflerfish | Makewave | → CTK PluginFramework 和 CppMicroServices 移植自它 |
OSGi 是什么(一句话背景)
OSGi = Java 世界的动态模块化规范:程序由多个 bundle (模块)组成,
bundle 可在运行期安装/启动/停止/卸载,bundle 之间通过服务注册表
以接口解耦通信。"动态服务注册 + 模块化系统"这两个概念就是 OSGi 规范的核心内容。
与 MITK 的血缘关系
Knopflerfish(Java, OSGi 实现)
│
├─ CTK PluginFramework(把 Java 代码手工翻译成 Qt/C++)
│ ctkServices、ctkLDAPExpr、ctkServiceSlotEntry ...
│
└─ CppMicroServices(翻译成纯 STL C++)
usServiceRegistry、usLDAPExpr、usServiceListenerEntry ...
翻译痕迹在源码里非常明显:
- 类名逐一对应:Java 的
Services→ctkServices/us::ServiceRegistry;
LDAPExpr→ctkLDAPExpr/usLDAPExpr - 数据结构逐字段对应:
services/classServices两张表的名字在三份代码里一模一样 - 连注释和算法(如 LDAP 过滤器求值、service ranking 排序)都是同一套
而 BlueBerry 走的是另一条线 :它移植的是 Equinox (Eclipse 的 OSGi 实现)
里的扩展注册表和 Workbench,所以 ExtensionRegistry / RegistryObjectManager
的设计(int ID 扁平图、磁盘缓存、孤儿表)与 Knopflerfish 系毫无相似之处
------那是 Eclipse 的家传手艺。
MITK 里"OSGi 味"的代码其实有两个不同的 Java 祖先:
两套微服务机制(CTK 服务层、CppMicroServices)是 Knopflerfish 的 C++ 后代;
BlueBerry 的扩展点机制则来自 Eclipse Equinox。
第五部分 三套注册表全景对照
| CTK ctkServices/ctkPlugins | CppMicroServices ServiceRegistry | BlueBerry ExtensionRegistry | |
|---|---|---|---|
| 血统 | Knopflerfish OSGi (Java→Qt) | Knopflerfish OSGi (Java→STL) | Eclipse Equinox (Java→Qt) |
| 管什么 | 插件 + 服务(活对象) | 模块 + 服务(活对象) | 扩展声明(静态元数据) |
| 主键 | 接口名字符串 / location | 接口名字符串 / 模块名 | int objectId |
| 核心结构 | QHash 双表 | unordered_map 双表 | int ID 扁平图 + 多索引 |
| 对象互连 | QSharedPointer | 裸指针/引用计数句柄 | int ID 数组间接引用 |
| 持久化 | SQLite(含资源) | 无 | 自制二进制缓存 + 偏移量 |
| 记账重点 | 谁在用(dependents) | 谁在用(dependents) | 谁贡献的(contributor 归属) |
| 事件 | Qt 信号槽 + LDAP | 回调 + LDAP | IRegistryEventListener |
| 锁 | QReadWriteLock | 单 Mutex | 单 QMutex |
| 规模假设 | 数十插件 | 数百服务,查询要快 | 数万扩展,启动要快 |
| 典型查询 | 接口名 → 服务 | 接口名 → 最高 ranking 实例 | 扩展点 ID → 配置元素树 |
本质差异一句话:Service 类注册表管"运行中的对象"(所以要引用计数、
使用记账、可用性标志),ExtensionRegistry 管"静态的声明"(所以要 ID 化、
可序列化、懒加载) ------分别对应 OSGi 的 Service Layer 和 Eclipse 的
Extension Registry。三者在 MITK 进程里同时运行:CTK 管 Plugins 层生命周期
与框架服务,us:: 管 Modules 层算法服务,BlueBerry ExtensionRegistry
管 UI 扩展声明------各司其职。
附 CppMicroServices 版本信息
版本 2.99.0 (内嵌定制版)。来源:Modules/CppMicroServices/CMakeLists.txt:
cmake
set(${PROJECT_NAME}_MAJOR_VERSION 2)
set(${PROJECT_NAME}_MINOR_VERSION 99)
set(${PROJECT_NAME}_PATCH_VERSION 0)
- 2.99.0 是"2.x 向 3.0 过渡"的开发版本号 (99 是惯用的预发布标记)。
不是上游正式 release,而是 MITK fork 进源码树内嵌维护 的版本------
所以它放在Modules/CppMicroServices/而不是CMakeExternals/作为外部依赖。 - API 特征印证:仍使用 2.x 时代术语 ------
us::命名空间、
Module/ModuleContext/US_INITIALIZE_MODULE(基于 OSGi Core Release 5 规范)。 - 上游独立项目(github.com/CppMicroServices/CppMicroServices)3.x 之后改成
cppmicroservices::命名空间和Bundle/BundleContext术语,
MITK 内嵌版没有跟进,由 MITK 团队(DKFZ)随主仓库维护。
关键源码文件索引
BlueBerry(Plugins/org.blueberry.core.runtime/src/internal/ 下)
| 内容 | 文件 | 关键行 |
|---|---|---|
| 对象管理器 | berryRegistryObjectManager.h |
L203-239(全部私有字段) |
| 对象基类 | berryRegistryObject.h |
L26-100 |
| 配置元素属性 | berryConfigurationElement.h |
L36-49 |
| 软/硬引用 Map | berryRegistryObjectReferenceMap.h |
L32-70 |
| 字符串→int 表 | berryHashtableOfStringAndInt.h |
L20 |
| KeyedHashSet | berryKeyedHashSet.h / berryKeyedElement.h |
- |
| Handle 基类 | berryHandle.h |
L31-56 |
CppMicroServices(Modules/CppMicroServices/core/src/ 下)
| 内容 | 文件 | 关键行 |
|---|---|---|
| 模块表 | module/usModuleRegistry.cpp |
L39-73 |
| 服务表 | service/usServiceRegistry_p.h |
L63-82 |
| 服务注册私有体 | service/usServiceRegistrationBasePrivate.h |
L40-115 |
| InterfaceMap 定义 | ../include/usServiceInterface.h |
L138 |
| 监听器表 | service/usServiceListeners_p.h |
L43-65 |
| 版本号 | ../CMakeLists.txt |
MAJOR/MINOR/PATCH_VERSION |
CTK(ep/src/CTK/Libs/PluginFramework/ 下)
| 内容 | 文件 | 关键行 |
|---|---|---|
| CTK 外部依赖声明 | D:/MITK/CMakeExternals/CTK.cmake |
L57-58(GIT_REPOSITORY / GIT_TAG) |
| 插件注册表 | ctkPlugins_p.h |
L45-63 |
| 服务注册表 | ctkServices_p.h |
L40-71 |
| SQLite 持久化 | ctkPluginStorageSQL_p.h/.cpp |
cpp L38-39(表名) |
| 服务事件槽 | ctkServiceSlotEntry_p.h |
L45-75 |
| LDAP 过滤器 | ctkLDAPExpr_p.h |
- |