MITK plugin.xml 读取解析的实现过程

MITK plugin.xml 读取解析的实现过程

概述

MITK 的每个插件都带一个 plugin.xml(声明该插件贡献的 View / Editor / Perspective 等扩展)。

BlueBerry 在插件被 CTK 框架 resolve 时 读取该文件,用 SAX 状态机解析器 把 XML 转成

注册表对象(ExtensionPoint / Extension / ConfigurationElement),存入全局唯一的

ExtensionRegistry ;此后 Workbench 各子系统按扩展点 ID 查询,并在需要时通过

Qt 元对象反射 实例化 class="..." 指定的类。

一句话链路:

CTK 插件事件 → CTKPluginListener 读资源 → ExtensionRegistry::AddContribution →
ExtensionsParser(SAX 状态机)→ RegistryObjectManager 存储 → 消费者查询 →
CreateExecutableExtension 反射实例化

所有代码位于 Plugins/org.blueberry.core.runtime/src/


一、启动与触发:谁发起了解析

1. Activator 创建注册表(进程启动时)

cpp 复制代码
// internal/berryCTKPluginActivator.cpp: L202-258
void org_blueberry_core_runtime_Activator::startRegistry()
{
  // 确定注册表缓存位置(本地配置区优先)
  QString configuration = context->getDataFile("").absoluteFilePath();
  strategy = new RegistryStrategy(registryLocations, readOnlyLocations, masterRegistryKey.data());

  auto registry = new ExtensionRegistry(strategy, masterRegistryKey.data(), userRegistryKey.data());
  defaultRegistry.reset(registry);

  // 注册为 CTK 服务,供全局 GetExtensionRegistry() 获取
  registryServiceReg = context->registerService<IExtensionRegistry>(registry);
}

2. RegistryStrategy::OnStart 挂监听 + 扫描存量插件

cpp 复制代码
// internal/berryRegistryStrategy.cpp: L82-101
void RegistryStrategy::OnStart(IExtensionRegistry* reg, bool loadedFromCache)
{
  // ① 注册 CTK 插件事件监听器,捕获之后安装/解析的插件
  pluginListener.reset(new CTKPluginListener(registry, token, this));
  getPluginContext()->connectPluginListener(
        pluginListener.data(), SLOT(PluginChanged(ctkPluginEvent)), Qt::DirectConnection);

  // ② 把当前已安装的所有插件一次性灌入注册表
  if (!loadedFromCache)
    pluginListener->ProcessPlugins(getPluginContext()->getPlugins());
}

两条触发路径由此形成:

  • 存量 :启动时 ProcessPlugins 遍历全部已安装插件
  • 增量 :运行期插件状态变为 RESOLVED 时,PluginChanged 事件回调(berryCTKPluginListener.cpp: L48-72

二、读取:从插件包里取出 plugin.xml 字节流

cpp 复制代码
// internal/berryCTKPluginListener.cpp
const QString CTKPluginListener::PLUGIN_MANIFEST = "plugin.xml";   // L24 固定文件名

void CTKPluginListener::AddPlugin(QSharedPointer<ctkPlugin> plugin)   // L104-142
{
  // ① 去重:该插件已在注册表则跳过
  IContributor::Pointer contributor = ContributorFactory::CreateContributor(plugin);
  if (registry->HasContributor(contributor)) return;

  // ② 系统插件(id==0)与无符号名插件直接排除(GetExtensionPath, L92-102)
  QString pluginManifest = GetExtensionPath(plugin);

  // ③ 关键:通过 CTK 从插件资源中读出 plugin.xml 的原始字节
  QByteArray ba = plugin->getResource(pluginManifest);
  if (ba.isEmpty()) return;          // 没有 plugin.xml 的插件是合法的,静默跳过

  // ④ 包成 QBuffer(QIODevice),交给注册表
  QBuffer buffer(&ba);
  registry->AddContribution(&buffer, contributor, true, pluginManifest, nullptr, token, timestamp);
}

要点:

  • plugin->getResource("plugin.xml") ------ plugin.xml 是编译进插件的 Qt 资源 (qrc),
    不是磁盘散文件;CTK 负责从插件包中取出字节。
  • 反向操作 RemovePlugin(L80-90):插件 UNRESOLVED 时按 pluginId 从注册表移除其全部贡献。

三、解析入口:ExtensionRegistry::AddContribution

cpp 复制代码
// internal/berryExtensionRegistry.cpp: L1100-1138
bool ExtensionRegistry::AddContribution(QIODevice* is, const SmartPointer<IContributor>& contributor,
                                        bool persist, const QString& contributionName, ...)
{
  // ① 令牌鉴权:masterToken 校验,防止未授权写注册表
  if (!CheckReadWriteAccess(key, persist))
    throw ctkInvalidArgumentException("Unauthorized access ...");

  // ② 登记贡献者(幂等)
  registryObjects->AddContributor(internalContributor);

  // ③ 准备错误收集器(MultiStatus 聚合所有解析问题)与解析器
  MultiStatus::Pointer problems(new MultiStatus(..., "Problems parsing plug-in manifest ..."));
  ExtensionsParser parser(problems, this);

  // ④ 创建本次贡献的容器对象(一个插件的 plugin.xml = 一个 RegistryContribution)
  RegistryContribution::Pointer contribution =
      GetElementFactory()->CreateContribution(internalContributor->GetActualId(), persist);

  // ⑤ 执行 SAX 解析
  QXmlInputSource xmlInput(is);
  bool success = parser.parseManifest(strategy->GetXMLParser(), &xmlInput, contributionName,
                                      GetObjectManager().GetPointer(), contribution, translationBundle);

  // ⑥ 错误只记日志不中断(WARNING 容忍,ERROR/CANCEL 才返回失败)
  if (status == IStatus::ERROR_TYPE || ... ) return false;

  // ⑦ 挂入注册表(内部加写锁 + 触发变更事件)
  Add(contribution);          // → BasicAdd + FireRegistryChangeEvent (L64-71)
  return true;
}

四、核心:ExtensionsParser ------ SAX 状态机解析器

文件:internal/berryExtensionsParser.{h,cpp}

继承 QXmlDefaultHandlerberryExtensionsParser.h: L34)------即 Qt SAX 接口:

XML 从头到尾流式扫描,每遇到开/闭标签、文本就回调 startElement / endElement / characters

不建 DOM 树,内存占用与文件大小无关。

1. 状态机定义(berryExtensionsParser.cpp: L36-45)

cpp 复制代码
const int IGNORED_ELEMENT_STATE        = 0;   // 未知元素,整棵子树忽略
const int INITIAL_STATE                = 1;   // 文档开始
const int PLUGIN_STATE                 = 2;   // 进入 <plugin>
const int PLUGIN_EXTENSION_POINT_STATE = 5;   // 进入 <extension-point>
const int PLUGIN_EXTENSION_STATE       = 6;   // 进入 <extension>
const int CONFIGURATION_ELEMENT_STATE  = 10;  // <extension> 下的任意子元素

解析器维护两个栈:

  • stateStack ------ 当前所处状态(进标签 push,出标签 pop)
  • objectStack ------ 正在构建的注册表对象(Contribution / Extension / ConfigurationElement)

2. 状态转移(startElement, L272-295)

cpp 复制代码
switch (stateStack.back()) {
  case INITIAL_STATE:                handleInitialState(...);        break; // →PLUGIN_STATE
  case PLUGIN_STATE:                 handlePluginState(...);         break; // 分派 ↓
  case PLUGIN_EXTENSION_POINT_STATE: handleExtensionPointState(...); break; // 子元素一律忽略
  case PLUGIN_EXTENSION_STATE:
  case CONFIGURATION_ELEMENT_STATE:  handleExtensionState(...);      break; // 递归配置元素
  default: stateStack.push(IGNORED_ELEMENT_STATE);  // 未知顶层元素 → 忽略并告警
}

状态转移图:

复制代码
INITIAL ──<plugin>──► PLUGIN ──┬─<extension-point>─► PLUGIN_EXTENSION_POINT(叶子,属性即全部)
                               └─<extension>───────► PLUGIN_EXTENSION
                                                        │ 任意子元素(如 <view>、<editor>)
                                                        ▼
                                              CONFIGURATION_ELEMENT ──嵌套子元素──► CONFIGURATION_ELEMENT ...
                                                        (任意深度递归,靠栈支撑)

3. 各状态干什么

<plugin>(handleInitialState, L360-365):push PLUGIN_STATE,把 contribution 压入对象栈。

<extension-point> (parseExtensionPointAttributes):读 id / name / schema 三个属性,

构造 ExtensionPoint 对象;ID 补全命名空间(id="views"org.blueberry.ui.views)。

子元素一律忽略(handleExtensionPointState, L324-329)------扩展点本身只是声明。

<extension> (parseExtensionAttributes, L429-458+):读 point(目标扩展点)、idname

构造 Extension 对象压栈;id 若带点号会拆出 simpleId 与 namespace。

<extension> 下的任何子元素(handleExtensionState, L331-358)------最关键的通用机制:

cpp 复制代码
stateStack.push(CONFIGURATION_ELEMENT_STATE);
// 每个子元素 = 一个 ConfigurationElement,元素名任意(<view>/<editor>/<perspective>...)
ConfigurationElement::Pointer cur =
    registry->GetElementFactory()->CreateConfigurationElement(...);
cur->SetName(elementName);                       // 记录标签名
parseConfigurationElementAttributes(attributes); // 所有属性存成 name/value 扁平表 (L408-427)
objectManager->Add(cur, true);                   // 立即入库并分配 objectId

解析器不认识 <view><editor> 这些具体标签------它把 <extension> 下的一切

统一建模为"带属性表的通用配置元素树"。语义由消费方(如 ViewRegistry)事后解释。

这就是为什么加新扩展点不用改解析器。

文本内容 (characters, L84-109):只在 CONFIGURATION_ELEMENT_STATE 下接收,

分片累积(SAX 可能把一段文本拆多次回调)后 SetValue

4. 出栈组装(endElement, L111-209)

  • 配置元素结束 :弹出对象,把自己的 objectId 追加进父对象的 RawChildren 整型数组,
    并记录父类型(EXTENSION 或 CONFIGURATION_ELEMENT)------树结构用 int ID 数组表达,不存指针
  • </extension>:弹出 Extension,补默认命名空间与贡献者 ID,暂存 scratchVectors。
  • </plugin> (L126-158):把攒下的 extension-point 和 extension 的 ID 汇总为
    namespaceChildren 数组写入 contribution ------ 一个插件的全部贡献封口。

5. 错误处理

  • warning/error(L211-215, L297-301):记入 MultiStatus,继续解析
  • fatalError(L217-222):XML 结构性损坏 → cleanup()(L308-322)回滚 本次已入库的
    所有对象(ExtensionPoint 按 ID 移除、其余按 objectId 移除),然后终止本文件解析。
    单个插件的 plugin.xml 坏了不影响其他插件。

五、存储:RegistryObjectManager

文件:internal/berryRegistryObjectManager.h

  • 所有对象(ExtensionPoint / Extension / ConfigurationElement)以 int objectId 为主键
    存入 KeyedHashSet / QHash;父子关系是 ID 数组 → 扁平化对象图 ,天然支持缓存序列化与懒加载
    (类注释 L36:"serves objects either directly obtained from memory or read from a cache")。
  • ExtensionRegistry::Add 后还要连线 (Link, L191):把 extension 挂到它声明的
    extension-point 名下;若目标扩展点还没注册(插件加载顺序不定),先进
    orphan(孤儿)表AddOrphan, L79-82),等扩展点出现时再认领(RemoveOrphans → Link, L95-108)。
    ------这解决了"贡献者先于扩展点定义者被加载"的时序问题。
  • 全程 QReadWriteLock access 保护(Add 中 QWriteLocker,查询走读锁),线程安全。
  • 每次 Add/Remove 触发 FireRegistryChangeEvent,动态安装/卸载插件时 UI 可实时响应。

六、消费:查询与反射实例化

1. 查询(谁在用这些数据)

cpp 复制代码
// internal/berryExtensionRegistry.cpp: L542+
QList<IConfigurationElement::Pointer>
ExtensionRegistry::GetConfigurationElementsFor(const QString& extensionPointId) const;

典型消费者:Workbench 的 ViewRegistry / EditorRegistry / PerspectiveRegistry 启动时各自调用

GetConfigurationElementsFor("org.blueberry.ui.views" / "...editors" / "...perspectives")

把配置元素登记成描述符(此时类还没实例化,只有字符串)。

2. 懒实例化:CreateExecutableExtension

用户第一次真正打开某 View/Editor 时:

cpp 复制代码
// internal/berryConfigurationElement.cpp: L159-245
QObject* ConfigurationElement::CreateExecutableExtension(const QString& attributeName)
{
  QString prop = GetAttribute(attributeName);      // 通常 attributeName = "class"
  // 解析 "plugin/class:initData" 复合语法 → contributorName / className / initData
  ...
  QObject* result = registry->CreateExecutableExtension(defaultContributor, className, contributorName);

  // 若实现了 IExecutableExtension 接口,回调 SetInitializationData 注入配置
  if (IExecutableExtension* execExt = qobject_cast<IExecutableExtension*>(result)) { ... }
}

真正"new 对象"的地方------不是 C++ 反射,而是 Qt 元对象 + 显式注册表

cpp 复制代码
// internal/berryRegistryStrategy.cpp: L111-160
QObject* RegistryStrategy::CreateExecutableExtension(contributor, className, ...)
{
  // ① 先把贡献者插件 start 起来(瞬态启动,不改自启动配置)
  plugin->start(ctkPlugin::START_TRANSIENT);

  // ② 按类名查 ExtensionType 注册表
  int extensionTypeId = ExtensionType::type(className.toLatin1().data());
  if (extensionTypeId == 0)
    // 报错信息直说了机制:"The class was either not registered via
    //  BERRY_REGISTER_EXTENSION_CLASS(type, pluginContext) or you forgot to run moc"

  // ③ 构造实例
  result = ExtensionType::construct(extensionTypeId);
}

插件侧的配合------每个可被 plugin.xml 引用的类必须在插件 Activator 里注册:

cpp 复制代码
// src/berryMacros.h: L80-86
#define BERRY_REGISTER_EXTENSION_CLASS(_ClassType, _PluginContext)        \
{                                                                         \
  QString typeName = _ClassType::staticMetaObject.className();            \
  ::berry::registerExtensionType<_ClassType>(typeName.toLatin1().data()); \
}

例如 MxN 编辑器插件的 Activator 里有

BERRY_REGISTER_EXTENSION_CLASS(QmitkMxNMultiWidgetEditor, context)

plugin.xml 中 class="QmitkMxNMultiWidgetEditor" 才能被查到并 new 出来。


七、完整时序图

RegistryStrategy ViewRegistry等消费者 RegistryObjectManager ExtensionsParser SAX状态机 ExtensionRegistry CTKPluginListener CTK插件框架 RegistryStrategy ViewRegistry等消费者 RegistryObjectManager ExtensionsParser SAX状态机 ExtensionRegistry CTKPluginListener CTK插件框架 #mermaid-svg-2cm72xsNcA9h7eso{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-2cm72xsNcA9h7eso .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-2cm72xsNcA9h7eso .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-2cm72xsNcA9h7eso .error-icon{fill:#552222;}#mermaid-svg-2cm72xsNcA9h7eso .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-2cm72xsNcA9h7eso .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-2cm72xsNcA9h7eso .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-2cm72xsNcA9h7eso .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-2cm72xsNcA9h7eso .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-2cm72xsNcA9h7eso .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-2cm72xsNcA9h7eso .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-2cm72xsNcA9h7eso .marker{fill:#333333;stroke:#333333;}#mermaid-svg-2cm72xsNcA9h7eso .marker.cross{stroke:#333333;}#mermaid-svg-2cm72xsNcA9h7eso svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-2cm72xsNcA9h7eso p{margin:0;}#mermaid-svg-2cm72xsNcA9h7eso .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-2cm72xsNcA9h7eso text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-2cm72xsNcA9h7eso .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-2cm72xsNcA9h7eso .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-2cm72xsNcA9h7eso .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-2cm72xsNcA9h7eso .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-2cm72xsNcA9h7eso #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-2cm72xsNcA9h7eso .sequenceNumber{fill:white;}#mermaid-svg-2cm72xsNcA9h7eso #sequencenumber{fill:#333;}#mermaid-svg-2cm72xsNcA9h7eso #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-2cm72xsNcA9h7eso .messageText{fill:#333;stroke:none;}#mermaid-svg-2cm72xsNcA9h7eso .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-2cm72xsNcA9h7eso .labelText,#mermaid-svg-2cm72xsNcA9h7eso .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-2cm72xsNcA9h7eso .loopText,#mermaid-svg-2cm72xsNcA9h7eso .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-2cm72xsNcA9h7eso .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-2cm72xsNcA9h7eso .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-2cm72xsNcA9h7eso .noteText,#mermaid-svg-2cm72xsNcA9h7eso .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-2cm72xsNcA9h7eso .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-2cm72xsNcA9h7eso .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-2cm72xsNcA9h7eso .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-2cm72xsNcA9h7eso .actorPopupMenu{position:absolute;}#mermaid-svg-2cm72xsNcA9h7eso .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-2cm72xsNcA9h7eso .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-2cm72xsNcA9h7eso .actor-man circle,#mermaid-svg-2cm72xsNcA9h7eso line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-2cm72xsNcA9h7eso :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 启动 Activator startRegistry 创建注册表并注册服务 altextension-pointextension任意子元素 loopSAX 流式扫描每个标签 启动或事件后查询 用户首次打开View或Editor 懒实例化 OnStart 创建监听器 connectPluginListenerProcessPlugins 遍历存量插件PluginChanged RESOLVED 增量插件AddPlugin 去重检查plugin getResource plugin.xmlQByteArray 原始字节AddContribution QBuffer contributorCheckReadWriteAccess 令牌鉴权parseManifest QXmlInputSourcestartElement 状态机转移 push构造ExtensionPoint 入库构造Extension 压对象栈构造ConfigurationElement 属性扁平表 入库endElement 弹栈 子ID写入父RawChildrensuccess + MultiStatus问题清单Add contribution 写锁Link extension到extension-point 孤儿认领FireRegistryChangeEventGetConfigurationElementsFor 扩展点ID配置元素列表 仅字符串描述element CreateExecutableExtension classCreateExecutableExtension classNameplugin start START_TRANSIENTExtensionType type className 查注册ExtensionType construct 新实例


八、设计要点小结

设计点 说明
SAX 而非 DOM QXmlDefaultHandler 流式解析,双栈状态机,内存 O(深度) 而非 O(文件)
通用配置元素模型 <extension> 下任意标签统一为 ConfigurationElement + 属性表;新扩展点零解析器改动
int ID 扁平对象图 父子关系存 objectId 数组,支持缓存序列化与懒加载
孤儿机制 extension 先于 extension-point 到达时挂 orphan 表,解决加载顺序问题
错误隔离 单文件 fatalError 回滚自己的对象,不影响其他插件
令牌鉴权 + 读写锁 AddContribution 需 masterToken;注册表线程安全
懒实例化 解析只存字符串;View/Editor 首次打开才 start 插件 + 反射构造
显式类型注册 C++ 无反射,靠BERRY_REGISTER_EXTENSION_CLASS 宏把类名登记进 ExtensionType 表

关键源码文件索引

环节 文件(Plugins/org.blueberry.core.runtime/src/ 下) 关键行
启动创建注册表 internal/berryCTKPluginActivator.cpp L202-258
挂监听/扫存量 internal/berryRegistryStrategy.cpp L82-101
读取 plugin.xml internal/berryCTKPluginListener.cpp L24, L104-142
解析入口/鉴权 internal/berryExtensionRegistry.cpp L1100-1138
SAX 状态机 internal/berryExtensionsParser.cpp 状态 L36-45;startElement L272;endElement L111;characters L84
通用配置元素 internal/berryExtensionsParser.cpp handleExtensionState L331-358
属性扁平化 internal/berryExtensionsParser.cpp L408-427
错误回滚 internal/berryExtensionsParser.cpp cleanup L308-322
对象存储 internal/berryRegistryObjectManager.h L36-42, L111
Link/孤儿 internal/berryExtensionRegistry.cpp L73-108, L191
消费查询 internal/berryExtensionRegistry.cpp L542+
反射实例化 internal/berryConfigurationElement.cpp L159-245
类型查找/构造 internal/berryRegistryStrategy.cpp L111-160
注册宏 berryMacros.h L80-86
相关推荐
beibeix20155 天前
深入解析 MITK/BlueBerry 编辑器:从菜单点击到 QTabBar 的完整架构与呈现链路
mitk