@react-native-native/nativ-fabric 0.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/NativFabric.podspec +41 -0
- package/android/build.gradle +128 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/cpp/CMakeLists.txt +59 -0
- package/android/src/main/cpp/NativBindingsInstaller.cpp +393 -0
- package/android/src/main/cpp/NativRuntime.cpp +508 -0
- package/android/src/main/java/com/nativfabric/ComposeHost.kt +26 -0
- package/android/src/main/java/com/nativfabric/NativContainerPackage.kt +35 -0
- package/android/src/main/java/com/nativfabric/NativContainerView.kt +51 -0
- package/android/src/main/java/com/nativfabric/NativContainerViewManager.kt +62 -0
- package/android/src/main/java/com/nativfabric/NativRuntime.kt +201 -0
- package/android/src/main/java/com/nativfabric/NativRuntimeModule.kt +37 -0
- package/android/src/main/java/com/nativfabric/compose/ComposeWrappers.kt +45 -0
- package/app.plugin.js +159 -0
- package/expo-module.config.json +6 -0
- package/ios/NativContainerComponentView.mm +137 -0
- package/ios/NativRuntime.h +36 -0
- package/ios/NativRuntime.mm +549 -0
- package/metro/Nativ.h +126 -0
- package/metro/compilers/android-compiler.js +339 -0
- package/metro/compilers/dylib-compiler.js +474 -0
- package/metro/compilers/kotlin-compiler.js +632 -0
- package/metro/compilers/rust-compiler.js +722 -0
- package/metro/compilers/static-compiler.js +1118 -0
- package/metro/compilers/swift-compiler.js +363 -0
- package/metro/extractors/cpp-ast-extractor.js +126 -0
- package/metro/extractors/kotlin-extractor.js +125 -0
- package/metro/extractors/rust-extractor.js +118 -0
- package/metro/index.js +236 -0
- package/metro/transformer.js +649 -0
- package/metro/utils/bridge-generator.js +50 -0
- package/metro/utils/compile-commands.js +104 -0
- package/metro/utils/cpp-daemon.js +344 -0
- package/metro/utils/dts-generator.js +32 -0
- package/metro/utils/include-resolver.js +73 -0
- package/metro/utils/kotlin-daemon.js +394 -0
- package/metro/utils/type-mapper.js +63 -0
- package/package.json +43 -0
- package/react-native.config.js +13 -0
- package/src/NativContainerNativeComponent.ts +9 -0
- package/src/NativeNativRuntime.ts +8 -0
- package/src/index.ts +4 -0
|
@@ -0,0 +1,1118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* static-compiler.js — Production build: generates bridges + compiles Rust.
|
|
3
|
+
*
|
|
4
|
+
* iOS (CocoaPods :before_compile):
|
|
5
|
+
* Three fixed-name files in --output dir:
|
|
6
|
+
* - nativ_bridges.mm — all C++/ObjC++ bridges + Swift/Rust C registration
|
|
7
|
+
* - nativ_bridges.swift — all Swift source + @_cdecl wrappers
|
|
8
|
+
* - libnativ_user.a — unified Rust static library
|
|
9
|
+
*
|
|
10
|
+
* Android (Gradle pre-build):
|
|
11
|
+
* Per-file bridges + Kotlin wrappers in .nativ/generated/
|
|
12
|
+
*
|
|
13
|
+
* Invoked by:
|
|
14
|
+
* - CocoaPods script phase: node static-compiler.js --platform ios --root $ROOT --output $DIR
|
|
15
|
+
* - Gradle pre-build task: node static-compiler.js --platform android --root $ROOT
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const path = require("path");
|
|
19
|
+
const fs = require("fs");
|
|
20
|
+
const { execSync } = require("child_process");
|
|
21
|
+
|
|
22
|
+
// Reuse existing extractors
|
|
23
|
+
const {
|
|
24
|
+
extractCppExports,
|
|
25
|
+
isCppComponent,
|
|
26
|
+
extractCppComponentProps,
|
|
27
|
+
} = require("../extractors/cpp-ast-extractor");
|
|
28
|
+
// extractRustExports used via lazy require in buildRustStatic
|
|
29
|
+
const { extractSwiftExports } = require("./swift-compiler");
|
|
30
|
+
const { extractKotlinExports } = require("../extractors/kotlin-extractor");
|
|
31
|
+
|
|
32
|
+
// Reuse existing bridge generators (Android per-file path)
|
|
33
|
+
const { generateBridge } = require("./dylib-compiler");
|
|
34
|
+
|
|
35
|
+
// ─── CLI ───────────────────────────────────────────────────────────────
|
|
36
|
+
|
|
37
|
+
// Skip in Debug builds — all native code loads via Metro dylibs
|
|
38
|
+
if (process.env.CONFIGURATION === "Debug") {
|
|
39
|
+
console.log("[nativ] Debug build — skipping static compilation");
|
|
40
|
+
process.exit(0);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const args = process.argv.slice(2);
|
|
44
|
+
const platformIdx = args.indexOf("--platform");
|
|
45
|
+
const platform = platformIdx >= 0 ? args[platformIdx + 1] : "ios";
|
|
46
|
+
const projectRoot = args.includes("--root")
|
|
47
|
+
? args[args.indexOf("--root") + 1]
|
|
48
|
+
: process.cwd();
|
|
49
|
+
const outputDir = args.includes("--output")
|
|
50
|
+
? args[args.indexOf("--output") + 1]
|
|
51
|
+
: null;
|
|
52
|
+
|
|
53
|
+
const isIOS = platform === "ios";
|
|
54
|
+
const isAndroid = platform === "android";
|
|
55
|
+
|
|
56
|
+
// iOS: output is the bridges directory (three fixed files)
|
|
57
|
+
// Android: output is .nativ/generated/ with subdirectories
|
|
58
|
+
const genDir = path.join(projectRoot, ".nativ/generated");
|
|
59
|
+
let bridgeDir, releaseDir;
|
|
60
|
+
if (isIOS) {
|
|
61
|
+
bridgeDir = outputDir || path.join(projectRoot, ".nativ/bridges");
|
|
62
|
+
releaseDir = bridgeDir; // libnativ_user.a goes alongside bridges
|
|
63
|
+
} else {
|
|
64
|
+
bridgeDir = path.join(genDir, "bridges/android");
|
|
65
|
+
releaseDir = path.join(genDir, "release");
|
|
66
|
+
}
|
|
67
|
+
fs.mkdirSync(bridgeDir, { recursive: true });
|
|
68
|
+
if (isAndroid) fs.mkdirSync(releaseDir, { recursive: true });
|
|
69
|
+
|
|
70
|
+
console.log(
|
|
71
|
+
`[nativ] Static compiler: platform=${platform}, root=${projectRoot}`,
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
// ─── Scan for user native files ────────────────────────────────────────
|
|
75
|
+
|
|
76
|
+
function findUserFiles(exts) {
|
|
77
|
+
const results = [];
|
|
78
|
+
const ignore = [
|
|
79
|
+
"node_modules",
|
|
80
|
+
".nativ",
|
|
81
|
+
"modules",
|
|
82
|
+
"ios",
|
|
83
|
+
"android",
|
|
84
|
+
"vendor",
|
|
85
|
+
];
|
|
86
|
+
|
|
87
|
+
function walk(dir) {
|
|
88
|
+
let entries;
|
|
89
|
+
try {
|
|
90
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
91
|
+
} catch {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
for (const entry of entries) {
|
|
95
|
+
if (ignore.includes(entry.name)) continue;
|
|
96
|
+
const full = path.join(dir, entry.name);
|
|
97
|
+
if (entry.isDirectory()) {
|
|
98
|
+
walk(full);
|
|
99
|
+
} else if (exts.some((ext) => entry.name.endsWith(ext))) {
|
|
100
|
+
results.push(full);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
walk(projectRoot);
|
|
105
|
+
return results;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ─── iOS: combined bridge generation ──────────────────────────────────
|
|
109
|
+
// All bridges go into two files: nativ_bridges.mm + nativ_bridges.swift
|
|
110
|
+
|
|
111
|
+
function buildCombinedIOSBridges() {
|
|
112
|
+
const mmSections = [];
|
|
113
|
+
|
|
114
|
+
// Shared .mm preamble
|
|
115
|
+
mmSections.push(`// Auto-generated by React Native Native — do not edit
|
|
116
|
+
// Combined production bridges for all native modules
|
|
117
|
+
|
|
118
|
+
#include <string>
|
|
119
|
+
#include <cstdlib>
|
|
120
|
+
#include <dispatch/dispatch.h>
|
|
121
|
+
|
|
122
|
+
// Forward declarations
|
|
123
|
+
extern "C" {
|
|
124
|
+
typedef const char* (*NativSyncFn)(const char*);
|
|
125
|
+
typedef void (*NativAsyncFn)(const char*, void (*)(const char*), void (*)(const char*, const char*));
|
|
126
|
+
typedef void (*NativRenderFn)(void*, float, float, void*, void*);
|
|
127
|
+
void nativ_register_sync(const char*, const char*, NativSyncFn);
|
|
128
|
+
void nativ_register_async(const char*, const char*, NativAsyncFn);
|
|
129
|
+
void nativ_register_render(const char*, NativRenderFn);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// JSON helpers
|
|
133
|
+
${generateJsonHelpers()}
|
|
134
|
+
`);
|
|
135
|
+
|
|
136
|
+
// ── C++/ObjC++ modules ─────────────────────────────────────
|
|
137
|
+
const cppFiles = findUserFiles([".cpp", ".cc", ".mm"]);
|
|
138
|
+
for (const filepath of cppFiles) {
|
|
139
|
+
if (isCppComponent(filepath)) {
|
|
140
|
+
mmSections.push(generateCppComponentSection(filepath));
|
|
141
|
+
} else {
|
|
142
|
+
const exports = extractCppExports(filepath, []);
|
|
143
|
+
if (exports.length === 0) continue;
|
|
144
|
+
const rel = path.relative(projectRoot, filepath);
|
|
145
|
+
const moduleId = rel
|
|
146
|
+
.replace(/\.(cpp|cc|mm)$/, "")
|
|
147
|
+
.replace(/[\/\\]/g, "_")
|
|
148
|
+
.replace(/[^a-zA-Z0-9_]/g, "_");
|
|
149
|
+
mmSections.push(generateCppModuleSection(filepath, exports, moduleId));
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ── Swift modules ──────────────────────────────────────────
|
|
154
|
+
const swiftFiles = findUserFiles([".swift"]);
|
|
155
|
+
const swiftBodies = [];
|
|
156
|
+
const swiftWrappers = [];
|
|
157
|
+
|
|
158
|
+
for (const filepath of swiftFiles) {
|
|
159
|
+
const name = path.basename(filepath, ".swift");
|
|
160
|
+
const moduleId = name.toLowerCase();
|
|
161
|
+
const src = fs.readFileSync(filepath, "utf8");
|
|
162
|
+
const isComp =
|
|
163
|
+
src.includes("@nativ_component") || src.includes("nativ::component");
|
|
164
|
+
|
|
165
|
+
if (isComp) {
|
|
166
|
+
// Component: generate render bridge in Swift, C registration in .mm
|
|
167
|
+
mmSections.push(generateSwiftComponentRegistration(moduleId));
|
|
168
|
+
const { swiftCode } = generateSwiftComponentBridge(
|
|
169
|
+
filepath,
|
|
170
|
+
src,
|
|
171
|
+
name,
|
|
172
|
+
moduleId,
|
|
173
|
+
);
|
|
174
|
+
swiftBodies.push(swiftCode);
|
|
175
|
+
console.log(`[nativ] Bridge: ${moduleId} (Swift component)`);
|
|
176
|
+
} else {
|
|
177
|
+
const exports = extractSwiftExports(filepath);
|
|
178
|
+
|
|
179
|
+
if (exports.length === 0) {
|
|
180
|
+
// No annotations — include as helper file (available to annotated files)
|
|
181
|
+
swiftBodies.push(`// From: ${name}.swift\n${stripAnnotations(src)}`);
|
|
182
|
+
console.log(`[nativ] Swift helper: ${name}.swift`);
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// C registration in .mm
|
|
187
|
+
mmSections.push(generateSwiftFunctionRegistration(exports, moduleId));
|
|
188
|
+
|
|
189
|
+
// User source (imports kept) + @_cdecl wrappers
|
|
190
|
+
swiftBodies.push(`// From: ${name}.swift\n${stripAnnotations(src)}`);
|
|
191
|
+
swiftWrappers.push(
|
|
192
|
+
...exports.map((fn) => generateSwiftCdeclWrapper(fn)),
|
|
193
|
+
);
|
|
194
|
+
console.log(
|
|
195
|
+
`[nativ] Bridge: ${moduleId} (${exports.length} Swift functions)`,
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// ── Write nativ_bridges.mm ─────────────────────────────────
|
|
201
|
+
const mmContent = mmSections.join("\n");
|
|
202
|
+
fs.writeFileSync(path.join(bridgeDir, "nativ_bridges.mm"), mmContent);
|
|
203
|
+
console.log(
|
|
204
|
+
`[nativ] Wrote nativ_bridges.mm (${(mmContent.length / 1024).toFixed(1)}KB)`,
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
// ── Write nativ_bridges.swift ──────────────────────────────
|
|
208
|
+
const swiftContent = [
|
|
209
|
+
"// Auto-generated by React Native Native — do not edit",
|
|
210
|
+
"",
|
|
211
|
+
...swiftBodies,
|
|
212
|
+
"",
|
|
213
|
+
...swiftWrappers,
|
|
214
|
+
"",
|
|
215
|
+
].join("\n");
|
|
216
|
+
fs.writeFileSync(path.join(bridgeDir, "nativ_bridges.swift"), swiftContent);
|
|
217
|
+
console.log(
|
|
218
|
+
`[nativ] Wrote nativ_bridges.swift (${(swiftContent.length / 1024).toFixed(1)}KB)`,
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// ─── Helpers: JSON parse/escape (emitted once in combined .mm) ────────
|
|
223
|
+
|
|
224
|
+
function generateJsonHelpers() {
|
|
225
|
+
return String.raw`static std::string _jsonEscapeString(const std::string& s) {
|
|
226
|
+
std::string buf = "\"";
|
|
227
|
+
for (unsigned char c : s) {
|
|
228
|
+
if (c == '"') buf += "\\\"";
|
|
229
|
+
else if (c == '\\') buf += "\\\\";
|
|
230
|
+
else if (c == '\n') buf += "\\n";
|
|
231
|
+
else if (c == '\r') buf += "\\r";
|
|
232
|
+
else if (c == '\t') buf += "\\t";
|
|
233
|
+
else if (c >= 0x20) buf += (char)c;
|
|
234
|
+
}
|
|
235
|
+
buf += "\"";
|
|
236
|
+
return buf;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
static double _parseNumber(const char* &p) {
|
|
240
|
+
while (*p == ' ' || *p == ',' || *p == '[') p++;
|
|
241
|
+
char* end; double v = strtod(p, &end); p = end; return v;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
static std::string _parseString(const char* &p) {
|
|
245
|
+
while (*p && *p != '"') p++; if (*p == '"') p++;
|
|
246
|
+
std::string s; while (*p && *p != '"') {
|
|
247
|
+
if (*p == '\\' && *(p+1)) { p++; s += *p; } else { s += *p; } p++; }
|
|
248
|
+
if (*p == '"') p++; return s;
|
|
249
|
+
}`;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// ─── Helpers: per-module C++ bridge code ──────────────────────────────
|
|
253
|
+
|
|
254
|
+
function generateCppModuleSection(filepath, exports, moduleId) {
|
|
255
|
+
const lines = [];
|
|
256
|
+
|
|
257
|
+
lines.push(`// ─── C++ module: ${moduleId} ────────────────────────────`);
|
|
258
|
+
lines.push(`#include "${path.resolve(filepath)}"`);
|
|
259
|
+
lines.push("");
|
|
260
|
+
lines.push("extern \"C\" {");
|
|
261
|
+
|
|
262
|
+
for (const fn of exports) {
|
|
263
|
+
lines.push(...generateCppFnWrapper(fn, moduleId));
|
|
264
|
+
lines.push("");
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Constructor
|
|
268
|
+
lines.push("__attribute__((constructor, used))");
|
|
269
|
+
lines.push(`static void nativ_cpp_register_${moduleId}() {`);
|
|
270
|
+
for (const fn of exports) {
|
|
271
|
+
if (fn.async) {
|
|
272
|
+
lines.push(
|
|
273
|
+
` nativ_register_async("${moduleId}", "${fn.name}", nativ_cpp_async_${moduleId}_${fn.name});`,
|
|
274
|
+
);
|
|
275
|
+
} else {
|
|
276
|
+
lines.push(
|
|
277
|
+
` nativ_register_sync("${moduleId}", "${fn.name}", nativ_cpp_${moduleId}_${fn.name});`,
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
lines.push("}");
|
|
282
|
+
lines.push("} // extern \"C\"");
|
|
283
|
+
lines.push("");
|
|
284
|
+
|
|
285
|
+
console.log(`[nativ] Bridge: ${moduleId} (${exports.length} C++ functions)`);
|
|
286
|
+
return lines.join("\n");
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function generateCppFnWrapper(fn, moduleId) {
|
|
290
|
+
const lines = [];
|
|
291
|
+
const retBase = fn.ret
|
|
292
|
+
.replace(/const\s+/, "")
|
|
293
|
+
.replace(/\s*&\s*$/, "")
|
|
294
|
+
.trim();
|
|
295
|
+
|
|
296
|
+
if (fn.async) {
|
|
297
|
+
lines.push(
|
|
298
|
+
`static void nativ_cpp_async_${moduleId}_${fn.name}(const char* argsJson, void (*resolve)(const char*), void (*reject)(const char*, const char*)) {`,
|
|
299
|
+
);
|
|
300
|
+
lines.push(...generateArgParsing(fn.args));
|
|
301
|
+
lines.push(" try {");
|
|
302
|
+
const call = `${fn.name}(${fn.args.map((a) => a.name).join(", ")})`;
|
|
303
|
+
if (retBase === "void") {
|
|
304
|
+
lines.push(` ${call};`);
|
|
305
|
+
lines.push(' resolve("null");');
|
|
306
|
+
} else if (retBase === "std::string") {
|
|
307
|
+
lines.push(` auto result = ${call};`);
|
|
308
|
+
lines.push(" resolve(_jsonEscapeString(result).c_str());");
|
|
309
|
+
} else {
|
|
310
|
+
lines.push(` auto result = ${call};`);
|
|
311
|
+
lines.push(" std::string buf = std::to_string(result);");
|
|
312
|
+
lines.push(" resolve(buf.c_str());");
|
|
313
|
+
}
|
|
314
|
+
lines.push(" } catch (const std::exception& e) {");
|
|
315
|
+
lines.push(' reject("NATIVE_ERROR", e.what());');
|
|
316
|
+
lines.push(" } catch (...) {");
|
|
317
|
+
lines.push(' reject("NATIVE_ERROR", "Unknown error");');
|
|
318
|
+
lines.push(" }");
|
|
319
|
+
lines.push("}");
|
|
320
|
+
return lines;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
lines.push(
|
|
324
|
+
`static const char* nativ_cpp_${moduleId}_${fn.name}(const char* argsJson) {`,
|
|
325
|
+
);
|
|
326
|
+
lines.push(...generateArgParsing(fn.args));
|
|
327
|
+
|
|
328
|
+
const call = `${fn.name}(${fn.args.map((a) => a.name).join(", ")})`;
|
|
329
|
+
|
|
330
|
+
if (fn.mainThread) {
|
|
331
|
+
lines.push(" static thread_local std::string buf;");
|
|
332
|
+
lines.push(" __block std::string _result;");
|
|
333
|
+
lines.push(" dispatch_sync(dispatch_get_main_queue(), ^{");
|
|
334
|
+
lines.push(` auto result = ${call};`);
|
|
335
|
+
if (retBase === "std::string") {
|
|
336
|
+
lines.push(" _result = _jsonEscapeString(result);");
|
|
337
|
+
} else {
|
|
338
|
+
lines.push(" _result = std::to_string(result);");
|
|
339
|
+
}
|
|
340
|
+
lines.push(" });");
|
|
341
|
+
lines.push(" buf = _result;");
|
|
342
|
+
lines.push(" return buf.c_str();");
|
|
343
|
+
} else {
|
|
344
|
+
lines.push(` auto result = ${call};`);
|
|
345
|
+
lines.push(" static thread_local std::string buf;");
|
|
346
|
+
if (retBase === "std::string") {
|
|
347
|
+
lines.push(" buf = _jsonEscapeString(result);");
|
|
348
|
+
} else {
|
|
349
|
+
lines.push(" buf = std::to_string(result);");
|
|
350
|
+
}
|
|
351
|
+
lines.push(" return buf.c_str();");
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
lines.push("}");
|
|
355
|
+
return lines;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function generateArgParsing(fnArgs) {
|
|
359
|
+
const lines = [];
|
|
360
|
+
lines.push(" const char* p = argsJson;");
|
|
361
|
+
lines.push(" while (*p && *p != '[') p++; if (*p == '[') p++;");
|
|
362
|
+
for (const arg of fnArgs) {
|
|
363
|
+
const t = arg.type
|
|
364
|
+
.replace(/const\s+/, "")
|
|
365
|
+
.replace(/\s*&\s*$/, "")
|
|
366
|
+
.trim();
|
|
367
|
+
if (t === "std::string") {
|
|
368
|
+
lines.push(` std::string ${arg.name} = _parseString(p);`);
|
|
369
|
+
} else {
|
|
370
|
+
lines.push(` ${arg.type} ${arg.name} = (${arg.type})_parseNumber(p);`);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
return lines;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// ─── Helpers: C++ component bridge ───────────────────────────────────
|
|
377
|
+
|
|
378
|
+
function generateCppComponentSection(filepath) {
|
|
379
|
+
const baseName = path
|
|
380
|
+
.basename(filepath)
|
|
381
|
+
.replace(/\.(cpp|cc|mm)$/, "")
|
|
382
|
+
.toLowerCase();
|
|
383
|
+
const cppProps = extractCppComponentProps(filepath);
|
|
384
|
+
const propsTypeName = (() => {
|
|
385
|
+
const src = fs.readFileSync(filepath, "utf8");
|
|
386
|
+
const m = src.match(/NATIV_COMPONENT\s*\(\s*(\w+)\s*,\s*(\w+)\s*\)/);
|
|
387
|
+
return m ? m[2] : null;
|
|
388
|
+
})();
|
|
389
|
+
|
|
390
|
+
const propExtractions = (cppProps || [])
|
|
391
|
+
.map((p) => {
|
|
392
|
+
if (p.cppType === "std::string")
|
|
393
|
+
return ` props.${p.name} = _nativ_get_string(rt, obj, "${p.jsName}", props.${p.name});`;
|
|
394
|
+
if (["double", "float", "int"].includes(p.cppType))
|
|
395
|
+
return ` props.${p.name} = _nativ_get_number(rt, obj, "${p.jsName}", props.${p.name});`;
|
|
396
|
+
if (p.cppType === "bool")
|
|
397
|
+
return ` props.${p.name} = _nativ_get_bool(rt, obj, "${p.jsName}", props.${p.name});`;
|
|
398
|
+
return "";
|
|
399
|
+
})
|
|
400
|
+
.join("\n");
|
|
401
|
+
|
|
402
|
+
const renderFnName = `nativ_${baseName}_render`;
|
|
403
|
+
|
|
404
|
+
console.log(`[nativ] Bridge: ${baseName} (C++ component)`);
|
|
405
|
+
|
|
406
|
+
return `// ─── C++ component: ${baseName} ─────────────────────────
|
|
407
|
+
#include "${path.resolve(filepath)}"
|
|
408
|
+
|
|
409
|
+
extern "C"
|
|
410
|
+
void ${renderFnName}(void* view, float width, float height,
|
|
411
|
+
void* jsi_runtime, void* jsi_props) {
|
|
412
|
+
void* rt = jsi_runtime;
|
|
413
|
+
void* obj = jsi_props;
|
|
414
|
+
${propsTypeName ? ` ${propsTypeName} props;\n${propExtractions}\n mount(view, width, height, props);` : " mount(view, width, height);"}
|
|
415
|
+
}
|
|
416
|
+
`;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// ─── Helpers: Swift registration in .mm ──────────────────────────────
|
|
420
|
+
|
|
421
|
+
function generateSwiftFunctionRegistration(exports, moduleId) {
|
|
422
|
+
const declarations = exports
|
|
423
|
+
.map(
|
|
424
|
+
(fn) =>
|
|
425
|
+
`extern const char* ${fn.cdeclName}(const char*);`,
|
|
426
|
+
)
|
|
427
|
+
.join("\n");
|
|
428
|
+
const registrations = exports
|
|
429
|
+
.map(
|
|
430
|
+
(fn) =>
|
|
431
|
+
` nativ_register_sync("${moduleId}", "${fn.name}", ${fn.cdeclName});`,
|
|
432
|
+
)
|
|
433
|
+
.join("\n");
|
|
434
|
+
|
|
435
|
+
return `// ─── Swift module: ${moduleId} ──────────────────────────
|
|
436
|
+
extern "C" {
|
|
437
|
+
${declarations}
|
|
438
|
+
|
|
439
|
+
__attribute__((constructor, used))
|
|
440
|
+
static void nativ_register_${moduleId}() {
|
|
441
|
+
${registrations}
|
|
442
|
+
}
|
|
443
|
+
} // extern "C"
|
|
444
|
+
`;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function generateSwiftComponentRegistration(moduleId) {
|
|
448
|
+
const renderFnName = `nativ_${moduleId}_render`;
|
|
449
|
+
return `// ─── Swift component: ${moduleId} ──────────────────────
|
|
450
|
+
extern "C" {
|
|
451
|
+
extern void ${renderFnName}(void*, float, float, void*, void*);
|
|
452
|
+
|
|
453
|
+
__attribute__((constructor, used))
|
|
454
|
+
static void nativ_register_${moduleId}() {
|
|
455
|
+
nativ_register_render("nativ.${moduleId}", ${renderFnName});
|
|
456
|
+
}
|
|
457
|
+
} // extern "C"
|
|
458
|
+
`;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// ─── Helpers: Swift @_cdecl wrappers ─────────────────────────────────
|
|
462
|
+
|
|
463
|
+
function generateSwiftCdeclWrapper(fn) {
|
|
464
|
+
const retType = fn.ret || "Void";
|
|
465
|
+
const argPassthrough = fn.args.map((a) => a.name).join(", ");
|
|
466
|
+
|
|
467
|
+
let resultExpr;
|
|
468
|
+
if (retType === "String")
|
|
469
|
+
resultExpr = `return UnsafePointer(strdup("\\"" + result + "\\"")!)`;
|
|
470
|
+
else if (retType === "Bool")
|
|
471
|
+
resultExpr = `return UnsafePointer(strdup(result ? "true" : "false")!)`;
|
|
472
|
+
else if (retType === "Void")
|
|
473
|
+
resultExpr = `return UnsafePointer(strdup("null")!)`;
|
|
474
|
+
else resultExpr = `return UnsafePointer(strdup(String(result))!)`;
|
|
475
|
+
|
|
476
|
+
if (fn.mainThread) {
|
|
477
|
+
return `
|
|
478
|
+
@_cdecl("${fn.cdeclName}")
|
|
479
|
+
func _nativ_${fn.name}(_ argsJson: UnsafePointer<CChar>) -> UnsafePointer<CChar> {
|
|
480
|
+
var ptr: UnsafePointer<CChar>!
|
|
481
|
+
DispatchQueue.main.sync {
|
|
482
|
+
let result = ${fn.name}(${argPassthrough})
|
|
483
|
+
ptr = ${resultExpr.replace("return ", "")}
|
|
484
|
+
}
|
|
485
|
+
return ptr
|
|
486
|
+
}`;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
return `
|
|
490
|
+
@_cdecl("${fn.cdeclName}")
|
|
491
|
+
func _nativ_${fn.name}(_ argsJson: UnsafePointer<CChar>) -> UnsafePointer<CChar> {
|
|
492
|
+
let result = ${fn.name}(${argPassthrough})
|
|
493
|
+
${resultExpr}
|
|
494
|
+
}`;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// ─── Helpers: Swift component bridge ─────────────────────────────────
|
|
498
|
+
|
|
499
|
+
function generateSwiftComponentBridge(_filepath, src, name, moduleId) {
|
|
500
|
+
const structMatch = src.match(
|
|
501
|
+
/\/\/\s*@nativ_component\s*\n\s*struct\s+(\w+)\s*:\s*View\s*\{([\s\S]*?)var\s+body\s*:\s*some\s+View/,
|
|
502
|
+
);
|
|
503
|
+
const structName = structMatch ? structMatch[1] : name;
|
|
504
|
+
const propsBlock = structMatch ? structMatch[2] : "";
|
|
505
|
+
// Parse fields
|
|
506
|
+
const props = [];
|
|
507
|
+
for (const line of propsBlock.split("\n")) {
|
|
508
|
+
const m = line.trim().match(/(?:let|var)\s+(\w+)\s*:\s*(\w+)/);
|
|
509
|
+
if (m && m[1] !== "body") {
|
|
510
|
+
props.push({ name: m[1], type: m[2] });
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
const renderFnName = `nativ_${moduleId}_render`;
|
|
515
|
+
const propExtractions = props
|
|
516
|
+
.map((p) => {
|
|
517
|
+
if (p.type === "String")
|
|
518
|
+
return ` let ${p.name} = String(cString: nativ_jsi_get_string(runtime, props, "${p.name}"))`;
|
|
519
|
+
if (
|
|
520
|
+
p.type === "Double" ||
|
|
521
|
+
p.type === "Float" ||
|
|
522
|
+
p.type === "CGFloat"
|
|
523
|
+
)
|
|
524
|
+
return ` let ${p.name} = ${p.type}(nativ_jsi_get_number(runtime, props, "${p.name}"))`;
|
|
525
|
+
if (p.type === "Int")
|
|
526
|
+
return ` let ${p.name} = Int(nativ_jsi_get_number(runtime, props, "${p.name}"))`;
|
|
527
|
+
if (p.type === "Bool")
|
|
528
|
+
return ` let ${p.name} = nativ_jsi_has_prop(runtime, props, "${p.name}") != 0 && nativ_jsi_get_number(runtime, props, "${p.name}") != 0`;
|
|
529
|
+
if (p.type === "Color")
|
|
530
|
+
return ` let ${p.name}: Color = {
|
|
531
|
+
let hex = String(cString: nativ_jsi_get_string(runtime, props, "${p.name}"))
|
|
532
|
+
let scanner = Scanner(string: hex.hasPrefix("#") ? String(hex.dropFirst()) : hex)
|
|
533
|
+
var rgb: UInt64 = 0; scanner.scanHexInt64(&rgb)
|
|
534
|
+
return Color(red: Double((rgb >> 16) & 0xFF) / 255, green: Double((rgb >> 8) & 0xFF) / 255, blue: Double(rgb & 0xFF) / 255)
|
|
535
|
+
}()`;
|
|
536
|
+
return ` let ${p.name} = String(cString: nativ_jsi_get_string(runtime, props, "${p.name}")) // unsupported type: ${p.type}`;
|
|
537
|
+
})
|
|
538
|
+
.join("\n");
|
|
539
|
+
|
|
540
|
+
const initArgs = props
|
|
541
|
+
.map((p) => `${p.name}: ${p.name}`)
|
|
542
|
+
.join(", ");
|
|
543
|
+
|
|
544
|
+
const swiftCode = `// From: ${name}.swift
|
|
545
|
+
${stripAnnotations(src)}
|
|
546
|
+
|
|
547
|
+
@_cdecl("${renderFnName}")
|
|
548
|
+
func ${renderFnName}(
|
|
549
|
+
_ view: UnsafeMutableRawPointer,
|
|
550
|
+
_ width: Float, _ height: Float,
|
|
551
|
+
_ runtime: UnsafeMutableRawPointer?,
|
|
552
|
+
_ props: UnsafeMutableRawPointer?
|
|
553
|
+
) {
|
|
554
|
+
let parentView = Unmanaged<UIView>.fromOpaque(view).takeUnretainedValue()
|
|
555
|
+
|
|
556
|
+
${propExtractions}
|
|
557
|
+
|
|
558
|
+
let swiftUIView = ${structName}(${initArgs})
|
|
559
|
+
let hostingController = UIHostingController(rootView: swiftUIView)
|
|
560
|
+
hostingController.view.frame = CGRect(x: 0, y: 0, width: CGFloat(width), height: CGFloat(height))
|
|
561
|
+
hostingController.view.backgroundColor = .clear
|
|
562
|
+
|
|
563
|
+
objc_setAssociatedObject(parentView, "nativHosting", hostingController, .OBJC_ASSOCIATION_RETAIN)
|
|
564
|
+
parentView.addSubview(hostingController.view)
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// JSI C API — resolved at link time from NativFabric
|
|
568
|
+
@_silgen_name("nativ_jsi_get_string")
|
|
569
|
+
func nativ_jsi_get_string(_ rt: UnsafeMutableRawPointer?, _ obj: UnsafeMutableRawPointer?, _ name: UnsafePointer<CChar>) -> UnsafePointer<CChar>
|
|
570
|
+
|
|
571
|
+
@_silgen_name("nativ_jsi_get_number")
|
|
572
|
+
func nativ_jsi_get_number(_ rt: UnsafeMutableRawPointer?, _ obj: UnsafeMutableRawPointer?, _ name: UnsafePointer<CChar>) -> Double
|
|
573
|
+
|
|
574
|
+
@_silgen_name("nativ_jsi_has_prop")
|
|
575
|
+
func nativ_jsi_has_prop(_ rt: UnsafeMutableRawPointer?, _ obj: UnsafeMutableRawPointer?, _ name: UnsafePointer<CChar>) -> Int32
|
|
576
|
+
`;
|
|
577
|
+
|
|
578
|
+
return { swiftCode };
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// ─── Helpers: Swift source handling ──────────────────────────────────
|
|
582
|
+
|
|
583
|
+
function stripAnnotations(src) {
|
|
584
|
+
return src
|
|
585
|
+
.replace(/\/\/\s*@nativ_export\s*\([^)]*\)\s*\n/g, "")
|
|
586
|
+
.replace(/\/\/\s*@nativ_component\s*\n/g, "");
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// ─── Android: per-file C++ bridges (unchanged) ──────────────────────
|
|
590
|
+
|
|
591
|
+
function buildCppBridges() {
|
|
592
|
+
const exts = isIOS ? [".cpp", ".cc", ".mm"] : [".cpp", ".cc"];
|
|
593
|
+
const cppFiles = findUserFiles(exts);
|
|
594
|
+
if (cppFiles.length === 0) return;
|
|
595
|
+
|
|
596
|
+
for (const filepath of cppFiles) {
|
|
597
|
+
if (isCppComponent(filepath)) {
|
|
598
|
+
const baseName = path
|
|
599
|
+
.basename(filepath)
|
|
600
|
+
.replace(/\.(cpp|cc|mm)$/, "")
|
|
601
|
+
.toLowerCase();
|
|
602
|
+
const componentId = `nativ.${baseName}`;
|
|
603
|
+
const cppProps = extractCppComponentProps(filepath);
|
|
604
|
+
const propsTypeName = (() => {
|
|
605
|
+
const src = fs.readFileSync(filepath, "utf8");
|
|
606
|
+
const m = src.match(/NATIV_COMPONENT\s*\(\s*(\w+)\s*,\s*(\w+)\s*\)/);
|
|
607
|
+
return m ? m[2] : null;
|
|
608
|
+
})();
|
|
609
|
+
|
|
610
|
+
const propExtractions = (cppProps || [])
|
|
611
|
+
.map((p) => {
|
|
612
|
+
if (p.cppType === "std::string")
|
|
613
|
+
return ` props.${p.name} = _nativ_get_string(rt, obj, "${p.jsName}", props.${p.name});`;
|
|
614
|
+
if (["double", "float", "int"].includes(p.cppType))
|
|
615
|
+
return ` props.${p.name} = _nativ_get_number(rt, obj, "${p.jsName}", props.${p.name});`;
|
|
616
|
+
if (p.cppType === "bool")
|
|
617
|
+
return ` props.${p.name} = _nativ_get_bool(rt, obj, "${p.jsName}", props.${p.name});`;
|
|
618
|
+
return "";
|
|
619
|
+
})
|
|
620
|
+
.join("\n");
|
|
621
|
+
|
|
622
|
+
const renderFnName = `nativ_${baseName}_render`;
|
|
623
|
+
const bridgeSrc = `
|
|
624
|
+
// Auto-generated production component bridge for ${baseName}
|
|
625
|
+
#include "${path.resolve(filepath)}"
|
|
626
|
+
|
|
627
|
+
extern "C"
|
|
628
|
+
void ${renderFnName}(void* view, float width, float height,
|
|
629
|
+
void* jsi_runtime, void* jsi_props) {
|
|
630
|
+
void* rt = jsi_runtime;
|
|
631
|
+
void* obj = jsi_props;
|
|
632
|
+
${propsTypeName ? ` ${propsTypeName} props;\n${propExtractions}\n mount(view, width, height, props);` : " mount(view, width, height);"}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
extern "C" {
|
|
636
|
+
typedef void (*NativRenderFn)(void*, float, float, void*, void*);
|
|
637
|
+
void nativ_register_render(const char*, NativRenderFn);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
__attribute__((constructor, used))
|
|
641
|
+
static void register_${baseName}() {
|
|
642
|
+
nativ_register_render("${componentId}", ${renderFnName});
|
|
643
|
+
}
|
|
644
|
+
`;
|
|
645
|
+
const ext = filepath.endsWith(".mm") ? "mm" : "cpp";
|
|
646
|
+
fs.writeFileSync(
|
|
647
|
+
path.join(bridgeDir, `nativ_${baseName}_bridge.${ext}`),
|
|
648
|
+
bridgeSrc,
|
|
649
|
+
);
|
|
650
|
+
console.log(`[nativ] Bridge: ${baseName} (component)`);
|
|
651
|
+
} else {
|
|
652
|
+
const exports = extractCppExports(filepath, []);
|
|
653
|
+
if (exports.length === 0) continue;
|
|
654
|
+
|
|
655
|
+
const rel = path.relative(projectRoot, filepath);
|
|
656
|
+
const moduleId = rel
|
|
657
|
+
.replace(/\.(cpp|cc|mm)$/, "")
|
|
658
|
+
.replace(/[\/\\]/g, "_")
|
|
659
|
+
.replace(/[^a-zA-Z0-9_]/g, "_");
|
|
660
|
+
const bridgeSrc = generateBridge(exports, moduleId);
|
|
661
|
+
const userInclude = `#include "${path.resolve(filepath)}"\n\n`;
|
|
662
|
+
const ext = filepath.endsWith(".mm") ? "mm" : "cpp";
|
|
663
|
+
fs.writeFileSync(
|
|
664
|
+
path.join(bridgeDir, `${moduleId}_bridge.${ext}`),
|
|
665
|
+
userInclude + bridgeSrc,
|
|
666
|
+
);
|
|
667
|
+
console.log(`[nativ] Bridge: ${moduleId} (${exports.length} functions)`);
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
// ─── Rust: single unified static library ───────────────────────────────
|
|
673
|
+
|
|
674
|
+
function buildRustStatic() {
|
|
675
|
+
const rsFiles = findUserFiles([".rs"]);
|
|
676
|
+
if (rsFiles.length === 0) return;
|
|
677
|
+
|
|
678
|
+
const abiTargets = isAndroid
|
|
679
|
+
? [
|
|
680
|
+
{
|
|
681
|
+
abi: "arm64-v8a",
|
|
682
|
+
rust: "aarch64-linux-android",
|
|
683
|
+
linkerPrefix: "aarch64-linux-android",
|
|
684
|
+
},
|
|
685
|
+
{
|
|
686
|
+
abi: "armeabi-v7a",
|
|
687
|
+
rust: "armv7-linux-androideabi",
|
|
688
|
+
linkerPrefix: "armv7a-linux-androideabi",
|
|
689
|
+
},
|
|
690
|
+
{
|
|
691
|
+
abi: "x86_64",
|
|
692
|
+
rust: "x86_64-linux-android",
|
|
693
|
+
linkerPrefix: "x86_64-linux-android",
|
|
694
|
+
},
|
|
695
|
+
]
|
|
696
|
+
: [{ abi: "arm64", rust: "aarch64-apple-ios" }];
|
|
697
|
+
|
|
698
|
+
// Check if all outputs are up to date
|
|
699
|
+
const outputPaths = abiTargets.map((t) => {
|
|
700
|
+
const dir = isAndroid ? path.join(releaseDir, t.abi) : releaseDir;
|
|
701
|
+
return path.join(dir, "libnativ_user.a");
|
|
702
|
+
});
|
|
703
|
+
const allUpToDate = outputPaths.every((p) => {
|
|
704
|
+
if (!fs.existsSync(p)) return false;
|
|
705
|
+
const stat = fs.statSync(p);
|
|
706
|
+
if (stat.size < 100) return false; // stub archive — must rebuild
|
|
707
|
+
return rsFiles.every((f) => fs.statSync(f).mtimeMs < stat.mtimeMs);
|
|
708
|
+
});
|
|
709
|
+
if (allUpToDate) {
|
|
710
|
+
console.log(`[nativ] Rust: all targets up to date, skipping`);
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
const {
|
|
715
|
+
generateFunctionWrapper,
|
|
716
|
+
generateComponentWrapper,
|
|
717
|
+
} = require("./rust-compiler");
|
|
718
|
+
const {
|
|
719
|
+
extractRustExports: _extractRust,
|
|
720
|
+
} = require("../extractors/rust-extractor");
|
|
721
|
+
|
|
722
|
+
const buildBase = path.join(projectRoot, ".nativ/build");
|
|
723
|
+
const unifiedDir = path.join(buildBase, "nativ_unified");
|
|
724
|
+
fs.mkdirSync(path.join(unifiedDir, "src"), { recursive: true });
|
|
725
|
+
|
|
726
|
+
// Forward deps from root Cargo.toml
|
|
727
|
+
let rootDeps = "";
|
|
728
|
+
try {
|
|
729
|
+
const rootToml = fs.readFileSync(
|
|
730
|
+
path.join(projectRoot, "Cargo.toml"),
|
|
731
|
+
"utf8",
|
|
732
|
+
);
|
|
733
|
+
const depsSection = rootToml.match(
|
|
734
|
+
/\[dependencies\]([\s\S]*?)(?:\n\[|\n*$)/,
|
|
735
|
+
);
|
|
736
|
+
if (depsSection) {
|
|
737
|
+
rootDeps = depsSection[1].replace(/path\s*=\s*"([^"]+)"/g, (_, p) => {
|
|
738
|
+
const absPath = path.resolve(projectRoot, p);
|
|
739
|
+
const relPath = path.relative(unifiedDir, absPath);
|
|
740
|
+
return `path = "${relPath}"`;
|
|
741
|
+
});
|
|
742
|
+
}
|
|
743
|
+
} catch {}
|
|
744
|
+
|
|
745
|
+
fs.writeFileSync(
|
|
746
|
+
path.join(unifiedDir, "Cargo.toml"),
|
|
747
|
+
`[package]
|
|
748
|
+
name = "nativ-user"
|
|
749
|
+
version = "0.1.0"
|
|
750
|
+
edition = "2024"
|
|
751
|
+
|
|
752
|
+
[lib]
|
|
753
|
+
crate-type = ["staticlib"]
|
|
754
|
+
|
|
755
|
+
[workspace]
|
|
756
|
+
|
|
757
|
+
[dependencies]
|
|
758
|
+
${rootDeps}
|
|
759
|
+
|
|
760
|
+
[profile.release]
|
|
761
|
+
opt-level = "z"
|
|
762
|
+
lto = true
|
|
763
|
+
`,
|
|
764
|
+
);
|
|
765
|
+
|
|
766
|
+
const modules = [];
|
|
767
|
+
for (const filepath of rsFiles) {
|
|
768
|
+
const { functions, isComponent } = _extractRust(filepath);
|
|
769
|
+
if (!isComponent && functions.length === 0) continue;
|
|
770
|
+
|
|
771
|
+
const name = path.basename(filepath, ".rs").toLowerCase();
|
|
772
|
+
const userSrc = fs.readFileSync(filepath, "utf8");
|
|
773
|
+
|
|
774
|
+
let moduleSrc;
|
|
775
|
+
if (isComponent) {
|
|
776
|
+
moduleSrc = generateComponentWrapper(userSrc, name, { unified: true });
|
|
777
|
+
} else {
|
|
778
|
+
moduleSrc = generateFunctionWrapper(userSrc, functions, name, {
|
|
779
|
+
unified: true,
|
|
780
|
+
});
|
|
781
|
+
}
|
|
782
|
+
fs.writeFileSync(path.join(unifiedDir, "src", `${name}.rs`), moduleSrc);
|
|
783
|
+
modules.push(name);
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
if (modules.length === 0) {
|
|
787
|
+
console.log("[nativ] Rust: no exported modules found");
|
|
788
|
+
return;
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
const libRs = [
|
|
792
|
+
"// Auto-generated by React Native Native — do not edit",
|
|
793
|
+
"#![allow(unused, non_snake_case, unused_unsafe)]",
|
|
794
|
+
"",
|
|
795
|
+
"pub use nativ_core::prelude::*;",
|
|
796
|
+
"",
|
|
797
|
+
...modules.map((m) => `pub mod ${m};`),
|
|
798
|
+
"",
|
|
799
|
+
].join("\n");
|
|
800
|
+
fs.writeFileSync(path.join(unifiedDir, "src/lib.rs"), libRs);
|
|
801
|
+
|
|
802
|
+
const sharedTarget = path.join(buildBase, "cargo-target");
|
|
803
|
+
|
|
804
|
+
// Resolve NDK toolchain for Android
|
|
805
|
+
let ndkBinDir = null;
|
|
806
|
+
if (isAndroid) {
|
|
807
|
+
const androidHome =
|
|
808
|
+
process.env.ANDROID_HOME ||
|
|
809
|
+
path.join(process.env.HOME, "Library/Android/sdk");
|
|
810
|
+
const ndkDir = path.join(androidHome, "ndk");
|
|
811
|
+
try {
|
|
812
|
+
const versions = fs.readdirSync(ndkDir).sort();
|
|
813
|
+
if (versions.length > 0) {
|
|
814
|
+
const toolchain = path.join(
|
|
815
|
+
ndkDir,
|
|
816
|
+
versions[versions.length - 1],
|
|
817
|
+
"toolchains/llvm/prebuilt",
|
|
818
|
+
);
|
|
819
|
+
const hosts = fs.readdirSync(toolchain);
|
|
820
|
+
if (hosts.length > 0)
|
|
821
|
+
ndkBinDir = path.join(toolchain, hosts[0], "bin");
|
|
822
|
+
}
|
|
823
|
+
} catch {}
|
|
824
|
+
if (!ndkBinDir) {
|
|
825
|
+
console.error(
|
|
826
|
+
"[nativ] Android NDK not found — cannot build Rust for Android",
|
|
827
|
+
);
|
|
828
|
+
return;
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
for (const { abi, rust: target, linkerPrefix } of abiTargets) {
|
|
833
|
+
const outDir = isAndroid ? path.join(releaseDir, abi) : releaseDir;
|
|
834
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
835
|
+
const outputLib = path.join(outDir, "libnativ_user.a");
|
|
836
|
+
|
|
837
|
+
const home = process.env.HOME || require("os").homedir();
|
|
838
|
+
const cargoPath = fs.existsSync(path.join(home, ".cargo/bin/cargo"))
|
|
839
|
+
? path.join(home, ".cargo/bin/cargo")
|
|
840
|
+
: "cargo";
|
|
841
|
+
|
|
842
|
+
const cmd = [
|
|
843
|
+
cargoPath,
|
|
844
|
+
"build",
|
|
845
|
+
"--release",
|
|
846
|
+
"--manifest-path",
|
|
847
|
+
path.join(unifiedDir, "Cargo.toml"),
|
|
848
|
+
`--target=${target}`,
|
|
849
|
+
"--lib",
|
|
850
|
+
];
|
|
851
|
+
|
|
852
|
+
const env = { ...process.env, CARGO_TARGET_DIR: sharedTarget };
|
|
853
|
+
if (isIOS) {
|
|
854
|
+
env.RUSTFLAGS =
|
|
855
|
+
"--cfg unified -C link-arg=-undefined -C link-arg=dynamic_lookup";
|
|
856
|
+
}
|
|
857
|
+
if (isAndroid) {
|
|
858
|
+
const envKey = `CARGO_TARGET_${target.toUpperCase().replace(/-/g, "_")}_LINKER`;
|
|
859
|
+
env[envKey] = path.join(ndkBinDir, `${linkerPrefix}24-clang`);
|
|
860
|
+
env.RUSTFLAGS = "--cfg unified -C link-arg=-llog";
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
console.log(
|
|
864
|
+
`[nativ] Compiling Rust (${modules.length} modules, ${abi} → ${target})...`,
|
|
865
|
+
);
|
|
866
|
+
try {
|
|
867
|
+
execSync(cmd.join(" "), { stdio: "pipe", encoding: "utf8", env });
|
|
868
|
+
} catch (err) {
|
|
869
|
+
console.error(`[nativ] Rust compile failed for ${abi}`);
|
|
870
|
+
console.error((err.stderr || "").slice(0, 3000));
|
|
871
|
+
continue;
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
const builtLib = path.join(sharedTarget, target, "release/libnativ_user.a");
|
|
875
|
+
if (fs.existsSync(builtLib)) {
|
|
876
|
+
fs.copyFileSync(builtLib, outputLib);
|
|
877
|
+
const size = fs.statSync(outputLib).size;
|
|
878
|
+
console.log(
|
|
879
|
+
`[nativ] Built ${abi}/libnativ_user.a (${(size / 1024).toFixed(1)}KB)`,
|
|
880
|
+
);
|
|
881
|
+
} else {
|
|
882
|
+
console.error(`[nativ] libnativ_user.a not found at ${builtLib}`);
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
// ─── Kotlin wrapper generation (Android only) ──────────────────────────
|
|
888
|
+
|
|
889
|
+
function buildKotlinSources() {
|
|
890
|
+
if (!isAndroid) return [];
|
|
891
|
+
const ktFiles = findUserFiles([".kt"]);
|
|
892
|
+
if (ktFiles.length === 0) return [];
|
|
893
|
+
|
|
894
|
+
const ktSrcDir = path.join(genDir, "kotlin-src/com/nativfabric/generated");
|
|
895
|
+
fs.mkdirSync(ktSrcDir, { recursive: true });
|
|
896
|
+
const registeredModules = [];
|
|
897
|
+
|
|
898
|
+
for (const filepath of ktFiles) {
|
|
899
|
+
const { functions, isComponent } = extractKotlinExports(filepath);
|
|
900
|
+
const baseName = path.basename(filepath, ".kt");
|
|
901
|
+
const moduleId = baseName.toLowerCase();
|
|
902
|
+
const className = `NativModule_${moduleId}`;
|
|
903
|
+
const userSrc = fs.readFileSync(filepath, "utf8");
|
|
904
|
+
|
|
905
|
+
if (isComponent) {
|
|
906
|
+
const cleanSrc = userSrc
|
|
907
|
+
.replace(/\/\/\s*@nativ_component\s*\n/g, "")
|
|
908
|
+
.replace(/^package\s+[^\n]+\n/m, "")
|
|
909
|
+
.replace(/^import\s+[^\n]+\n/gm, "");
|
|
910
|
+
const userImports = [...userSrc.matchAll(/^(import\s+[^\n]+)\n/gm)].map(
|
|
911
|
+
(m) => m[1],
|
|
912
|
+
);
|
|
913
|
+
const isCompose = userSrc.includes("@Composable");
|
|
914
|
+
|
|
915
|
+
if (isCompose) {
|
|
916
|
+
const compFnMatch = cleanSrc.match(
|
|
917
|
+
/@Composable\s+fun\s+(\w+)\s*\(([^)]*)\)/,
|
|
918
|
+
);
|
|
919
|
+
const compFnName = compFnMatch ? compFnMatch[1] : baseName;
|
|
920
|
+
const compParams =
|
|
921
|
+
compFnMatch && compFnMatch[2] ? compFnMatch[2].trim() : "";
|
|
922
|
+
|
|
923
|
+
let compCall;
|
|
924
|
+
if (compParams) {
|
|
925
|
+
const compArgs = compParams
|
|
926
|
+
.split(",")
|
|
927
|
+
.map((p) => p.trim())
|
|
928
|
+
.filter(Boolean);
|
|
929
|
+
const argExprs = compArgs
|
|
930
|
+
.map((p) => {
|
|
931
|
+
const m = p.match(/(\w+)\s*:\s*(.+)/);
|
|
932
|
+
if (!m) return null;
|
|
933
|
+
const [, pName, pType] = m;
|
|
934
|
+
const t = pType.trim();
|
|
935
|
+
if (t === "String")
|
|
936
|
+
return ` ${pName} = props["${pName}"] as? String ?: ""`;
|
|
937
|
+
if (t === "Int")
|
|
938
|
+
return ` ${pName} = (props["${pName}"] as? Number)?.toInt() ?: 0`;
|
|
939
|
+
if (t === "Float" || t === "Double")
|
|
940
|
+
return ` ${pName} = (props["${pName}"] as? Number)?.toDouble() ?: 0.0`;
|
|
941
|
+
if (t === "Boolean")
|
|
942
|
+
return ` ${pName} = props["${pName}"] as? Boolean ?: false`;
|
|
943
|
+
if (t.includes("->"))
|
|
944
|
+
return ` ${pName} = {}`;
|
|
945
|
+
return ` ${pName} = props["${pName}"]`;
|
|
946
|
+
})
|
|
947
|
+
.filter(Boolean);
|
|
948
|
+
compCall = ` ${compFnName}(\n${argExprs.join(",\n")}\n )`;
|
|
949
|
+
} else {
|
|
950
|
+
compCall = ` ${compFnName}()`;
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
const wrapper = [
|
|
954
|
+
`package com.nativfabric.generated`,
|
|
955
|
+
"",
|
|
956
|
+
"import android.view.ViewGroup",
|
|
957
|
+
"import android.widget.FrameLayout",
|
|
958
|
+
"import androidx.compose.runtime.*",
|
|
959
|
+
"import androidx.compose.ui.platform.ComposeView",
|
|
960
|
+
...userImports,
|
|
961
|
+
"",
|
|
962
|
+
cleanSrc,
|
|
963
|
+
"",
|
|
964
|
+
`object ${className} {`,
|
|
965
|
+
" @JvmStatic",
|
|
966
|
+
" fun render(parent: ViewGroup, props: Map<String, Any?>) {",
|
|
967
|
+
" val composeView = ComposeView(parent.context)",
|
|
968
|
+
" composeView.setContent {",
|
|
969
|
+
compCall,
|
|
970
|
+
" }",
|
|
971
|
+
" parent.addView(composeView, FrameLayout.LayoutParams(",
|
|
972
|
+
" FrameLayout.LayoutParams.MATCH_PARENT,",
|
|
973
|
+
" FrameLayout.LayoutParams.MATCH_PARENT))",
|
|
974
|
+
" }",
|
|
975
|
+
"}",
|
|
976
|
+
].join("\n");
|
|
977
|
+
fs.writeFileSync(path.join(ktSrcDir, `${className}.kt`), wrapper);
|
|
978
|
+
} else {
|
|
979
|
+
const fnMatch = cleanSrc.match(/fun\s+(\w+)\s*\(\s*parent\s*:/);
|
|
980
|
+
const fnName = fnMatch ? fnMatch[1] : baseName;
|
|
981
|
+
const wrapper = [
|
|
982
|
+
`package com.nativfabric.generated`,
|
|
983
|
+
"",
|
|
984
|
+
...userImports,
|
|
985
|
+
"",
|
|
986
|
+
cleanSrc,
|
|
987
|
+
"",
|
|
988
|
+
`object ${className} {`,
|
|
989
|
+
" @JvmStatic",
|
|
990
|
+
" fun render(parent: android.view.ViewGroup, props: Map<String, Any?>) {",
|
|
991
|
+
` ${fnName}(parent, props)`,
|
|
992
|
+
" }",
|
|
993
|
+
"}",
|
|
994
|
+
].join("\n");
|
|
995
|
+
fs.writeFileSync(path.join(ktSrcDir, `${className}.kt`), wrapper);
|
|
996
|
+
}
|
|
997
|
+
registeredModules.push(moduleId);
|
|
998
|
+
console.log(`[nativ] Kotlin wrapper: ${moduleId} (component)`);
|
|
999
|
+
} else if (functions.length > 0) {
|
|
1000
|
+
const cleanSrc = userSrc
|
|
1001
|
+
.replace(/\/\/\s*@nativ_export\s*\([^)]*\)\s*\n/g, "")
|
|
1002
|
+
.replace(/^package\s+[^\n]+\n/m, "");
|
|
1003
|
+
|
|
1004
|
+
const lines = [
|
|
1005
|
+
`package com.nativfabric.generated`,
|
|
1006
|
+
"",
|
|
1007
|
+
cleanSrc,
|
|
1008
|
+
"",
|
|
1009
|
+
`private fun _parseArgs(json: String): List<String> {`,
|
|
1010
|
+
` val s = json.trim().removePrefix("[").removeSuffix("]")`,
|
|
1011
|
+
` if (s.isBlank()) return emptyList()`,
|
|
1012
|
+
` val result = mutableListOf<String>()`,
|
|
1013
|
+
` var i = 0; var inStr = false; var esc = false; val buf = StringBuilder()`,
|
|
1014
|
+
` while (i < s.length) { val c = s[i]; when {`,
|
|
1015
|
+
` esc -> { buf.append(c); esc = false }; c == '\\\\' -> esc = true`,
|
|
1016
|
+
` c == '"' -> inStr = !inStr; c == ',' && !inStr -> { result.add(buf.toString().trim()); buf.clear() }`,
|
|
1017
|
+
` else -> buf.append(c) }; i++ }`,
|
|
1018
|
+
` if (buf.isNotEmpty()) result.add(buf.toString().trim()); return result }`,
|
|
1019
|
+
"",
|
|
1020
|
+
`object ${className} {`,
|
|
1021
|
+
" @JvmStatic",
|
|
1022
|
+
" fun dispatch(fnName: String, argsJson: String): String {",
|
|
1023
|
+
" val args = _parseArgs(argsJson)",
|
|
1024
|
+
" return when (fnName) {",
|
|
1025
|
+
];
|
|
1026
|
+
for (const fn of functions) {
|
|
1027
|
+
const argExprs = fn.args.map((a, i) => {
|
|
1028
|
+
switch (a.type) {
|
|
1029
|
+
case "Int":
|
|
1030
|
+
return `args[${i}].toInt()`;
|
|
1031
|
+
case "Long":
|
|
1032
|
+
return `args[${i}].toLong()`;
|
|
1033
|
+
case "Float":
|
|
1034
|
+
return `args[${i}].toFloat()`;
|
|
1035
|
+
case "Double":
|
|
1036
|
+
return `args[${i}].toDouble()`;
|
|
1037
|
+
case "Boolean":
|
|
1038
|
+
return `args[${i}].toBoolean()`;
|
|
1039
|
+
default:
|
|
1040
|
+
return `args[${i}]`;
|
|
1041
|
+
}
|
|
1042
|
+
});
|
|
1043
|
+
const call = `${fn.name}(${argExprs.join(", ")})`;
|
|
1044
|
+
if (fn.ret === "String")
|
|
1045
|
+
lines.push(` "${fn.name}" -> "\\"" + ${call} + "\\""`);
|
|
1046
|
+
else if (fn.ret === "Unit")
|
|
1047
|
+
lines.push(` "${fn.name}" -> { ${call}; "null" }`);
|
|
1048
|
+
else lines.push(` "${fn.name}" -> ${call}.toString()`);
|
|
1049
|
+
}
|
|
1050
|
+
lines.push(' else -> "null"', " }", " }", "}");
|
|
1051
|
+
fs.writeFileSync(
|
|
1052
|
+
path.join(ktSrcDir, `${className}.kt`),
|
|
1053
|
+
lines.join("\n"),
|
|
1054
|
+
);
|
|
1055
|
+
registeredModules.push(moduleId);
|
|
1056
|
+
console.log(
|
|
1057
|
+
`[nativ] Kotlin wrapper: ${moduleId} (${functions.length} functions)`,
|
|
1058
|
+
);
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
return registeredModules;
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
// ─── Run ───────────────────────────────────────────────────────────────
|
|
1065
|
+
|
|
1066
|
+
const t0 = Date.now();
|
|
1067
|
+
|
|
1068
|
+
if (isIOS) {
|
|
1069
|
+
buildCombinedIOSBridges();
|
|
1070
|
+
buildRustStatic();
|
|
1071
|
+
} else {
|
|
1072
|
+
buildCppBridges();
|
|
1073
|
+
buildRustStatic();
|
|
1074
|
+
const kotlinModules = buildKotlinSources();
|
|
1075
|
+
|
|
1076
|
+
// Kotlin registry
|
|
1077
|
+
if (kotlinModules && kotlinModules.length > 0) {
|
|
1078
|
+
const ktSrcDir = path.join(
|
|
1079
|
+
genDir,
|
|
1080
|
+
"kotlin-src/com/nativfabric/generated",
|
|
1081
|
+
);
|
|
1082
|
+
const registrations = kotlinModules
|
|
1083
|
+
.map((moduleId) => {
|
|
1084
|
+
const className = `NativModule_${moduleId}`;
|
|
1085
|
+
return ` try {
|
|
1086
|
+
val clazz = Class.forName("com.nativfabric.generated.${className}")
|
|
1087
|
+
try {
|
|
1088
|
+
val dispatch = clazz.getMethod("dispatch", String::class.java, String::class.java)
|
|
1089
|
+
com.nativfabric.NativRuntime.registerKotlinDispatch("${moduleId}", dispatch)
|
|
1090
|
+
} catch (_: NoSuchMethodException) {}
|
|
1091
|
+
try {
|
|
1092
|
+
val render = clazz.getMethod("render", android.view.ViewGroup::class.java, Map::class.java)
|
|
1093
|
+
com.nativfabric.NativRuntime.registerKotlinRenderer("nativ.${moduleId}", render)
|
|
1094
|
+
} catch (_: NoSuchMethodException) {}
|
|
1095
|
+
} catch (e: Exception) {
|
|
1096
|
+
android.util.Log.w("NativRegistry", "Module ${moduleId}: \${e.message}")
|
|
1097
|
+
}`;
|
|
1098
|
+
})
|
|
1099
|
+
.join("\n");
|
|
1100
|
+
|
|
1101
|
+
fs.writeFileSync(
|
|
1102
|
+
path.join(ktSrcDir, "NativModuleRegistry.kt"),
|
|
1103
|
+
`package com.nativfabric.generated
|
|
1104
|
+
|
|
1105
|
+
object NativModuleRegistry {
|
|
1106
|
+
init {
|
|
1107
|
+
${registrations}
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
fun ensure() {} // Called to trigger class loading
|
|
1111
|
+
}
|
|
1112
|
+
`,
|
|
1113
|
+
);
|
|
1114
|
+
console.log(`[nativ] Kotlin registry: ${kotlinModules.length} modules`);
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
console.log(`[nativ] Static compilation done in ${Date.now() - t0}ms`);
|