P6.UB14-load-dynamic-plugins-api · framework 净新增 PC 第三方 dll 加载 API
Worker:ClaudeB(P6 主战场 · DSP 算法库 daemon)
部门:DSP · 原生 C(dspalgo/framework/)
预计:2.0d
优先级:P1(ADR-14 Phase 1 立即行动 · ABI 基石 · 后续 4 fork 全依赖)
状态:dispatched
🔍 触发与解锁链
- 触发:用户 2026-06-02 09:32 同步指令
accept ADR-AIOS-14+start P6.UB14-load-dynamic-plugins-api - 本任务 zombie 后解锁:
- P6.UB14-pc-host-sdk-export(导出 ModuleRegistry_Register · 1.0d ClaudeB)
- P5.UB14-plugin-management-api(C# /api/plugins/list · 1.5d ClaudeB)
- P1.UB14-module-library-from-registry(前端 ModuleLibrary 改造 · 1.0d ClaudeA)
- xivst-sdk.UB14-pc-only-bootstrap(SDK 包 · 含 hello-effect-v1.dll 模板 · 2.0d)
任务定义(基于 ADR-14 §2.3)
实现 v7 framework 公开 API 的唯一净新增 DynChain_LoadDynamicPlugins(dir, platform, stats) · PC only · 扫描 plugins/thirdparty/<vendor>/*.dll · 通过 LoadLibraryW + GetProcAddress("xisound_plugin_register") 加载 · dll 内部走 v7 ModuleRegistry_Register 注册 · 错误隔离不阻塞 host 启动。
完整 prompt(直接复制粘贴 worker 终端)
[U-thread] P6.UB14-load-dynamic-plugins-api(ADR-14 §2.3 net-new framework API)
[部门] DSP · 原生 C
[Worker CWD] d:/work/25_claude/workspace/AlgoDepartment/04_development/
[Occupies] P6.K1-framework + P6.K4-include
[优先级] P1 · 2.0d · ABI 基石(后续 4 fork 全依赖)
[ADR] docs/08-implementation/40-aios/ADR/ADR-AIOS-14-pc-dynamic-plugin-loading.md
[上游必读] docs/03-platform/40-dsp-algo/10-architecture-v7.md(2455 行 · §3 平台抽象 + §4.2 ModuleFuncTable + §5 ModuleRegistry)
[参考文档] ADR-14 §2.2(dll 入口符号约定)+ §2.3(API 签名 + 加载流程)+ §2.5(业务行为契约 5 必填段)
[隔离] 🚀 task · 与并行 fork 文件正交(本任务仅写 framework/dynchain_loader_pc.c · dll/dspalgo_dll.c 由 fork 2 处理)
【背景】
ADR-14 仅保留 PC 端 LoadLibrary 一项净新增 · 严守"v7 ABI 不二造"边界铁律。
v7 §5 ModuleRegistry 已实现 builtin 编译期注册(module_registry_all.c)· 本 API 扩展为运行时注册第三方 dll 提供的 module · 走完全相同的 ModuleFuncTable 接口 · host 不感知 builtin/thirdparty 差异。
【执行步骤】
Step 1 · 上游真值核查(必跑)
- 读 v7 §4.2 module_interface.h · 确认 ModuleFuncTable 6 函数指针签名 · 第三方 dll module 必须实现完全一致的 6 函数
- 读 v7 §5 dynchain_registry.c · 确认 ModuleRegistry_Register(typeNumId, typeName, *pFuncs) 签名 + 内部线性扫描查表 · 不允许重号(后者拒绝 + log warn)
- 读 v7 §3 platform_dynamic.c · 确认 DynChainPlatformOps.log 签名(level, fmt, ...)用于错误报告
- 读 ADR-04 §2.1.2 vendor 段位 0xAAAA0000~0xFFFEFFFF · 加载时校验 typeNumId 必须落在此段(本 API 不强制 · 仅 log warn 越界)
Step 2 · 新建 framework/dynchain_loader_pc.c(主体)
- 文件头守卫:#ifdef _WIN32 ... #endif(整个文件 PC only · DSP 编译时跳过)
- 导出 API 签名(完全对齐 ADR-14 §2.3):
int32_t DynChain_LoadDynamicPlugins(
const char *pluginsDir,
const DynChainPlatformOps *pPlatform,
LoadDynamicPluginsStats *pStats);
- 实现:
① pluginsDir NULL/空 → 返回 OK(stats 全 0 · 不报错 · 用户场景:首次安装无第三方 dll)
② pPlatform NULL → 返回 DYNCHAIN_ERR_INVALID
③ pluginsDir 不存在 → log INFO + 返回 OK(stats.scanned=0 · 不报错)
④ FindFirstFileW + 递归 1 层扫子目录 plugins/thirdparty/<vendor>/*.dll
⑤ 对每个 dll:LoadLibraryW → GetProcAddress("xisound_plugin_register")
- 任一失败 → log ERR + FreeLibrary + stats.failed++ + continue(不阻塞)
- 成功 → 调 registerFn(&ModuleRegistry_Register) · dll 内部循环注册
· ModuleRegistry_Register 内部已处理重号(后者拒绝 + log warn) · 本 API 通过外部 counter 统计 stats.skipped(查 ModuleRegistry_Count 前后差值)
⑥ stats.loaded++ · 记录 LoadedPluginEntry{ dllPath, hModule, registeredCount } 到内部静态数组(MAX_LOADED_PLUGINS = 32)
Step 3 · 加 framework/dynchain_loader_pc.h(公开头)
- typedef struct LoadDynamicPluginsStats_ { uint32_t scanned/loaded/failed/skipped; }
- 函数声明 DynChain_LoadDynamicPlugins · 含 doxygen 注释(从 ADR-14 §2.3 复制)
- 注:LoadedPluginEntry 内部结构 · 不导出
Step 4 · 加 include/dynchain_interface.h 增量(将 dynchain_loader_pc.h 暴露)
- 在文件末尾加 #ifdef _WIN32 #include "dynchain_loader_pc.h" #endif
- 不破坏现有 v7 公开 API
Step 5 · 单元测试(MSVC + Google Test 或现有 dsp_algo 测试框架)
- 5 个 case 必填(对齐 ADR-14 §2.5 ③ 失败回退路径 5 类):
① happy path:fixtures/test-plugin-good.dll(导出 register · 注册 1 module typeNumId=0xAAAA0001)→ stats.loaded=1, failed=0, skipped=0
② dll 文件损坏:fixtures/corrupt.dll(随机字节)→ LoadLibrary 失败 · stats.failed=1, loaded=0
③ 入口符号缺失:fixtures/test-plugin-no-entry.dll(不导出 xisound_plugin_register)→ GetProcAddress 失败 · stats.failed=1
④ typeNumId 冲突:fixtures/test-plugin-conflict.dll(注册 typeNumId=0x10010001 · 与 builtin gain_v1 冲突)→ register 时 ModuleRegistry 拒绝 · stats.skipped=1, loaded=1(dll 本身加载成功)
⑤ pluginsDir 不存在:DynChain_LoadDynamicPlugins("C:/non-existent", ...) → 返回 OK · stats 全 0 · 不报错
Step 6 · CMake 集成
- 修改 CMakeLists.txt(顶层或 framework/CMakeLists.txt)· 加 if(WIN32) target_sources(dspalgo PRIVATE framework/dynchain_loader_pc.c) endif()
- DSP 平台编译时排除(确保 DSPAlgoStatic 不带本文件)
- 单元测试 target 链接 framework + 现有 ModuleRegistry · 调本 API
Step 7 · 验收
- cmake --build 0 错 0 警(MSVC)
- 单元测试 5/5 全过
- DSPAlgoStatic 编译产物体积无变化(嵌入式 .a 不含本文件)
- DSPAlgo SHARED(PC dll)体积增量 < 5KB(loader 代码量小)
- 跑现有 dsp_algo 测试套件回归全过(P/Invoke 端点不破坏)
【验收】
✓ framework/dynchain_loader_pc.c + .h 落盘
✓ include/dynchain_interface.h #ifdef _WIN32 暴露
✓ CMakeLists.txt 加 WIN32 条件 source
✓ 单元测试 5 case 全过(覆盖 ADR-14 §2.5 ③ 失败回退 5 类)
✓ MSVC 编译 0 错 0 警 + DSPAlgoStatic 编译产物零增量
✓ 现有 dsp_algo 测试套件 0 回归
✓ commit 含三元组 trailer:[step=7/7] [pid=P6] [uid=UB14-load-dynamic-plugins-api] [occupies=P6.K1+P6.K4] [files=framework/dynchain_loader_pc.{c,h}+include/dynchain_interface.h+CMakeLists.txt+tests/]
【commit 规范】
subject:feat(P6.UB14/load-dynamic-plugins-api): framework 净新增 PC dll 动态加载 API(ADR-14 §2.3)
trailer 必填:
[step=7/7] [pid=P6] [uid=UB14-load-dynamic-plugins-api] [occupies=P6.K1+P6.K4] [files=...]
[ipc=none]
参考 done/ 12 标本 commit message 结构 · 不允许跳三元组(7 天宽限期内 warning · 6/2 起 strict mode hook 硬拒)
【禁止】
❌ 引入第二套 module ABI(本 ADR §2.6 #1 边界铁律 · 必须复用 v7 ModuleFuncTable)
❌ 修改 v7 §4.2 module_interface.h(冻结)+ §5 dynchain_registry.c(冻结)+ §3 platform_dynamic.c(冻结)
❌ 修改 dll/dspalgo_dll.c(本 fork 不动 · fork 2 P6.UB14-pc-host-sdk-export 处理)
❌ 实施热卸载 / 沙箱 / 签名校验(MVP 不做 · ADR-14 §3.3)
❌ 改 builtin module 注册流程(modules_config.h + module_registry_all.c 永久不动)
❌ DSP 平台执行任何动态加载(本文件 #ifdef _WIN32 整体排除)
❌ 仅靠 typecheck/build 全绿就 commit · 必须跑单元测试 5/5(ADR-14 §2.5 ⑤ e2e 真值精神)
解锁链(本任务 zombie 后)
- ✅ P6.UB14-pc-host-sdk-export(1.0d ClaudeB · 文件正交 dll/dspalgo_dll.c · 可立即并派)
- ✅ P5.UB14-plugin-management-api(1.5d ClaudeB · C# 端通过 P/Invoke 调本 API · 等本 API 落地)
- ✅ P1.UB14-module-library-from-registry(1.0d ClaudeA · 等 P5 API 200 OK)
- ✅ xivst-sdk.UB14-pc-only-bootstrap(2.0d · 等 fork 2 host SDK lib 落地 · hello-effect-v1.dll 模板需链接 host SDK)
风险评估
| 风险 | 缓解 |
|---|---|
| Windows API LoadLibraryW 路径编码(UTF-8 → wchar)处理错误 | 用 MultiByteToWideChar(CP_UTF8) · 测试用例 case 5 故意混入中文路径 |
| ModuleRegistry_Count 在 register 调用前后获取以统计 skipped 不是原子操作(若 register 函数内多线程) | 假定 dll 内部 register 单线程同步调用 · 若 vendor 违规 → 后续 ADR-14-R1 加锁 |
| 单元测试 fixtures dll 编译需 MSVC 工具链 | 用 vcpkg 或 直接 cl.exe + /LD · CI 环境可用 |
| 嵌入式 DSP 端 #ifdef _WIN32 误判失效 | CMake 测试:cmake -DTARGET=DSP 编译验证 dynchain_loader_pc.c 不被链入 |
| 第三方 dll register 时抛 SEH 异常拖死 host | 用 __try / __except SEH 包裹 registerFn 调用 · MVP 不实施(留 R1) · log + skip 即可 |
历史
| 时间 | 事件 | hash |
|---|---|---|
| 2026-06-02 09:33 | dispatched · ClaudeB 立即接 | (待 stop 时回填) |