Skip to content

Shield Plugin System v1 ​

本文是 Shield 插件系统的权威设计。v1 不兼容旧实验实现。插件系统采用 metadata-first 发现模型:磁盘包用 manifest 描述,运行时实例和 binding 通过 Shield 主配置的 plugins 子树声明。

配置口径:

  • 插件包 manifest 只支持 manifest.yaml。
  • config_schema 使用 JSON Schema 子集表达。
  • plugins.instances[].config 是 JSON-compatible 值模型;在当前主程序中它作为 YAML 子树写入 app.yaml,由 Config 子系统转换为 JSON 值后交给插件系统。
  • manifest 的语义模型保持单一:YAML 是唯一磁盘来源,字段结构与本文档定义的 JSON-compatible manifest 一致(YAML 是 JSON 的超集,所有 manifest 字段都能用 JSON-compatible 值模型表达)。

Goals ​

目标说明
Metadata-first先扫描 manifest 建立 catalog,不加载插件代码即可知道有哪些包可用。
Discovery / load / start 分离发现、解析依赖、加载共享库、创建实例、启动实例是不同阶段。
JSON-compatible config modelmanifest 字段模型与 schema 保持 JSON-compatible;运行时实例配置使用 Shield Config 子树承载,进入插件系统后按 JSON 值模型处理。
Stable C ABI插件共享库只暴露 C ABI 入口,不依赖 Shield 内部 C++ 类型。
Explicit wiring核心架构决策:用户在主配置中显式声明每个 instance、binding 和 dependency;host 不做隐式实例创建、不做能力自动发现、不做 starter 模板编排。详见 Design Rationale。
Provider-oriented插件提供明确接口,例如 shield.database.v1、shield.cache.v1。
Structured errors加载、配置、依赖和运行期错误都带 code/message/hint/package/instance/phase。

Non-goals ​

非目标说明
热加载v1 不承诺运行期替换共享库。基础设施热加载与插件系统本质冲突(连接池/TCP 句柄无法搬家、vtable 指针稳定性、并发调用屏障),业界无成功先例。业务代码热重载走 Lua runtime(require 重载),与插件热加载是两个层面的问题。详见 Design Rationale。
请求级生命周期v1 只定义进程级 init / shutdown。worker/request 生命周期后续再加。
插件间任意互调v1 不提供全局 find_plugin 或裸 vtable 强转。插件通过声明依赖拿到 host 注入的接口。
自动配置 / Starter 编排v1 不引入 Spring Boot Starter 式的"声明聚合 + 自动配置"层。starter 模式适合标准化 Web 业务(CRUD/REST),不适合游戏后端的异构部署、多实例与高事故代价场景。详见 Design Rationale。
Auto-discoveryv1 不扫描 plugin 目录自动创建实例。隐式实例违背 Explicit Wiring,破坏可审计性。快速起步走 examples/kickstart/(三条命令跑通四件套全链路)与 tools/new_plugin.sh(新包脚手架),见 第三方插件开发指引。
沙箱v1 不隔离 native 插件的内存、线程或系统调用权限。
多套插件配置入口不为插件系统引入额外 TOML/INI/专用 YAML 文件。运行时配置只来自 Shield 主配置的 plugins 子树。

Design Rationale ​

本节解释 Shield 插件系统为什么以 Explicit Wiring 为核心架构决策,以及为什么不支持热加载、starter 编排和 auto-discovery。这些不是 v1 的临时取舍,而是基于游戏后端固有特征和业界对比得出的长期决策。

Explicit Wiring 是核心架构决策 ​

定义:用户在 app.yaml 的 plugins 子树中显式声明:

  • 每个 instance 的 id / package / required / dependencies / config
  • 每个 binding 的 logical name → instance id

host 的职责仅限于解析这些声明、注入依赖、调度生命周期。host 不做的事:

  • 隐式创建实例("看到 sqlite 包就自动起一个 db.default")
  • 能力自动发现("扫描目录猜你需要哪些插件")
  • Starter 模板编排("声明 matchmaking 能力,自动拉起完整 stack")
  • 默认 binding("instance 提供 shield.database.v1 就自动绑到 database.default")

所有"自动行为"都被 Explicit Wiring 拒绝——这不是 v1 的能力限制,是基于游戏后端特征的长期架构选择。

为什么适合游戏行业 ​

游戏后端有 7 个固有特征,每个都把配置模型推向 Explicit Wiring:

固有特征对配置模型的影响
部署异构性极高不同游戏类型用完全不同栈(MMO=MySQL+Redis、SLG=MongoDB+Redis Cluster、FPS=Redis-only);starter 默认值经常被覆盖,自动配置无收益
多实例是常态db.main + db.audit + db.shard0..N + db.log、cache.session + cache.rank + cache.pubsub;starter 默认实例根本不够用,最终还是 escape hatch 写 explicit
生产事故代价极高玩家在线=流失+口碑+收入;事故定位每分钟都在烧钱;必须"一眼看配置就知道拓扑",不允许 starter → override → 展开三层反查
运维团队规模不大不像互联网大厂有专职 SRE;主程/技术总监常兼任运维;配置可读性 > 简洁性
复杂依赖需精确控制leaderboard 依赖 cache + database;matchmaking 依赖 leaderboard + database + cache;protocol codec 依赖 descriptor 资产;必须精确指定"哪个实例连哪个实例"
环境差异大开发用 sqlite 内存库,生产用 MySQL cluster;业务代码不变,差异全在配置层
运营活动 + 紧急 hotfix 频繁配置变更要快速、可逆、可审计;隐式行为阻断紧急回滚

Explicit Wiring 在这 7 项里全部占优。Starter / Auto-discovery 只在"新项目快速原型"这一边缘场景有收益,且该收益可通过 examples/ 模板项目满足。

业界对比 ​

系统类型配置模型
Skynet游戏服框架(C+Lua)Explicit
Kbengine游戏服引擎(C++)Explicit
Photon Server游戏服(C#)Explicit
Colyseus游戏服(Node)Explicit
Kubernetes基础设施Explicit(Helm chart 是配置期模板,运行时仍 explicit)
Terraform基础设施Explicit(module 是配置期模板,运行时仍 explicit)
Spring Boot业务 WebStarter 主导

关键观察:所有游戏服框架都用 Explicit Wiring。基础设施系统(K8s/Terraform)即使提供模板层(Helm/Module),运行时模型仍然是 Explicit。Starter 主导只出现在标准化程度高的业务 Web 场景。

Shield 定位是通用游戏后端框架,配置模型与游戏服框架对齐是必然选择。

为什么不采用 Starter / Auto-discovery ​

Starter 模式(Spring Boot 风格的"声明聚合 + 自动配置")在游戏后端场景下的核心问题:

问题表现
多实例场景失效中等规模游戏有 15+ 实例(多分片 db、多用途 cache、多通道 queue),starter 默认的"db + cache + mm"三件套完全不够
事故定位链变长Explicit 一行 grep 3 秒定位;Starter 要查 starter.yaml + override + 展开结果三份,3 秒变 3 分钟
部署异构性击穿默认值不同游戏 auth 方案(Steam/微信/Firebase/JWT)完全不同,starter 默认值必然被覆盖
团队维护成本不支撑starter 库需要版本管理、profile 测试、兼容性矩阵、文档;中小游戏团队没这个人力
隐式行为阻断紧急回滚starter 升级会隐式改变所有引用方的实际配置,生产环境不可接受

Auto-discovery(扫描目录自动创建实例)的问题更严重:隐式实例违背 Explicit Wiring 核心原则,"哪些实例在跑"变成不可预测的状态,调试时无法从配置反推运行态,还有安全风险(恶意放置的 plugin 目录被自动加载)。

需要快速起步时,正确做法是提供模板项目(examples/kickstart/,含完整 explicit yaml,三条命令跑通)和插件脚手架(tools/new_plugin.sh),用户 copy 后修改,而不是引入配置自动展开层。

为什么不支持基础设施热加载 ​

"插件热加载"经常与"业务热加载"混淆,但两者是完全不同的问题:

维度业务代码热加载基础设施热加载
加载对象Lua 脚本C ABI 共享库
变更频率日频甚至小时频季度甚至年级别
状态依赖无状态或可序列化重状态(连接池、TCP 句柄、事务上下文)
业界先例Erlang BEAM / Lua require / JVM JRebel无主流系统支持
Shield 支持方式已支持(Lua require 重载)不支持(架构层面排除)

基础设施热加载的真实障碍:

  • 连接池无法搬家:TCP 连接是进程级资源,不能在 .so 之间迁移
  • vtable 指针稳定性:worker 线程刚解引用 vtable,旧 .so 被 dlclose 触发 use-after-free
  • ABI 兼容:新 .so 的 struct 布局变了,旧 instance 的内存布局失效
  • 并发屏障:vtable 切换瞬间所有 worker 必须进 barrier,实现复杂度爆炸

业界所有"热加载成功"的系统热的都是业务层(字节码/脚本),不是基础设施层。Nginx / PostgreSQL / Redis 升级扩展都走 restart,不走热加载。

结论:基础设施热加载的成本远超收益。Shield 业务热重载走 Lua runtime(已有路径),插件层不支持热加载。

Spring Boot 模式的适用边界 ​

文档对比时常引用 Spring Boot Starter 作为参考。需要明确边界:

场景Spring Boot Starter 适用Shield Explicit Wiring 适用
标准 CRUD 业务适用—
REST API 服务适用—
内部管理后端适用—
游戏后端—适用
高异构性部署—适用
多实例精细控制—适用
高事故代价生产环境—适用

Spring Boot Starter 的"约定优于配置"哲学建立在"大部分项目都用同一套"的前提上。游戏后端不满足这个前提——每个游戏的栈都不一样。

借鉴 Spring Boot 可以借鉴的点:接口/实现分离、依赖注入、配置绑定(这些 Shield 都已有)。不应借鉴的点:starter 自动配置、auto-discovery、隐式默认值。

Terminology ​

名词含义
Package磁盘上的一个插件包目录,包含 manifest 文件和共享库。
Manifestmanifest.yaml,描述包元数据、库路径、接口、依赖和配置 schema。
Catalog扫描 manifest 后得到的可用包索引,不要求加载共享库。
Instance主配置里创建的一个插件实例。同一个 package 可以有多个 instance。
Binding主配置里把一个接口名或逻辑名字绑定到某个 instance,例如 database.default -> db.main。
Interfacehost 与插件之间的稳定 ABI 契约,例如 shield.database.v1。
Capability接口下的能力标签,例如 sql、transactions、pubsub。用于查询和选择,不替代接口契约。

Disk Layout ​

插件目录只扫描 manifest 文件,不扫描所有 DLL/SO。这样可以在不执行第三方代码的情况下建立 catalog。

text
plugins/
  database.mysql/
    manifest.yaml
    bin/
      shield_database_mysql.dll
      libshield_database_mysql.so
    lua/                      # 可选:插件自带的 Lua 搜索路径
      shield_mysql.lua
  database.mongodb/
    manifest.yaml
    bin/
      shield_doc_mongodb.dll
      libshield_doc_mongodb.so
    lua/
      init.lua
  cache.redis/
    manifest.yaml
    bin/
      shield_cache_redis.dll
      libshield_cache_redis.so

带 lua/ 子目录的插件,host 会在 Lua runtime 初始化后把 lua/?.lua 注入 package.path,并通过插件的 register_lua 钩子把 shield.<package>.* 注册到 Lua 侧。

主配置指定插件根目录。当前主程序使用 app.yaml:

yaml
plugins:
  directory: "./plugins"

Manifest ​

每个插件包必须有一个 manifest,文件名固定为 manifest.yaml。

yaml
schema_version: 1
id: database.mysql
name: MySQL Database
version: 1.0.0
kind: database
description: MySQL provider for shield.database.v1
entry: shield_plugin_get_v1
library:
  windows: bin/shield_database_mysql.dll
  linux: bin/libshield_database_mysql.so
  macos: bin/libshield_database_mysql.dylib
provides:
  - interface: shield.database.v1
    capabilities: [sql, transactions]
requires: []
lua:
  namespace: database.mysql
  search_paths: [lua/?.lua]
documentation:
  url: https://cuihairu.github.io/shield/plugins/database-mysql
  description: MySQL provider for the shield.database.v1 interface via prepared statements
config_schema:
  type: object
  required: [host, database, username]
  properties:
    host:
      type: string
      default: 127.0.0.1
    port:
      type: integer
      default: 3306
      minimum: 1
      maximum: 65535
    database:
      type: string
    username:
      type: string
    password:
      type: string
      secret: true

Manifest 字段规则:

字段规则
schema_version必须为 1。
id全局唯一 package id,建议使用 <domain>.<provider>,如 database.mysql。
versionpackage 版本,使用 semver 字符串。
entry共享库导出的 C 符号,v1 固定建议为 shield_plugin_get_v1。
library按平台声明相对 package 根目录的库路径;必须是相对路径,不能用 .. 逃逸 package root。
provides声明 package 提供的 interface 和 capability;至少声明一个 interface,同一 package 内不能重复。
requires声明 instance 创建时需要 host 注入的依赖接口。
lua可选。声明插件的 Lua 元数据:namespace(Lua 侧注册到 shield.<namespace>)、search_paths(相对 package 根的 Lua 搜索路径,host 启动时自动注入 package.path)。
documentation可选。声明插件的在线文档 URL 和一句话描述。host 通过 PluginHost::list_packages() 暴露这份元数据,当前 shield.plugin.packages() Lua API 会直接返回 docs_url 和 docs_description。第三方插件强烈推荐设置此字段。
config_schema插件配置 schema,直接使用 JSON Schema 可实现子集表达。

Runtime Config ​

运行时插件配置写在 Shield 主配置的 plugins 子树中。当前仓库默认使用 YAML 文件,但该子树必须保持 JSON-compatible:object、array、string、number、boolean、null,不使用 YAML 特有语义作为插件契约。

yaml
plugins:
  directory: "./plugins"
  instances:
    - id: db.main
      package: database.mysql
      required: true
      config:
        host: "127.0.0.1"
        port: 3306
        database: shield
        username: root
        password: secret

    - id: cache.main
      package: cache.redis
      required: false
      config:
        uri: "redis://127.0.0.1:6379/0"

  bindings:
    database.default: db.main
    cache.default: cache.main

规则:

规则说明
不再有 plugins.enabled是否启用由 plugins.instances 决定。没有实例就只是可用包。
required: true该实例加载、配置、依赖解析或启动失败时进程启动失败。
required: false失败时记录结构化错误,实例状态为 unavailable,依赖它的 required 实例仍会失败。
bindings当前格式为 logical_name: instance_id。C++ 调用方通过 get_by_binding<T>() 提供目标 interface,host 在访问时向实例请求该 interface。
多实例同一个 package 可以创建多个 instance,例如 db.main 和 db.audit。

说明:显式 interface binding(例如 { instance: "db.main", interface: "shield.database.v1" })仍是可选演进方向;v1 当前配置格式保持最小化,避免在 binding 层重复声明 package manifest 已经提供的信息。

Bootstrap Pipeline ​

启动流程固定为 9 个语义阶段:

text
scan -> catalog -> plan -> resolve -> load -> create -> start -> lua_init -> lua_register
                                                              stop <- shutdown
阶段说明
scan遍历 plugins.directory/*/manifest.yaml。只读 manifest,不加载共享库。
catalog校验 manifest schema、package id 唯一性、平台库路径安全性和 interface/dependency 声明。
plan从主配置的 plugins.instances 建立启动计划。
resolve校验 package 存在、实例 id 唯一、依赖可满足、拓扑无环,并在这一阶段应用 config 默认值、执行 schema 校验、校验 binding 名唯一且目标 instance 存在。
loaddlopen / LoadLibrary 共享库,解析 entry。
create调用入口函数,校验 ABI guard,创建 instance handle。
start调用实例 start,插件初始化后端连接、后台线程等资源。
lua_inithost 初始化 Lua runtime(包括把所有声明了 lua.search_paths 的插件路径注入 package.path)。
lua_registerhost 拿到一个 Lua state 后,遍历所有已 started 的实例,依次调用 register_lua(self, L, err),插件把自身 API 注册到该 VM 的 shield.<namespace>。多 VM 场景下每个 VM 都会执行一次。
stop进程退出时按依赖反序调用 shutdown。

实现说明:上表是语义阶段,与代码方法的对应关系是 scan / catalog / plan_and_resolve(plan + resolve 合并)/ load_all / create_all / start_all;Lua 两个阶段对应 PluginHost::inject_lua_paths / PluginHost::register_lua_all(src/plugin/plugin_host.hpp)两个方法,而非 start_all 的延续,且不在 startup() 内部调用——由 LuaRuntime::register_api 在每个 Lua VM 创建时调用,仅在 host 配置了 Lua runtime 时执行,纯 C++ 运行模式下跳过。

lua_init 和 lua_register 只在 host 配置了 Lua runtime 时触发;纯 C++ 运行模式下这两个阶段跳过,register_lua 钩子不会被调用。

Binary ABI ​

Manifest 解决发现问题,二进制入口解决真实加载和 ABI 守门问题。两者都必须存在。

c
#define SHIELD_PLUGIN_ABI_VERSION 1

struct lua_State;  // 前向声明,abi.h 不直接 include lua.h

typedef struct shield_plugin_abi_v1 {
    uint32_t abi_version;
    uint32_t struct_size;
    const char* package_id;
    const char* package_version;
    int (*create)(const shield_plugin_create_args_v1* args,
                  shield_plugin_instance_v1** out,
                  shield_error_v1* err);
} shield_plugin_abi_v1;

typedef struct shield_plugin_instance_v1 {
    uint32_t struct_size;
    const char* instance_id;

    const void* (*get_interface)(shield_plugin_instance_v1* self,
                                 const char* interface_name,
                                 shield_error_v1* err);

    int (*start)(shield_plugin_instance_v1* self,
                 shield_error_v1* err);

    void (*shutdown)(shield_plugin_instance_v1* self);

    // host 在每个 Lua VM 初始化后调用一次,插件用 shd 绑定层把自身
    // API 注册到 shield.<namespace>。没有 Lua 绑定的插件也必须提供此字段
    // (直接 return 0 即可)。纯 C++ 运行模式会跳过此回调;被调用时 L 非 NULL。
    int (*register_lua)(shield_plugin_instance_v1* self,
                        struct lua_State* L,
                        shield_error_v1* err);
} shield_plugin_instance_v1;

SHIELD_PLUGIN_EXPORT
const shield_plugin_abi_v1* shield_plugin_get_v1(void);

ABI 校验规则:

校验项失败处理
入口符号不存在load 失败。
abi_version != 1load 失败。
struct_size 小于 host 需要的最小大小load 失败。
package_id 与 manifest id 不一致load 失败。
共享库不提供 manifest 声明的 interfacecreate 失败。

Host API ​

host API 只提供运行环境能力和已解析依赖,不提供全局插件查找。

c
typedef struct shield_host_api_v1 {
    void (*log)(shield_log_level level,
                const char* plugin,
                const char* instance,
                const char* message);

    void (*report_error)(const shield_error_v1* err);

    // 返回字符串值;object/array 返回 JSON fragment。返回指针有效期到同一
    // 线程下一次 config_get 调用。
    const char* (*config_get)(shield_plugin_context_v1* ctx,
                              const char* path);

    const void* (*dependency)(shield_plugin_context_v1* ctx,
                              const char* name,
                              const char* interface_name);

    // 返回 host 的 lua_State*。如果 host 以纯 C++ 模式运行(未启 Lua
    // runtime),返回 NULL。插件 register_lua 被调用时此值保证非 NULL。
    struct lua_State* (*lua_state)(shield_plugin_context_v1* ctx);

    // 把一个路径加入 Lua package.path(is_cpath=0)或 package.cpath
    // (is_cpath=1)。相对路径以插件 package 根目录为基准解析为绝对路径。
    // 多次调用累加。返回 0 成功,非 0 失败。
    int (*lua_add_path)(shield_plugin_context_v1* ctx,
                        const char* path,
                        int is_cpath);

    // 把 plugins.bindings 的逻辑名解析为目标 instance id。返回指针有效期到
    // 同一线程下一次调用;未命中返回 NULL。
    const char* (*binding_instance_id)(shield_plugin_context_v1* ctx,
                                       const char* binding);

    // 返回当前线程正在执行的 Lua service dispatch 的 service id;不在
    // dispatch 中(模块加载、console eval、插件线程)返回 NULL。异步回调
    // 需要在注册时捕获它,并经 lua_post_to_service 回到该 service ——
    // 插件线程上绝不允许直接调用 Lua 函数。
    const char* (*lua_current_service_id)(shield_plugin_context_v1* ctx);

    // 投递一个任务到指定 service 的 actor 调度点执行(与该 service 的
    // handler 串行,因此 fn 可以安全触碰该 service 的 lua_State)。host
    // 保证 destroy_fn(user_data) 恰好被调用一次:任务执行时在 fn 之后;
    // 任务被丢弃(如 service exit)时单独调用。返回 0 成功;非 0 表示
    // 未调度,user_data 仍归调用方所有。
    int (*lua_post_to_service)(shield_plugin_context_v1* ctx,
                               const char* service_id,
                               void (*fn)(void* user_data),
                               void* user_data,
                               void (*destroy_fn)(void* user_data));
} shield_host_api_v1;

依赖访问规则:

规则说明
dependency by name插件只能访问自己 manifest requires 中声明的依赖名。
interface checkedhost 校验依赖实例确实提供请求的 interface。
no global registry插件不能枚举或任意获取其他插件。
lua access插件通过 lua_state 拿到 host 的 Lua state;通过 lua_add_path 动态扩展 Lua 搜索路径(manifest lua.search_paths 是声明式快捷方式)。

Dependency Model ​

插件 package 在 manifest.yaml 中声明需要哪些依赖:

yaml
requires:
  - name: cache
    interface: shield.cache.v1
    optional: true
  - name: database
    interface: shield.database.v1
    optional: false

主配置在 instance 上绑定依赖(app.yaml 的 plugins 子树):

yaml
plugins:
  instances:
    - id: leaderboard.main
      package: leaderboard.redis
      dependencies:
        cache: cache.main
        database: db.main
      config:
        key_prefix: "lb:"

规则:

规则说明
必需依赖缺失resolve 失败。
可选依赖缺失create/start 时传入 null,插件必须降级或报错。
循环依赖resolve 失败。
启动顺序依赖先 start,被依赖者后 stop。

Interface Model ​

接口名是稳定 ABI 契约,不使用单一 shield_plugin_type 枚举承载所有类型。新增接口不要求修改全局枚举。

第一批建议接口:

Interface用途
shield.database.v1SQL database provider。
shield.document.v1文档数据库 provider(MongoDB 等),collection 级 CRUD + 聚合管道 + 事务。
shield.cache.v1cache provider,Redis 只是实现之一。
shield.queue.v1queue/pubsub provider。
shield.leaderboard.v1leaderboard provider。
shield.metrics.v1metrics exporter。
shield.health.v1health contributor。
shield.protocol.codec.v1网络协议 BodyCodec provider,例如 protobuf、msgpack、xmldef-native。
shield.redis.v1共享 Redis 连接池 driver(typed 命令 + pipeline + raw command),供上层 Redis 插件依赖。
shield.auth.v1已弃用(2026-09-28):认证/JWT 属业务语义,不放 C++ 插件层。参考实现改为 Lua 层 scripts/lib/jwt.lua(HS256/EdDSA 双算法 + JWKS,基于 shield.crypto 原语,API 表见 lua-api.md);plugins/auth_jwt 保留一个过渡期后移除,新项目勿采用。
shield.matchmaking.v1匹配 provider(ELO 评分,plugins/matchmaking_elo 提供)。

shield.document.v1 与 shield.database.v1 并列而非继承:文档库的 filter / aggregation / _id 语义与 SQL 差距过大,强行套用 SQL 接口(query/execute/last_insert_id)会丢掉文档库的核心价值。两个接口可以由不同 package 各自提供。

面向业务的 Redis 能力不直接暴露连接,而是落到具体 provider:cache.redis、queue.redis、leaderboard.redis。这些 provider 不各自建连——它们通过 manifest requires + plugins.instances[].dependencies 依赖 redis.driver(shield.redis.v1),共享驱动实例的连接池(当前为 Phase 2 可选依赖双路径,见 redis.driver)。

官方插件矩阵 ​

仓库 plugins/ 目录随附 16 个官方插件包,全部遵循本文的 manifest / C ABI / register_lua 契约。构建默认全关,按需用对应 CMake option 开启(产物 staging 到 bin/plugins/<package.id>/):

Package接口CMake 开关Lua namespace说明
database.sqliteshield.database.v1SHIELD_BUILD_DB_PLUGIN_SQLITEshield.database.sqlite内嵌 SQLite;异步路径(M2/M4)+ 事务。kickstart 示例即用它。
database.mysqlshield.database.v1SHIELD_BUILD_DB_PLUGIN_MYSQLshield.database.mysqlMySQL X DevAPI。
database.postgresqlshield.database.v1SHIELD_BUILD_DB_PLUGIN_POSTGRESQLshield.database.postgresqllibpq。
database.mongodbshield.document.v1SHIELD_BUILD_PLUGIN_MONGODBshield.database.mongodb文档库(find/aggregate/transaction)。
cache.redisshield.cache.v1SHIELD_BUILD_PLUGIN_CACHE_REDISshield.cache.redis依赖 redis.driver 连接池。
queue.redisshield.queue.v1SHIELD_BUILD_PLUGIN_QUEUE_REDISshield.queue.redispub/sub,依赖 redis.driver。
leaderboard.redisshield.leaderboard.v1SHIELD_BUILD_PLUGIN_LEADERBOARD_REDISshield.leaderboard.redisZSET 排行榜,依赖 redis.driver。
redis.drivershield.redis.v1SHIELD_BUILD_PLUGIN_REDIS_DRIVERshield.redis基础设施包:共享连接池 driver,供上层 Redis provider 依赖,也可直接 shield.redis(binding) 调用。
health.httpshield.health.v1SHIELD_BUILD_PLUGIN_HEALTH—HTTP 健康端点(/health、/ready),内嵌 beast 监听。
metrics.prometheusshield.metrics.v1SHIELD_BUILD_PLUGIN_METRICshield.metricsPrometheus 文本端点;Lua 面 shield.metrics(binding) 提供 counter / gauge / histogram 记录。
matchmaking.eloshield.matchmaking.v1SHIELD_BUILD_PLUGIN_MATCHMAKING—ELO 匹配。
protocol.protobufshield.protocol.codec.v1SHIELD_BUILD_PLUGIN_PROTOBUF—BodyCodec provider,见 Protocol Codec Plugins。
protocol.msgpackshield.protocol.codec.v1SHIELD_BUILD_PLUGIN_MSGPACK—同上。
protocol.flatbuffersshield.protocol.codec.v1SHIELD_BUILD_PLUGIN_FLATBUFFERS—同上。
protocol.jsonshield.protocol.codec.v1SHIELD_BUILD_PLUGIN_JSON—同上。
auth.jwtshield.auth.v1SHIELD_BUILD_PLUGIN_AUTH—已弃用(2026-09-28):认证属业务语义,参考实现改为 Lua 层 scripts/lib/jwt.lua(基于 shield.crypto 原语);过渡期保留,新项目勿采用。

接口与实现的对应关系是声明式匹配:manifest provides 声明 interface,plugins.instances 选择 package,plugins.bindings 把逻辑名绑到 instance——业务代码只认逻辑名(见 Plugin Lua Bindings 的 binding 规则)。

Protocol Codec Plugins ​

协议 codec 插件是插件系统 v1 的普通 provider,不享有自动发现或自动启用特权。它们通过 shield.protocol.codec.v1 暴露 codec 能力,由 network.protocol.body.provider 显式引用 binding。

设计原则:

原则说明
核心最小化shield_transport 目标只默认内置 raw/json 和固定 protocol pipeline。
依赖隔离protobuf、flatbuffers、xmldef-native 的 runtime/compiler 依赖留在插件包内。
C ABI 边界插件不继承 host C++ BodyCodec,不跨共享库边界传 STL、异常或 C++ 对象。
显式启用必须配置 plugins.instances、plugins.bindings 和 body.provider。
session 固定插件只填充当前 session 绑定 profile 的 BodyCodec 槽位,不允许每包切换协议族。

示例:

yaml
plugins:
  instances:
    - id: protocol.protobuf.game
      package: protocol.protobuf
      required: true
      config:
        descriptor_set: conf/game.pb

  bindings:
    protocol.protobuf: protocol.protobuf.game

actors:
  - name: gateway
    script: scripts/auth.lua
    network:
      tcp: "0.0.0.0:8001"
      protocol:
        name: game.protobuf
        envelope:
          type: idlen
          route_id_bytes: 4
          length_bytes: 4
        body:
          codec: protobuf
          provider: protocol.protobuf
        routing:
          source: header.route_id
        routes:
          - id: 1001
            name: auth.LoginRequest
            action: decode

详细 ABI、protobuf 优先落地范围和各协议目标形态见 Protocol Codec Plugins。

Config Schema ​

配置 schema 由插件声明。v1 目标语义是 host 负责基本类型、必填字段、默认值、范围和 secret 标记处理;插件仍可以在 create / start 中做业务校验。

v1 schema 支持以下最小集合:

关键字说明
typeobject、string、integer、number、boolean、array。
requiredobject 必填字段列表。
propertiesobject 字段定义。
default默认值。
minimum / maximum数值范围。
enum字符串或数字枚举。
itemsarray 元素 schema。
secret日志和 introspection 中脱敏。
additionalProperties仅布尔 false 强制拒绝未声明键;缺省/true/非布尔值宽松忽略。
minLength / maxLength字符串长度闭区间。
minItems / maxItemsarray 元素个数闭区间。

同一校验器也被 codec 插件复用为 payload schema 校验(键类型检查用于 manifest config_schema,payload 校验用于 protocol.json / protocol.msgpack,见 Protocol Codec Plugins)。

Error Model ​

错误对象结构:

c
typedef struct shield_error_v1 {
    const char* code;
    const char* message;
    const char* hint;
    const char* package_id;
    const char* instance_id;
    const char* phase;
} shield_error_v1;

错误语义:

场景code 示例
manifest 无效plugin.manifest.invalid
package 缺失plugin.package.not_found
配置无效plugin.config.invalid
依赖缺失plugin.dependency.missing
ABI 不匹配plugin.abi.mismatch
入口符号缺失plugin.entry.missing
init 失败plugin.init.failed

多 provider 路由规划软拒绝语义:如果某接口方法是 selector/dispatcher 类型,插件可以返回 SHIELD_DECLINED 表示“不处理,让下一个候选处理”。真正错误返回 SHIELD_ERROR 并携带 shield_error_v1。未实现(目标契约):SHIELD_DECLINED / SHIELD_ERROR 两个宏当前源码尚未定义,软拒绝语义保留待实现。

Lua Introspection ​

Lua 侧有两类 API:

  1. Host 内置 API:由 host 的 src/lua/lua_api.cpp 注册,提供运行时核心能力(shield.spawn / shield.send / shield.timer / shield.log / shield.config / shield.plugin.* 等)。
  2. 插件自治 API:由每个插件的 register_lua 钩子注册到 shield.<namespace>,提供该插件的业务能力(如 shield.database.mongodb("document.default"):find(...))。详见下一节 "Plugin Lua Bindings"。

Host 内置 API 只暴露查询和状态,不暴露裸 vtable 或跨插件调用:

lua
local packages = shield.plugin.packages()
local instances = shield.plugin.instances()
local db = shield.plugin.instance("db.main")
local binding = shield.plugin.binding("database.default")

返回信息应来自 catalog 和 runtime state:

API说明
shield.plugin.packages()返回可用 package 列表,包括 id、version、kind、provides、docs_url、docs_description。
shield.plugin.instances()返回实例列表,包括 id、package、state、required。
shield.plugin.instance(id)返回单个实例状态。
shield.plugin.binding(name)返回 binding 指向的 instance id 和 interface。

状态枚举:

State含义
planned已进入启动计划,尚未加载。
loaded共享库已加载,ABI 已校验。
startedinit 成功。
unavailable非 required 实例失败并被跳过。
failedrequired 实例失败,启动流程会失败。
stopped已调用 shutdown。

Plugin Lua Bindings ​

插件不只是 C ABI 的载体,还要自带 Lua 绑定和业务封装 Lua 文件。这让每个插件目录真正自包含——host 端代码零修改即可引入新插件。

设计原则 ​

原则说明
插件自治C ABI + Lua 绑定 + Lua 业务文件,全在 plugins/<package>/ 下,不污染 host。
namespace 统一插件 package id 直接映射 Lua namespace:database.mongodb → shield.database.mongodb。
显式 register_lua每个 instance 必须实现 register_lua 钩子,即使空实现。host 不猜测插件是否需要 Lua。
双层路径注入manifest lua.search_paths 是声明式快捷方式;host_api.lua_add_path 是命令式兜底,用于运行时动态决策。
多实例 proxy同一 package 多个 instance 时,namespace 是 callable table:shield.database.mongodb("document.default") 通过 binding 逻辑名返回绑定到目标 instance 的 proxy。

ABI 接入点 ​

shield_plugin_instance_v1.register_lua(self, L, err) 是唯一接入点:

  • host 在 lua_register 阶段遍历所有已 started 的实例,按依赖拓扑启动顺序调用。
  • L 是 host 的 Lua state,保证非 NULL(除非 host 跳过 Lua runtime,此时整个阶段跳过,register_lua 不被调用)。
  • 插件用 shd 绑定层(shd::state_view(L))创建 namespace、注册方法。
  • 返回 0 成功;非 0 视为 register_lua 失败,host 上报结构化错误。

namespace 与多实例 ​

namespace 直接来自 manifest 的 lua.namespace 字段(或省略时从 id 派生)。注册时插件创建一个 callable table:

lua
-- 业务 Lua 代码
local mongo_default = shield.database.mongodb("database.default")  -- 指定 binding 逻辑名
local mongo_audit   = shield.database.mongodb("document.audit")    -- 另一 binding

-- 返回的 proxy 绑定到 binding 解析出的 instance,调用时直接路由到 C ABI
mongo_audit:insert_one("events", { type = "login", user = "alice" })

local cursor = mongo_default:find("users", { age = { ["$gt"] = 18 } })

-- 缺失/未配置 binding 的软失败形态(使用规则 5):
local proxy, err = shield.database.mongodb("ghost.binding")
-- proxy == nil, err = { code = "module_unavailable", message = ..., binding = "ghost.binding" }

binding 逻辑名是必填参数(省略或解析失败一律软失败返回 nil, { code = "module_unavailable" });没有「插件默认 binding」隐式语义——业务代码显式声明依赖哪个逻辑名,与使用规则 2 的 <interface-type>.<purpose> 命名约定一致。

C 侧实现:register_lua 创建 shield.database.mongodb 这个 table,metatable 的 __call 接收 binding 逻辑名,经共享 binding 解析(resolve_lua_binding)查 PluginHost;解析失败走 push_module_unavailable 软失败,成功则取得 shield_document_v1* 和插件连接句柄构造 proxy。

为什么 Lua 访问用 binding 而非 instance_id ​

业务代码访问插件时必须用 binding 逻辑名,不得用 instance_id(诊断 API 除外)。这是"间接引用"设计——业务依赖角色/语义,不依赖部署实例。理由:

  • 部署与代码解耦:database.default 指向哪个 instance 由 plugins.bindings 决定,运维迁移、重命名、拆分实例不改业务代码。instance_id 是部署细节,binding 是业务契约。
  • 多环境一致:dev / staging / prod 的 instance_id 必然不同,业务代码恒用 database.default。
  • 游戏服务器刚需场景:读写分离(database.read → replica)、分片(database.shard1)、灰度(database.canary)、故障切换——用 binding 都是改配置,用 instance_id 都要改业务代码。
  • 测试:binding 指向 mock instance,业务代码零改动。
  • 与 C++ 路径统一:C++ 消费者经 PluginHost::get_by_binding<T>(binding) 访问,Lua 走同一套 plugins.bindings,两侧语义一致,避免"C++ 用 binding、Lua 用 instance_id"的割裂。
  • 业界先例:DNS(域名→IP)、Kubernetes Service(service name→pod)、Spring @Qualifier、Envoy cluster——长期维护的资源访问都采用逻辑引用而非物理标识。直接用 instance_id 等于让业务代码写 IP 而非域名。

使用规则 ​

  1. 业务 Lua 只传 binding:shield.database.mysql("database.default") ✓;shield.database.mysql("db.main") ✗(db.main 是 instance_id)。
  2. binding 命名约定 <interface-type>.<purpose>:database.default / database.read / cache.session / queue.events / leaderboard.arena。
  3. binding 在主配置 plugins.bindings 声明(logical: instance_id),运维拥有,不在业务代码硬编码。
  4. instance_id 只出现在配置 plugins.instances[].id 与只读诊断 API(shield.plugin.instances());业务代码不直接用。
  5. 缺失/未配置 binding → nil, { code = "module_unavailable" }(软失败,不 panic)。
  6. 诊断/调试可直接指定 instance_id,但只能经专门诊断入口,非常规业务路径。

实现要求:插件 register_lua 创建的 callable table,其 __call 必须接收 binding 逻辑名并经 PluginHost 解析到 instance,不得直接用 instance_id 查插件内部表。当前 mysql / postgresql / sqlite / mongodb / cache.redis / queue.redis / leaderboard.redis 已按此规则接入;后续新增插件也必须遵守。

Lua 文件位置 ​

plugins/mongodb/
├── shield_doc_mongodb.cpp        # C ABI 实现 + register_lua(创建 namespace + 方法)
├── lua/                          # 纯 Lua 封装(经 manifest lua.search_paths 注入 package.path)
│   └── init.lua
├── manifest.yaml
└── CMakeLists.txt

带 lua/ 目录的插件,host 启动时:

  1. 扫描 manifest 的 lua.search_paths 字段,把 <plugin_dir>/lua/?.lua 注入 package.path(在 lua_init 阶段)。
  2. 调用 register_lua,插件自行决定是否加载 lua/ 下的业务封装(mongodb 当前仅 init.lua,由 register_lua 注入 helper,业务无需显式 require)。

register_lua 示例(插件侧) ​

cpp
int my_register_lua(shield_plugin_instance_v1* self,
                    lua_State* L,
                    shield_error_v1* err) {
    shd::state_view lua(L);
    shd::table shield =
        shield::plugins::get_or_create_global_subtable(lua, "shield");
    shd::table database =
        shield::plugins::get_or_create_subtable(shield, "database");

    // callable table: mongodb(binding) -> proxy
    auto ns = lua.create_table();
    auto mt = lua.create_table();
    mt.set_function(
        "__call",
        [](shd::this_state s, shd::table /*self*/,
           std::optional<std::string> binding) -> shd::variadic_results {
            // 经 PluginHost 解析 binding,拿到目标 instance 的
            // shield_document_v1* + shield_doc_conn*
            // 构造 proxy table,注册 find/insert_one/... 方法
            shd::variadic_results results;
            results.push_back(
                shd::make_object(s, make_proxy(binding.value_or(""))));
            return results;
        });
    shield::plugins::set_metatable(ns, mt);
    database["mongodb"] = ns;

    return 0;
}

host 端实现要点 ​

实现文件
register_lua_all(L)src/plugin/plugin_host.cpp — 遍历 started 实例,调用 register_lua
lua_state host_api 回调src/plugin/plugin_host.cpp — 返回 thread-local g_current_lua_state(仅在 inject_lua_paths / register_lua_all 注入期间非空,其余时刻为 NULL)
lua_add_path host_api 回调src/plugin/plugin_host.cpp — 改写 package.path
Manifest 解析 lua 字段src/plugin/manifest.cpp — 填充 Manifest::LuaMeta(定义于 include/shield/plugin/plugin_host.hpp)
Bootstrap 时序src/bootstrap/bootstrap.cpp — 仅调 global_host().startup()(scan → … → start_all,不含 Lua 阶段);Lua 阶段由每个 Lua VM 创建时 LuaRuntime::register_api(src/lua/lua_runtime.cpp)调 inject_lua_paths + register_lua_all

当前 namespace 列表 ​

PackageLua namespace主要方法
database.sqliteshield.database.sqlitequery / execute / transaction
database.mysqlshield.database.mysqlquery / execute / transaction
database.postgresqlshield.database.postgresqlquery / execute / transaction
database.mongodbshield.database.mongodbfind / insert_one / update_one / delete_one / aggregate / count / transaction
cache.redisshield.cache.redisget / set / del / incr / hget / hset
queue.redisshield.queue.redispublish / subscribe / unsubscribe
leaderboard.redisshield.leaderboard.redisset_entry / get_rank / top_n / remove_entry
redis.drivershield.redisshield.redis(binding) 返回连接池代理:get / set / del / hget / hset / hgetall / zadd / zrange / command / pipeline
metrics.prometheusshield.metricsshield.metrics(binding) 返回记录代理:counter / gauge / histogram
health.http暂无当前仅提供 C ABI,Lua 绑定未实现
matchmaking.elo暂无当前仅提供 C ABI,Lua 绑定未实现

第三方插件开发指引 ​

脚手架:tools/new_plugin.sh ​

新插件包从脚手架开始,一条命令生成可构建、可测试的完整骨架:

bash
tools/new_plugin.sh inventory_events
# -> plugins/inventory_events/{manifest.yaml, inventory_events.h,
#    shield_inventory_events.cpp, CMakeLists.txt, test/test_abi.cpp}

生成物:

文件内容
manifest.yamlschema v1 完整 manifest(_ 折叠为 . 的 package id、三平台 library 路径、一个可用的 shield.echo.v1 demo interface、config_schema)。
<name>.hdemo interface 头文件(vtable + interface 名宏),替换成真实接口契约时保持 vtable 只追加。
shield_<name>.cpp完整 v1 ABI 桩:instance shell 首成员结构、create/start/shutdown/get_interface/register_lua(空 Lua 面返回 0)+ shield_error_v1 填充范例。
CMakeLists.txtMODULE 库 + bin/plugins/<package.id>/ staging(manifest 同步拷贝)+ POSIX ABI 冒烟测试(Windows 挂起,与既有 scaffold 惯例一致)。
test/test_abi.cppdlopen → get_v1 → ABI guard → create/start/get_interface echo 往返 → shutdown 的 Boost.Test 用例。

脚本同时完成构建接线:在根 CMakeLists.txt 的 add_subdirectory(plugins) 守卫 if() 前插入 option(SHIELD_BUILD_PLUGIN_<UPPER> ... OFF) 并把该 OR 链接上新选项(只开新选项也能配置 plugins 树),再向 plugins/CMakeLists.txt 追加守卫的 add_subdirectory(<name>)。验证路径:

bash
cmake -B build -DSHIELD_BUILD_PLUGIN_<UPPER>=ON
cmake --build build --target test_<name>_abi
ctest --test-dir build -R test_<name>_abi

手写契约清单 ​

不用脚手架时,第三方包必须满足的完整清单(每条在本文对应章节展开):

  1. manifest.yaml:schema_version: 1、唯一 package id、三平台相对 library 路径、至少一个 provides interface。
  2. 二进制入口:SHIELD_PLUGIN_EXPORT const shield_plugin_abi_v1* shield_plugin_get_v1(void),package_id 与 manifest 一致。
  3. instance shell(shield_plugin_instance_v1)必须是实例结构体首成员;register_lua 必须提供(无 Lua 面就 return 0)。
  4. 跨界只说 C ABI:不链 host 静态库(shield_net 等),不跨边界传 STL/异常/C++ 对象;与 host 的全部交互走 args->host_api。
  5. 配置:config_schema 声明契约,host 在 resolve 阶段应用默认值并校验;业务校验留在 create/start。
  6. 依赖:manifest requires 声明 + 实例 dependencies 绑定,不做全局查找。
  7. Lua 面:callable namespace(shield.<package.id>),__call 接收 binding 逻辑名(不得收 instance_id),解析失败软失败 nil, { code = "module_unavailable" }。
  8. 库形态:MODULE 共享库,staging 目录与 manifest library 路径一致(相对 package root)。

一键体验参考 ​

examples/kickstart/ 是官方插件四件套(Lua 业务脚本 + database.sqlite + health.http + metrics.prometheus)的最小可跑组合,三条命令跑通全链路(构建 → curl 健康/指标 → 改 Lua 验证数据落库),是显式 instances + bindings 配置的权威范例。快速起步的正确姿势:copy kickstart 改配置,或用脚手架起新包——而不是给 host 添加隐式装配行为。

Current Rules ​

v1 的强约束:

规则说明
发现来源仅扫描 manifest.yaml。
运行时启用只看 plugins.instances 和 plugins.bindings。
二进制入口统一为 shield_plugin_get_v1()。
类型系统以 interface name 为主,例如 shield.database.v1、shield.document.v1。
依赖注入通过 manifest requires 和实例 dependencies 解析。
Redis 能力通过具体 provider 暴露,不提供公共基础设施插件类型。
协议 codec 能力通过 shield.protocol.codec.v1 provider 暴露;核心不默认加载 protobuf/xmldef-native。
Lua 自治插件必须实现 register_lua 钩子。业务 Lua 绑定跟随插件目录,host 端 src/lua/lua_api.cpp 不感知具体插件。
namespace 派生Lua namespace 直接来自 manifest lua.namespace(或从 id 派生)。多实例用 shield.<ns>(binding) proxy。
文档口径当前文档只描述有效设计,不声明历史完成阶段。

Implementation Status ​

当前实现已经具备插件系统主路径:manifest 扫描、catalog、依赖拓扑、动态库加载、ABI 校验、实例创建、启动、C++ binding 访问、Lua path 注入、register_lua 分发和只读 introspection。

旧插件包需要迁移到 manifest.yaml 后才能被 host 发现。

以下条目是 v1 收敛项,文档中按目标语义描述,但实现仍需补齐:

项目当前状态目标
config_schema 校验resolve 阶段已应用默认值,并执行类型/必填/范围/enum 校验。继续扩展 JSON Schema 子集和 secret 脱敏输出。
required: falseoptional 实例在 package/load/create/start 失败时进入 unavailable;required 实例失败会中断启动。后续可增加 binding fallback 候选列表。
binding 校验resolve 阶段校验 binding 名唯一且目标 instance 存在;访问时仍由 get_by_binding<T>() 请求 interface。必要时增加显式 interface binding 格式。
结构化错误C ABI 有 shield_error_v1;host 侧错误字符串仍有部分路径是拼接文本。所有阶段统一携带 code/message/hint/package/instance/phase。

Open Decisions ​

这些问题不阻塞 v1 文档定稿,但实现前需要在 open-decisions.md 记录结论:

问题候选方向
schema 校验实现当前已有自行开发最小子集;后续决定是否扩展或替换为现成 JSON Schema validator。
插件包版本约束v1 可先只支持精确 package id,不做 semver range。
可选实例失败策略当前设计为 unavailable,是否允许 binding 自动 fallback 到候选列表需另定。
Windows/Linux 库命名是否要求 manifest 显式写全,还是允许按约定推导。

Apache License 2.0