plugctx-derive 0.1.3

Optional proc-macro helpers for the plugctx plugin framework (FR27)
Documentation

plugin-system

Rust 插件系统工作区:既有 C ABI cdylib 脚手架plugin-api / plugin-host),以及演进中的进程内插件框架核心库 plugctx。二者并存演进——plugctx 不立即替换 ABI 脚手架;动态加载能力按 feature 渐进接入同一 Context 生命周期。

crates.io:工作区 0.1.3(相对 0.1.2 含背压 / WASI 0.3;发版见 tag)— plugctx / plugctx-derive。曾用名 pluggable 已被无关方占用,勿与 crates.io 上其他 pluggable 混淆。已发布版本的 README 绑在该版本 .crate 上,改文案须 bump 再 publish;详见 docs/publishing.md

cargo add plugctx
cargo add plugctx-derive   # 可选过程宏

布局

plugin-system/
├── crates/plugctx         # 新核心:同步插件框架(Context/DI/事件等,按故事迭代)
│   └── examples/            # 包内示例:combo / async-start / stages-lifecycle
├── crates/plugctx-derive  # 可选过程宏:`#[derive(Plugin)]`(FR27;核心不依赖)
├── examples/                # 工作区演示包 plugctx-examples(publish=false;derive/wasm/component)
├── crates/plugin-api        # ABI 常量、PluginVTable、缓冲分配
├── crates/plugin-host       # libloading 加载器 + CLI
└── crates/plugins/
    ├── hello                # greet 示例
    └── echo                 # echo 示例

plugctx-derive可选独立 crate:用 #[derive(Plugin)] + #[plugin(depends(...))] 生成 dependencies(),并将 build 委托到 on_build。仅依赖 plugctx、不引入 derive 时,手写 impl Plugin 仍完全可用。

发布切片(FR42 / FR54)

crates.io 发布边界(FR51–FR53)

  • 工作区版本:plugctxplugctx-derive 0.1.3(crates.io 此前最高 0.1.2;0.1.0 / 0.1.1 仍保留;license / description / documentation;repository 见 docs/publishing.md
  • 不可发布(publish = false):plugin-apiplugin-host、示例插件、WASM/WIT fixtures
  • CI dry-run 门禁:./scripts/ci-publish-dry-run.sh(接入 ci-test.sh;失败阻断)
  • 后续发版(锁步 bump、先 plugctxplugctx-derive、token / trusted publishing / 速率限制):见 docs/publishing.md

详情与必填字段清单:docs/publishing.md

Feature 矩阵

完整对照与偏离说明见 docs/feature-matrix.md。摘要:

Feature 能力 默认依赖图 说明
(默认) 进程内同步 Plugin / DI / 事件 / Effect 无额外依赖 start / dispose 核心路径
async AsyncPlugin + start_async async-trait / futures 不绑定具体运行时
parallel emit_parallel 宿主侧 fan-out 隐含 async 不假定 guest 多线程
thread-safe Send+Sync Context 存储 parking_lot ServiceRef 时勿重入写路径
dynamic-native C ABI + libloading libloading(包内 c_abi dispose 后 Drop Librarydlclose;热插拔 = load → dispose → load;脚手架 plugin-api 同源且 publish = false
dynamic-wasm WASM 适配器(Extism) 可选 extism 实例显式 close/free
dynamic-wasm-component wasmtime::component 嵌入 可选 wasmtime 47.x 与 Extism 经 PluginBackend 分路径 / 分制品(FR48);见 docs/component-model-versions.md
tracing build / emit / dispose 诊断 span tracing 门面 非默认;不引入 tracing-subscriber(应用侧自选后端)
stages InitEvent / PostStartEvent / PreDisposeEvent 无额外依赖 非默认;核心仍保证 Ready/Dispose(FR32 / §4.7)

启用诊断(示例):

cargo test -p plugctx --features tracing --test acceptance_story_5_4

启用扩展生命周期阶段:

cargo test -p plugctx --features stages --test acceptance_story_6_1

应用侧自行安装订阅端,例如:

// 依赖:tracing-subscriber(由应用引入,非 plugctx 默认依赖)
tracing_subscriber::fmt().with_max_level(tracing::Level::DEBUG).init();

动态扩展细节

Feature 路径 卸载语义 ABI 协商
(默认) 进程内 Plugin PluginHandle::dispose / Context dispose 无跨边界 ABI
dynamic-native C ABI + libloading 先撤销注册与 Effect,再 Drop Librarydlclose PLUGIN_ABI_VERSION(vtable)
dynamic-wasm WASM 适配器(Extism) 实例显式 close/free(FR26) WASM_ABI_VERSION(custom section / abi_override
dynamic-wasm-component wasmtime::component 销毁=Drop Store(FR49,store_drop_count);dispose Effect 触发 最小组件导出;WIT wasip2 样例(FR50);版本矩阵 NFR12

动态适配器(NativePlugin / WasmPlugin / ComponentPlugin)实现同一 Plugin trait,经 Context::plugin / start / PluginHandle::dispose 与进程内插件对齐(可混合安装、依赖排序与事件共存)。

统一扩展入口:DynamicLoader(与 Interceptor / AsyncPlugin 同级)+ DylibLoader / WasmLoader / ComponentLoader;WASM 双后端另有 PluginBackendExtism | Component)。load(DynamicSource)Box<dyn Plugin>,可直接 ctx.plugin(...)。失败返回可诊断错误,不半初始化 Context。便捷函数 load_native_plugin / load_wasm_plugin / load_wasm_component 仍可用。

分制品(FR48):Extism PDK .wasm 与 Component Model 组件二进制不兼容。同一宿主可同时启用两 feature 并在同一 Context 安装两种插件,但客人须分别编译(或另建适配层);禁止暗示一份 .wasm 两吃。

ABI 与工具链锁定(NFR6)

  • native:宿主与插件须同工具链、同 plugin-api;布局/语义不兼容时递增 PLUGIN_ABI_VERSION。加载前协商,不匹配返回 Error::AbiMismatch执行 create/init
  • wasm:宿主 WASM_ABI_VERSION 与制品声明(custom section plugctx.abi / abi_override;缺省视为与宿主相同)协商;不匹配返回 Error::WasmAbiMismatch,不实例化。
  • 不以 abi_stable 为基线;禁止跨 DSO 传递不稳定 dyn Trait

生命周期与常见错误

典型流程:Context::newplugin(...)(可多次)→ start → 业务(get / emit / …)→ dispose

API 语义
start 按依赖序构建插件;成功后触发 ReadyEvent;失败不进入 Started。启用 stages 时:InitEvent → 构建 → ReadyEventPostStartEvent
dispose 触发 DisposeEvent → effect 逆序 cleanup → 级联子上下文;幂等。启用 stages 时:PreDisposeEventDisposeEvent → …

未启用 stages 时,扩展阶段事件类型不可用;Ready/Dispose 行为不变。

错误 典型场景
AlreadyStarted 重复 start
AlreadyDisposed 销毁后再 start / plugin / isolate,或 Context 已毁后 PluginHandle::dispose
MissingDependency 插件 dependencies 所需服务未被 provide
CircularDependency 依赖成环,乐观构建 ≥2 个插件同时无法进展(各自缺不同依赖也属此变体)
BuildFailed 插件 build 返回失败
PluginAlreadyDisposed 上下文仍存活时插件句柄再次 dispose(与 AlreadyDisposed 区分:后者是 Context 级)
ServiceNotFound 预留(get/get_trait 仍返回 Option

核心 API / Error 与设计 §6 的冻结对照(含偏差说明):docs/api-freeze.md
销毁窗口内 provide/on/effect 何时允许:docs/dispose-registration-window.md
ContextData 拆分评估(推迟):docs/adr-contextdata-split.md
更完整的 API 说明:cargo doc -p plugctx --no-deps --open

测试金字塔与回归门禁

关键路径(生命周期 / DI 错误 / 事件重入 / effect 逆序 / isolate 级联)已由 acceptance_story_1_21_7 覆盖;层次与映射见 docs/testing.md。属性测试(proptest 随机安装/卸载/emit 序列,FR38)见 acceptance_story_5_5。核心路径基准(get/emit/start,FR40,已交付)见 cargo bench -p plugctx --bench core_paths。扩展模块专项(async/parallel/thread-safe/dynamic,FR41,已交付)见 ./scripts/ci-extension-matrix.sh

cd plugin-system
# 一键回归:先 rustfmt,再 ci-test.sh(fmt check + clippy + 默认门 + trybuild + bench 编译 + rustdoc + FR41)
just test
./scripts/ci-test.sh              # 不先 fmt 写回,仅检查 + 测试
./scripts/ci-extension-matrix.sh   # 仅 FR41 扩展矩阵
./scripts/ci-cargo-hack.sh         # FR3:cargo-hack check;thread-safe 与 default 互斥,排除 --all-features

cargo test -p plugctx
cargo test -p plugctx --test acceptance_story_5_5   # proptest 属性测试(Story 5.5)
cargo test -p plugctx --test ui          # trybuild ≥3 compile_fail(Story 5.6 / FR39,已交付)
cargo test -p plugctx --test acceptance_story_5_6   # trybuild 套件护栏
cargo test -p plugctx --test acceptance_story_5_7   # 核心路径 bench 护栏(Story 5.7 / FR40)
cargo test -p plugctx --test acceptance_story_5_8   # 扩展矩阵护栏(Story 5.8 / FR41)
cargo test -p plugctx --test acceptance_story_6_2   # Error/API 冻结(Story 6.2 / FR35)
cargo test -p plugctx --test acceptance_story_6_3   # 0.1/0.2 发布切片(Story 6.3 / FR42)
# 基准(本地满量;默认 CI 仅 --no-run)
cargo bench -p plugctx --bench core_paths
cargo bench -p plugctx --bench core_paths -- --save-baseline main   # 记录基线
cargo doc -p plugctx --no-deps

可运行示例

索引与命令见下表。设计说明:examples/README.md;研究卷宗:_agile-output/.../technical-plugctx-examples-directory-2026-08-17/

包内(crates/plugctx/examples/

示例 演示 命令
combo ≥2 插件 / DI / 事件 / Effect / start·dispose(FR29) cargo run -p plugctx --example combo
async-start AsyncPlugin + start_async cargo run -p plugctx --example async-start --features async
stages-lifecycle Init→Ready→PostStart / PreDispose→Dispose cargo run -p plugctx --example stages-lifecycle --features stages
cd plugin-system
cargo check -p plugctx --examples
cargo check -p plugctx --examples --features async,stages

工作区包(examples/plugctx-examplespublish = false

示例 演示 命令
derive-plugin #[derive(Plugin)] cargo run -p plugctx-examples --example derive-plugin
component-add wasip2 WIT 组件 add cargo run -p plugctx-examples --example component-add --features component
wasm-echo Extism echo.wasm cargo run -p plugctx-examples --example wasm-echo --features wasm
cli-hotplug native 加载 / 调用 / 物理卸载 cargo build -p hello_plugin && cargo run -p plugctx-examples --example cli-hotplug --features native
web-service tiny_http + plugctx cargo run -p plugctx-examples --example web-service --features web
game-loop 无引擎 tick + 卸载后停手 cargo run -p plugctx-examples --example game-loop
cargo check -p plugctx-examples --examples
cargo check -p plugctx-examples --examples --features component
cargo check -p plugctx-examples --examples --features wasm

plugin-host + hello/echoC ABI cdylib 脚手架演示,与上表互补。

构建与演示

cd plugin-system
cargo build --workspace
cargo test --workspace
cargo test -p plugctx

# 组合场景示例(FR29)与其它演示见上文「可运行示例」
cargo run -p plugctx --example combo
cargo run -p plugctx-examples --example derive-plugin

# 可选过程宏(独立 crate;核心不依赖)
cargo test -p plugctx-derive

# 原生动态加载(feature dynamic-native;dispose 后物理卸载 / dlclose)
cargo build -p hello_plugin -p echo_plugin
cargo test -p plugctx --features dynamic-native --test acceptance_story_4_2

# WASM 动态路径(feature dynamic-wasm;Extism + 实例显式 close / FR26)
cargo test -p plugctx --features dynamic-wasm --test acceptance_story_4_3

# WASM 实例池有界 checkout(Story 7.1 / FR43)
cargo test -p plugctx --features dynamic-wasm --test acceptance_story_7_1

# WASM 实例池归还/reset/destroy(Story 7.2 / FR44–FR45)
cargo test -p plugctx --features dynamic-wasm --test acceptance_story_7_2

# 池概念文档门禁(Story 7.3 / FR46;无需 dynamic-wasm)
cargo test -p plugctx --test acceptance_story_7_3

# Component Model 宿主嵌入骨架(Story 8.1 / FR47;wasmtime 不进 default)
cargo test -p plugctx --features dynamic-wasm-component --test acceptance_story_8_1

# PluginBackend 双路径共存 / 分制品(Story 8.2 / FR48)
cargo test -p plugctx --features "dynamic-wasm,dynamic-wasm-component" --test acceptance_story_8_2
# 一 Store 一实例销毁探针(Story 8.3 / FR49)
cargo test -p plugctx --features dynamic-wasm-component --test acceptance_story_8_3
# 最小 WIT world + wasip2 样例客人(Story 8.4 / FR50)
cargo test -p plugctx --features dynamic-wasm-component --test acceptance_story_8_4
# 重建客人(需 rustup target wasm32-wasip2;CI 用检入 .wasm)
# ./scripts/build-wit-sample-guest.sh

# 混合接入 + ABI 协商(native + wasm)
cargo test -p plugctx --features "dynamic-native,dynamic-wasm" --test acceptance_story_4_4

# DynamicLoader 统一入口
cargo test -p plugctx --features "dynamic-native,dynamic-wasm" --test acceptance_story_4_5

# tracing 诊断(Story 5.4 / FR37;仅门面,无强制 subscriber)
cargo test -p plugctx --features tracing --test acceptance_story_5_4

# 列出 target/debug 下的插件 .so
cargo run -p plugin-host -- target/debug list

# 调用
cargo run -p plugin-host -- target/debug call hello greet rust
cargo run -p plugin-host -- target/debug call echo echo ping

plugctx 启用 dynamic-native 后可用 load_native_plugin 将 C ABI 插件安装进同一 Context。卸载先撤销注册与 Effect,再 Drop libloading::Librarydlclose / FreeLibrary)。热插拔:load → use → dispose → load(不提供 reload())。Windows 上若映射期间文件被锁,须先 dispose 或换路径再写制品。

启用 dynamic-wasm 后可用 load_wasm_plugin 加载 Extism WASM 实例(制品须为合法 \0asm,验收用 bundled_echo_wasm / testdata/echo.wasm)。dispose / close 显式释放 Extism 插件实例(FR26)。亦提供 WasmInstancePool:可配置 max_instances、带超时 checkout(FR43);Guard Drop 归还reset + 工厂重建,防跨借出串态,FR44),[WasmCheckoutGuard::destroy] 销毁不归还(FR45)。extism 仅经本 feature 进入依赖图,不进默认构建。

启用 dynamic-wasm-component 后可用 load_wasm_component / ComponentLoader / PluginBackend::Componentwasmtime::component 加载组件制品并调用导出(FR47;骨架 fixture testdata/component_add.watFR50 真实 wasip2 WIT 客人 testdata/wit_sample_add.wasm,源码 guests/wit-sample/)。ComponentPlugin 接入同一 Context 生命周期(provide(ComponentInvoker) + dispose Effect Drop Store)。销毁语义为「一 Store 一实例」:close/dispose → Store Drop,store_drop_count 可观测(FR49)。wasmtime 进入默认依赖图(NFR14);版本钉死与客人三元组见 docs/component-model-versions.md。与 Extism 路径经 PluginBackend 分路径、分制品(FR48),勿假设同一 .wasm 两吃。

池 vs PluginHandle::disposePluginHandle::dispose 卸载 Context 已安装插件;池 Drop/destroy 只管理借出 WASM 实例——归还 ≠ 销毁 ≠ Context 精确卸载(详见 dynamic_wasm 模块文档表)。

逻辑 InstancePool ≠ Wasmtime 资源 pooling(FR46)

是什么 本仓库入口
逻辑 InstancePool 应用层有界 checkout / 超时 / 归还 reset / 显式 destroy feature dynamic-wasmWasmInstancePool / WasmPoolConfig / WasmCheckoutGuard;验收 acceptance_story_7_17_2
Wasmtime 资源 pooling 运行时 PoolingAllocationConfig 等内存/表槽复用 本 crate 不封装;Extism 底层或使用 Wasmtime,但不向宿主暴露该配置

概念与 Feature 对照见 docs/feature-matrix.md。文档门禁:cargo test -p plugctx --test acceptance_story_7_3

上手指南(最小插件、native 热插拔、CLI/Web/游戏示例命令):docs/guide.md

写一个插件(C ABI 脚手架)

  1. 新建 cdylib crate,依赖 plugin-api
  2. 导出 #[no_mangle] pub extern "C" fn plugin_entry() -> PluginVTable
  3. 实现 create / name / init / call / free_buffer / destroy
  4. 输出缓冲必须用 plugin_api::alloc_output,由宿主通过 vtable free_buffer 释放。

当前 ABI 版本:PLUGIN_ABI_VERSION = 1(native)/ WASM_ABI_VERSION = 1(wasm / Extism)。布局变更时递增版本,宿主会拒绝不匹配的制品。