docs: complete STEP-077 V4 bridge investigation
Some checks failed
validate-scripts / validate-scripts (push) Has been cancelled

This commit is contained in:
OpenAI Codex
2026-09-13 15:25:34 +08:00
parent 0759a06104
commit e0088a5f08
4 changed files with 322 additions and 0 deletions

View File

@@ -0,0 +1,117 @@
# STEP-077 QBIN2 V4 Android / JNI / RN 桥接调查报告 v1
状态:完成(只读调查)
日期:2026-09-13
基线:`0759a06104e1fb30f036db2efb6ef34ca5859759`(`particle/main` = `origin/particle/main` 于调查开始时)
## 结论
V4 应新增一条与 V3 完全平行的、低频的 `WeatherRuntime` / `MapLibreMap` / `NativeMapView` 通道;不得接到现有 `submitField`、`clearFields`、`setForecastTime`、`setPlaybackState` 或 `setPlaybackSpeed`。这些现有命令分别写入 V3 Host field、`clearHostFields()`、`seekRuntimeTimelineToTimestamp()` 和 `controlRuntimeTimeline()`,会操作 V3 timeline,不能承载 V4 的 `EncodedFieldStore` / `ForecastClock`。
推荐 RN 只传绝对本地 `qbinFileUri`(或 `file://` URI)及小型、typed primitive ABI 元数据;Kotlin 有界读取为 Java `byte[]`,JNI 复制为 C++ `std::vector<uint8_t>`,再调用 `RasterArraySource::submitEncodedField(...)`。不传 Base64,不由 JS 构造 `number[]` / 大型 `ReadableArray`。V4 Core 独占每个 field 的 T0/T1/MIX 选择;桥接只传 requested time、play/pause 与速度,绝不传 endpoint、mix 或 renderer snapshot。
## 实际当前链路
```text
FieldRuntime.ts public facade
-> NativeWeatherRuntimeBridge.ts TurboModule Spec (primitive arguments / Promise)
-> WeatherRuntimeBridgeModule.kt
runOnUiQueueThread -> Fabric reactTag
UIManagerHelper(FABRIC).resolveView -> MLRNMapView.mapLibreMap
local file bounded read -> WeatherRuntime
-> MapLibreMap (map-owned WeatherRuntime facade)
-> NativeMapView Java (checkState) -> registered NativeMapView JNI
-> native_map_view.cpp: style.getSource(id), dynamic_cast<RasterArraySource*>
-> RasterArraySource V3 Host API OR isolated V4 Host API
```
源码事实如下。
- `FieldRuntime.submitField` 仍是 V3 public API,封装 `FieldPayloadRefs` 为 validity/scalar/vector 三个 URI;`submitLegacyVectorField` 保留 v1 兼容入口。`NativeWeatherRuntimeBridge.ts` 的 codegen Spec 已刻意使用 primitive 参数,空 string 仅表达 V3 optional plane。
- Kotlin 对 V3 已验证本地绝对路径或 `file://`、单文件和 aggregate 64 MiB、读中长度变化;再形成 `ByteArray`。它不保存 reactTag、map 或 timeline,而是每次在 UI queue 解析当前 Fabric view。
- `WeatherRuntime.submitField` 把 V3 bytes 重新 pack 后调用 `submitRasterArraySourceHostFieldV2`;`NativeMapView` 用 `GetArrayRegion` 复制到 C++,然后拆 planes 并调用 `submitHostField`。这是可复用的“file -> Java byte[] -> JNI copy -> vector” seam,而不是可复用的 V4 API。
- V4 `RasterArraySource` 已有独立 `EncodedFieldStore`、`ForecastClock`、binding intent 和 `submitEncodedField` / `set*Binding` / `setRequestedForecastTime` / `setForecastPlayback*` / `clear*EncodedFields`;注释和 STEP-076 public-path 测试都证明它不写 V3 T0/T1/MIX 或 renderer snapshot。
## QBIN 传输形态
| 方案 | 当前支持与成本 | 结论 |
| --- | --- | --- |
| A:RN `qbinFileUri`,Kotlin 读取 | 已有 `readLocalPayload` / `resolveLocalFile` 精确先例;仅接受本地文件、1..64 MiB,16 KiB buffer,有 read-time size guard;Java/JNI 仍各有一次受控 copy | 推荐。新增 V4 方法复用读取 helper,但不复用 V3 submit/timeline API。 |
| B:RN `byte[]` / `number[]` | 当前 TurboModule Spec 无 binary array ABI;`ReadableArray` 是现有 color stops 的小对象集合,不适合 QBIN。JS number array 造成 JS/native 转换、对象/number 开销与额外 copy;Base64 更扩大内容且无现有 ABI | 拒绝。不得为了方便绕过 64 MiB file guard。 |
| C:直接复用 Java/JNI `byte[]` seam | Kotlin -> `WeatherRuntime` -> Map -> `NativeMapView` 的 `byte[]` 和 `GetArrayRegion` 已存在,适合作为 A 的下半段 | 复用实现模式;新增命名为 V4-specific JNI 方法,不复用 V3 方法。 |
64 MiB 是现有 Kotlin `MAX_PAYLOAD_BYTES` 与 Java `MAX_FIELD_PAYLOAD_BYTES` 的边界;V4 要在 Kotlin、Java、JNI 均检查 raw QBIN 长度 `1..64 MiB`,随后 Core Decoder 继续执行其自身 QBIN 全文件/decoded-raw 限制。该限制不授权桥接解析 QBIN。
## 推荐 V4 API 与跨层 ABI
所有 String 以 UTF-8 byte length 做小型上限校验;`reactTag`、generation、dataVersion、time 使用 JS 安全整数检查(当前 bridge 对 timestamp 已采用 finite/nonnegative/integral guard)。枚举使用固定 string;所有 enum numeric 值只在 Kotlin/Java/C++ 边界转换。
| RN public / Turbo Spec | Kotlin / Java facade | JNI / Core | 参数形态及责任 |
| --- | --- | --- | --- |
| `submitEncodedField(tag, fieldKey, fieldID, generation, dataVersion, contractJson, qbinFileUri)` | `submitEncodedField(...)` | `submitRasterArraySourceEncodedField(sourceId, fieldKey, fieldID, long generation, long dataVersion, contractJson, byte[] raw)` | URI 为文件;metadata 用 primitive;contractJson 是唯一结构化小协议,Kotlin/Java 各只作长度、空值、range;JNI 解析为 Core structs 并调用 V4 API。 |
| `setColorBinding(tag, fieldKey, semanticID, groupID, expectedValueClass, expectedUnitID)` | 同名 | `setRasterArraySourceEncodedColorBinding(...)` | 完全 primitive;JNI 构造 `ColorBinding`,Core resolver 仍做最终 plane assertion。 |
| `setParticleBinding(tag, mode, fieldKey, firstSemanticID, firstGroupID, secondSemanticID, secondGroupID, expectedUnitID)` | 同名 | `setRasterArraySourceEncodedParticleBinding(...)` | mode-dependent selector 仍占固定 primitive slots;Direction 使用 first selector,second 传 0/0。Kotlin/Java 检查 mode 和 uint16 range,不引入 `ReadableMap`。 |
| `setRequestedForecastTime(tag, timestampMs)` | `setEncodedForecastTime` | `setRasterArraySourceEncodedForecastTime` | 独立 V4 名称,`long`;Core ForecastClock 选择各 field window/mix。 |
| `setForecastPlaybackState(tag, "PLAY"|"PAUSE")` | `setEncodedForecastPlaybackState` | `setRasterArraySourceEncodedForecastPlaybackState` | V4 enum,绝不调用 V3 `controlRasterArraySourceTimeline`。 |
| `setForecastPlaybackSpeed(tag, "HALF"|"NORMAL"|"DOUBLE")` | `setEncodedForecastPlaybackSpeed` | `setRasterArraySourceEncodedForecastPlaybackSpeed` | Kotlin/Java 映射为 0.5/1/2;Core 保留 speed validity。 |
| `clearEncodedField(tag, fieldKey)` / `clearAllEncodedFields(tag)` | 同名 | 同名 V4-only JNI | 不清 V3 Host fields、不改变 V3 cadence/timeline。single-clear 返回 `CLEARED` / `FIELD_NOT_FOUND`;all-clear 返回 accepted command。 |
`ResolvedFieldProfileContract` 需要 `contractSchemaVersion, profileID, profileVersion, allowAdditionalPlanes, sharedValidityRequired, timeRelation, requiredPlanes[]`,每个 plane 为 `semanticID, valueClass, unitID, groupID, component, requiredFlag`。它是唯一确实需要 array 的稳定数据;当前 TurboModule codegen 对 public object ABI 已有历史约束,故不建议 `ReadableMap/ReadableArray`,也不应将每个平面拆成无边界的 JS 数组。推荐在 RN/Spec 中传受大小限制的 `contractJson`,在 JNI 的单一 parser 转为 C++ contract;Kotlin/Java 不语义解析。这样 Host/SDK 负责已经 resolved 的 profile contract,Native 不知道 provider/profile 业务;Core `FieldProfileValidator` 仍是 structural/semantic 唯一裁决者。
这不是让 JSON 承载 QBIN 或 server manifest:`contractJson` 是受 schema 版本约束的小 control-plane ABI。若 RN codegen 后续稳定支持 readonly typed object + typed array,才可在破坏性 ABI 版本中替换;本 Step 不建议 `ReadableMap`,因为它绕过 generated Spec、会把多层 field parsing 复制进 Kotlin。
## 兼容与所有权策略
- V3 `FieldRuntime.submitField`、`submitLegacyVectorField` 与现有 `FieldSubmitResult` 保留不变;V4 新增 `submitEncodedField` 与新的 `EncodedFieldSubmitResult` union,不能改名覆盖。
- `WeatherRuntime.setForecastTime`、`setPlaybackState`、`setPlaybackSpeed` 必须不复用:它们当前调用 V3 `seekRasterArraySourceTimelineToTimestamp` / `controlRasterArraySourceTimeline`。V4 用 `setEncoded...` Java/JNI names;RN public 名可按任务书使用 `setRequestedForecastTime` 等,但需明确是 V4 namespace / facade(例如 `EncodedFieldRuntime`),避免误导。
- `clearFields` 和 `clearEncodedFields` 必须分离。前者还参与 V3 state/cadence/disable 行为并调用 `clearHostFields()`;后者只清 `EncodedFieldStore`,保留 V4 bindings,正如 STEP-076 证明,且不得触及 V3。
- `setColorBinding` / `setParticleBinding` 不修改 WeatherColor/RasterParticle renderer properties;它们只存 V4 binding intent。实际 renderer 接线不在本 Step 范围。
## 错误模型
Promise rejection 仅用于桥接前条件、生命周期或无法执行;已进入 Core 的 `RasterArrayEncodedFieldSubmitResult` 必须 resolve 成可区分的 V4 result string,不能折叠为 `UNKNOWN` 或 `NATIVE_REJECTED`。
| Core result | 推荐 RN resolved result |
| --- | --- |
| `AcceptedInserted` | `ACCEPTED_INSERTED` |
| `AcceptedReplacedRevision` | `ACCEPTED_REPLACED_REVISION` |
| `Duplicate` | `DUPLICATE` |
| `RejectedDecode` | `REJECTED_DECODE` |
| `RejectedContract` | `REJECTED_CONTRACT` |
| `RejectedInvalidFieldKey` | `REJECTED_INVALID_FIELD_KEY` |
| `RejectedIdentityConflict` | `REJECTED_IDENTITY_CONFLICT` |
| `RejectedStaleRevision` | `REJECTED_STALE_REVISION` |
| `RejectedIncompatible` | `REJECTED_INCOMPATIBLE` |
| `RejectedCapacity` | `REJECTED_CAPACITY` |
桥接层 rejection code 应为:`INVALID_ARGUMENT`(tag、enum、整数、UTF-8/control size)、`PAYLOAD_FILE_UNREADABLE`(空/越限/非本地/不可读/读中变更)、`MAP_VIEW_NOT_FOUND`、`MAP_NOT_READY`、`STYLE_NOT_READY`、`WEATHER_OBJECTS_NOT_READY`、`RUNTIME_DISPOSED`,以及 V4 source 获取失败时的 `SOURCE_MISSING`。JNI 在 Source 不存在或类型不符时不得伪装为 Core `RejectedDecode`;应返回明确 native bridge status,再由 Java 映射为 `SOURCE_MISSING`。style 已存在但 source 尚未 ready 与 runtime disposed 同样不能混成 submit result。
## 生命周期与线程
`WeatherRuntime` 被 `MapLibreMap` 私有拥有,且是 style-scoped;`replaceWeatherRuntime` 先 dispose,`setStyle` 也 dispose,Kotlin 的 MapView detach listener 调用 `map.disposeWeatherRuntime()`。每个 bridge call 在 `runOnUiQueueThread` 通过 Fabric `reactTag -> UIManagerHelper -> MLRNMapView -> mapLibreMap` 当场解析;不允许新增 map registry、缓存 tag、长期保存 `MapLibreMap*` 或 native source pointer。
未来 V4 command 应沿用 `withRuntime`,在 Android UI 主线程上从当前 style 查 source。`NativeMapView.checkState` 是 Java 层 native lifetime guard;C++ 必须每次 `style.getSource(sourceId)` 和 `dynamic_cast<RasterArraySource*>`,不可缓存指针。style reload/detach/dispose 后,后续命令得到上述 lifecycle error;旧 V4 state 随 Source/style 销毁失效,Host 要在重新 attach、style ready 后按自己的缓存重新提交 raw QBIN 与 binding intent。
## 可用验证与后续验收
本调查没有执行 Android 编译、APK、ADB 或实体机操作。当前 Linux 可用入口与边界:
| 层 | 后续可验证项 | 边界 |
| --- | --- | --- |
| TS | `npx tsc -p packages/maplibre-particle-rn/tsconfig.json --noEmit`;Test Host `npm run typecheck` | package 未定义独立 typecheck script;依赖由 Test Host node_modules/paths 提供。 |
| TurboModule | RN codegen 由 package `codegenConfig`(`WeatherRuntimeBridgeSpec`、`src`、Kotlin package)与 Android Gradle/RN integration 驱动 | 没有发现完全脱离 Gradle 的仓库级 codegen 命令;实现 Step 应记录实际 generation task。 |
| C++/JNI | 头文件/`native_map_view.cpp` 的精确 compile 与 Linux Core unit tests;JNI registration list 与 Java native declarations 静态一一审计 | 精确 Android ABI/JNI compile 仍需 Android NDK Gradle/native build;Linux Core build 不证明 Android linkage。 |
| Java/Kotlin | `WeatherRuntimeStateTest` 是 Mockito/JVM facade 状态测试,可新增 V4 mapping/隔离测试 | 它依赖 Android Gradle test classpath;未发现无需 Gradle 的现成 Kotlin/Java standalone validation。 |
建议实施拆分:
1. 先冻结 RN Spec/types 与 Kotlin file/control validation,新增 V4 result union,并以 typecheck + codegen 审计验收。
2. 再新增 Java `WeatherRuntime` V4-only facade、`MapLibreMap` / `NativeMap` / `NativeMapView` Java declarations、C++ JNI registration 与 source lookup;JVM Mockito 覆盖 V3/V4 time-clear 不串线和 error mapping。
3. 最后才接 renderer consumer(独立 Step),以 C++ Source tests 证明 V4 projection、style reload/detach rejection、JNI exact build、Android Debug OpenGL/Vulkan 与离线 fixture 验收。不得下载真实 QBIN。
## 实施文件清单与风险
预期修改:`packages/maplibre-particle-rn/src/{types.ts,NativeWeatherRuntimeBridge.ts,FieldRuntime.ts}`、`WeatherRuntimeBridgeModule.kt`、`WeatherRuntime.java`、`MapLibreMap.java`、`NativeMap.java`、`NativeMapView.java`、`native_map_view.hpp/.cpp`,以及相应 JVM/C++ tests。不会改 package version、Test Host、Renderer/GPU。
主要风险为 codegen 对 complex object/array 的限制、64 MiB 下 Kotlin/Java/JNI 三份瞬时内存、JS number 精度(所有 uint64 必须限制在 safe integer 或改 decimal string ABI)、source/style 生命周期竞争、以及错接旧 V3 timeline。前四项通过 primitive+small-contract JSON、固定 64 MiB guards、UI queue late lookup 和 typed result 处理;最后一项通过 V4-only 方法命名、独立 NativeMap API 和隔离测试消除。