Skip to content

Commit 3851614

Browse files
committed
test(ffi): add React Native JSI compatibility suite
1 parent 9a4fd00 commit 3851614

9 files changed

Lines changed: 996 additions & 61 deletions

File tree

NativeScript/ffi/jsi/NativeApiJsi.mm

Lines changed: 266 additions & 53 deletions
Large diffs are not rendered by default.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
"build-node-api": "./scripts/build_all_react_native.sh",
3030
"build-rn-turbomodule": "./scripts/build_react_native_turbomodule.sh",
3131
"test-rn-turbomodule": "./scripts/test_react_native_turbomodule.sh",
32+
"test-rn-ffi": "./scripts/test_react_native_ffi_compat.sh",
3233
"demo-rn-turbomodule": "./scripts/create_react_native_demo.sh",
3334
"pack:ios": "./scripts/build_npm_ios.sh",
3435
"pack:macos": "./scripts/build_npm_macos.sh",

packages/react-native/ios/NativeScriptNativeApiModule.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ class NativeScriptNativeApiModule
1818
bool isInstalled(jsi::Runtime& runtime);
1919
std::string defaultMetadataPath(jsi::Runtime& runtime);
2020
std::string getRuntimeBackend(jsi::Runtime& runtime);
21+
bool __writeTestMarker(jsi::Runtime& runtime, std::string content);
2122

2223
private:
2324
std::shared_ptr<CallInvoker> jsInvoker_;

packages/react-native/ios/NativeScriptNativeApiModule.mm

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,32 @@ void writeSmokeMarkerIfRequested(const char* stage) {
6262
[content writeToFile:path atomically:YES encoding:NSUTF8StringEncoding error:nil];
6363
}
6464

65+
bool writeSmokeMarkerContentIfRequested(const std::string& content) {
66+
const char* enabled = getenv("NATIVESCRIPT_RN_TURBO_SMOKE_MARKER");
67+
if (enabled == nullptr || enabled[0] == '\0') {
68+
return false;
69+
}
70+
71+
NSString* path = [NSTemporaryDirectory()
72+
stringByAppendingPathComponent:@"NativeScriptNativeApiSmoke.marker"];
73+
NSString* nativeContent =
74+
[[NSString alloc] initWithBytes:content.data()
75+
length:content.size()
76+
encoding:NSUTF8StringEncoding];
77+
if (nativeContent == nil) {
78+
nativeContent = @"";
79+
}
80+
81+
BOOL ok = [nativeContent writeToFile:path
82+
atomically:YES
83+
encoding:NSUTF8StringEncoding
84+
error:nil];
85+
#if !__has_feature(objc_arc)
86+
[nativeContent release];
87+
#endif
88+
return ok == YES;
89+
}
90+
6591
} // namespace
6692

6793
namespace facebook::react {
@@ -96,4 +122,9 @@ void writeSmokeMarkerIfRequested(const char* stage) {
96122
return "hermes-jsi";
97123
}
98124

125+
bool NativeScriptNativeApiModule::__writeTestMarker(jsi::Runtime&,
126+
std::string content) {
127+
return writeSmokeMarkerContentIfRequested(content);
128+
}
129+
99130
} // namespace facebook::react

packages/react-native/src/NativeScriptNativeApi.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export interface Spec extends TurboModule {
66
readonly isInstalled: () => boolean;
77
readonly defaultMetadataPath: () => string;
88
readonly getRuntimeBackend: () => string;
9+
readonly __writeTestMarker: (content: string) => boolean;
910
}
1011

1112
export default TurboModuleRegistry.getEnforcing<Spec>('NativeScriptNativeApi');

packages/react-native/src/index.d.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,19 @@ export type NativeApiHost = {
99
constants?: number;
1010
protocols?: number;
1111
enums?: number;
12+
structs?: number;
13+
unions?: number;
1214
classNames?: () => string[];
1315
functionNames?: () => string[];
1416
constantNames?: () => string[];
17+
protocolNames?: () => string[];
1518
enumNames?: () => string[];
19+
structNames?: () => string[];
20+
unionNames?: () => string[];
1621
};
22+
getProtocol?: (name: string) => unknown;
23+
getStruct?: (name: string) => unknown;
24+
getUnion?: (name: string) => unknown;
1725
runOnUI?: (callback?: () => void) => Promise<void>;
1826
[name: string]: unknown;
1927
};

packages/react-native/src/index.ts

Lines changed: 152 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,14 @@ type NativeApiHost = {
55
classNames?: () => string[];
66
functionNames?: () => string[];
77
constantNames?: () => string[];
8+
protocolNames?: () => string[];
89
enumNames?: () => string[];
10+
structNames?: () => string[];
11+
unionNames?: () => string[];
912
};
13+
getProtocol?: (name: string) => unknown;
14+
getStruct?: (name: string) => unknown;
15+
getUnion?: (name: string) => unknown;
1016
runOnUI?: (callback?: () => void) => Promise<void>;
1117
[name: string]: unknown;
1218
};
@@ -34,24 +40,131 @@ function requireNativeApiHost(): NativeApiHost {
3440
function defineLazyNativeGlobal(
3541
name: string,
3642
resolve: (name: string) => unknown,
43+
force = false,
3744
) {
38-
if (!name || name in globalThis) {
45+
if (!name || (!force && Object.prototype.hasOwnProperty.call(globalThis, name))) {
3946
return;
4047
}
4148

42-
Object.defineProperty(globalThis, name, {
43-
configurable: true,
44-
enumerable: false,
45-
get() {
46-
const value = resolve(name);
49+
try {
50+
Object.defineProperty(globalThis, name, {
51+
configurable: true,
52+
enumerable: false,
53+
get() {
54+
const value = resolve(name);
55+
Object.defineProperty(globalThis, name, {
56+
configurable: true,
57+
enumerable: false,
58+
writable: false,
59+
value,
60+
});
61+
return value;
62+
},
63+
});
64+
} catch {
65+
const value = resolve(name);
66+
if (value !== undefined) {
4767
Object.defineProperty(globalThis, name, {
4868
configurable: true,
4969
enumerable: false,
5070
writable: false,
5171
value,
5272
});
53-
return value;
54-
},
73+
}
74+
}
75+
}
76+
77+
function wrapAggregateConstructor(nativeConstructor: unknown): unknown {
78+
if (typeof nativeConstructor !== 'function') {
79+
return nativeConstructor;
80+
}
81+
82+
const aggregate = function NativeScriptAggregate(initialValue?: unknown) {
83+
return nativeConstructor(initialValue);
84+
};
85+
86+
for (const key of ['kind', 'runtimeName', 'metadataOffset', 'sizeof', 'fields']) {
87+
try {
88+
Object.defineProperty(aggregate, key, {
89+
configurable: true,
90+
enumerable: false,
91+
writable: false,
92+
value: (nativeConstructor as Record<string, unknown>)[key],
93+
});
94+
} catch {
95+
// Best effort metadata copy for runtimes with stricter function objects.
96+
}
97+
}
98+
99+
return aggregate;
100+
}
101+
102+
function wrapInteropFactory(
103+
nativeFactory: unknown,
104+
properties: Record<string, unknown>,
105+
): unknown {
106+
if (typeof nativeFactory !== 'function') {
107+
return nativeFactory;
108+
}
109+
110+
if ((nativeFactory as Record<string, unknown>).__nativeScriptConstructable) {
111+
return nativeFactory;
112+
}
113+
114+
const constructable = function NativeScriptInteropValue(...args: unknown[]) {
115+
return (nativeFactory as (...args: unknown[]) => unknown)(...args);
116+
};
117+
118+
for (const [key, value] of Object.entries(properties)) {
119+
try {
120+
Object.defineProperty(constructable, key, {
121+
configurable: true,
122+
enumerable: false,
123+
writable: false,
124+
value,
125+
});
126+
} catch {
127+
// Best effort metadata copy for runtimes with stricter function objects.
128+
}
129+
}
130+
131+
Object.defineProperty(constructable, '__nativeScriptConstructable', {
132+
configurable: false,
133+
enumerable: false,
134+
writable: false,
135+
value: true,
136+
});
137+
138+
return constructable;
139+
}
140+
141+
function installInteropConstructors(): void {
142+
const interop = (globalThis as Record<string, unknown>).interop as
143+
| Record<string, unknown>
144+
| undefined;
145+
if (!interop || typeof interop !== 'object') {
146+
return;
147+
}
148+
149+
const sizeof = interop.sizeof;
150+
const pointerType = (interop.types as Record<string, unknown> | undefined)
151+
?.pointer;
152+
let pointerSize: unknown = undefined;
153+
if (typeof sizeof === 'function' && pointerType !== undefined) {
154+
try {
155+
pointerSize = sizeof(pointerType);
156+
} catch {
157+
pointerSize = undefined;
158+
}
159+
}
160+
161+
interop.Pointer = wrapInteropFactory(interop.Pointer, {
162+
kind: 'pointer',
163+
sizeof: pointerSize,
164+
});
165+
interop.Reference = wrapInteropFactory(interop.Reference, {
166+
kind: 'reference',
167+
sizeof: pointerSize,
55168
});
56169
}
57170

@@ -76,11 +189,39 @@ export function installGlobals(): boolean {
76189
defineLazyNativeGlobal(name, (constantName) => api[constantName]);
77190
}
78191

192+
const protocolNames = api.metadata?.protocolNames?.() ?? [];
193+
for (const name of protocolNames) {
194+
defineLazyNativeGlobal(
195+
name,
196+
(protocolName) => api.getProtocol?.(protocolName) ?? api[protocolName],
197+
);
198+
}
199+
79200
const enumNames = api.metadata?.enumNames?.() ?? [];
80201
for (const name of enumNames) {
81202
defineLazyNativeGlobal(name, (enumName) => api[enumName]);
82203
}
83204

205+
const structNames = api.metadata?.structNames?.() ?? [];
206+
for (const name of structNames) {
207+
defineLazyNativeGlobal(
208+
name,
209+
(structName) =>
210+
wrapAggregateConstructor(api.getStruct?.(structName) ?? api[structName]),
211+
true,
212+
);
213+
}
214+
215+
const unionNames = api.metadata?.unionNames?.() ?? [];
216+
for (const name of unionNames) {
217+
defineLazyNativeGlobal(
218+
name,
219+
(unionName) =>
220+
wrapAggregateConstructor(api.getUnion?.(unionName) ?? api[unionName]),
221+
true,
222+
);
223+
}
224+
84225
return true;
85226
}
86227

@@ -89,6 +230,9 @@ export function init(
89230
options: InstallOptions = {},
90231
): boolean {
91232
const installed = NativeScriptNativeApi.install(metadataPath);
233+
if (installed) {
234+
installInteropConstructors();
235+
}
92236
if (installed && options.globals !== false) {
93237
installGlobals();
94238
}

0 commit comments

Comments
 (0)