@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.
Files changed (91) hide show
  1. package/README.md +39 -14
  2. package/android/CMakeLists.txt +51 -0
  3. package/android/build.gradle +90 -0
  4. package/android/src/main/AndroidManifest.xml +1 -0
  5. package/android/src/main/cpp/SymbioteEngineJni.cpp +72 -0
  6. package/android/src/main/java/dev/symbiotenative/engine/SymbioteEngineModule.kt +43 -0
  7. package/android/src/main/java/dev/symbiotenative/engine/SymbioteEnginePackage.kt +35 -0
  8. package/build/accessibility-info/shared.js +1 -1
  9. package/build/accessibility-props.d.ts +1 -8
  10. package/build/accessibility-props.js +13 -16
  11. package/build/animated/animations/composition.d.ts +1 -1
  12. package/build/animated/animations/composition.js +18 -4
  13. package/build/animated/easing.d.ts +3 -2
  14. package/build/animated/easing.js +17 -88
  15. package/build/animated/event.js +6 -1
  16. package/build/animated/host-binding.d.ts +1 -1
  17. package/build/animated/host-binding.js +19 -4
  18. package/build/animated/index.d.ts +1 -1
  19. package/build/animated/mock.d.ts +1 -19
  20. package/build/animated/props.js +1 -1
  21. package/build/animated/rgba.js +16 -50
  22. package/build/events/index.js +88 -40
  23. package/build/fabric-props.d.ts +1 -1
  24. package/build/fabric-props.js +116 -184
  25. package/build/fabric.d.ts +9 -0
  26. package/build/fabric.js +32 -0
  27. package/build/host-access.d.ts +145 -0
  28. package/build/host-access.js +315 -0
  29. package/build/host-behavior.d.ts +84 -21
  30. package/build/host-behavior.js +236 -51
  31. package/build/image-source-write.d.ts +16 -0
  32. package/build/image-source-write.js +65 -0
  33. package/build/imperative.d.ts +49 -0
  34. package/build/imperative.js +258 -0
  35. package/build/index.d.ts +14 -7
  36. package/build/index.js +53 -10
  37. package/build/mutation-buffer.d.ts +238 -0
  38. package/build/mutation-buffer.js +513 -0
  39. package/build/native-engine.d.ts +185 -0
  40. package/build/native-engine.js +182 -0
  41. package/build/native-tree-host.d.ts +25 -0
  42. package/build/native-tree-host.js +68 -0
  43. package/build/node.d.ts +195 -58
  44. package/build/node.js +852 -383
  45. package/build/pan-responder/index.js +27 -52
  46. package/build/platform-color/index.d.ts +1 -1
  47. package/build/platform-color/index.js +11 -4
  48. package/build/process-background-image/index.js +30 -566
  49. package/build/process-background-longhands.d.ts +4 -0
  50. package/build/process-background-longhands.js +44 -0
  51. package/build/process-box-shadow/index.js +23 -187
  52. package/build/process-filter.js +27 -300
  53. package/build/process-transform/index.d.ts +1 -1
  54. package/build/process-transform/index.js +25 -107
  55. package/build/process-transform-origin/index.d.ts +1 -1
  56. package/build/process-transform-origin/index.js +29 -102
  57. package/build/registry.d.ts +36 -0
  58. package/build/registry.js +73 -0
  59. package/build/sound-manager/index.d.ts +3 -0
  60. package/build/sound-manager/index.js +36 -0
  61. package/build/structured-style.d.ts +10 -0
  62. package/build/structured-style.js +180 -0
  63. package/build/style-registry/index.d.ts +14 -0
  64. package/build/style-registry/index.js +60 -11
  65. package/build/surface.d.ts +31 -2
  66. package/build/surface.js +138 -56
  67. package/build/text-input-state.d.ts +1 -0
  68. package/build/text-input-state.js +17 -3
  69. package/build/tree-host.d.ts +322 -0
  70. package/build/tree-host.js +211 -0
  71. package/build/view-config.js +4 -4
  72. package/codegen-specs/NativeSymbioteEngine.ts +27 -0
  73. package/cpp/SymbioteDebug.cpp +51 -0
  74. package/cpp/SymbioteDebug.h +54 -0
  75. package/cpp/SymbioteEngineBindings.cpp +234 -0
  76. package/cpp/SymbioteEngineBindings.h +59 -0
  77. package/cpp/SymbioteFabricProps.cpp +2619 -0
  78. package/cpp/SymbioteFabricProps.h +223 -0
  79. package/cpp/SymbioteTree.cpp +2593 -0
  80. package/cpp/SymbioteTree.h +294 -0
  81. package/ios/SymbioteEngineModule.h +25 -0
  82. package/ios/SymbioteEngineModule.mm +44 -0
  83. package/package.json +31 -3
  84. package/react-native.config.cjs +23 -0
  85. package/symbiote-engine.podspec +42 -0
  86. package/build/animated/bezier.d.ts +0 -1
  87. package/build/animated/bezier.js +0 -102
  88. package/build/commit.d.ts +0 -49
  89. package/build/commit.js +0 -1058
  90. package/build/tags.d.ts +0 -2
  91. package/build/tags.js +0 -40
@@ -0,0 +1,54 @@
1
+ #pragma once
2
+
3
+ #include <string>
4
+ #include <vector>
5
+
6
+ // Diagnostic logging for the C++ half of the engine — the twin of `core/engine/src/debug.ts`, and
7
+ // until 2026-09-18 it did not exist at all.
8
+ //
9
+ // WHY IT HAD TO. The commit path, the payload builder and every tag rule live here now, and this
10
+ // translation unit's only way to say anything was `throw jsi::JSError` — so a C++ rule could crash
11
+ // or stay silent, with nothing in between. `<keep_logs_gate_behind_DEBUG>` asks new code with
12
+ // non-trivial runtime behavior to log at its seam as a matter of course; that was unsatisfiable on
13
+ // this side of the wire, and the first rule that wanted a developer WARNING rather than a crash
14
+ // (ScrollView's ignored `horizontal`) is what made the gap block a port.
15
+ //
16
+ // THE SWITCH IS THE SAME ONE, pushed down rather than re-invented: `DEBUG=1` in the environment or
17
+ // `globalThis.__SYMBIOTE_DEBUG__` in JS. `installBindings` reads both at install, and
18
+ // `setDebugEnabled` is exposed so a later JS toggle reaches this side too — otherwise the runtime
19
+ // escape hatch that exists for hosts where the env is unreachable would silently stop at the
20
+ // boundary.
21
+ //
22
+ // THE MESSAGE IS BUILT ONLY WHEN THE SWITCH IS ON, and the macro is what guarantees it. C++ has the
23
+ // same trap the JS module's header describes: an argument is evaluated at the CALL SITE, so a
24
+ // `debugLog("x " + std::to_string(y))` on the per-node commit path costs its concatenation whether
25
+ // or not anything is listening. `SYMBIOTE_DLOG` tests the flag first, so the expression is not
26
+ // evaluated at all when logging is off — which makes the cheap thing the DEFAULT rather than a rule
27
+ // every call site has to remember. Call `debugLog` directly only where the argument is already a
28
+ // built string.
29
+ //
30
+ // WHERE IT GOES. stderr, so a device build surfaces it in the Xcode console and in logcat without
31
+ // any bridge of its own. It is also RETAINED while the switch is on, which is what makes a log
32
+ // assertable from a test rather than merely visible to a human — `takeDebugLog` drains it. Nothing
33
+ // is retained while the switch is off, because nothing is called.
34
+
35
+ namespace symbiote {
36
+
37
+ /** One relaxed atomic read. The gate on every call site; see `SYMBIOTE_DLOG`. */
38
+ bool debugEnabled();
39
+
40
+ void setDebugEnabled(bool enabled);
41
+
42
+ /** Prefixed, written to stderr, and retained for `takeDebugLog`. */
43
+ void debugLog(const std::string &message);
44
+
45
+ /** Drains the retained lines. The test-facing read, and the reason retention exists. */
46
+ std::vector<std::string> takeDebugLog();
47
+
48
+ } // namespace symbiote
49
+
50
+ // Guards BEFORE evaluating, so building the message costs nothing with logging off.
51
+ #define SYMBIOTE_DLOG(expr) \
52
+ do { \
53
+ if (::symbiote::debugEnabled()) ::symbiote::debugLog((expr)); \
54
+ } while (false)
@@ -0,0 +1,234 @@
1
+ #include "SymbioteEngineBindings.h"
2
+
3
+ #include "SymbioteDebug.h"
4
+ #include "SymbioteTree.h"
5
+
6
+ #include <react/renderer/mounting/ShadowTreeRegistry.h>
7
+ #include <react/renderer/uimanager/UIManager.h>
8
+ #include <react/renderer/uimanager/UIManagerBinding.h>
9
+
10
+ #include <cmath>
11
+ #include <cstdint>
12
+ #include <cstdlib>
13
+ #include <memory>
14
+ #include <string>
15
+ #include <vector>
16
+
17
+ namespace symbiote {
18
+
19
+ using namespace facebook;
20
+
21
+ namespace {
22
+
23
+ /**
24
+ * A block of `int32_t` owned by native and handed to JS as the backing store of an `Int32Array`.
25
+ *
26
+ * The node table's parent edge is already a flat `Int32Array` in JS (item 8b); the endpoint is that
27
+ * the SAME array is memory the native applier writes. A `MutableBuffer`-backed `jsi::ArrayBuffer` is
28
+ * the only JSI shape that gives JS a VIEW instead of a copy — every other route costs a crossing per
29
+ * element, and the census that sized this design counted ~4 000 reads for a two-row swap.
30
+ *
31
+ * Note what this does and does not fork. `node-table.ts` is byte-identical whichever store it gets:
32
+ * one implementation, two allocators. That is why the JS store may stand indefinitely, while the JS
33
+ * APPLIER may not — see `native-engine.ts`'s header for the distinction, which is load-bearing.
34
+ *
35
+ * Lifetime is the ArrayBuffer's: `jsi::ArrayBuffer` retains the `shared_ptr`, so the store outlives
36
+ * every JS view of it and is freed with the last one. A registry of live stores would be the leak
37
+ * rather than the fix.
38
+ */
39
+ class Int32Store : public jsi::MutableBuffer {
40
+ public:
41
+ explicit Int32Store(size_t lengthInInts) : data_(lengthInInts, 0) {}
42
+
43
+ size_t size() const override {
44
+ return data_.size() * sizeof(int32_t);
45
+ }
46
+
47
+ uint8_t *data() override {
48
+ return reinterpret_cast<uint8_t *>(data_.data());
49
+ }
50
+
51
+ private:
52
+ std::vector<int32_t> data_;
53
+ };
54
+
55
+ jsi::Value allocInt32Array(
56
+ jsi::Runtime &runtime,
57
+ const jsi::Value & /*thisValue*/,
58
+ const jsi::Value *arguments,
59
+ size_t count) {
60
+ if (count < 1 || !arguments[0].isNumber()) {
61
+ throw jsi::JSError(runtime, "allocInt32Array expects a length in elements");
62
+ }
63
+
64
+ const double requested = arguments[0].getNumber();
65
+ // A negative or fractional length reaches `std::vector` as a wildly wrong `size_t`. Reject it here,
66
+ // where there is a message, rather than at the allocation, where the failure is a bare crash.
67
+ if (!(requested >= 0) || requested != std::floor(requested)) {
68
+ throw jsi::JSError(runtime, "allocInt32Array expects a non-negative integer length");
69
+ }
70
+
71
+ auto store = std::make_shared<Int32Store>(static_cast<size_t>(requested));
72
+ auto buffer = jsi::ArrayBuffer(runtime, store);
73
+ auto constructor = runtime.global().getPropertyAsFunction(runtime, "Int32Array");
74
+ return constructor.callAsConstructor(runtime, std::move(buffer));
75
+ }
76
+
77
+ /**
78
+ * How many shadow trees the real `UIManager` is holding — or -1 when we could not reach one.
79
+ *
80
+ * This is item 8c-1's bring-up probe and it is deliberately NOT a commit hook. What it retires is
81
+ * the whole reach chain in one device run: that our pod compiles against ReactCommon's renderer
82
+ * headers, that it LINKS against the prebuilt `React.xcframework`, and that a plain JSI runtime
83
+ * handed to a third-party TurboModule can resolve the UIManager with no app-side wiring. Every one
84
+ * of those is a hard blocker for the native applier and none of them is visible from JS.
85
+ *
86
+ * -1 and 0 are different answers and that is the point. `getBinding` reads
87
+ * `global.nativeFabricUIManager` and returns null when it is absent, so -1 means the ORDERING is
88
+ * wrong — we were created before Fabric installed its binding. 0 means we reached a real UIManager
89
+ * that happens to hold no surface. A boolean would collapse the two and cost a second build.
90
+ *
91
+ * The ordering is in fact safe by construction, and this probe is what confirms it rather than
92
+ * assumes it: `getSlot()` (fabric.ts) reads `globalThis.nativeFabricUIManager` and throws when it is
93
+ * absent, and only THEN resolves our module — so by the time this file's install hook runs, the
94
+ * binding `getBinding` looks for is guaranteed to be there. That was luck when 8c-0 chose the seam.
95
+ */
96
+ jsi::Value probeUIManager(
97
+ jsi::Runtime &runtime,
98
+ const jsi::Value & /*thisValue*/,
99
+ const jsi::Value * /*arguments*/,
100
+ size_t /*count*/) {
101
+ auto binding = react::UIManagerBinding::getBinding(runtime);
102
+ if (binding == nullptr) {
103
+ return jsi::Value(-1.0);
104
+ }
105
+
106
+ int surfaces = 0;
107
+ binding->getUIManager().getShadowTreeRegistry().enumerate(
108
+ [&surfaces](const react::ShadowTree & /*shadowTree*/, bool & /*stop*/) { surfaces += 1; });
109
+ return jsi::Value(static_cast<double>(surfaces));
110
+ }
111
+
112
+ } // namespace
113
+
114
+ void installBindings(jsi::Runtime &runtime) {
115
+ auto bindings = jsi::Object(runtime);
116
+
117
+ // THE SWITCH, pushed down from the same two places `debug.ts` reads. Done at install so a host
118
+ // that sets `__SYMBIOTE_DEBUG__` in its bootstrap (every example's `index.js` does) gets the C++
119
+ // half armed without calling anything; `setDebugEnabled` below is what a LATER toggle uses, since
120
+ // this read happens exactly once.
121
+ const auto flag = runtime.global().getProperty(runtime, "__SYMBIOTE_DEBUG__");
122
+ const char *const env = std::getenv("DEBUG");
123
+ setDebugEnabled(
124
+ (flag.isBool() && flag.getBool()) ||
125
+ (env != nullptr && std::string(env) == "1"));
126
+
127
+ bindings.setProperty(
128
+ runtime,
129
+ "setDebugEnabled",
130
+ jsi::Function::createFromHostFunction(
131
+ runtime,
132
+ jsi::PropNameID::forAscii(runtime, "setDebugEnabled"),
133
+ 1,
134
+ [](jsi::Runtime & /*rt*/, const jsi::Value & /*thisVal*/, const jsi::Value *args, size_t count)
135
+ -> jsi::Value {
136
+ if (count > 0 && args[0].isBool()) setDebugEnabled(args[0].getBool());
137
+ return jsi::Value::undefined();
138
+ }));
139
+
140
+ // Drains what the C++ side logged. The reason the lines are retained at all: a diagnostic nobody
141
+ // can assert on is a diagnostic that rots — this is what lets a test say "the engine warned"
142
+ // rather than a human noticing a line scroll past.
143
+ bindings.setProperty(
144
+ runtime,
145
+ "takeDebugLog",
146
+ jsi::Function::createFromHostFunction(
147
+ runtime,
148
+ jsi::PropNameID::forAscii(runtime, "takeDebugLog"),
149
+ 0,
150
+ [](jsi::Runtime &rt, const jsi::Value & /*thisVal*/, const jsi::Value * /*args*/, size_t /*count*/)
151
+ -> jsi::Value {
152
+ const std::vector<std::string> lines = takeDebugLog();
153
+ auto out = jsi::Array(rt, lines.size());
154
+ for (size_t at = 0; at < lines.size(); at += 1)
155
+ out.setValueAtIndex(rt, at, jsi::String::createFromUtf8(rt, lines[at]));
156
+ return out;
157
+ }));
158
+
159
+ bindings.setProperty(runtime, "version", jsi::Value(kNativeVersion));
160
+ bindings.setProperty(
161
+ runtime,
162
+ "allocInt32Array",
163
+ jsi::Function::createFromHostFunction(
164
+ runtime, jsi::PropNameID::forAscii(runtime, "allocInt32Array"), 1, allocInt32Array));
165
+ bindings.setProperty(
166
+ runtime,
167
+ "probeUIManager",
168
+ jsi::Function::createFromHostFunction(
169
+ runtime, jsi::PropNameID::forAscii(runtime, "probeUIManager"), 0, probeUIManager));
170
+
171
+ // One tree per runtime, and it holds NOTHING — a node's owner is the JS placeholder object,
172
+ // through `NativeState`. So this instance exists only to give the methods a `this` to hang off,
173
+ // and a per-call one would work identically. It is shared because an earlier version DID hold a
174
+ // table, and keeping the shape makes the diff that removed it readable.
175
+ auto tree = std::make_shared<Tree>();
176
+
177
+ // Installed by name rather than through a switch so a JS caller's mistake is `undefined is not a
178
+ // function` at the call site, instead of a runtime string comparison failing somewhere inside C++.
179
+ // `isBindings` in `native-engine.ts` checks these names one by one for the same reason.
180
+ const auto install = [&](const char *name,
181
+ unsigned int arity,
182
+ jsi::Value (Tree::*method)(jsi::Runtime &, const jsi::Value *, size_t)) {
183
+ bindings.setProperty(
184
+ runtime,
185
+ name,
186
+ jsi::Function::createFromHostFunction(
187
+ runtime,
188
+ jsi::PropNameID::forAscii(runtime, name),
189
+ arity,
190
+ [tree, method](
191
+ jsi::Runtime &rt,
192
+ const jsi::Value & /*thisValue*/,
193
+ const jsi::Value *arguments,
194
+ size_t count) { return (tree.get()->*method)(rt, arguments, count); }));
195
+ };
196
+
197
+ // The one member on a commit path.
198
+ install("applyOps", 5, &Tree::applyOps);
199
+
200
+ // The reads — value and structural — and the imperative six. All of them take the same placeholder
201
+ // object `applyOps` attached the node to, and none is on a commit path: they run at gesture or
202
+ // lifecycle rate. `census` is deliberately absent; see `native-tree-host.ts`.
203
+ install("getProp", 2, &Tree::getProp);
204
+ install("getProps", 1, &Tree::getProps);
205
+ install("markPropsDirty", 1, &Tree::markPropsDirty);
206
+ install("getViewName", 1, &Tree::getViewName);
207
+ install("parentOf", 1, &Tree::parentOf);
208
+ install("childrenOf", 1, &Tree::childrenOf);
209
+ install("firstChildOf", 1, &Tree::firstChildOf);
210
+ install("nextSiblingOf", 1, &Tree::nextSiblingOf);
211
+ install("parentsOf", 1, &Tree::parentsOf);
212
+ install("subtreesOf", 1, &Tree::subtreesOf);
213
+ install("teardownSubtreesOf", 1, &Tree::teardownSubtreesOf);
214
+ install("ancestorsOf", 1, &Tree::ancestorsOf);
215
+ install("committedRecordOf", 1, &Tree::committedRecordOf);
216
+ // A TEST read, and it is on this list rather than behind a build flag because the bag it returns
217
+ // is already retained per node for diffing — see `Tree::committedPayloadOf` for why the complete
218
+ // props read-back cannot come from React Native's own `getDebugProps` or `rawProps`.
219
+ install("committedPayloadOf", 1, &Tree::committedPayloadOf);
220
+
221
+ install("dispatchCommand", 3, &Tree::dispatchCommand);
222
+ install("sendAccessibilityEvent", 2, &Tree::sendAccessibilityEvent);
223
+ install("measure", 2, &Tree::measure);
224
+ install("measureInWindow", 2, &Tree::measureInWindow);
225
+ install("measureLayout", 4, &Tree::measureLayout);
226
+ install("setIsJSResponder", 3, &Tree::setIsJSResponder);
227
+
228
+ // Diagnostic, read on demand rather than per op. See `Tree::readSurfaceTelemetry`.
229
+ install("readSurfaceTelemetry", 1, &Tree::readSurfaceTelemetry);
230
+
231
+ runtime.global().setProperty(runtime, "__symbioteEngineNative", std::move(bindings));
232
+ }
233
+
234
+ } // namespace symbiote
@@ -0,0 +1,59 @@
1
+ // The engine's native half, PLATFORM-SHARED. Every line that does real work lives here; the per-
2
+ // platform files under `ios/` (and, when item 8c-1 lands, `android/`) do nothing but register a
3
+ // module and call `installBindings`.
4
+ //
5
+ // That split is not tidiness, it is the arithmetic that decides whether Android is affordable.
6
+ // React Native's own equivalent — `AnimatedModule`, which reaches the runtime through the same
7
+ // `TurboModuleWithJSIBindings` hook and applies its per-frame edits with `ShadowNode::cloneMultiple`
8
+ // inside a registered commit hook — is a pure C++ TurboModule in `ReactCommon/react/renderer/`, with
9
+ // no platform directory at all. Every one of the eight `registerCommitHook` call sites in RN is
10
+ // likewise shared code. So the applier this file will eventually carry is ONE implementation for both
11
+ // platforms, and Android's cost is a JNI registration shim rather than a second applier.
12
+ //
13
+ // Why we still need a platform shim at all, when RN's own modules do not: RN registers its C++
14
+ // modules from inside its own TurboModule manager. A third-party package has no such hook in 0.86 —
15
+ // the codegen generator has no `cxxModules` path — so the module must be an ObjC class on iOS and a
16
+ // Java/JNI one on Android. That shim is the only thing that is written twice.
17
+
18
+ #pragma once
19
+
20
+ #include <jsi/jsi.h>
21
+
22
+ namespace symbiote {
23
+
24
+ /**
25
+ * The ABI the JS side checks before it will use any of this.
26
+ *
27
+ * The two halves ship as separate artefacts — a CocoaPods sandbox and an npm package — with separate
28
+ * install steps, so an `npm install` without a `pod install` leaves them disagreeing. That is the
29
+ * ordinary state of this repo's local-dev loop, not an exotic one, which is why the version is a
30
+ * gate rather than a diagnostic.
31
+ *
32
+ * 1 -> 2: nodes stopped being addressed by an integer id out of a C++ table and became JS objects
33
+ * carrying their own `shared_ptr` as `NativeState`. Every method NAME survived that, so the JS
34
+ * shape guard cannot see the difference — only this number can. A v1 pod given a v2 batch ignores
35
+ * the `handles` argument and commits nothing; a v2 pod given v1 calls reads an integer as an object.
36
+ * Must move with `SUPPORTED_NATIVE_VERSION` in `core/engine/src/native-engine.ts`.
37
+ *
38
+ * 3 -> 4: the bindings stopped replaying FABRIC operations (`Applier`) and started owning the shadow
39
+ * tree (`Tree`). The three structural reads are new NAMES, which the JS shape guard would refuse on
40
+ * its own — but `applyOps` went from six arguments to five under the SAME name, and a v3 binary
41
+ * reads argument 1 as an `Int32Array` of child ids where JS now passes a string table. That read
42
+ * succeeds against the wrong object and commits a wrong tree rather than throwing, which is exactly
43
+ * the class this number exists to refuse. A refusal by luck, on the reads, is not a refusal.
44
+ *
45
+ * It does NOT move for a diagnostic: `readSurfaceTelemetry` changes no layout and no calling
46
+ * convention, and JS reads it optionally, so a pod without it degrades to "no answer" rather than
47
+ * throwing. Same reasoning `native-engine.ts` records for `probeUIManager`.
48
+ */
49
+ constexpr double kNativeVersion = 4;
50
+
51
+ /**
52
+ * Install `global.__symbioteEngineNative`.
53
+ *
54
+ * Called once, from whichever platform shim got hold of the runtime. Everything JS can reach on the
55
+ * native side goes through the object this installs.
56
+ */
57
+ void installBindings(facebook::jsi::Runtime &runtime);
58
+
59
+ } // namespace symbiote