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}。
继承 QXmlDefaultHandler (berryExtensionsParser.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(目标扩展点)、id、name,
构造 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 |