Skip to content

Commit bd5c264

Browse files
committed
fix: preserve initializer bridge state
1 parent 7242ea9 commit bd5c264

6 files changed

Lines changed: 84 additions & 42 deletions

File tree

NativeScript/ffi/shared/bridge/ClassBuilder.mm

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,19 @@ void rememberNativeApiKnownExposedMethod(
105105
return std::nullopt;
106106
}
107107

108+
std::shared_ptr<NativeApiObjectHostObject> resultHostObject;
109+
if (result.isObject()) {
110+
Object resultObjectValue = result.asObject(runtime);
111+
if (resultObjectValue.isHostObject<NativeApiObjectHostObject>(runtime)) {
112+
resultHostObject =
113+
resultObjectValue.getHostObject<NativeApiObjectHostObject>(runtime);
114+
}
115+
}
116+
117+
if (resultHostObject != nullptr && resultHostObject != receiverHostObject) {
118+
resultHostObject->detachObjectPreservingBridgeState(receiver);
119+
}
120+
108121
Value preserved(runtime, receiverValue);
109122
bridge->rememberRoundTripValue(runtime, receiver, preserved);
110123
return preserved;

NativeScript/ffi/shared/bridge/HostObjects.mm

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1044,6 +1044,24 @@ void disownObject(id expected) {
10441044
}
10451045
}
10461046

1047+
void detachObjectPreservingBridgeState(id expected) {
1048+
if (object_ != expected) {
1049+
return;
1050+
}
1051+
1052+
id object = object_;
1053+
bool releaseObject = ownsObject_;
1054+
ownsObject_ = false;
1055+
wrapperRetainedObject_ = false;
1056+
object_ = nil;
1057+
if (lifetimeState_ != nullptr) {
1058+
lifetimeState_->clear();
1059+
}
1060+
if (releaseObject && object != nil) {
1061+
[object release];
1062+
}
1063+
}
1064+
10471065
static bool isInitializerSelector(const std::string& selectorName) {
10481066
return selectorName.rfind("init", 0) == 0;
10491067
}

PROGRESS.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,43 @@ TypeScript/UI worklets.
1717

1818
## Latest Update - 2026-06-29
1919

20+
### 2026-06-29 23:30 EDT - initializer result wrapper cache preservation
21+
22+
- Scope check:
23+
- This is still RN-module branch CI stabilization after the branch split from
24+
`refactor`. The change is a generic NativeScript JS-subclass runtime fix, not
25+
a React Native Screens native implementation and not a Node-API/direct-engine
26+
backend refactor change.
27+
- Simulator-only rule remains active; no physical devices were used.
28+
- Finding:
29+
- The remaining `NSMutableArrayMethods` failure path still points at
30+
JS-backed Objective-C subclass wrapper churn around `init`/native callbacks.
31+
- Initializer self-preservation restored the original JS receiver, but the
32+
temporary native init-result wrapper could still tear down round-trip/object
33+
expando bridge state for the same native pointer when it was collected.
34+
- Change:
35+
- Added a `NativeApiObjectHostObject` detach helper for temporary initializer
36+
result wrappers. It releases that wrapper's native ownership without clearing
37+
bridge round-trip or object-expando state owned by the restored receiver.
38+
- `preservedNativeApiInitializerSelfReturn` now detaches a distinct temporary
39+
result wrapper before remembering the original receiver as the authoritative
40+
round-trip value.
41+
- Removed the temporary `NSMutableArrayMethods` JS/Objective-C trace logging
42+
from the test fixture.
43+
- Verification:
44+
- Focused runtime source guards passed:
45+
`runtime-instance-selector-base-dispatch`, `runtime-js-subclass-expando`, and
46+
`runtime-indexed-collection-alias`.
47+
- Runtime RN JS tests passed:
48+
`for f in packages/react-native/test/*.test.js; do node "$f" || exit 1; done`.
49+
- `git diff --check` passed.
50+
- `npm run check:ffi-boundaries` passed.
51+
- `npm run build:macos-cli` passed.
52+
- Still next:
53+
- Commit/push this clean runtime fix and watch fresh PR CI. If it clears the
54+
iOS simulator runtime tests, return to the dedicated RNS simulator parity
55+
sweep against original RNS.
56+
2057
### 2026-06-29 22:20 EDT - indexed collection fast enumeration for JS subclasses
2158

2259
- CI finding:

packages/react-native/test/runtime-instance-selector-base-dispatch.test.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ for (const relativePath of [
4141
assert(
4242
source.includes("receiverHostObject->object() != receiver") &&
4343
source.includes("NativeApiObjectHostObject::nativeObjectFromValue(runtime, result)") &&
44+
source.includes("resultHostObject != receiverHostObject") &&
45+
source.includes("detachObjectPreservingBridgeState(receiver)") &&
4446
source.includes("bridge->rememberRoundTripValue(runtime, receiver,") &&
4547
source.includes("return preserved;"),
4648
`${relativePath}: initializer dispatch should preserve the original JS receiver when native init returns self`,
@@ -71,4 +73,18 @@ for (const relativePath of [
7173
);
7274
}
7375

76+
for (const relativePath of [
77+
"NativeScript/ffi/shared/bridge/HostObjects.mm",
78+
"packages/react-native/native-api/ffi/shared/bridge/HostObjects.mm",
79+
]) {
80+
const source = fs.readFileSync(path.join(repoRoot, relativePath), "utf8");
81+
assert(
82+
source.includes("void detachObjectPreservingBridgeState(id expected)") &&
83+
source.includes("if (releaseObject && object != nil)") &&
84+
source.includes("[object release];") &&
85+
!source.includes("detachObjectPreservingBridgeState(id expected) {\n if (object_ == expected) {\n if (bridge_ != nullptr"),
86+
`${relativePath}: temporary initializer result wrappers should detach without clearing live receiver bridge state`,
87+
);
88+
}
89+
7490
console.log("runtime instance selector base dispatch tests passed");

test/runtime/fixtures/TNSTestNativeCallbacks.m

Lines changed: 0 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -331,44 +331,20 @@ + (void)recordsPointer:(TNSSimpleStruct*)object {
331331
}
332332

333333
+ (void)apiNSMutableArrayMethods:(NSMutableArray*)object {
334-
NSLog(@"[NSMutableArrayMethods] native enter object=%p class=%@", object, NSStringFromClass([object class]));
335-
NSLog(@"[NSMutableArrayMethods] native before add b");
336334
[object addObject:@"b"];
337-
NSLog(@"[NSMutableArrayMethods] native after add b");
338-
NSLog(@"[NSMutableArrayMethods] native before add x");
339335
[object addObject:@"x"];
340-
NSLog(@"[NSMutableArrayMethods] native after add x");
341-
NSLog(@"[NSMutableArrayMethods] native before add c");
342336
[object addObject:@"c"];
343-
NSLog(@"[NSMutableArrayMethods] native after add c");
344-
NSLog(@"[NSMutableArrayMethods] native before add y");
345337
[object addObject:@"y"];
346-
NSLog(@"[NSMutableArrayMethods] native after add y");
347-
NSLog(@"[NSMutableArrayMethods] native before add z");
348338
[object addObject:@"z"];
349-
NSLog(@"[NSMutableArrayMethods] native after add z");
350-
NSLog(@"[NSMutableArrayMethods] native before insert a");
351339
[object insertObject:@"a" atIndex:0];
352-
NSLog(@"[NSMutableArrayMethods] native after insert a");
353-
NSLog(@"[NSMutableArrayMethods] native before remove index 2");
354340
[object removeObjectAtIndex:2];
355-
NSLog(@"[NSMutableArrayMethods] native after remove index 2");
356-
NSLog(@"[NSMutableArrayMethods] native before remove last");
357341
[object removeLastObject];
358-
NSLog(@"[NSMutableArrayMethods] native after remove last");
359-
NSLog(@"[NSMutableArrayMethods] native before subscript replace");
360342
object[3] = @"d";
361-
NSLog(@"[NSMutableArrayMethods] native after subscript replace");
362-
NSLog(@"[NSMutableArrayMethods] native before count/hash");
363343
TNSLog([NSString stringWithFormat:@"%tu%tu", [object count], [object hash]]);
364-
NSLog(@"[NSMutableArrayMethods] native after count/hash");
365344

366-
NSLog(@"[NSMutableArrayMethods] native before enumeration");
367345
for (id x in object) {
368-
NSLog(@"[NSMutableArrayMethods] native enumerate %@", x);
369346
TNSLog([NSString stringWithFormat:@"%@", x]);
370347
}
371-
NSLog(@"[NSMutableArrayMethods] native exit");
372348
}
373349

374350
+ (void)apiSwizzle:(TNSSwizzleKlass*)object {

test/runtime/runner/app/tests/ApiTests.js

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -249,11 +249,8 @@ describe(module.id, function () {
249249
it("NSMutableArrayMethods", function () {
250250
var JSMutableArray = NSMutableArray.extend({
251251
init: function () {
252-
console.log("[NSMutableArrayMethods] js init enter", this.nativeAddress);
253252
var self = NSMutableArray.prototype.init.apply(this, arguments);
254-
console.log("[NSMutableArrayMethods] js init base returned", self === this, self.nativeAddress);
255253
self._array = [];
256-
console.log("[NSMutableArrayMethods] js init array ready", self._array.length);
257254
return self;
258255
},
259256
// TODO
@@ -263,40 +260,27 @@ describe(module.id, function () {
263260
// NSMutableArray.prototype.dealloc.apply(this, arguments);
264261
// },
265262
insertObjectAtIndex: function (anObject, index) {
266-
console.log("[NSMutableArrayMethods] js insert enter", anObject, index, this._array && this._array.length);
267263
this._array.splice(index, 0, anObject);
268-
console.log("[NSMutableArrayMethods] js insert exit", this._array.length);
269264
},
270265
removeObjectAtIndex: function (index) {
271-
console.log("[NSMutableArrayMethods] js remove enter", index, this._array && this._array.length);
272266
this._array.splice(index, 1);
273-
console.log("[NSMutableArrayMethods] js remove exit", this._array.length);
274267
},
275268
addObject: function (anObject) {
276-
console.log("[NSMutableArrayMethods] js add enter", anObject, this._array && this._array.length);
277269
this._array.push(anObject);
278-
console.log("[NSMutableArrayMethods] js add exit", this._array.length);
279270
},
280271
removeLastObject: function () {
281-
console.log("[NSMutableArrayMethods] js removeLast enter", this._array && this._array.length);
282272
this._array.pop();
283-
console.log("[NSMutableArrayMethods] js removeLast exit", this._array.length);
284273
},
285274
replaceObjectAtIndexWithObject: function (index, anObject) {
286-
console.log("[NSMutableArrayMethods] js replace enter", index, anObject, this._array && this._array.length);
287275
this._array[index] = anObject;
288-
console.log("[NSMutableArrayMethods] js replace exit", this._array.length);
289276
},
290277
objectAtIndex: function (index) {
291-
console.log("[NSMutableArrayMethods] js objectAtIndex", index, this._array && this._array.length);
292278
return this._array[index];
293279
},
294280
get count() {
295-
console.log("[NSMutableArrayMethods] js count", this._array && this._array.length);
296281
return this._array.length;
297282
},
298283
get hash() {
299-
console.log("[NSMutableArrayMethods] js hash enter");
300284
return this.count;
301285
}
302286
}, {
@@ -305,9 +289,7 @@ describe(module.id, function () {
305289

306290
(function () {
307291
var array = new JSMutableArray();
308-
console.log("[NSMutableArrayMethods] js before native callback", array.nativeAddress, array._array && array._array.length);
309292
TNSTestNativeCallbacks.apiNSMutableArrayMethods(array);
310-
console.log("[NSMutableArrayMethods] js after native callback", array._array && array._array.length);
311293
}());
312294
gc();
313295

0 commit comments

Comments
 (0)