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)共享同一基类:

class RegistryObject : public KeyedElement {
  uint objectId;            // 主键(nextId 自增分配)
  QList<int> children;      // ★ 子对象只存 ID 数组,不存指针
  int extraDataOffset;      // 位打包:bit31=无额外数据, bit30=持久化标志,
                            //         bit0-29=缓存文件偏移(≤1GB)
};

两个关键决策:

  1. childrenQList<int>——树结构完全靠 ID 间接引用。
    好处:对象可独立序列化到磁盘缓存、可懒加载、删除时无悬空指针。
  2. extraDataOffset 位打包——一个 int 同时编码"持久化标志 + 缓存文件偏移"。

ConfigurationElement 的属性存法berryConfigurationElement.h: L36-40)极致紧凑:

// 格式: [p1, v1, p2, v2, ..., configurationElementValue]
// 偶数长度 = 没有元素文本值;属性名/值交替排列
QList<QString> propertiesAndValue;   // 不用 QHash!一个扁平字符串数组

每元素通常只有 2~4 个属性,线性扫比哈希省内存。

三、主存储:RegistryObjectReferenceMap(ID → 对象)

berryRegistryObjectManager.h: L208

RegistryObjectReferenceMap* cache;   // key: object id, value: 对象
KeyedHashSet heldObjects;            // 必须常驻内存对象的强引用

RegistryObjectReferenceMap 支持 HARD/SOFT 两种引用模式
berryRegistryObjectReferenceMap.h: L32-37)——软引用模式下不常用的对象
可被回收,需要时从磁盘缓存按 extraDataOffset 重新加载(Load(id, type))。
Add(object, hold)hold 参数决定是否放进 heldObjects 锚定。

四、索引表:按不同维度加速查询

索引类型用途
extensionPointsHashtableOfStringAndIntQHash<QString,int> 薄封装)扩展点全名 → objectId
namespacesIndexKeyedHashSet(内部 QSet<KeyedElement>,元素自带 GetKey)命名空间 → 该插件的 extension/extensionPoint 集合
contributorsQHash<QString, RegistryContributor>贡献者 ID → 贡献者(哪个插件)
orphanExtensionsQHash<QString, QList<int>>孤儿表:目标扩展点未注册的 extension 暂存,等扩展点出现再认领
newContributions / formerContributionsKeyedHashSet本次会话新增贡献 vs 上次会话缓存贡献(懒加载)

五、对外暴露:Handle 层(安全间接引用)

消费者从来拿不到 RegistryObject 裸指针,拿到的是 HandleberryHandle.h: L31):

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 布尔标志(orphanExtensionsLoadedcontributorsLoaded
    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

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

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

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;       // 细粒度锁
};

关键设计:

  1. InterfaceMap = std::map<std::string, void*>——服务本体就是
    "接口名 → 对象指针"的 map。C++ 无反射,多接口注册靠模板在编译期
    T*void* 存入,取出时按接口名转回。
  2. dependents 使用计数——每次 GetService +1、UngetService -1;
    模块卸载时框架据此知道"谁还欠着我的服务没还",实现自动清理。
  3. handle/body 分离——对外的 ServiceRegistrationBase / ServiceReferenceBase
    都是指向 Private 的引用计数薄壳;服务注销后 Private 仍可存活
    (available=false),旧引用访问得到受控失败。

五、ServiceListeners —— 监听器表(service/usServiceListeners_p.h: L43-65

过滤器优化的两级结构:

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

class ctkPlugins {
  QHash<QString, QSharedPointer<ctkPlugin>> plugins;  // location(URL) → 插件
  mutable QReadWriteLock pluginsLock;                 // 读写锁(比 us:: 的单 Mutex 更细)
};

2. ctkServices —— 服务注册表(ctkServices_p.h: L40-71

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 都没有的能力:

// 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 字节从哪来CTKplugin->getResource() 从 SQLite 资源表取出
何时触发解析CTK:插件 RESOLVED 事件 → CTKPluginListener::PluginChanged
ExtensionRegistry 注册为服务CTKcontext->registerService<IExtensionRegistry>()
XML 解析BlueBerry 自己ExtensionsParser(SAX 状态机)
存储结构BlueBerry 自己RegistryObjectManager(第一部分详析)
查询/Handle APIBlueBerry 自己

所以: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 实现维护方著名衍生物
EquinoxEclipse 基金会Eclipse IDE 的内核 → BlueBerry 移植自它
FelixApache众多 Java 服务器
KnopflerfishMakewaveCTK PluginFrameworkCppMicroServices 移植自它

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 的 ServicesctkServices / us::ServiceRegistry
    LDAPExprctkLDAPExpr / 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/ctkPluginsCppMicroServices ServiceRegistryBlueBerry 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回调 + LDAPIRegistryEventListener
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

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.hL203-239(全部私有字段)
对象基类berryRegistryObject.hL26-100
配置元素属性berryConfigurationElement.hL36-49
软/硬引用 MapberryRegistryObjectReferenceMap.hL32-70
字符串→int 表berryHashtableOfStringAndInt.hL20
KeyedHashSetberryKeyedHashSet.h / berryKeyedElement.h-
Handle 基类berryHandle.hL31-56

CppMicroServices(Modules/CppMicroServices/core/src/ 下)

内容文件关键行
模块表module/usModuleRegistry.cppL39-73
服务表service/usServiceRegistry_p.hL63-82
服务注册私有体service/usServiceRegistrationBasePrivate.hL40-115
InterfaceMap 定义../include/usServiceInterface.hL138
监听器表service/usServiceListeners_p.hL43-65
版本号../CMakeLists.txtMAJOR/MINOR/PATCH_VERSION

CTK(ep/src/CTK/Libs/PluginFramework/ 下)

内容文件关键行
CTK 外部依赖声明D:/MITK/CMakeExternals/CTK.cmakeL57-58(GIT_REPOSITORY / GIT_TAG)
插件注册表ctkPlugins_p.hL45-63
服务注册表ctkServices_p.hL40-71
SQLite 持久化ctkPluginStorageSQL_p.h/.cppcpp L38-39(表名)
服务事件槽ctkServiceSlotEntry_p.hL45-75
LDAP 过滤器ctkLDAPExpr_p.h-

更多推荐