主题
晞晗IM(XHIM)内核与产品 Adapter 接入指南
适用版本:0.1 Commercial Beta
本文面向负责构建 XHIM 制品、实现服务端 Adapter 或直接扩展 C++ 内核的工程师。 普通 App 集成请阅读客户端 SDK 接入总览;平台 Wrapper 和 发行维护请阅读平台 Facade 维护指南。
XHIM 当前提供可安装的 CMake 包、版本化 xhim_v1 C ABI、Go 参考服务端以及 adapters/reference_server 的 HTTPS/WSS Product Adapter。参考实现已经通过 真实 TLS 的 login → initial sync → Ready → send → ACK/Sync 纵向用例。 平台 Wrapper/UI 基础组件源码、自定义消息插件契约,以及预研 Call Actor、 服务端信令与签名 RTC Token Provider 已交付;实时音视频推迟为后续可选厂商 Adapter。平台 Business Provider 的自动凭证刷新已经落地;生产 KMS/吊销/滚动 黑盒证据、跨平台二进制签名产物和纯 IM 真机验收仍未封板,因此当前仍不是最终 Stable 售卖发行包。
1. 选择接入边界
普通接入方应只依赖:
text
XHIM::XHIM
└── <xhim/xhim_v1.h>
└── xhim_v1_* C ABI这是跨编译器、跨标准库和跨平台 Wrapper 的稳定兼容边界。
以下 CMake Target 用于内核扩展、后端 Adapter 开发和联合调试:
text
XHIM::Engine
XHIM::Core
XHIM::Storage
XHIM::Sync
XHIM::Protocol
XHIM::Transport
XHIM::Media
XHIM::RTC这些 Target 暴露 C++17 接口,在 0.x 阶段属于实验性扩展接口,不承诺源码或 C++ ABI 兼容。业务 App 不应绕过平台 Facade 或 C ABI 直接持有这些模块的对象。
2. 构建和安装
系统需要 CMake 3.20+、C++17 编译器和 SQLite3。下面使用独立构建目录安装 Developer Preview:
bash
cmake -S /path/to/xhim -B /path/to/build-xhim \
-DXHIM_BUILD_SHARED=ON \
-DXHIM_BUILD_TESTS=OFF \
-DXHIM_BUILD_EXAMPLES=OFF
cmake --build /path/to/build-xhim
cmake --install /path/to/build-xhim --prefix /path/to/xhim-installSQLite Provider 默认为 AUTO。也可以显式选择系统 SQLite:
bash
cmake -S /path/to/xhim -B /path/to/build-xhim \
-DXHIM_SQLITE_PROVIDER=SYSTEM离线集成 SQLite amalgamation 时,使用 XHIM_SQLITE_PROVIDER=BUNDLED 和 XHIM_SQLITE_BUNDLED_DIR。父工程已提供 SQLite::SQLite3 时也可以直接注入; 如果安装包由注入 Target 构建,下游必须在 find_package(XHIM) 前定义等价的 SQLite::SQLite3。
3. 使用 find_package(XHIM)
下游工程至少声明 C 和 C++ 语言。即使业务只包含 C 头文件,静态链接最终仍 需要 C++ linker 或相应 C++ runtime。
cmake
cmake_minimum_required(VERSION 3.20)
project(MyXHIMApp LANGUAGES C CXX)
find_package(XHIM CONFIG REQUIRED)
add_executable(my_app main.c)
target_link_libraries(my_app PRIVATE XHIM::XHIM)
set_target_properties(my_app PROPERTIES LINKER_LANGUAGE CXX)配置下游工程时,将安装前缀加入搜索路径:
bash
cmake -S . -B build \
-DCMAKE_PREFIX_PATH=/path/to/xhim-install
cmake --build build公共 C 头文件:
c
#include <stddef.h>
#include <xhim/xhim_v1.h>创建 Client 时必须清零配置,并填写 struct_size 和 ABI 版本:
c
xhim_v1_client_config_t config = {0};
config.struct_size = (uint32_t)sizeof(config);
config.abi_version = XHIM_V1_ABI_VERSION;
config.app_id.data = (const uint8_t *)app_id;
config.app_id.len = app_id_len;
config.storage_path.data = (const uint8_t *)storage_path;
config.storage_path.len = storage_path_len;
xhim_v1_client_t *client = NULL;
int32_t status = xhim_v1_client_create(&config, &client);app_id 和 storage_path 的内存在 xhim_v1_client_create 调用期间必须有效。 生产接入必须保持 config.flags == 0。仓库提供 Reference 与 Apple 真实 Product Adapter;发行包必须在构建期显式装配其中一个或客户自己的等价实现。通用未配置 构建会让 xhim_v1_client_login 立即返回 XHIM_V1_STATUS_BACKEND_NOT_CONFIGURED,不会进入假 Ready。 XHIM_V1_CLIENT_FLAG_LOCAL_PREVIEW 只供仓库 CLI 和确定性自动化测试使用; 它通过真实 ClientEngine、SQLite 和 Outbox 路径执行确定性的本地身份、 空 high-watermark Sync 和本地 ACK,但不认证真实服务或执行网络 I/O。正式 制品必须设置 -DXHIM_ENABLE_LOCAL_PREVIEW=OFF。
创建成功后,按 start → login → 使用 → logout/shutdown → destroy 的生命周期 驱动 Client。异步 API 的立即返回值只表示参数校验或任务入队结果;业务结果由 Completion 回调返回。
4. 把产品 Adapter 编译进稳定 C ABI
普通 App 不在运行时注册 C++ Backend,也不通过 C ABI 传虚函数表。XHIM 在构建 制品时选择唯一的强工厂实现,并把其 OBJECT 文件直接嵌入 XHIM::XHIM:
bash
cmake -S /path/to/xhim -B /path/to/product-build \
-DXHIM_PRODUCT_ADAPTER_MODE=EXTERNAL \
-DXHIM_PRODUCT_ADAPTER_SOURCE_DIR=/absolute/path/to/my-xhim-adapter \
-DXHIM_PRODUCT_ADAPTER_OBJECT_TARGET=my_xhim_product_adapter \
-DXHIM_ENABLE_LOCAL_PREVIEW=OFF产品目录的最小 CMakeLists.txt:
cmake
add_library(my_xhim_product_adapter OBJECT product_adapter.cpp)product_adapter.cpp 包含 build-only <xhim/product_adapter/product_adapter_spi.h>,并且恰好实现一次 xhim::product_adapter::create_build_product_adapter()。成功结果必须同时移交 唯一所有权的 SessionBackend 和 MessageTransportFactory;状态与指针组合 不一致会让 xhim_v1_client_create fail-closed,而不会降级到未配置模式。 外部 target 必须是真实、非 imported 的 OBJECT library;目录、target 或类型 不正确会在 CMake configure 阶段直接失败。
产品网络、Codec、加密等最终链接依赖可以在该子目录调用 xhim_product_runtime_link_libraries(target...) 附加。静态制品不会把另一个 .a 自动吞入自身,因此 Adapter 必须用 OBJECT 嵌入;其运行时依赖则必须是 可安装、可发现的 CMake target,产品包还要在 XHIMTargets.cmake 载入前提供 对应 find_dependency。build-only SPI 头不会安装,不属于 xhim_v1 或 C++ 稳定 ABI,产品 Adapter 必须与选定 XHIM 源码、编译器和 C++ 标准库一起构建。
默认 XHIM_PRODUCT_ADAPTER_MODE=UNCONFIGURED 编译一个强 fail-closed 工厂。 此时 create/start/shutdown 仍可工作,但 login 同步返回 XHIM_V1_STATUS_BACKEND_NOT_CONFIGURED、request_id=0 且不产生 Completion。 无 weak symbol、进程全局注册表或运行时“最后一次注册覆盖”行为。
4.1 使用仓库参考服务端 Adapter
本机完整验证优先运行:
bash
./scripts/run_server_e2e.sh产品构建的核心参数如下:
bash
cmake -S /path/to/xhim -B /path/to/product-build \
-DXHIM_BUILD_SHARED=ON \
-DXHIM_ENABLE_LOCAL_PREVIEW=OFF \
-DXHIM_PRODUCT_ADAPTER_MODE=EXTERNAL \
-DXHIM_PRODUCT_ADAPTER_SOURCE_DIR=/path/to/xhim/adapters/reference_server \
-DXHIM_PRODUCT_ADAPTER_OBJECT_TARGET=xhim_reference_product_adapter \
-DXHIM_REFERENCE_BOOTSTRAP_URL=https://im.example.com \
-DXHIM_REFERENCE_API_URL=https://im.example.com \
-DXHIM_REFERENCE_WEBSOCKET_URL=wss://im.example.com/v1/realtime \
-DXHIM_REFERENCE_UPLOAD_URL=https://upload.example.com \
-DXHIM_REFERENCE_MEDIA_URL=https://media.example.com \
-DXHIM_REFERENCE_SIGNING_KEY_ID=prod-ed25519-1 \
-DXHIM_REFERENCE_SIGNING_PUBLIC_KEY_BASE64=<base64-public-key>XHIM_REFERENCE_CA_BUNDLE 只用于企业私有 CA 或自动化;公网证书通常留空并使用 系统信任库。Adapter 始终开启证书和主机名校验,禁止重定向,并对响应、帧、 超时和 Endpoint 做有界检查。Apple 系统 libcurl 未启用 ws/wss 协议时, Adapter 仍由 libcurl 建立严格 TLS,再自行执行 RFC 6455 HTTP/1.1 Upgrade 和 帧解析;不会回退到明文连接。
参考 Adapter 当前使用受限的手写 C++ Protobuf v1 Codec,服务端使用由 Buf 固定版本生成的 Go Codec。Endpoint Bundle 优先使用 DeploymentConfig 中 客户部署的 Ed25519 公钥和 OpenSSL 3 Crypto API 验签;没有运行时配置时才使用 发行包内置的默认信任根。错误签名在建立 WebSocket 前失败。要发布 Stable 商业包,仍应将 C++ Codec 切换为固定版本生成代码,并完成全平台 OpenSSL 打包、许可证与漏洞响应流程;在此之前不能把参考 Adapter 标记为 Stable。
5. 实验性 ClientEngine 直接接入
XHIM::Engine 提供 0.x 实验性的 C++ 组合根 ClientEngine。它适合开发并 验证自有后端 Adapter,不是稳定 C ABI 的替代品。下游可以只声明这一个 Target; Core、Storage、Sync、Protocol 和 Transport 依赖由安装包传递:
cmake
add_executable(my_engine_host
main.cpp
my_session_backend.cpp
my_message_transport_factory.cpp
)
target_link_libraries(my_engine_host PRIVATE XHIM::Engine)
target_compile_features(my_engine_host PRIVATE cxx_std_17)调用 ClientEngine::create() 时,宿主移交以下对象或配置:
ClientEngineConfig,包含app_id、独立数据库路径、Inbox retention、 Session 和 Outbox 配置;- 一个实现了
SessionBackend的后端 Adapter; - 一个实现了
MessageTransportFactory的发送 Adapter 工厂。
创建过程同步打开、配置并迁移数据库,然后构造 ClientRuntime、 SessionCoordinator、MessageTransport 和 OutboxWorker。创建成功后, ClientEngine 统一拥有 Backend、Store、Runtime、Coordinator、Transport Factory、Transport 和 Worker,并按依赖逆序关闭;宿主不得再单独销毁移交的 对象。推荐调用顺序为 start → login → send_text/send_message/get_message/list_messages/list_conversations/使用 → logout → shutdown。 start 只启动内核;login(account_hint, access_token) 才开始认证、连接和 初始同步,其中 account_hint 只是路由提示,数据库和会话最终绑定认证结果 返回的 canonical account_id 与 self_user_id。logout 回到可再次登录的 状态;shutdown 是终止操作,完成后不得复用该 Engine。
SessionBackend 实现必须遵守以下异步合同:
authenticate、connect和fetch_sync_page必须迅速返回;返回true表示任务已接收;在 Backend shutdown 开始前,Completion 应恰好调用一次, 包括超时和取消结果。Completion 可以来自任意线程,不能依赖 UI/Main 线程。- 这些虚函数是
noexcept;错误应通过BackendFailure返回,不能让异常跨越 Adapter 边界。 - 每个结果必须原样携带请求中的 account epoch;连接和 Sync 结果还必须携带 connection generation。Coordinator 用它们隔离旧账号和旧连接的回调。
- 每次
connect都会收到该连接专属的RealtimeEventSink。成功结果必须 返回一个非空、唯一所有权的RealtimeSession;失败结果不得携带 Session。RealtimeSession::close()必须线程安全且幂等,析构也必须释放底层连接。 - Backend 在该连接存活期间保留 Event Sink,只上报
kSyncHint或kDisconnected。Hint 是“服务端持久化状态可能变化”的轻量唤醒,不得把 WebSocket Payload 直接当作已提交业务数据;所有持久状态仍通过fetch_sync_page顺序补拉并在 SQLite 中原子提交。 - Event Sink 可以与 connect Completion、
close、取消、logout 和 shutdown 并发。事件必须携带建立连接时的 epoch/generation,且 Adapter 不得在应用 新 fence 后把旧 Socket 事件改写成新 fence。 cancel_account_epoch是线程安全、幂等且持久的 epoch tombstone。取消后 底层可以返回迟到回调,但同一 epoch 随后竞态启动的任务也必须被拒绝或取消; Adapter 不能复用 fence,Coordinator 会丢弃重复或过期结果。cancel_connection_generation只为当前账号废弃指定连接代次,不注销账号; 它同样是线程安全、幂等且持久的 tombstone。cancel_account_epoch、连接 取消和shutdown可能与普通请求并发;shutdown必须停止接受新工作并 收束 Adapter 自己持有的资源。
MessageTransportFactory 和 MessageTransport 还必须遵守发送合同:
- Factory 由 Engine 持有且比其创建的 Transport 活得更久;Transport 可以引用 Engine 提供的
SessionSnapshotProvider,但不得保存临时快照中的借用地址。 send_message()是阻塞 Port,只由OutboxWorker的专用 joinable I/O 线程调用;Adapter 必须执行请求硬超时、响应大小上限、TLS 和重定向策略, 且 Transport 超时必须严格小于 Outbox lease。cancel_account_epoch()必须线程安全、幂等并建立不可复用的 durable tombstone;它可能与阻塞发送及另一个取消并发,必须尽快返回。取消前后竞态 注册的发送必须被拒绝或立即取消,迟到结果仍会被 Core 的 Epoch fence 丢弃。- 注入的
MessageTransport::cancel_account_epoch()与OutboxWorkerClock::interrupt_wait()不得同步重入同一个OutboxWorker的公开 API、触发其析构或等待其 Owner 回调;需要上报状态时必须异步投递到 自己的串行边界。该限制避免 shutdown 的 join 串行化与 Adapter 重入形成环。
Coordinator 只会在以下条件全部成立后发布可用 Session 并进入 Ready:
- 认证成功并得到 canonical
account_id、self_user_id和可用凭证; - Backend 已验证并返回完整的
EndpointBundle,包括 API/Upload/Media 的 HTTPS 地址、WebSocket 的 WSS 地址,以及版本、有效期和签名; - 带正确 epoch/generation fence 的连接建立成功;
- 初始 Sync 按 Cursor 顺序逐页获取,每页均成功原子提交到 SQLite;
- 最后一页同时满足
has_more == false和reached_high_watermark == true。
空页本身不代表同步完成;同步期间 SessionCoordinator::snapshot() 不会提前 返回 Available。Coordinator 到达 Ready 后,Engine 还会交叉校验 Coordinator、Runtime、认证 Session 和 canonical identity,排空上一账号 Epoch,再激活 OutboxWorker 并发布不可变 send permit。只有这些步骤全部成功, ClientEngine::login() 的成功 Completion 才代表当前账号可以调用 send_text() 或 send_message()。Coordinator 的 login 结果本身必须证明该 Epoch 曾到达 Ready;如果从该结果进入 Engine 控制线程前已经发生即时断线,同一 Epoch 的 Connecting/Synchronizing 仍可通过最终复核,但必须同时保持认证 Session Available、canonical identity 和全部 Epoch 一致。
进入 Ready 后,Engine 和 Coordinator 继续管理在线生命周期:
- 当前连接的多个 Sync hint 会在 Event relay 和串行 Worker 上合并;若一次 持续 Sync 正在执行,只记录一个后续补拉请求,避免无界排队;
- 当前连接断开或持续 Sync 遇到可重试网络错误时,先关闭
RealtimeSession、取消旧 generation,再让 Runtime 推进 generation; - 第一次重连立即执行;后续可重试失败使用 capped exponential Full Jitter, Backend 的
retry_after_ms作为最早重试下限并截断到最大延迟;恢复后必须 连续健康达到reconnect_stable_window_ms才重置预算,达到max_reconnect_attempts则结束该会话;平台网络恢复提示只可通过稳定 C APIxhim_v1_client_notify_network_available()唤醒当前 fenced timer,不能 建立旁路重试; - 新连接建立后必须从 SQLite 已提交 Cursor 做恢复 Sync,并再次到达 high watermark,才能回到
Ready; - 旧 relay、旧 generation、重复 Completion、logout/shutdown 后到达的事件 均被 fence 丢弃,不能推进 Cursor 或复活会话。
send_text() 和 send_message() 先使用不可变 permit 校验账号 Epoch 和 canonical identity,再在 Runtime 账号 fence 内完成 Message + Outbox 的单个 SQLite 事务;成功 Completion 仅表示本地 Queued/IdempotentReplay,不表示 服务端已经接受。事务提交后 Engine 只把 wake() 当作非阻塞发送提示,Worker 会按账号范围和会话 FIFO 认领 Outbox、执行阻塞 Transport、处理 future retry timer、租约恢复和 typed fault。claim、send 与每次 Store mutation 都分别取得 Epoch stage lease; revoke_and_wait() 会等待已获准的阶段、后置查询和事件回调全部退出。
xhim_v1_client_get_message() 是稳定 C ABI 的单消息读取 API。调用方 传入发送时使用或返回的 client_message_id,Completion 会借用返回 xhim_v1_message_snapshot_t,其中包含不可变消息类型、版本、Payload、 Fallback、会话/发送者身份、 本地顺序、服务端序号和当前发送状态。调用方必须在 Callback 返回前复制需要的 byte view。未知 ID 异步完成为 XHIM_V1_STATUS_NOT_FOUND 且 message 为 null;输入错误或 Client 尚未 Ready 属于同步拒绝,不分配 request ID。查询从 当前 Engine permit 获取 canonical account ID,并通过 Runtime execute_for_account() 在 Client Actor 上执行,因此平台代码不得直接打开 SQLite,也不能用调用方传入账号绕过隔离。
xhim_v1_client_list_messages() 在同一隔离边界上分页读取一条会话的时间线。 空 Cursor 从最新消息开始;limit=0 使用默认 50 条,单页最多 200 条。 Completion 返回借用的 xhim_v1_message_page_t 和消息快照数组,平台 Facade 必须在 Callback 返回前复制需要的消息及 next_cursor。当 has_more != 0 时,将该不透明二进制 Cursor 原样传给同一账号、同一会话的下一次调用;不得 把它当 UTF-8、数字或数据库 Offset 解析。Core 使用与会话展示顺序一致的复合 keyset 游标,因此翻页不会随历史消息数量线性退化。新的 ACK 或 Sync 可能改变 正在发送消息的权威排序,UI 应在消息状态变化后允许刷新当前窗口,不能把 Cursor 当作永久快照令牌。
xhim_v1_client_list_conversations() 分页读取当前账号的持久化会话索引。 空 Cursor 从列表顶部开始;limit=0 使用默认 50 条,单页最多 200 条。 排序固定为置顶优先、持久化活动顺序倒序、会话 ID 稳定补序,不使用 Offset。 每个借用的 xhim_v1_conversation_snapshot_t 包含会话 ID、最后消息 ID 和摘要、 最后消息本地顺序、未读数、已读序号、置顶状态和更新时间。平台 Facade 必须在 Callback 返回前复制快照以及二进制 next_cursor,并在 has_more != 0 时将 Cursor 原样用于同一账号的下一页。排序键刻意不进入公共 ABI,业务代码不得从 Cursor 推断数据库字段。
xhim_v1_client_mark_conversation_read() 提交当前用户的服务端权威已读 watermark。调用方必须传入本地已同步消息上的正 server_seq,不能使用本地 顺序、设备时间或 Pending 消息。Core 在发出网络请求前校验会话存在且目标不 超过本地已知最大序号;服务端响应还必须与当前 canonical 用户、会话、请求 序号和 account/connection generation 匹配。校验通过后,Core 才在事务中单调 推进 read_seq 并按“发送者不是当前用户且 server_seq > read_seq”重算 unread_count。同值/低值为 no-op;只有实际改变才发布 XHIM_V1_EVENT_CONVERSATION_READ_CHANGED,Payload 是需要重新查询的会话 ID。 同一用户其他设备通过不可跳过的 ConversationReadUpsert Sync 事件得到相同 状态,WebSocket 仍只提供 Sync Hint。
xhim_v1_client_delete_friendship()、xhim_v1_client_leave_group() 和 xhim_v1_client_dismiss_group() 是稳定 C ABI 的社交生命周期写入口。三者都 要求 Client 已经 Ready,并要求调用方为一次业务意图生成非空、稳定的 mutation_id;网络重试必须复用该 ID,改变目标用户、群或 expected revision 时必须换新 ID,否则服务端按幂等冲突拒绝。Core 会在同步返回前深拷贝 Input 中的所有 byte view,因此平台 Facade 不需要把临时字符串保留到异步 Completion。
退群和解散群还必须传入最近一次权威群投影中的非零 expected_revision,不能 用本地计数器、设备时间或猜测值。服务端只接受精确 CAS,并返回更大的 revision: 退群回包必须包含当前用户的 Removed 成员变化;解散群仅允许群主执行,回包的 member_count 必须为 0,并包含服务端返回范围内的全体 Removed 成员变化。 删除好友只写入 inactive friendship,不隐式创建黑名单关系。
好友备注使用独立的 xhim_v1_client_set_friend_remark() 写入口。备注是“当前 账号看该好友”的单向、服务端权威字段,不会镜像到对方账号。Input 必须包含与 业务意图一一对应的 mutation_id、好友 peer_user_id、最多 512 字节的严格 UTF-8 remark,以及最近一次权威投影中的 expected_revision;首次设置传 0,清空备注传空 byte view,但清空仍是一次 CAS 写入并推进 revision。服务端 Adapter 将它路由到 POST /v1/social/friendships:setRemark,要求 operation ID 与 mutation ID 完全一致,并返回 active friendship、原样备注以及 expected_revision + 1。同一 mutation ID 只能重放完全相同的 peer、备注字节 和 expected revision。SDK 的错误、诊断和日志不得包含备注明文。
成功回包会先作为 friendship projection echo 原子写入现有 SQLite 投影,再发布 friendships 失效通知,最后完成 xhim_v1_friendship_remark_completion_callback。回调结果把原有 xhim_v1_friendship_snapshot_t 与独立 xhim_v1_friendship_remark_snapshot_t 放在同一 change snapshot 中,避免改变 旧 friendship 数组步长。好友列表中的备注则通过 xhim_v1_social_page_get_friendship_remarks() 取得与 page->friendships 等长的平行数组,必须按相同 index 关联,并在下一次同句柄 调用前深拷贝。好友请求接受回包和 Sync upsert 会携带备注与 revision;删除好友 的 inactive 回包及投影必须清空备注并把 remark revision 归零。
成功响应不会先于本地状态落库。Engine 会用受认证账号的不可变 permit,把 friendship 或 group+members 作为一个 SQLite projection-echo batch 原子提交, 随后排队发布 friendships/groups/group-members 失效通知,最后才投递业务 Completion。服务端返回 idempotent_replay 时仍执行相同 revision-fenced 投影,因此重复响应与随后到达的 Sync 事件不会制造第二份关系。若落库失败, Completion 返回 product_projection_echo_storage_failure,不会报告业务成功; 若 logout、凭证或 connection generation 在提交前失效,请求被取消或判为 stale。极窄竞态下旧账号的已提交 echo 可以保留在旧账号隔离区,但不能向新 生命周期发布失效通知或成功结果。
xhim_v1_friendship_deletion_snapshot_t、 xhim_v1_friendship_remark_change_snapshot_t、 xhim_v1_group_lifecycle_snapshot_t、嵌套 members 以及所有 byte view 都只在 Callback 执行期间借用;平台 Wrapper 必须在 Callback 返回前深拷贝。这些 API 返回的 request_id 可交给通用 xhim_v1_client_cancel_request();取消成功后恰好完成一次,晚到的 HTTP 回包 不得再次回调或写入投影。
在线 Client 的读取 API 当前要求会话满足各自的生命周期条件。需要在 Client 未运行时读取既有数据,应使用独立稳定 xhim_v1_offline_reader_* 句柄及五端 OfflineReader Facade;它以只读/query-only 方式校验账号、Schema 和数据库 密钥,不执行迁移。平台业务代码仍不得自行打开或修改 SDK 数据库来绕过账号隔离。
Worker 支持凭证错误后的暂停,以及同一有效 Epoch 安装新凭证后的恢复; Engine 和稳定 C ABI 的 update_credential 产品链路会在 canonical identity 不变的前提下替换当前凭证并恢复被暂停的在线工作。 一个已经构造并送达 Engine 的 Worker fault 会先同步撤销匹配的当前 permit, 并按 fault 所属账号 Epoch 本地锁存:凭证错误立即派生 CredentialRequired,其他终止错误立即派生 Fatal,然后才异步进入 Coordinator。Coordinator 接受事件不代表 Runtime 一定完成状态迁移,因此本地 锁存会保留到同一 Epoch 的权威终止状态或新账号 Epoch 建立;新 Epoch 不继承 旧凭证错误。report_session_fault() 会异步确认匹配的 Runtime 终止迁移是否 真正应用;Coordinator 控制准入或下游 Runtime 迁移任一拒绝时,Engine 都会把 匹配 Epoch 的本地状态升级为 Fatal。后续发送会 fail closed。这只兜住了已到达 Engine 的事件,不等于解决了进程级分配耗尽时 fault 对象本身无法构造或投递的 问题。 logout() 被 Coordinator 接受后会立即关闭新发送准入,并以非阻塞方式撤销 Worker,其 Completion 在旧 Epoch drain 完成后才投递。shutdown() 永久关闭 API/send 准入,等待 Coordinator 和 Runtime 收束,再 join Worker、排空发送/ Store/事件工作并关闭数据库。若 shutdown 已越过 Engine 本地关闭线性化点、但 Coordinator 随后拒绝受理,Engine 不会重新开放:调用方不会收到该 Completion,Engine 会做 best-effort 本地收束,并把非 Closed snapshot 派生为 Fatal,最终应由宿主安全 释放。若请求在本地关闭线性化点之前就未获准,则不会改变 Engine 生命周期。
state_snapshot() 报告连接恢复阶段;已经发布的认证 Session 快照和不可变 send permit 在同一 Epoch 的受控重连期间继续保留,可供有自己失败/重试策略的 HTTP/Outbox 发送使用。Connecting/Synchronizing 仍不是对外 Ready,不能 据此把 UI 标成在线;CredentialRequired、logout、Closed、Fatal 或 Epoch 改变 会撤销该 permit。
需要持续观察状态时,使用 ClientEngine::subscribe_state(),并保存返回的非零 订阅 ID。它不重放当前值;需要初始值的接入方应先建立订阅,再读取 state_snapshot(),并把后续回调视为失效通知。这个顺序保证状态迁移不会落在 “读取旧快照、尚未进入发布受众”的窗口。状态观察和操作 Completion 共用 Engine 私有 串行回调线程,unsubscribe_state() 返回后不会再开始该观察者的新回调;在观察 回调自身调用退订也受支持。首次 Runtime Ready 不会被直接透传,只有 canonical 身份复核、旧 Epoch drain、Worker 激活和 send permit 安装全部完成后,Engine 才发布权威 Ready。每次发布都会捕获当时已有的订阅者,因此后订阅者不会收到 已经排队的历史状态。状态投递使用独立的单泵有界 mailbox;观察者极慢并造成 积压时,可以按 latest-wins 合并中间快照,但会保留最新终止快照。观察回调应被 视为“状态已变化”的失效通知,需要当前权威值时重新读取 state_snapshot()。 为了保留 Completion 公平性,单泵每次只投递一条;在控制回调内同步发布权威 Ready/Fatal 前,Engine 会先按 FIFO 冲刷已经发布的旧 mailbox 项,避免观察者 随后收到旧 Connecting/Synchronizing 而发生状态回退。 观察回调必须尽快返回,不能同步等待另一个 Engine 回调。
当前退避实现采用有上限的 exponential Full Jitter,Retry-After 作为重试 下限;恢复后只有连续健康达到稳定窗口才重置尝试预算。平台网络监听通过 xhim_v1_client_notify_network_available() 唤醒当前 fenced timer;这个同步 无回调提示在没有待重连任务时是成功 no-op,重复调用也不能绕过最大次数、 凭证、TLS、logout 或 shutdown 栅栏。正式发行仍须在五端真机上验证 Wi-Fi/ 蜂窝切换、前后台、代理断流和系统网络回调竞态。
当前仓库已提供可连接参考 Go 服务端的 SessionBackend、Apple/libcurl BlockingHttpClient、严格 TLS WebSocket Connector 和固定 v1 Codec, xhim_v1 FFI 通过 build-time Product Adapter 在真实实现与 fail-closed 实现之间选择;iOS、macOS、Android、Windows、HarmonyOS Wrapper 和可选 UI 源码 也已落地。正式产品仍需把经过固定版本生成器验证的 Codec、Token 刷新/撤销、各平台网络变化桥接和最终签名原生产物一起封板。Reference Adapter 纵向联调证明了真实网络主链路,但不等于所有平台和故障矩阵已经达到生产门禁。
5.1 媒体传输 Adapter
XHIM::Media 已提供供应商无关的 MediaTransport、MediaProcessor 和 MediaWorker。产品侧实现 MediaTransport::transfer() 时必须把它当作阻塞式 Port:负责流式读取 SDK 沙箱文件、上传授权、可恢复会话/分片、硬超时、服务端 完成确认和最终 MediaRef,不能把网络 Client 或对象存储 SDK 暴露给平台 Facade。
MediaWorker 只拥有一个可 join 的专用 I/O 线程;Store、Transport、Event Sink 和外部 Clock 必须比 Worker 活得更久。产品集成还必须遵守:
transfer()中的进度必须是当前逻辑对象的累计字节数,不能倒退或超过byte_size;函数返回后不得继续调用 Observer;- 每个结果必须原样回显
account_id/account_epoch/task_id,成功结果还必须 提供经过服务端确认的稳定MediaRef; cancel_account_epoch()必须线程安全、幂等并持久拒绝该 Epoch 的后续工作;- Event Sink 在 Media Worker 线程串行执行,不能销毁 Worker、等待 Worker drain,或同步重入形成 join 环;
transport_timeout_ms必须小于lease_duration_ms;临时失败和服务端retry_after_ms由 Processor 按上限调度,不在 Adapter 内建立第二套无限重试;- 本地路径、上传凭证、分片临时 URL 和密钥不得写进日志或消息 Payload。
ClientEngine 现在统一拥有可选 MediaTransportFactory、MediaTransport 和 MediaWorker。稳定 C ABI 提供 xhim_v1_client_create_media_upload/create_media_download/get_media_task/cancel_media_task; XHIM_V1_EVENT_MEDIA_TASK_UPDATED@1 的 Payload 是 UTF-8 task_id,详细状态 由查询返回。创建成功只表示任务和依赖消息意图已持久化,不表示上传完成。 完成后的稳定 MediaRef 会生成不可变媒体 Payload 并进入 Message Outbox; 重启登录会恢复“媒体已完成、消息尚未入队”的依赖。完整状态机与 RTC 边界见 商用媒体消息与实时音视频架构。
5.2 通用消息信封
xhim_v1_client_send_text() 是 text/plain@1 的便捷入口。图片、文件和产品 自定义消息使用 xhim_v1_client_send_message():
c
static xhim_v1_bytes_view_t bytes(const void *data, uint64_t len) {
xhim_v1_bytes_view_t value = {(const uint8_t *)data, len};
return value;
}
const uint8_t descriptor[] = {
/* 由产品固定版本 Codec 生成的 ProductCard 二进制 Payload */
0x0A, 0x00
};
const char fallback[] = "[商品卡片]";
xhim_v1_message_input_t message = {0};
message.struct_size = (uint32_t)sizeof(message);
message.abi_version = XHIM_V1_ABI_VERSION;
const char content_type[] = "com.xihan.product-card";
message.content_type = bytes(
content_type, (uint64_t)(sizeof(content_type) - 1));
message.content_version = 1;
message.payload = bytes(descriptor, (uint64_t)sizeof(descriptor));
message.fallback_text = bytes(fallback, (uint64_t)(sizeof(fallback) - 1));
uint64_t request_id = 0;
int32_t status = xhim_v1_client_send_message(
client, conversation_id, client_message_id, &message,
completion_callback, user_data, &request_id);输入 Buffer 只需在函数调用期间有效,Core 在异步使用前会复制。Payload 是不透明 二进制数据,当前上限 1 MiB;Fallback 必须是非空 UTF-8。读取 xhim_v1_message_snapshot_t.content_type 前,Wrapper 必须先按 struct_size 确认尾部字段存在,不能假设所有 v1 动态库都由同一版头文件构建。
媒体 Payload 只能包含稳定 MediaRef 描述。不要把原始音视频、大文件、本地 路径、平台 URI、Token 或临时签名 URL 直接塞入消息。当前仓库已提供内置媒体 Descriptor Codec、Reference MediaTransport 的 Prepare → 流式 PUT/PATCH 续传 → Complete、SQLite 任务恢复,以及上述 Engine/C ABI/Outbox 编排。 媒体任务快照和事件不返回 local_path、Bearer Token、签名 URL 或上传会话 凭证;所有 Byte View 只在回调期间有效,平台层必须深拷贝。
Reference Adapter 已补齐授权下载、严格 Range、断点 partial、安全原子缓存和 TTL/LRU 配额执行器,也已提供稳定 scoped reader ABI。reader 由 Product MediaTransport 可选端口创建,持有已经 no-follow 打开且通过大小/SHA-256 复验的原生文件句柄;FFI 只提供 size 和 offset read,不返回绝对缓存路径。 平台必须在 I/O 线程循环读取并串行管理一个 reader,停止循环就是取消;reader 可在 Client 销毁后继续读。iOS、macOS、Android、Windows 和 HarmonyOS 已完成 原生 reader 包装;正式发行仍须完成 Windows reparse-point-safe Reference 文件端口、真实 S3 multipart/CDN、病毒与内容 审核、缩略图/转码及五端真机弱网矩阵。实时 RTC 已从纯 IM 首发移出;未来接入 时走独立 Call 控制面,不使用普通消息 Outbox。 完整设计和发布门禁见 商用媒体消息与实时音视频架构。
6. 所有权
ClientEngine::create()成功后,返回的unique_ptr<ClientEngine>统一拥有 Backend、Store、Runtime、Coordinator、Transport Factory、Transport 和 Outbox Worker。最终 Owner 不得在 Engine 自己的 Completion 或 Worker 事件 回调线程中释放;应把最终析构移交到另一个宿主线程。xhim_v1_client_create成功后,调用方拥有 Client handle,最终必须调用xhim_v1_client_destroy。xhim_v1_client_subscribe成功后,调用方拥有 Subscription handle;它应在 Client 销毁前调用一次xhim_v1_subscription_cancel。- Callback 收到的 byte view 均为借用内存,只在本次 Callback 返回前有效。 需要异步处理时,必须先复制或解码。
- Handle 不是普通可复制对象。平台 Facade 应使用明确的 RAII、
close或dispose包装,不把裸指针暴露给业务层。 client_destroy开始后,不得再发起任何使用该 Client 的调用。
7. 线程和回调
- Client API 在 Client 存活期间可以从多个线程调用,但
xhim_v1_client_destroy必须与其他 Client API 外部串行化。 ClientRuntime的普通宿主命令和 Task 使用有界队列准入;受信任的认证、 连接、同步结果转换以及 logout/fatal/shutdown 等终止控制使用保留控制准入, 避免仅因普通队列达到深度上限而无法推进状态收敛。- 保留控制准入与普通任务共用同一个 Actor 和同一串行执行顺序,不是并行快车道, 也不会抢占正在执行或已经排队的命令。它只使用一段有界保留容量来绕过普通 深度检查;控制容量耗尽、分配失败、Runtime 已关闭或 shutdown 已开始时仍可 返回未受理,宿主必须检查 API 的立即返回值。
SessionCoordinator已受理流程的 Backend/Runtime Completion、实时事件和 重连定时器唤醒也通过其私有串行 Worker 的控制准入回到唯一状态路径;普通 Worker 任务仍受深度上限约束,所有状态变更仍按 Worker 顺序执行。OutboxWorker使用一个专用 joinable I/O 线程串行执行阻塞发送,不运行在 UI/Main、Runtime Actor 或 Coordinator Worker 上。future retry 由同一 Worker 的可中断 timer 唤醒;账号撤销直接到达 Transport cancel Port,不会 排在阻塞发送之后。- 同一 Client 的 Event 和 Completion 在该 Client 私有的串行回调线程执行, 不保证运行在 UI/Main 线程。平台 Facade 必须显式切换到宿主需要的线程。
- Engine 回调只向 FFI 做非阻塞投递,不直接执行 C Callback。生命周期状态使用 单泵有界 relay;极端积压时合并中间状态并保留最新状态。首次本地消息入队的
MESSAGE_QUEUED与对应 send Completion 使用同一个预留任务,事件先于 Completion;幂等重放不重复发事件。 - FFI 对并发和重复
shutdown做 fan-out:首个请求驱动 Engine,进行中的请求 共享终止结果,已经完成后的请求收到异步OK/NoChange,每个请求仍有独立request_id。 - Callback 必须尽快返回。Callback 内可以提交新的异步请求,但不能阻塞等待 另一个 XHIM Callback,否则会阻塞该 Client 唯一的回调线程。
xhim_v1_subscription_cancel对同一 handle 只能调用一次,且不得并发调用。- Client 销毁会停止接收新的 Callback,并等待已经开始的 Callback 完成; Callback 内触发 Client 销毁是受支持的,但应由平台 Facade 统一封装。
- 上述 Completion 与 Worker fault 只在受支持的资源 envelope 内保证可构造和 投递。进程级分配耗尽可能使
noexcept路径 fail closed 而无法再分配错误 对象;当前没有 allocation-free emergency health latch。宿主不能把未收到 Callback 当作 OOM 下仍然健康,Stable 前必须补充可无分配读取的单向故障 状态。
8. 账号和存储
- 一个 Client 同时只绑定一个账号。
- 多账号使用不同 Client,并为每个账号配置隔离的数据库路径。
- 服务端认证完成后,宿主必须可靠绑定
(account_id, self_user_id);Sync 事件只能校验该身份,不能修改数据库归属。 - 不要直接读写 XHIM 的 SQLite 表。数据库结构、事务边界和迁移只由内核维护。
- 同一数据库不得由不兼容版本或多个进程并发打开。
9. 深度定制
需要适配真实后端时,可以在实验性 C++ 扩展层实现 SessionBackend、 MessageTransportFactory、平台 HTTP Client、WebSocket Connector 和生成的 固定协议版本 Codec,再交给 ClientEngine 统一组装。Adapter 必须遵守请求 超时、取消、响应大小上限、TLS 校验、重定向限制、凭证代次隔离和上述禁止 同步重入 Worker 的契约。
这些扩展不改变稳定边界:平台业务 API 应继续通过 xhim_v1 C ABI 暴露,避免 把 C++ STL 类型、异常或对象生命周期传播到 Swift、Kotlin、C# 或 ArkTS。
升级前请阅读 兼容性策略。