Skip to content

Commit ca53096

Browse files
committed
fix: construct JS subclasses through remembered init
1 parent bd5c264 commit ca53096

3 files changed

Lines changed: 96 additions & 12 deletions

File tree

NativeScript/ffi/shared/bridge/Install.mm

Lines changed: 39 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -626,6 +626,15 @@ function unavailableInitializerError(error) {
626626
/Objective-C selector is not available/.test(String(error.message || error));
627627
}
628628
629+
function shouldUseAllocInitConstructor(constructable, wrapper) {
630+
var target = wrapper || constructable;
631+
try {
632+
return !!(target && target.__nativeApiUseAllocInitConstructor);
633+
} catch (_) {
634+
return false;
635+
}
636+
}
637+
629638
function constructNativeInstance(nativeClass, args, rememberInstance) {
630639
if (args.length === 1 &&
631640
args[0] &&
@@ -784,7 +793,8 @@ function wrapNativeClass(nativeClass) {
784793
);
785794
}
786795
}
787-
if (args.length > 0) {
796+
if (args.length > 0 ||
797+
shouldUseAllocInitConstructor(constructable, wrapper)) {
788798
return rememberInstanceClass(constructNativeInstance(nativeClass, args, rememberInstanceClass));
789799
}
790800
if (typeof nativeClass.new !== 'function') {
@@ -817,6 +827,15 @@ function rememberInstanceClass(instance) {
817827
nativeExtensionOptionsWithIterator(options, extensionMethods);
818828
var extendedNativeClass = api.__extendClass(nativeClass, extensionMethods, extendOptions);
819829
var extended = wrapNativeClass(extendedNativeClass);
830+
try {
831+
Object.defineProperty(extended, '__nativeApiUseAllocInitConstructor', {
832+
configurable: false,
833+
enumerable: false,
834+
writable: false,
835+
value: true
836+
});
837+
} catch (_) {
838+
}
820839
try {
821840
Object.setPrototypeOf(extended, wrapper || constructable);
822841
} catch (_) {
@@ -1456,17 +1475,26 @@ function materializeTypeScriptNativeClass(constructor) {
14561475
options.exposedMethods = constructor.ObjCExposedMethods;
14571476
}
14581477
1459-
var nativeBase = nativeClassLikeHandle(baseWrapper);
1460-
var extensionMethods =
1461-
nativeExtensionMethodsWithIndexedCollectionAliases(constructor.prototype || {});
1462-
options = nativeExtensionOptionsWithIterator(options, extensionMethods);
1463-
var nativeClass = api.__extendClass(nativeBase, extensionMethods, options);
1464-
var wrapper = wrapNativeClass(nativeClass);
1465-
state.wrapper = wrapper;
1478+
var nativeBase = nativeClassLikeHandle(baseWrapper);
1479+
var extensionMethods =
1480+
nativeExtensionMethodsWithIndexedCollectionAliases(constructor.prototype || {});
1481+
options = nativeExtensionOptionsWithIterator(options, extensionMethods);
1482+
var nativeClass = api.__extendClass(nativeBase, extensionMethods, options);
1483+
var wrapper = wrapNativeClass(nativeClass);
1484+
state.wrapper = wrapper;
1485+
try {
1486+
Object.defineProperty(wrapper, '__nativeApiUseAllocInitConstructor', {
1487+
configurable: false,
1488+
enumerable: false,
1489+
writable: false,
1490+
value: true
1491+
});
1492+
} catch (_) {
1493+
}
14661494
1467-
try {
1468-
Object.setPrototypeOf(constructor, wrapper);
1469-
} catch (_) {
1495+
try {
1496+
Object.setPrototypeOf(constructor, wrapper);
1497+
} catch (_) {
14701498
}
14711499
try {
14721500
api.__rememberClassWrapper(nativeClass, constructor, extensionMethods);

PROGRESS.md

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,53 @@ TypeScript/UI worklets.
1515
Objective-C/UIKit.
1616
- Track the runtime API surface in `RN_API.md`.
1717

18-
## Latest Update - 2026-06-29
18+
## Latest Update - 2026-06-30
19+
20+
### 2026-06-30 00:10 EDT - JS subclasses use remembered alloc/init construction
21+
22+
- Scope check:
23+
- This remains PR #46 RN-module branch stabilization after the split from
24+
`refactor`. The change is a generic NativeScript JS-subclass construction
25+
primitive, not a React Native Screens shim and not direct engine backend
26+
refactor work.
27+
- Simulator-only rule remains active; no physical devices were used.
28+
- CI finding:
29+
- GitHub Actions run `28418328502` on `bd5c2640` kept setup, dependency
30+
install, FFI boundary check, V8 download, libffi build, metadata
31+
generation, NativeScript build, CLI build, and macOS tests green.
32+
- iOS simulator still timed out in `ApiTests.js NSMutableArrayMethods`.
33+
- The canceled diagnostic run `28417161267` showed macOS entering JS `init`
34+
and the native callback normally, but iOS repeatedly called the JS `count`
35+
getter with missing `_array` before JS `init` or the native callback logged
36+
anything. That points to Objective-C/Foundation probing the JS-backed
37+
`NSMutableArray` subclass during zero-argument `+new`, before the JS
38+
receiver has been associated and initialized.
39+
- Change:
40+
- JS-extended native classes now opt into alloc/init construction even for
41+
zero-argument constructors. `.extend(...)` wrappers and TypeScript
42+
`NativeClass` materialization wrappers are tagged with
43+
`__nativeApiUseAllocInitConstructor`.
44+
- The generic native-class constructor now routes flagged zero-argument
45+
subclasses through `alloc`, remembers the receiver, and then invokes the
46+
selected initializer instead of raw `nativeClass.new()`.
47+
- Source coverage now guards the alloc/init flag and constructor path so this
48+
does not regress back to raw `+new` for JS-backed subclasses.
49+
- Verification:
50+
- `node packages/react-native/test/runtime-instance-selector-base-dispatch.test.js`
51+
passed.
52+
- `node packages/react-native/test/runtime-indexed-collection-alias.test.js`
53+
passed.
54+
- Runtime RN JS tests passed:
55+
`for f in packages/react-native/test/*.test.js; do node "$f" || exit 1; done`.
56+
- `git diff --check` passed.
57+
- `npm run check:ffi-boundaries` passed.
58+
- `npm run build:macos-cli` passed.
59+
- Still next:
60+
- Commit/push this alloc/init construction fix and watch fresh PR CI for the
61+
iOS simulator `ApiTests.js NSMutableArrayMethods` result.
62+
- If iOS still hangs, compare the verbose spec/log output: if JS `init` now
63+
appears before the first missing `_array` count call, the next fix should be
64+
a generic construction-state callback policy rather than an RNS workaround.
1965

2066
### 2026-06-29 23:30 EDT - initializer result wrapper cache preservation
2167

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,16 @@ for (const relativePath of [
2626
source.includes(": selectorFunction"),
2727
`${relativePath}: class selector wrappers should keep base invocation support while instance selectors use engine dispatch`,
2828
);
29+
const allocInitFlagDefinitions =
30+
source.match(/Object\.defineProperty\([^,]+, '__nativeApiUseAllocInitConstructor'/g) || [];
31+
assert(
32+
source.includes("function shouldUseAllocInitConstructor(constructable, wrapper)") &&
33+
source.includes("target.__nativeApiUseAllocInitConstructor") &&
34+
source.includes("args.length > 0 ||") &&
35+
source.includes("shouldUseAllocInitConstructor(constructable, wrapper)") &&
36+
allocInitFlagDefinitions.length >= 2,
37+
`${relativePath}: JS-extended native classes should use alloc/init construction so receivers are remembered before init dispatch`,
38+
);
2939
}
3040

3141
for (const relativePath of [

0 commit comments

Comments
 (0)