@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.
Files changed (42) hide show
  1. package/NativFabric.podspec +41 -0
  2. package/android/build.gradle +128 -0
  3. package/android/src/main/AndroidManifest.xml +2 -0
  4. package/android/src/main/cpp/CMakeLists.txt +59 -0
  5. package/android/src/main/cpp/NativBindingsInstaller.cpp +393 -0
  6. package/android/src/main/cpp/NativRuntime.cpp +508 -0
  7. package/android/src/main/java/com/nativfabric/ComposeHost.kt +26 -0
  8. package/android/src/main/java/com/nativfabric/NativContainerPackage.kt +35 -0
  9. package/android/src/main/java/com/nativfabric/NativContainerView.kt +51 -0
  10. package/android/src/main/java/com/nativfabric/NativContainerViewManager.kt +62 -0
  11. package/android/src/main/java/com/nativfabric/NativRuntime.kt +201 -0
  12. package/android/src/main/java/com/nativfabric/NativRuntimeModule.kt +37 -0
  13. package/android/src/main/java/com/nativfabric/compose/ComposeWrappers.kt +45 -0
  14. package/app.plugin.js +159 -0
  15. package/expo-module.config.json +6 -0
  16. package/ios/NativContainerComponentView.mm +137 -0
  17. package/ios/NativRuntime.h +36 -0
  18. package/ios/NativRuntime.mm +549 -0
  19. package/metro/Nativ.h +126 -0
  20. package/metro/compilers/android-compiler.js +339 -0
  21. package/metro/compilers/dylib-compiler.js +474 -0
  22. package/metro/compilers/kotlin-compiler.js +632 -0
  23. package/metro/compilers/rust-compiler.js +722 -0
  24. package/metro/compilers/static-compiler.js +1118 -0
  25. package/metro/compilers/swift-compiler.js +363 -0
  26. package/metro/extractors/cpp-ast-extractor.js +126 -0
  27. package/metro/extractors/kotlin-extractor.js +125 -0
  28. package/metro/extractors/rust-extractor.js +118 -0
  29. package/metro/index.js +236 -0
  30. package/metro/transformer.js +649 -0
  31. package/metro/utils/bridge-generator.js +50 -0
  32. package/metro/utils/compile-commands.js +104 -0
  33. package/metro/utils/cpp-daemon.js +344 -0
  34. package/metro/utils/dts-generator.js +32 -0
  35. package/metro/utils/include-resolver.js +73 -0
  36. package/metro/utils/kotlin-daemon.js +394 -0
  37. package/metro/utils/type-mapper.js +63 -0
  38. package/package.json +43 -0
  39. package/react-native.config.js +13 -0
  40. package/src/NativContainerNativeComponent.ts +9 -0
  41. package/src/NativeNativRuntime.ts +8 -0
  42. package/src/index.ts +4 -0
@@ -0,0 +1,722 @@
1
+ /**
2
+ * Compiles a .rs file to a signed arm64 iOS cdylib.
3
+ *
4
+ * Handles two modes:
5
+ * - Component: file has `pub fn render(view, w, h)` → registers via nativ_register_render
6
+ * - Functions: file has `#[function]` annotations → registers via nativ_register_sync
7
+ */
8
+
9
+ const { execSync } = require("child_process");
10
+ const path = require("path");
11
+ const fs = require("fs");
12
+ const { extractRustExports } = require("../extractors/rust-extractor");
13
+
14
+ let _signingIdentity = null;
15
+ let _resolved = false;
16
+
17
+ function resolveOnce(projectRoot) {
18
+ if (_resolved) return;
19
+ _resolved = true;
20
+
21
+ try {
22
+ let appTeamId = null;
23
+ try {
24
+ const appJson = JSON.parse(
25
+ fs.readFileSync(path.join(projectRoot, "app.json"), "utf8"),
26
+ );
27
+ appTeamId = appJson?.expo?.ios?.appleTeamId || null;
28
+ } catch {}
29
+
30
+ if (!appTeamId) {
31
+ try {
32
+ const pbx = execSync(
33
+ `find "${projectRoot}/ios" -name "project.pbxproj" -maxdepth 3 2>/dev/null`,
34
+ { encoding: "utf8" },
35
+ )
36
+ .trim()
37
+ .split("\n")[0];
38
+ if (pbx) {
39
+ const m = fs
40
+ .readFileSync(pbx, "utf8")
41
+ .match(/DEVELOPMENT_TEAM\s*=\s*(\w+)/);
42
+ if (m) appTeamId = m[1];
43
+ }
44
+ } catch {}
45
+ }
46
+
47
+ if (appTeamId) {
48
+ const identities = execSync("security find-identity -v -p codesigning", {
49
+ encoding: "utf8",
50
+ });
51
+ const entries = [...identities.matchAll(/([A-F0-9]{40})\s+"([^"]+)"/g)];
52
+ for (const [, , name] of entries) {
53
+ try {
54
+ const subject = execSync(
55
+ `security find-certificate -c "${name}" -p 2>/dev/null | openssl x509 -noout -subject 2>/dev/null`,
56
+ { encoding: "utf8" },
57
+ );
58
+ if (subject.includes(`OU=${appTeamId}`)) {
59
+ _signingIdentity = name;
60
+ break;
61
+ }
62
+ } catch {}
63
+ }
64
+ }
65
+ } catch {}
66
+ }
67
+
68
+ let _nativeDylibBuilt = false;
69
+
70
+ /**
71
+ * Build the root `native` crate as a shared dylib containing all deps.
72
+ * Runs once per Metro session. Cargo fingerprinting handles Cargo.toml changes.
73
+ */
74
+ function ensureNativeDylib(projectRoot) {
75
+ if (_nativeDylibBuilt) return;
76
+ _nativeDylibBuilt = true;
77
+
78
+ const sharedTarget = path.join(projectRoot, ".nativ/cargo-target");
79
+ const outputDir = path.join(projectRoot, ".nativ/dylibs");
80
+ fs.mkdirSync(outputDir, { recursive: true });
81
+
82
+ console.log("[nativ] Building native.dylib (shared deps)...");
83
+ try {
84
+ execSync(
85
+ [
86
+ "cargo",
87
+ "build",
88
+ "--manifest-path",
89
+ path.join(projectRoot, "Cargo.toml"),
90
+ "--target=aarch64-apple-ios",
91
+ "--lib",
92
+ ].join(" "),
93
+ {
94
+ stdio: "pipe",
95
+ encoding: "utf8",
96
+ env: {
97
+ ...process.env,
98
+ RUSTFLAGS: "-C link-arg=-undefined -C link-arg=dynamic_lookup",
99
+ CARGO_TARGET_DIR: sharedTarget,
100
+ },
101
+ },
102
+ );
103
+
104
+ // Copy to dylibs dir
105
+ const built = path.join(
106
+ sharedTarget,
107
+ "aarch64-apple-ios/debug/libnative.dylib",
108
+ );
109
+ if (fs.existsSync(built)) {
110
+ fs.copyFileSync(built, path.join(outputDir, "native.dylib"));
111
+
112
+ // Sign
113
+ resolveOnce(projectRoot);
114
+ if (_signingIdentity) {
115
+ try {
116
+ execSync(
117
+ `codesign -fs "${_signingIdentity}" "${path.join(outputDir, "native.dylib")}"`,
118
+ { stdio: "pipe" },
119
+ );
120
+ } catch {}
121
+ }
122
+
123
+ const size = fs.statSync(path.join(outputDir, "native.dylib")).size;
124
+ console.log(
125
+ `[nativ] Built native.dylib (${(size / 1024).toFixed(1)}KB) — shared deps`,
126
+ );
127
+ }
128
+ } catch (err) {
129
+ console.error(
130
+ "[nativ] native.dylib build failed:",
131
+ (err.stderr || "").slice(0, 1000),
132
+ );
133
+ }
134
+ }
135
+
136
+ /**
137
+ * Ensure the per-file Cargo crate exists and src/lib.rs is up to date.
138
+ * Shared between iOS and Android compilers.
139
+ * Returns { crateDir, moduleId, isComponent, functions } or null if no exports.
140
+ */
141
+ function ensureRustCrate(filepath, projectRoot) {
142
+ const name = path.basename(filepath, ".rs");
143
+ const moduleId = name.toLowerCase();
144
+
145
+ const userSrc = fs.readFileSync(filepath, "utf8");
146
+ const { functions, isComponent } = extractRustExports(filepath);
147
+
148
+ if (!isComponent && functions.length === 0) {
149
+ console.warn(
150
+ `[nativ] ${name}.rs: no #[component] or #[function] found, skipping`,
151
+ );
152
+ return null;
153
+ }
154
+
155
+ // The user's Cargo.toml must exist — created by `npx @react-native-native/cli setup-rust`
156
+ const cargoTomlPath = path.join(projectRoot, "Cargo.toml");
157
+ if (!fs.existsSync(cargoTomlPath)) {
158
+ console.error(
159
+ "[nativ] No Cargo.toml found. Run: npx @react-native-native/cli setup-rust",
160
+ );
161
+ return null;
162
+ }
163
+
164
+ // Per-file build crate
165
+ const crateDir = path.join(projectRoot, ".nativ/build", moduleId);
166
+ fs.mkdirSync(path.join(crateDir, "src"), { recursive: true });
167
+
168
+ const buildCargoPath = path.join(crateDir, "Cargo.toml");
169
+
170
+ // Forward ALL deps from root Cargo.toml (including target-specific)
171
+ let rootDeps = "";
172
+ let targetDeps = "";
173
+ try {
174
+ const rootToml = fs.readFileSync(
175
+ path.join(projectRoot, "Cargo.toml"),
176
+ "utf8",
177
+ );
178
+
179
+ // [dependencies] section
180
+ const depsSection = rootToml.match(
181
+ /\[dependencies\]([\s\S]*?)(?:\n\[|\n*$)/,
182
+ );
183
+ if (depsSection) {
184
+ rootDeps = depsSection[1].replace(/path\s*=\s*"([^"]+)"/g, (_, p) => {
185
+ const absPath = path.resolve(projectRoot, p);
186
+ const relPath = path.relative(crateDir, absPath);
187
+ return `path = "${relPath}"`;
188
+ });
189
+ }
190
+
191
+ // [target.'cfg(...)'.dependencies] sections — forward as-is with path rewriting
192
+ const targetSections = rootToml.matchAll(
193
+ /(\[target\.[^\]]+\.dependencies\])([\s\S]*?)(?=\n\[|\n*$)/g,
194
+ );
195
+ for (const m of targetSections) {
196
+ const header = m[1];
197
+ const deps = m[2].replace(/path\s*=\s*"([^"]+)"/g, (_, p) => {
198
+ const absPath = path.resolve(projectRoot, p);
199
+ const relPath = path.relative(crateDir, absPath);
200
+ return `path = "${relPath}"`;
201
+ });
202
+ targetDeps += `\n${header}${deps}\n`;
203
+ }
204
+ } catch {}
205
+
206
+ const buildCargo = `[package]
207
+ name = "nativ-${moduleId}"
208
+ version = "0.1.0"
209
+ edition = "2024"
210
+
211
+ [lib]
212
+ crate-type = ["cdylib"]
213
+
214
+ [workspace]
215
+
216
+ [dependencies]
217
+ ${rootDeps}
218
+ ${targetDeps}
219
+
220
+ [profile.dev]
221
+ opt-level = 1
222
+ `;
223
+ fs.writeFileSync(buildCargoPath, buildCargo);
224
+
225
+ // Write src/lib.rs
226
+ let libSrc;
227
+ if (isComponent) {
228
+ libSrc = userSrc;
229
+ } else {
230
+ libSrc = generateFunctionWrapper(userSrc, functions, moduleId);
231
+ }
232
+ const libPath = path.join(crateDir, "src/lib.rs");
233
+ fs.writeFileSync(libPath, libSrc);
234
+
235
+ return { crateDir, moduleId, isComponent, functions };
236
+ }
237
+
238
+ function compileRustDylib(filepath, projectRoot, { target = "device" } = {}) {
239
+ resolveOnce(projectRoot);
240
+
241
+ const crate = ensureRustCrate(filepath, projectRoot);
242
+ if (!crate) return null;
243
+
244
+ const { crateDir, moduleId, isComponent, functions } = crate;
245
+ const rustTarget =
246
+ target === "simulator" ? "aarch64-apple-ios-sim" : "aarch64-apple-ios";
247
+ const outputDir = path.join(projectRoot, ".nativ/dylibs", target);
248
+ fs.mkdirSync(outputDir, { recursive: true });
249
+ const dylibPath = path.join(outputDir, `nativ_${moduleId}.dylib`);
250
+
251
+ const sharedTarget = path.join(projectRoot, ".nativ/cargo-target");
252
+ const cargoOutDir = path.join(sharedTarget, `${rustTarget}/debug`);
253
+
254
+ const name = path.basename(filepath, ".rs");
255
+
256
+ // Force rebuild: remove the old dylib so Cargo can't skip
257
+ const oldDylib = path.join(cargoOutDir, `libnativ_${moduleId}.dylib`);
258
+ try {
259
+ fs.unlinkSync(oldDylib);
260
+ } catch {}
261
+
262
+ const cmd = [
263
+ "cargo",
264
+ "build",
265
+ "--manifest-path",
266
+ path.join(crateDir, "Cargo.toml"),
267
+ `--target=${rustTarget}`,
268
+ "--lib",
269
+ ];
270
+
271
+ const rustFlags = "-C link-arg=-undefined -C link-arg=dynamic_lookup";
272
+
273
+ console.log(`[nativ] Compiling ${name}.rs via cargo...`);
274
+ try {
275
+ const output = execSync(cmd.join(" "), {
276
+ stdio: "pipe",
277
+ encoding: "utf8",
278
+ env: {
279
+ ...process.env,
280
+ RUSTFLAGS: rustFlags,
281
+ CARGO_TARGET_DIR: sharedTarget,
282
+ },
283
+ });
284
+ if (output) console.log(output.trim());
285
+ } catch (err) {
286
+ console.error(`[nativ] Rust compile failed: ${name}.rs`);
287
+ console.error((err.stderr || "").slice(0, 5000));
288
+ return null;
289
+ }
290
+
291
+ // Copy the built dylib from Cargo's target dir to our dylib output dir
292
+ const builtDylib = path.join(cargoOutDir, `libnativ_${moduleId}.dylib`);
293
+ if (!fs.existsSync(builtDylib)) {
294
+ console.error(`[nativ] Built dylib not found: ${builtDylib}`);
295
+ return null;
296
+ }
297
+ fs.copyFileSync(builtDylib, dylibPath);
298
+
299
+ if (_signingIdentity) {
300
+ try {
301
+ execSync(`codesign -fs "${_signingIdentity}" "${dylibPath}"`, {
302
+ stdio: "pipe",
303
+ });
304
+ } catch {}
305
+ }
306
+
307
+ const size = fs.statSync(dylibPath).size;
308
+ console.log(
309
+ `[nativ] Built nativ_${moduleId}.dylib (${(size / 1024).toFixed(1)}KB)`,
310
+ );
311
+ return { dylibPath, isComponent, functions };
312
+ }
313
+
314
+ // ─── Component wrapper ─────────────────────────────────────────────────
315
+
316
+ function generateComponentWrapper(userSrc, moduleId, { unified = false } = {}) {
317
+ const componentId = `nativ.${moduleId}`;
318
+
319
+ // In unified mode, nativ-core is available via the crate root.
320
+ // The #[component] proc macro handles render function generation,
321
+ // prop extraction, and registration. Just pass through user source.
322
+ if (unified) {
323
+ return `#![allow(unused, non_snake_case, unused_unsafe)]
324
+ use crate::*;
325
+
326
+ ${userSrc}
327
+ `;
328
+ }
329
+
330
+ // ── Standalone mode (dev hot-reload) ─────────────────────────────────
331
+ // nativ-core can't be linked into a hot-reload dylib, so all types
332
+ // are defined inline and #[component] is stripped.
333
+
334
+ // Extract struct name from #[component] pub struct Foo;
335
+ const structMatch = userSrc.match(/#\[component\]\s*pub\s+struct\s+(\w+)/);
336
+ const structName = structMatch ? structMatch[1] : "Component";
337
+
338
+ // Strip #[component] attribute — it's not real Rust
339
+ const cleanSrc = userSrc.replace(/#\[component\]\s*/g, "");
340
+
341
+ const typeImports = `use nativ_core::prelude::*;`;
342
+
343
+ const inlineTypes = `
344
+ // ─── ObjC runtime FFI ──────────────────────────────────────────────────
345
+ #[link(name = "objc", kind = "dylib")]
346
+ unsafe extern "C" {
347
+ fn objc_getClass(name: *const c_char) -> *mut c_void;
348
+ fn sel_registerName(name: *const c_char) -> *mut c_void;
349
+ fn objc_msgSend() -> *mut c_void;
350
+ }
351
+
352
+ type MsgSendPtr = unsafe extern "C" fn(*mut c_void, *mut c_void) -> *mut c_void;
353
+ type MsgSendVoidPtr = unsafe extern "C" fn(*mut c_void, *mut c_void, *mut c_void);
354
+
355
+ fn _class(name: &str) -> *mut c_void {
356
+ let c = CString::new(name).unwrap();
357
+ unsafe { objc_getClass(c.as_ptr()) }
358
+ }
359
+ fn _sel(name: &str) -> *mut c_void {
360
+ let c = CString::new(name).unwrap();
361
+ unsafe { sel_registerName(c.as_ptr()) }
362
+ }
363
+ fn _new(cls: &str) -> *mut c_void {
364
+ let alloc: MsgSendPtr = unsafe { std::mem::transmute(objc_msgSend as *mut c_void) };
365
+ let init: MsgSendPtr = unsafe { std::mem::transmute(objc_msgSend as *mut c_void) };
366
+ unsafe { init(alloc(_class(cls), _sel("alloc")), _sel("init")) }
367
+ }
368
+ fn _nsstring(s: &str) -> *mut c_void {
369
+ let cstr = CString::new(s).unwrap();
370
+ let send: unsafe extern "C" fn(*mut c_void, *mut c_void, *const c_char) -> *mut c_void =
371
+ unsafe { std::mem::transmute(objc_msgSend as *mut c_void) };
372
+ unsafe { send(_class("NSString"), _sel("stringWithUTF8String:"), cstr.as_ptr()) }
373
+ }
374
+ fn _uicolor(r: f64, g: f64, b: f64, a: f64) -> *mut c_void {
375
+ let send: unsafe extern "C" fn(*mut c_void, *mut c_void, f64, f64, f64, f64) -> *mut c_void =
376
+ unsafe { std::mem::transmute(objc_msgSend as *mut c_void) };
377
+ unsafe { send(_class("UIColor"), _sel("colorWithRed:green:blue:alpha:"), r, g, b, a) }
378
+ }
379
+
380
+ pub struct NativeViewHandle {
381
+ view: *mut c_void,
382
+ width: f32,
383
+ height: f32,
384
+ }
385
+
386
+ impl NativeViewHandle {
387
+ fn new(view: *mut c_void, width: f32, height: f32) -> Self {
388
+ Self { view, width, height }
389
+ }
390
+
391
+ pub fn set_background_color(&self, r: f64, g: f64, b: f64, a: f64) {
392
+ let color = _uicolor(r, g, b, a);
393
+ let send: MsgSendVoidPtr = unsafe { std::mem::transmute(objc_msgSend as *mut c_void) };
394
+ unsafe { send(self.view, _sel("setBackgroundColor:"), color); }
395
+ }
396
+
397
+ pub fn add_label(&self, text: &str, r: f64, g: f64, b: f64) {
398
+ let label = _new("UILabel");
399
+ let w = self.width as f64;
400
+ let h = self.height as f64;
401
+
402
+ let set_text: MsgSendVoidPtr = unsafe { std::mem::transmute(objc_msgSend as *mut c_void) };
403
+ unsafe { set_text(label, _sel("setText:"), _nsstring(text)); }
404
+
405
+ let color = _uicolor(r, g, b, 1.0);
406
+ unsafe { set_text(label, _sel("setTextColor:"), color); }
407
+
408
+ let set_align: unsafe extern "C" fn(*mut c_void, *mut c_void, i64) =
409
+ unsafe { std::mem::transmute(objc_msgSend as *mut c_void) };
410
+ unsafe { set_align(label, _sel("setTextAlignment:"), 1); }
411
+
412
+ let set_frame: unsafe extern "C" fn(*mut c_void, *mut c_void, f64, f64, f64, f64) =
413
+ unsafe { std::mem::transmute(objc_msgSend as *mut c_void) };
414
+ unsafe { set_frame(label, _sel("setFrame:"), 0.0, 0.0, w, h); }
415
+
416
+ let add_sub: MsgSendVoidPtr = unsafe { std::mem::transmute(objc_msgSend as *mut c_void) };
417
+ unsafe { add_sub(self.view, _sel("addSubview:"), label); }
418
+ }
419
+ }
420
+
421
+ pub trait NativeView {
422
+ fn mount(&mut self, view: NativeViewHandle);
423
+ }
424
+ `;
425
+
426
+ return `
427
+ #![allow(unused, non_snake_case, unused_unsafe)]
428
+ use std::ffi::{c_void, c_char, c_float, CString};
429
+ ${typeImports}
430
+ ${inlineTypes}
431
+ // ─── User code ─────────────────────────────────────────────────────────
432
+ ${cleanSrc}
433
+
434
+ // ─── Registration ──────────────────────────────────────────────────────
435
+ unsafe extern "C" {
436
+ fn nativ_register_render(id: *const c_char, f: unsafe extern "C" fn(*mut c_void, c_float, c_float));
437
+ }
438
+
439
+ #[unsafe(no_mangle)]
440
+ pub unsafe extern "C" fn nativ_${moduleId}_render(view: *mut c_void, w: c_float, h: c_float) {
441
+ let handle = NativeViewHandle::new(view, w as f32, h as f32);
442
+ let mut component = ${structName};
443
+ component.mount(handle);
444
+ }
445
+
446
+ #[used]
447
+ #[cfg_attr(target_os = "ios", unsafe(link_section = "__DATA,__mod_init_func"))]
448
+ static REGISTER: extern "C" fn() = {
449
+ extern "C" fn register() {
450
+ let id = CString::new("${componentId}").unwrap();
451
+ unsafe { nativ_register_render(id.as_ptr(), nativ_${moduleId}_render); }
452
+ }
453
+ register
454
+ };
455
+ `;
456
+ }
457
+
458
+ // ─── Function wrapper ──────────────────────────────────────────────────
459
+
460
+ function generateFunctionWrapper(
461
+ userSrc,
462
+ functions,
463
+ moduleId,
464
+ { unified = false } = {},
465
+ ) {
466
+ const argParsers = [];
467
+ const registrations = [];
468
+
469
+ const asyncParsers = [];
470
+ const asyncRegistrations = [];
471
+
472
+ for (const fn of functions) {
473
+ // Generate a C ABI wrapper that parses JSON args and calls the Rust function
474
+ const argExtractions = fn.args.map((arg, i) => {
475
+ const t = arg.type;
476
+ if (t === "String" || t === "&str") {
477
+ return ` let ${arg.name}: String = _parse_string(&mut p);`;
478
+ } else if (t === "bool") {
479
+ return ` let ${arg.name}: bool = _parse_number(&mut p) != 0.0;`;
480
+ } else {
481
+ return ` let ${arg.name}: ${t} = _parse_number(&mut p) as ${t};`;
482
+ }
483
+ });
484
+
485
+ const argNames = fn.args.map((a) => {
486
+ if (a.type === "&str") return `&${a.name}`;
487
+ return a.name;
488
+ });
489
+
490
+ const retConvert = (() => {
491
+ const rt = fn.ret;
492
+ if (rt === "()") return " let _result = ";
493
+ if (rt.startsWith("Result<")) return " let _result = ";
494
+ return " let _result = ";
495
+ })();
496
+
497
+ const retSerialize = (() => {
498
+ const rt = fn.ret;
499
+ if (rt === "()" || rt === "void") return '"null"';
500
+ if (rt === "String" || rt === "&str") {
501
+ return 'format!("\\\"{}\\\"", _result)'; // JSON string
502
+ }
503
+ if (rt === "bool") return 'if _result { "true" } else { "false" }';
504
+ if (rt.startsWith("Result<")) {
505
+ // Unwrap the Result
506
+ return null; // handled specially below
507
+ }
508
+ return "_result.to_string()"; // numbers
509
+ })();
510
+
511
+ const isResult = fn.ret.startsWith("Result<");
512
+
513
+ if (fn.async) {
514
+ // Async wrapper: receives resolve/reject C function pointers
515
+ asyncParsers.push(`
516
+ #[unsafe(no_mangle)]
517
+ pub extern "C" fn nativ_rust_async_${moduleId}_${fn.name}(
518
+ args_json: *const c_char,
519
+ resolve: extern "C" fn(*const c_char),
520
+ reject: extern "C" fn(*const c_char, *const c_char),
521
+ ) {
522
+ let args_str = unsafe { std::ffi::CStr::from_ptr(args_json).to_str().unwrap_or("[]") };
523
+ let mut p = args_str;
524
+ if let Some(i) = p.find('[') { p = &p[i+1..]; }
525
+ ${argExtractions.join("\n")}
526
+ let _result = ${fn.name}(${argNames.join(", ")});
527
+ let json = serde_like_serialize(&_result);
528
+ let c = CString::new(json).unwrap();
529
+ resolve(c.as_ptr());
530
+ }`);
531
+
532
+ asyncRegistrations.push(
533
+ ` nativ_register_async(
534
+ CString::new("nativ.${moduleId}").unwrap().as_ptr(),
535
+ CString::new("${fn.name}").unwrap().as_ptr(),
536
+ nativ_rust_async_${moduleId}_${fn.name},
537
+ );`,
538
+ );
539
+ continue;
540
+ }
541
+
542
+ argParsers.push(`
543
+ #[unsafe(no_mangle)]
544
+ pub extern "C" fn nativ_rust_${moduleId}_${fn.name}(args_json: *const c_char) -> *const c_char {
545
+ let args_str = unsafe { std::ffi::CStr::from_ptr(args_json).to_str().unwrap_or("[]") };
546
+ let mut p = args_str;
547
+ // Skip to first [
548
+ if let Some(i) = p.find('[') { p = &p[i+1..]; }
549
+ ${argExtractions.join("\n")}
550
+ let _result = ${fn.name}(${argNames.join(", ")});
551
+ ${
552
+ isResult
553
+ ? `
554
+ match _result {
555
+ Ok(v) => {
556
+ let json = format!("{}", serde_like_serialize(&v));
557
+ let c = CString::new(json).unwrap();
558
+ c.into_raw()
559
+ }
560
+ Err(e) => {
561
+ let json = format!("null");
562
+ let c = CString::new(json).unwrap();
563
+ c.into_raw()
564
+ }
565
+ }`
566
+ : `
567
+ let json = serde_like_serialize(&_result);
568
+ let c = CString::new(json).unwrap();
569
+ c.into_raw()`
570
+ }
571
+ }`);
572
+
573
+ registrations.push(
574
+ ` nativ_register_sync(
575
+ CString::new("nativ.${moduleId}").unwrap().as_ptr(),
576
+ CString::new("${fn.name}").unwrap().as_ptr(),
577
+ nativ_rust_${moduleId}_${fn.name},
578
+ );`,
579
+ );
580
+ }
581
+
582
+ return `
583
+ #![allow(unused, non_snake_case)]
584
+ use std::ffi::{c_void, c_char, CString};
585
+
586
+ // Minimal JSON arg parsing
587
+ fn _parse_number(p: &mut &str) -> f64 {
588
+ // Skip whitespace and commas
589
+ *p = p.trim_start_matches(|c: char| c == ' ' || c == ',' || c == '[' || c == ']');
590
+ if p.is_empty() { return 0.0; }
591
+ let end = p.find(|c: char| c == ',' || c == ']' || c == ' ').unwrap_or(p.len());
592
+ let num_str = &p[..end];
593
+ *p = &p[end..];
594
+ num_str.parse::<f64>().unwrap_or(0.0)
595
+ }
596
+
597
+ fn _parse_string(p: &mut &str) -> String {
598
+ // Skip to opening quote
599
+ if let Some(i) = p.find('"') {
600
+ *p = &p[i+1..];
601
+ }
602
+ let mut s = String::new();
603
+ let bytes = p.as_bytes();
604
+ let mut i = 0;
605
+ while i < bytes.len() {
606
+ if bytes[i] == b'"' { *p = &p[i+1..]; return s; }
607
+ if bytes[i] == b'\\\\' && i + 1 < bytes.len() {
608
+ i += 1;
609
+ s.push(bytes[i] as char);
610
+ } else {
611
+ s.push(bytes[i] as char);
612
+ }
613
+ i += 1;
614
+ }
615
+ s
616
+ }
617
+
618
+ // Simple serialization
619
+ trait SerializeLike { fn serialize(&self) -> String; }
620
+ impl SerializeLike for i32 { fn serialize(&self) -> String { self.to_string() } }
621
+ impl SerializeLike for i64 { fn serialize(&self) -> String { self.to_string() } }
622
+ impl SerializeLike for u32 { fn serialize(&self) -> String { self.to_string() } }
623
+ impl SerializeLike for f32 { fn serialize(&self) -> String { self.to_string() } }
624
+ impl SerializeLike for f64 { fn serialize(&self) -> String { self.to_string() } }
625
+ impl SerializeLike for bool { fn serialize(&self) -> String { if *self { "true".into() } else { "false".into() } } }
626
+ impl SerializeLike for String { fn serialize(&self) -> String { format!("\\\"{}\\\"", self) } }
627
+ impl SerializeLike for () { fn serialize(&self) -> String { "null".into() } }
628
+
629
+ fn serde_like_serialize<T: SerializeLike>(v: &T) -> String { v.serialize() }
630
+
631
+ // Registry
632
+ type NativSyncFn = extern "C" fn(*const c_char) -> *const c_char;
633
+ type NativAsyncFn = extern "C" fn(*const c_char, extern "C" fn(*const c_char), extern "C" fn(*const c_char, *const c_char));
634
+
635
+ // On iOS: direct extern reference (resolved via -undefined dynamic_lookup)
636
+ #[cfg(target_os = "ios")]
637
+ unsafe extern "C" {
638
+ fn nativ_register_sync(module_id: *const c_char, fn_name: *const c_char, f: NativSyncFn);
639
+ fn nativ_register_async(module_id: *const c_char, fn_name: *const c_char, f: NativAsyncFn);
640
+ }
641
+
642
+ // ─── User code ─────────────────────────────────────────────────────────
643
+ ${userSrc.replace(/#\[function[^\]]*\]/g, "")}
644
+
645
+ // ─── C ABI wrappers ───────────────────────────────────────────────────
646
+ ${argParsers.join("\n")}
647
+ ${asyncParsers.join("\n")}
648
+
649
+ // ─── Registration ──────────────────────────────────────────────────────
650
+
651
+ // On iOS: auto-register via constructor (symbols resolved via -undefined dynamic_lookup)
652
+ #[cfg(target_os = "ios")]
653
+ #[used]
654
+ #[unsafe(link_section = "__DATA,__mod_init_func")]
655
+ static REGISTER: extern "C" fn() = {
656
+ extern "C" fn register() {
657
+ unsafe {
658
+ ${registrations.join("\n")}
659
+ ${asyncRegistrations.join("\n")}
660
+ }
661
+ }
662
+ register
663
+ };
664
+
665
+ // On Android dev: host calls nativ_init after dlopen, passing the registry function pointer.
666
+ // Android linker namespaces prevent dlsym(RTLD_DEFAULT) from finding host symbols.
667
+ #[cfg(all(target_os = "android", not(unified)))]
668
+ static mut NATIV_REGISTER_SYNC: Option<unsafe extern "C" fn(*const c_char, *const c_char, NativSyncFn)> = None;
669
+ #[cfg(all(target_os = "android", not(unified)))]
670
+ static mut NATIV_REGISTER_ASYNC: Option<unsafe extern "C" fn(*const c_char, *const c_char, NativAsyncFn)> = None;
671
+
672
+ #[cfg(all(target_os = "android", not(unified)))]
673
+ #[unsafe(no_mangle)]
674
+ pub extern "C" fn nativ_init(reg_fn: *mut std::ffi::c_void) {
675
+ unsafe {
676
+ NATIV_REGISTER_SYNC = Some(std::mem::transmute(reg_fn));
677
+ if let Some(reg) = NATIV_REGISTER_SYNC {
678
+ ${registrations.map((r) => r.replace(/nativ_register_sync/g, "reg")).join("\n")}
679
+ }
680
+ }
681
+ }
682
+
683
+ #[cfg(all(target_os = "android", not(unified)))]
684
+ #[unsafe(no_mangle)]
685
+ pub extern "C" fn nativ_init_async(reg_fn: *mut std::ffi::c_void) {
686
+ unsafe {
687
+ NATIV_REGISTER_ASYNC = Some(std::mem::transmute(reg_fn));
688
+ if let Some(reg) = NATIV_REGISTER_ASYNC {
689
+ ${asyncRegistrations.map((r) => r.replace(/nativ_register_async/g, "reg")).join("\n")}
690
+ }
691
+ }
692
+ }
693
+
694
+ // On Android production (unified crate): direct extern, constructor registration.
695
+ // All code is in the same .so — symbols resolved at link time.
696
+ #[cfg(all(target_os = "android", unified))]
697
+ unsafe extern "C" {
698
+ fn nativ_register_sync(module_id: *const c_char, fn_name: *const c_char, f: NativSyncFn);
699
+ fn nativ_register_async(module_id: *const c_char, fn_name: *const c_char, f: NativAsyncFn);
700
+ }
701
+
702
+ #[cfg(all(target_os = "android", unified))]
703
+ #[used]
704
+ #[unsafe(link_section = ".init_array")]
705
+ static REGISTER_ANDROID: extern "C" fn() = {
706
+ extern "C" fn register() {
707
+ unsafe {
708
+ ${registrations.join("\n")}
709
+ ${asyncRegistrations.join("\n")}
710
+ }
711
+ }
712
+ register
713
+ };
714
+ `;
715
+ }
716
+
717
+ module.exports = {
718
+ compileRustDylib,
719
+ ensureRustCrate,
720
+ generateFunctionWrapper,
721
+ generateComponentWrapper,
722
+ };