@symbiote-native/engine 0.5.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +39 -14
- package/android/CMakeLists.txt +51 -0
- package/android/build.gradle +90 -0
- package/android/src/main/AndroidManifest.xml +1 -0
- package/android/src/main/cpp/SymbioteEngineJni.cpp +72 -0
- package/android/src/main/java/dev/symbiotenative/engine/SymbioteEngineModule.kt +43 -0
- package/android/src/main/java/dev/symbiotenative/engine/SymbioteEnginePackage.kt +35 -0
- package/build/accessibility-info/shared.js +1 -1
- package/build/accessibility-props.d.ts +1 -8
- package/build/accessibility-props.js +13 -16
- package/build/animated/animations/composition.d.ts +1 -1
- package/build/animated/animations/composition.js +18 -4
- package/build/animated/easing.d.ts +3 -2
- package/build/animated/easing.js +17 -88
- package/build/animated/event.js +6 -1
- package/build/animated/host-binding.d.ts +1 -1
- package/build/animated/host-binding.js +19 -4
- package/build/animated/index.d.ts +1 -1
- package/build/animated/mock.d.ts +1 -19
- package/build/animated/props.js +1 -1
- package/build/animated/rgba.js +16 -50
- package/build/events/index.js +88 -40
- package/build/fabric-props.d.ts +1 -1
- package/build/fabric-props.js +116 -184
- package/build/fabric.d.ts +9 -0
- package/build/fabric.js +32 -0
- package/build/host-access.d.ts +145 -0
- package/build/host-access.js +315 -0
- package/build/host-behavior.d.ts +84 -21
- package/build/host-behavior.js +236 -51
- package/build/image-source-write.d.ts +16 -0
- package/build/image-source-write.js +65 -0
- package/build/imperative.d.ts +49 -0
- package/build/imperative.js +258 -0
- package/build/index.d.ts +14 -7
- package/build/index.js +53 -10
- package/build/mutation-buffer.d.ts +238 -0
- package/build/mutation-buffer.js +513 -0
- package/build/native-engine.d.ts +185 -0
- package/build/native-engine.js +182 -0
- package/build/native-tree-host.d.ts +25 -0
- package/build/native-tree-host.js +68 -0
- package/build/node.d.ts +195 -58
- package/build/node.js +852 -383
- package/build/pan-responder/index.js +27 -52
- package/build/platform-color/index.d.ts +1 -1
- package/build/platform-color/index.js +11 -4
- package/build/process-background-image/index.js +30 -566
- package/build/process-background-longhands.d.ts +4 -0
- package/build/process-background-longhands.js +44 -0
- package/build/process-box-shadow/index.js +23 -187
- package/build/process-filter.js +27 -300
- package/build/process-transform/index.d.ts +1 -1
- package/build/process-transform/index.js +25 -107
- package/build/process-transform-origin/index.d.ts +1 -1
- package/build/process-transform-origin/index.js +29 -102
- package/build/registry.d.ts +36 -0
- package/build/registry.js +73 -0
- package/build/sound-manager/index.d.ts +3 -0
- package/build/sound-manager/index.js +36 -0
- package/build/structured-style.d.ts +10 -0
- package/build/structured-style.js +180 -0
- package/build/style-registry/index.d.ts +14 -0
- package/build/style-registry/index.js +60 -11
- package/build/surface.d.ts +31 -2
- package/build/surface.js +138 -56
- package/build/text-input-state.d.ts +1 -0
- package/build/text-input-state.js +17 -3
- package/build/tree-host.d.ts +322 -0
- package/build/tree-host.js +211 -0
- package/build/view-config.js +4 -4
- package/codegen-specs/NativeSymbioteEngine.ts +27 -0
- package/cpp/SymbioteDebug.cpp +51 -0
- package/cpp/SymbioteDebug.h +54 -0
- package/cpp/SymbioteEngineBindings.cpp +234 -0
- package/cpp/SymbioteEngineBindings.h +59 -0
- package/cpp/SymbioteFabricProps.cpp +2619 -0
- package/cpp/SymbioteFabricProps.h +223 -0
- package/cpp/SymbioteTree.cpp +2593 -0
- package/cpp/SymbioteTree.h +294 -0
- package/ios/SymbioteEngineModule.h +25 -0
- package/ios/SymbioteEngineModule.mm +44 -0
- package/package.json +31 -3
- package/react-native.config.cjs +23 -0
- package/symbiote-engine.podspec +42 -0
- package/build/animated/bezier.d.ts +0 -1
- package/build/animated/bezier.js +0 -102
- package/build/commit.d.ts +0 -49
- package/build/commit.js +0 -1058
- package/build/tags.d.ts +0 -2
- package/build/tags.js +0 -40
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import type { ICommittedRecord } from './tree-host';
|
|
2
|
+
/**
|
|
3
|
+
* The ABI this file knows how to talk to. A binary reporting anything else is refused outright rather
|
|
4
|
+
* than probed method by method — the two artefacts ship separately (a pod and an npm package), so
|
|
5
|
+
* disagreement is routine, and a partial match is the shape that corrupts memory quietly.
|
|
6
|
+
*
|
|
7
|
+
* Exported for the tests, which must DERIVE their supported and unsupported arms from it rather than
|
|
8
|
+
* restate the number: a bump that leaves a fixture behind reads as "the binary is stale", which is
|
|
9
|
+
* exactly the message this constant exists to produce, so the failure looks like the feature working.
|
|
10
|
+
*/
|
|
11
|
+
export declare const SUPPORTED_NATIVE_VERSION = 4;
|
|
12
|
+
/**
|
|
13
|
+
* What `installJSIBindingsWithRuntime:` puts on the global.
|
|
14
|
+
*
|
|
15
|
+
* `allocInt32Array` hands back a view over memory NATIVE owns — not a copy — which is the only reason
|
|
16
|
+
* any of this exists. Every other route from JS to native structure costs a JSI crossing per element,
|
|
17
|
+
* and the census that sized this design counted ~4 000 reads for a two-row swap.
|
|
18
|
+
*/
|
|
19
|
+
export type INativeEngineBindings = {
|
|
20
|
+
version: number;
|
|
21
|
+
allocInt32Array: (lengthInElements: number) => Int32Array;
|
|
22
|
+
/**
|
|
23
|
+
* Item 8c-1's bring-up probe: shadow trees the real `UIManager` holds, or -1 when we could not
|
|
24
|
+
* reach one at all. Not a capability the engine uses — it exists so one device run answers whether
|
|
25
|
+
* our pod compiles against ReactCommon's renderer, links against the prebuilt framework, and can
|
|
26
|
+
* resolve the UIManager from a plain JSI runtime. See the C++ side for why -1 and 0 differ.
|
|
27
|
+
*/
|
|
28
|
+
probeUIManager: () => number;
|
|
29
|
+
/**
|
|
30
|
+
* Replay one recorded mutation batch — the five fields of `IMutationBatch`, spread, because JSI
|
|
31
|
+
* reads five arguments cheaper than it reads five properties off one object.
|
|
32
|
+
*
|
|
33
|
+
* `ops` is read as MEMORY on the native side and never becomes JS values; the four side tables
|
|
34
|
+
* carry what JSI has to marshal either way.
|
|
35
|
+
*
|
|
36
|
+
* `handles` is the load-bearing one and it travels OUT, not back. It holds the placeholder object
|
|
37
|
+
* for every slot the ops address, and the host attaches each created node's
|
|
38
|
+
* `shared_ptr<const ShadowNode>` to the object at that slot as JSI `NativeState` — so the objects
|
|
39
|
+
* the adapter is already holding become the real handles in place. Nothing is returned.
|
|
40
|
+
*
|
|
41
|
+
* That is the whole lifetime design, and it is RN's own: `nativeFabricUIManager.createNode()`
|
|
42
|
+
* hands back an object whose NativeState owns the node, so Hermes collecting the object is what
|
|
43
|
+
* frees it. The version this replaced kept a `Map<int, shared_ptr>` in C++ instead — a second
|
|
44
|
+
* owner nothing could tell to let go, which cost 792 -> 1492 MB across one benchmark suite.
|
|
45
|
+
*/
|
|
46
|
+
applyOps: (ops: Int32Array, strings: readonly string[], values: readonly unknown[], instanceHandles: readonly unknown[], handles: readonly object[]) => void;
|
|
47
|
+
/**
|
|
48
|
+
* The four reads of `ITreeHost`, taking the same placeholder object `applyOps` put the node on.
|
|
49
|
+
*
|
|
50
|
+
* None is on a commit path — they run at GESTURE or lifecycle rate (a host behavior seeing the
|
|
51
|
+
* props it reacts to, an app measuring a ref, a framework seam navigating what it just built), so
|
|
52
|
+
* the crossing cost is irrelevant. `undefined` / empty is an ordinary answer from all of them: a
|
|
53
|
+
* handle native has not seen and a genuinely absent value are indistinguishable here, and both
|
|
54
|
+
* degrade.
|
|
55
|
+
*
|
|
56
|
+
* `getViewName` answers the RESOLVED name, which native may have changed at insert — a `<Text>`
|
|
57
|
+
* inside another `<Text>` commits as `RCTVirtualText`, and only the side holding the parent link
|
|
58
|
+
* knows.
|
|
59
|
+
*/
|
|
60
|
+
getProp: (handle: object, key: string) => unknown;
|
|
61
|
+
getProps: (handle: object) => Readonly<Record<string, unknown>>;
|
|
62
|
+
/** The one WRITE among them: dirty a node no op named. See `markPropsDirty` (node.ts). */
|
|
63
|
+
markPropsDirty: (handle: object) => void;
|
|
64
|
+
getViewName: (handle: object) => string;
|
|
65
|
+
parentOf: (handle: object) => object | undefined;
|
|
66
|
+
childrenOf: (handle: object) => readonly object[];
|
|
67
|
+
/** One entry, not the whole list — see `ITreeHost` for the quadratic each of these replaces. */
|
|
68
|
+
firstChildOf: (handle: object) => object | undefined;
|
|
69
|
+
nextSiblingOf: (handle: object) => object | undefined;
|
|
70
|
+
/** The batched twins of `parentOf` / `childrenOf`. See `ITreeHost` for why the sweep needs them. */
|
|
71
|
+
parentsOf: (handles: readonly object[]) => readonly (object | undefined)[];
|
|
72
|
+
subtreesOf: (roots: readonly object[]) => readonly object[];
|
|
73
|
+
/** The same walk narrowed to what a teardown visits. See `ITreeHost.teardownSubtreesOf`. */
|
|
74
|
+
teardownSubtreesOf: (roots: readonly object[]) => readonly object[];
|
|
75
|
+
/** The upward twin, deepest first — one crossing for a chain the event path walks per event. */
|
|
76
|
+
ancestorsOf: (handle: object) => readonly object[];
|
|
77
|
+
committedRecordOf: (handle: object) => ICommittedRecord | undefined;
|
|
78
|
+
/** A TEST read — the payload the last commit sent. See `ITreeHost.committedPayloadOf`. */
|
|
79
|
+
committedPayloadOf: (handle: object) => Readonly<Record<string, unknown>> | undefined;
|
|
80
|
+
/**
|
|
81
|
+
* The imperative six, taking the same placeholder object `applyOps` put the node on.
|
|
82
|
+
*
|
|
83
|
+
* They are here because `nativeFabricUIManager`'s own copies unwrap a handle IT minted, and under
|
|
84
|
+
* the batched applier every handle in play was minted by the batching slot. Ours carry the node on
|
|
85
|
+
* their `NativeState` exactly as RN's do, so these six are the same code reading a different
|
|
86
|
+
* object. An app calling `measure()` on a ref reaches native through here or not at all.
|
|
87
|
+
*
|
|
88
|
+
* The callback protocol is Fabric's own, not ours: `measure` answers six numbers, `measureInWindow`
|
|
89
|
+
* four, and `measureLayout` calls `onFail` when the surface has no committed revision — copied
|
|
90
|
+
* from `UIManagerBinding` so a component that already handles those cases keeps working.
|
|
91
|
+
*/
|
|
92
|
+
dispatchCommand: (handle: object, commandName: string, args: readonly unknown[]) => void;
|
|
93
|
+
sendAccessibilityEvent: (handle: object, eventType: string) => void;
|
|
94
|
+
measure: (handle: object, callback: (x: number, y: number, width: number, height: number, pageX: number, pageY: number) => void) => void;
|
|
95
|
+
measureInWindow: (handle: object, callback: (x: number, y: number, width: number, height: number) => void) => void;
|
|
96
|
+
measureLayout: (handle: object, relativeTo: object, onFail: () => void, onSuccess: (x: number, y: number, width: number, height: number) => void) => void;
|
|
97
|
+
setIsJSResponder: (handle: object, isResponder: boolean, blockNativeResponder: boolean) => void;
|
|
98
|
+
/**
|
|
99
|
+
* RN's own commit telemetry for ANY surface, including one this host never drove.
|
|
100
|
+
*
|
|
101
|
+
* OPTIONAL: it is read with `?.`, so a pod predating it degrades instead of throwing, unlike the
|
|
102
|
+
* members above that `isBindings` requires. Treat an absent member as "no answer", never as
|
|
103
|
+
* zeroes — a zero reads as "React's commit measures no text", the claim this exists to test.
|
|
104
|
+
*/
|
|
105
|
+
/**
|
|
106
|
+
* The C++ half's diagnostics (`SymbioteDebug.h`). OPTIONAL, like `readSurfaceTelemetry` and for
|
|
107
|
+
* the same reason: an older native binary that predates them must still pass `isBindings` rather
|
|
108
|
+
* than failing bring-up over a member nothing on the critical path needs.
|
|
109
|
+
*/
|
|
110
|
+
setDebugEnabled?: (enabled: boolean) => void;
|
|
111
|
+
takeDebugLog?: () => readonly string[];
|
|
112
|
+
readSurfaceTelemetry?: (surfaceId: number) => {
|
|
113
|
+
/**
|
|
114
|
+
* The inside of a commit, read out of RN's OWN `TransactionTelemetry` rather than timed by us
|
|
115
|
+
* — a `ShadowTreeRevision` carries the telemetry of the commit that produced it.
|
|
116
|
+
*
|
|
117
|
+
* `layoutNodes` is the one that answers the question the timings only pose: Yoga reports how
|
|
118
|
+
* many layoutable nodes it actually touched, so a one-row change that reports the whole tree is
|
|
119
|
+
* a re-layout, not a slow layout.
|
|
120
|
+
*/
|
|
121
|
+
layoutMs: number;
|
|
122
|
+
textMs: number;
|
|
123
|
+
/**
|
|
124
|
+
* `ShadowTree::commit`'s own window, and NOT `materialize`'s — this used to say otherwise, and
|
|
125
|
+
* three rounds of investigation read the number that way. `materialize` runs in `kOpCommit`
|
|
126
|
+
* before `completeSurface` is called, so it falls outside this window and layout's alike.
|
|
127
|
+
*/
|
|
128
|
+
commitMs: number;
|
|
129
|
+
layoutNodes: number;
|
|
130
|
+
textMeasures: number;
|
|
131
|
+
/**
|
|
132
|
+
* Parents that took the targeted-replace path since the last read, zeroed on read. OURS, not
|
|
133
|
+
* RN's — a LIVENESS signal for a fast path whose absence no correctness test can see.
|
|
134
|
+
*/
|
|
135
|
+
targetedReplaces: number;
|
|
136
|
+
/** `materialize`'s own walk and its breakdown — ours, zeroed on read. See `ISurfaceTelemetry`. */
|
|
137
|
+
walkMs: number;
|
|
138
|
+
propsMs: number;
|
|
139
|
+
foldLookupMs: number;
|
|
140
|
+
foldsFound: number;
|
|
141
|
+
foldToJsMs: number;
|
|
142
|
+
foldCallMs: number;
|
|
143
|
+
foldFromJsMs: number;
|
|
144
|
+
rawPropsMs: number;
|
|
145
|
+
createNodeMs: number;
|
|
146
|
+
appendChildMs: number;
|
|
147
|
+
diffPropsMs: number;
|
|
148
|
+
nodesCreated: number;
|
|
149
|
+
nodesCloned: number;
|
|
150
|
+
nodesReused: number;
|
|
151
|
+
decodeMs: number;
|
|
152
|
+
instanceHandleMs: number;
|
|
153
|
+
publishMs: number;
|
|
154
|
+
nativeStateMs: number;
|
|
155
|
+
nodesDecoded: number;
|
|
156
|
+
setPropMs: number;
|
|
157
|
+
propConvertMs: number;
|
|
158
|
+
setProps: number;
|
|
159
|
+
deletesOfAbsent: number;
|
|
160
|
+
writesOfUnchanged: number;
|
|
161
|
+
valueEntries: number;
|
|
162
|
+
valueConversions: number;
|
|
163
|
+
applyMs: number;
|
|
164
|
+
stringDecodeMs: number;
|
|
165
|
+
structureMs: number;
|
|
166
|
+
holdHandleMs: number;
|
|
167
|
+
hostReadMs: number;
|
|
168
|
+
hostReadHandles: number;
|
|
169
|
+
applyCalls: number;
|
|
170
|
+
};
|
|
171
|
+
};
|
|
172
|
+
declare global {
|
|
173
|
+
var __symbioteEngineNative: unknown;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* The native bindings, or `undefined` when this platform has none.
|
|
177
|
+
*
|
|
178
|
+
* Resolving the TurboModule is done for its SIDE EFFECT: `RCTTurboModuleManager` runs
|
|
179
|
+
* `installJSIBindingsWithRuntime:` at the moment it creates a module, so touching the module by name
|
|
180
|
+
* is what puts the global there. The module's own methods are not the capability and are not called
|
|
181
|
+
* here — which is why a reader looking for the payload in the spec file will not find it.
|
|
182
|
+
*/
|
|
183
|
+
export declare function nativeEngine(): INativeEngineBindings | undefined;
|
|
184
|
+
/** Test seam: forget what was resolved, so a fixture can install or remove the global between cases. */
|
|
185
|
+
export declare function resetNativeEngine(): void;
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
// The JS half of our own native module (item 8c of `symbiote-fabric-cxx-surface`). It answers one
|
|
2
|
+
// question — is there a native store on this platform — and every caller must be able to take `no`.
|
|
3
|
+
//
|
|
4
|
+
// `no` is not an error case, it is the majority case:
|
|
5
|
+
//
|
|
6
|
+
// headless (vitest, 5 600 tests) no native anything; the JS backing store is the only one
|
|
7
|
+
// Android the module is iOS-only for now, by choice, not by oversight
|
|
8
|
+
// an app that skipped `pod install` the standing hazard of this repo's local-dev loop
|
|
9
|
+
// an older pod against newer JS two artefacts, two install steps, no shared version gate
|
|
10
|
+
//
|
|
11
|
+
// So the contract is: `nativeEngine()` returns the bindings or `undefined`, and nothing in the engine
|
|
12
|
+
// may require them. The native store is an ACCELERATOR behind the same seam the JS one sits behind.
|
|
13
|
+
//
|
|
14
|
+
// ── THIS IS NOT A LICENCE FOR TWO IMPLEMENTATIONS, AND THE DISTINCTION IS THE POINT ──────────────
|
|
15
|
+
//
|
|
16
|
+
// Two different things get called "the native path" and only one of them is a fork:
|
|
17
|
+
//
|
|
18
|
+
// the STORE (8b') `new Int32Array(n)` vs an `Int32Array` over native memory. `node-table.ts`
|
|
19
|
+
// is byte-identical either way — one implementation, two allocators. There is
|
|
20
|
+
// nothing here that can drift.
|
|
21
|
+
// the APPLIER (8c-1) `replayChildOps` in commit.ts vs `cloneMultiple` inside a commit hook. That
|
|
22
|
+
// IS two implementations of one fold, and a permanent fallback to the JS half
|
|
23
|
+
// would be exactly the silent divergence this repo keeps finding.
|
|
24
|
+
//
|
|
25
|
+
// So the graceful `undefined` below is correct for the store and TRANSITIONAL for the applier. When
|
|
26
|
+
// the applier moves, the JS one is DELETED — the way `node.children` and `node.parent` were — and a
|
|
27
|
+
// missing or mismatched module has to fail LOUD rather than quietly run a second implementation. A
|
|
28
|
+
// silent fallback at that point is a measurement that lies, not resilience.
|
|
29
|
+
//
|
|
30
|
+
// The expensive part of that transition, named here because it is easy to discover too late: ~5 600
|
|
31
|
+
// headless tests drive the JS applier against a fake Fabric slot. Faking `new Int32Array` is free;
|
|
32
|
+
// faking C++ tree cloning is not, so those tests stop covering what actually runs.
|
|
33
|
+
//
|
|
34
|
+
// Fantom was the intended answer and it is NOT available — measured 2026-09-08, four independent
|
|
35
|
+
// blockers. `@react-native/fantom` is not on npm at all (404); it exists only as vendored source
|
|
36
|
+
// under `.vendors/react-native/private/`, marked `private: true`. That vendored tree is RN **main**
|
|
37
|
+
// while this repo consumes 0.86.0, so a tester built from it would exercise the wrong C++. Building
|
|
38
|
+
// it needs a yarn install plus a gradle/CMake build of Hermes, folly and ReactCommon, all written
|
|
39
|
+
// INTO `.vendors/`. And `config/metro.config.js` computes `projectRoot` from `__dirname` with an
|
|
40
|
+
// empty `watchFolders`, so a test file outside that monorepo cannot be resolved — not overridable
|
|
41
|
+
// by env, config merge or CLI. RN's own README says it plainly: "not currently supported for
|
|
42
|
+
// testing application-specific code in React Native apps."
|
|
43
|
+
//
|
|
44
|
+
// So the oracle for a C++ commit path has to be built here: a TypeScript reference implementation
|
|
45
|
+
// the existing suite already exercises, plus a gtest target linking ReactCommon from the installed
|
|
46
|
+
// react-native. Do not cite Fantom as the plan again without re-running that check.
|
|
47
|
+
import { dlog } from './debug.js';
|
|
48
|
+
import { getNativeModule } from './native-modules/index.js';
|
|
49
|
+
import { isRecord } from './type-guards.js';
|
|
50
|
+
/**
|
|
51
|
+
* The ABI this file knows how to talk to. A binary reporting anything else is refused outright rather
|
|
52
|
+
* than probed method by method — the two artefacts ship separately (a pod and an npm package), so
|
|
53
|
+
* disagreement is routine, and a partial match is the shape that corrupts memory quietly.
|
|
54
|
+
*
|
|
55
|
+
* Exported for the tests, which must DERIVE their supported and unsupported arms from it rather than
|
|
56
|
+
* restate the number: a bump that leaves a fixture behind reads as "the binary is stale", which is
|
|
57
|
+
* exactly the message this constant exists to produce, so the failure looks like the feature working.
|
|
58
|
+
*/
|
|
59
|
+
export const SUPPORTED_NATIVE_VERSION = 4;
|
|
60
|
+
function isBindings(value) {
|
|
61
|
+
if (!isRecord(value))
|
|
62
|
+
return false;
|
|
63
|
+
if (typeof value.version !== 'number')
|
|
64
|
+
return false;
|
|
65
|
+
if (typeof value.allocInt32Array !== 'function')
|
|
66
|
+
return false;
|
|
67
|
+
if (typeof value.probeUIManager !== 'function')
|
|
68
|
+
return false;
|
|
69
|
+
if (typeof value.applyOps !== 'function')
|
|
70
|
+
return false;
|
|
71
|
+
// The tree reads, checked one by one rather than as a blob for the reason the whole guard is
|
|
72
|
+
// written this way: a member added to the type without a line here resolves fine at bring-up and
|
|
73
|
+
// throws at the first call site, which is a gesture or a `measure()` — one language and several
|
|
74
|
+
// seconds away from the install that caused it.
|
|
75
|
+
if (typeof value.getProp !== 'function')
|
|
76
|
+
return false;
|
|
77
|
+
if (typeof value.getProps !== 'function')
|
|
78
|
+
return false;
|
|
79
|
+
if (typeof value.markPropsDirty !== 'function')
|
|
80
|
+
return false;
|
|
81
|
+
if (typeof value.getViewName !== 'function')
|
|
82
|
+
return false;
|
|
83
|
+
if (typeof value.parentOf !== 'function')
|
|
84
|
+
return false;
|
|
85
|
+
if (typeof value.childrenOf !== 'function')
|
|
86
|
+
return false;
|
|
87
|
+
if (typeof value.firstChildOf !== 'function')
|
|
88
|
+
return false;
|
|
89
|
+
if (typeof value.nextSiblingOf !== 'function')
|
|
90
|
+
return false;
|
|
91
|
+
if (typeof value.parentsOf !== 'function')
|
|
92
|
+
return false;
|
|
93
|
+
if (typeof value.subtreesOf !== 'function')
|
|
94
|
+
return false;
|
|
95
|
+
if (typeof value.teardownSubtreesOf !== 'function')
|
|
96
|
+
return false;
|
|
97
|
+
if (typeof value.ancestorsOf !== 'function')
|
|
98
|
+
return false;
|
|
99
|
+
if (typeof value.committedRecordOf !== 'function')
|
|
100
|
+
return false;
|
|
101
|
+
// The imperative six, checked by name for the same reason as the rest: a pod that has `applyOps`
|
|
102
|
+
// but not these is an OLDER binary, and accepting it would leave `measure()` reaching a method
|
|
103
|
+
// that is not there — at the moment an app measures a ref, not at bring-up.
|
|
104
|
+
if (typeof value.dispatchCommand !== 'function')
|
|
105
|
+
return false;
|
|
106
|
+
if (typeof value.sendAccessibilityEvent !== 'function')
|
|
107
|
+
return false;
|
|
108
|
+
if (typeof value.measure !== 'function')
|
|
109
|
+
return false;
|
|
110
|
+
if (typeof value.measureInWindow !== 'function')
|
|
111
|
+
return false;
|
|
112
|
+
if (typeof value.measureLayout !== 'function')
|
|
113
|
+
return false;
|
|
114
|
+
return typeof value.setIsJSResponder === 'function';
|
|
115
|
+
}
|
|
116
|
+
// Why `SUPPORTED_NATIVE_VERSION` did NOT move when `probeUIManager` was added, since bumping it is
|
|
117
|
+
// the reflex. The version field guards a MEMORY LAYOUT — it is what stops JS reading a store whose
|
|
118
|
+
// fields moved. Adding a host function changes no layout, and the shape guard above already refuses
|
|
119
|
+
// an older pod that lacks the name, giving the same clean degrade for free. Bumping instead would
|
|
120
|
+
// cost every app on an older pod its native STORE to announce a probe it does not use. Bump the
|
|
121
|
+
// version when the meaning of the bytes changes, not when the object gains a key.
|
|
122
|
+
//
|
|
123
|
+
// ── AND WHY IT DID MOVE, 1 -> 2, FOR THE HANDLE REWRITE ──────────────────────────────────────────
|
|
124
|
+
//
|
|
125
|
+
// Because the shape guard cannot see this one. Every NAME is unchanged: a v1 pod has `applyOps` and
|
|
126
|
+
// all five, so `isBindings` passes it. What changed is what the arguments MEAN — `applyOps` gained
|
|
127
|
+
// `handles`, which a v1 binary silently ignores, and the five went from taking an integer id to
|
|
128
|
+
// taking the handle object, which a v1 binary reads with `.asNumber()`. The first fails by
|
|
129
|
+
// committing nothing and leaking; the second throws one language away from the cause. That is the
|
|
130
|
+
// same class as a moved field, arriving through a calling convention rather than a struct, and it is
|
|
131
|
+
// exactly what this number exists to refuse.
|
|
132
|
+
//
|
|
133
|
+
// ── AND 3 -> 4, FOR THE SAME REASON THE SHAPE GUARD CANNOT HELP WITH ─────────────────────────────
|
|
134
|
+
//
|
|
135
|
+
// The four tree reads are new NAMES, so the guard above would turn a v3 pod away on its own. But
|
|
136
|
+
// `applyOps` also went from six arguments to five, and the name did not move: a v3 binary reads
|
|
137
|
+
// argument 1 as `childIds` (an `Int32Array`) where JS now passes `strings`, and argument 2 as a
|
|
138
|
+
// props table where JS now passes `values`. Every one of those reads succeeds against the wrong
|
|
139
|
+
// object and commits a wrong tree rather than throwing. That is a calling convention change, which
|
|
140
|
+
// is what this number is for — the guard would have refused this pod by luck, on the reads, and a
|
|
141
|
+
// refusal by luck is not a refusal.
|
|
142
|
+
// `undefined` means "resolved, and there is none" — distinct from `resolved === false`, which means
|
|
143
|
+
// nobody has looked. Collapsing the two would re-run the lookup on every miss, and the miss is the
|
|
144
|
+
// common case.
|
|
145
|
+
let resolved = false;
|
|
146
|
+
let bindings;
|
|
147
|
+
/**
|
|
148
|
+
* The native bindings, or `undefined` when this platform has none.
|
|
149
|
+
*
|
|
150
|
+
* Resolving the TurboModule is done for its SIDE EFFECT: `RCTTurboModuleManager` runs
|
|
151
|
+
* `installJSIBindingsWithRuntime:` at the moment it creates a module, so touching the module by name
|
|
152
|
+
* is what puts the global there. The module's own methods are not the capability and are not called
|
|
153
|
+
* here — which is why a reader looking for the payload in the spec file will not find it.
|
|
154
|
+
*/
|
|
155
|
+
export function nativeEngine() {
|
|
156
|
+
if (resolved)
|
|
157
|
+
return bindings;
|
|
158
|
+
resolved = true;
|
|
159
|
+
getNativeModule('SymbioteEngine');
|
|
160
|
+
const installed = globalThis.__symbioteEngineNative;
|
|
161
|
+
if (!isBindings(installed)) {
|
|
162
|
+
dlog('native-engine: no native store on this platform ' +
|
|
163
|
+
`(global is ${typeof installed}) — using the JS backing arrays`);
|
|
164
|
+
return undefined;
|
|
165
|
+
}
|
|
166
|
+
if (installed.version !== SUPPORTED_NATIVE_VERSION) {
|
|
167
|
+
// Loud, because the repair is a `pod install` and the symptom otherwise is a wrong memory layout
|
|
168
|
+
// rather than a missing feature. `<examples_vs_dot_examples>` records how routinely an install
|
|
169
|
+
// replaces the JS half without the native one.
|
|
170
|
+
dlog(`native-engine: REFUSING native store, ABI ${installed.version} ` +
|
|
171
|
+
`against supported ${SUPPORTED_NATIVE_VERSION} — run \`pod install\`. Using the JS arrays`);
|
|
172
|
+
return undefined;
|
|
173
|
+
}
|
|
174
|
+
dlog(`native-engine: native store available, ABI ${installed.version}`);
|
|
175
|
+
bindings = installed;
|
|
176
|
+
return bindings;
|
|
177
|
+
}
|
|
178
|
+
/** Test seam: forget what was resolved, so a fixture can install or remove the global between cases. */
|
|
179
|
+
export function resetNativeEngine() {
|
|
180
|
+
resolved = false;
|
|
181
|
+
bindings = undefined;
|
|
182
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { type INativeEngineBindings } from './native-engine';
|
|
2
|
+
import { type ITreeHost } from './tree-host';
|
|
3
|
+
/**
|
|
4
|
+
* Present one resolved set of bindings as the engine's tree host.
|
|
5
|
+
*
|
|
6
|
+
* `census` is the only member that does not reach native, and it is deliberately not on the ABI: it
|
|
7
|
+
* has exactly ONE engine caller (`censusRetainedTree`), it is diagnostics, and answering it honestly
|
|
8
|
+
* would cost a full native walk of the very tree the design exists to stop walking. The empty census
|
|
9
|
+
* is what `censusRetainedTree` already answers with no host at all, so a probe reading it sees the
|
|
10
|
+
* same zeroes it has always seen off a device.
|
|
11
|
+
*/
|
|
12
|
+
export declare function nativeTreeHost(bindings: INativeEngineBindings): ITreeHost;
|
|
13
|
+
/**
|
|
14
|
+
* Install it, if this runtime has a native module and nothing has claimed the seam already.
|
|
15
|
+
*
|
|
16
|
+
* PRECEDENCE: an installed host WINS. `installFabric()` is the only other caller of `setTreeHost`,
|
|
17
|
+
* and it puts the TypeScript applier in before any fixture can bind a slot — so a headless run that
|
|
18
|
+
* also happens to carry fake bindings must keep the applier, or the ~5 500 tests written against it
|
|
19
|
+
* would silently start driving a stub. The reverse ordering cannot occur on a device: nothing there
|
|
20
|
+
* installs a host but this.
|
|
21
|
+
*
|
|
22
|
+
* No native module is the ORDINARY answer (`native-engine.ts`'s header lists where), and it stays a
|
|
23
|
+
* quiet one here: the ops simply keep accumulating, exactly as they did before this file existed.
|
|
24
|
+
*/
|
|
25
|
+
export declare function installNativeTreeHost(): void;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// The NATIVE tree host — `INativeEngineBindings` presented as an `ITreeHost`.
|
|
2
|
+
//
|
|
3
|
+
// It is the half that makes the buffer mean anything on a device. `tree-host.ts` records ops and
|
|
4
|
+
// then asks a host to turn them into a tree; headlessly `installFabric()` installs the TypeScript
|
|
5
|
+
// applier, and until this file existed a device installed NOTHING — `commitSurfaceOps` returned
|
|
6
|
+
// early, the ops stayed pending forever, and the screen stayed blank with nothing red anywhere.
|
|
7
|
+
//
|
|
8
|
+
// There is no logic here on purpose. Everything the host is asked is something native already
|
|
9
|
+
// answers, so this is a rename and an argument spread; a mapping thin enough to read in one pass is
|
|
10
|
+
// what keeps the two sides auditable against each other.
|
|
11
|
+
import { nativeEngine } from './native-engine.js';
|
|
12
|
+
import { EMPTY_CENSUS, setTreeHost, treeHost, } from './tree-host.js';
|
|
13
|
+
/**
|
|
14
|
+
* Present one resolved set of bindings as the engine's tree host.
|
|
15
|
+
*
|
|
16
|
+
* `census` is the only member that does not reach native, and it is deliberately not on the ABI: it
|
|
17
|
+
* has exactly ONE engine caller (`censusRetainedTree`), it is diagnostics, and answering it honestly
|
|
18
|
+
* would cost a full native walk of the very tree the design exists to stop walking. The empty census
|
|
19
|
+
* is what `censusRetainedTree` already answers with no host at all, so a probe reading it sees the
|
|
20
|
+
* same zeroes it has always seen off a device.
|
|
21
|
+
*/
|
|
22
|
+
export function nativeTreeHost(bindings) {
|
|
23
|
+
return {
|
|
24
|
+
// Spread rather than passed as the batch object: JSI reads five arguments cheaper than five
|
|
25
|
+
// properties, and this is the one member on a commit path.
|
|
26
|
+
applyOps: batch => bindings.applyOps(batch.ops, batch.strings, batch.values, batch.instanceHandles, batch.handles),
|
|
27
|
+
propOf: bindings.getProp,
|
|
28
|
+
propsOf: bindings.getProps,
|
|
29
|
+
markPropsDirty: bindings.markPropsDirty,
|
|
30
|
+
committedRecordOf: bindings.committedRecordOf,
|
|
31
|
+
committedPayloadOf: bindings.committedPayloadOf,
|
|
32
|
+
parentOf: bindings.parentOf,
|
|
33
|
+
childrenOf: bindings.childrenOf,
|
|
34
|
+
firstChildOf: bindings.firstChildOf,
|
|
35
|
+
nextSiblingOf: bindings.nextSiblingOf,
|
|
36
|
+
parentsOf: bindings.parentsOf,
|
|
37
|
+
subtreesOf: bindings.subtreesOf,
|
|
38
|
+
teardownSubtreesOf: bindings.teardownSubtreesOf,
|
|
39
|
+
ancestorsOf: bindings.ancestorsOf,
|
|
40
|
+
census: () => EMPTY_CENSUS,
|
|
41
|
+
// Straight through: native already takes the placeholder, which is what `committedRecordOf`
|
|
42
|
+
// above hands back in its `handle` field for exactly this reason.
|
|
43
|
+
dispatchCommand: bindings.dispatchCommand,
|
|
44
|
+
sendAccessibilityEvent: bindings.sendAccessibilityEvent,
|
|
45
|
+
measure: bindings.measure,
|
|
46
|
+
measureInWindow: bindings.measureInWindow,
|
|
47
|
+
measureLayout: bindings.measureLayout,
|
|
48
|
+
setIsJSResponder: bindings.setIsJSResponder,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Install it, if this runtime has a native module and nothing has claimed the seam already.
|
|
53
|
+
*
|
|
54
|
+
* PRECEDENCE: an installed host WINS. `installFabric()` is the only other caller of `setTreeHost`,
|
|
55
|
+
* and it puts the TypeScript applier in before any fixture can bind a slot — so a headless run that
|
|
56
|
+
* also happens to carry fake bindings must keep the applier, or the ~5 500 tests written against it
|
|
57
|
+
* would silently start driving a stub. The reverse ordering cannot occur on a device: nothing there
|
|
58
|
+
* installs a host but this.
|
|
59
|
+
*
|
|
60
|
+
* No native module is the ORDINARY answer (`native-engine.ts`'s header lists where), and it stays a
|
|
61
|
+
* quiet one here: the ops simply keep accumulating, exactly as they did before this file existed.
|
|
62
|
+
*/
|
|
63
|
+
export function installNativeTreeHost() {
|
|
64
|
+
const bindings = nativeEngine();
|
|
65
|
+
if (bindings === undefined || treeHost() !== undefined)
|
|
66
|
+
return;
|
|
67
|
+
setTreeHost(nativeTreeHost(bindings));
|
|
68
|
+
}
|