Skip to main content

nodejs/
aot_native.rs

1//! Native AOT for node-js via `fusevm::aot` (`node --build`).
2//!
3//! Mirrors the pythonrs/elisprs/vimlrs approach: lower the program to a
4//! `fusevm::Chunk`, embed the function/try tables (which live on the host, not
5//! the chunk) as a JSON image inside `chunk.names`, emit a relocatable object
6//! with `fusevm::aot::compile_object`, then link it against the node-js runtime
7//! staticlib (which carries fusevm's AOT runtime + this module's
8//! `fusevm_aot_register_builtins`) and a tiny C entry into a standalone
9//! executable.
10//!
11//! The node-js catch is simpler than elisp's: node-js chunk constants are native
12//! `Value::Str`/`Int`/`Float` only (JS strings/arrays/objects are built at
13//! runtime via `MKSTR`/`MKARR`/`MKOBJ`), so there is no heap image to
14//! reconstruct — the only host state a chunk depends on is the function/try
15//! tables, which the image below restores before the main chunk runs.
16
17use crate::compiler::Program;
18use crate::host::{self, FuncDef, TryDef};
19use fusevm::VM;
20use std::path::{Path, PathBuf};
21use std::sync::Arc;
22
23/// Marker prefix for the embedded program image in `chunk.names`.
24const PROG_IMAGE_TAG: &str = "\u{1}node-js-prog-image:";
25
26#[derive(serde::Serialize, serde::Deserialize)]
27struct ProgImage {
28    functions: Vec<FuncDef>,
29    tries: Vec<TryDef>,
30}
31
32/// Compile a program to a standalone native executable at `out`.
33pub fn emit_executable(prog: &Program, out: &Path) -> Result<(), String> {
34    let obj = std::env::temp_dir().join("node_js_aot.o");
35    emit_object(prog, &obj)?;
36
37    let main_c = std::env::temp_dir().join("node_js_aot_main.c");
38    std::fs::write(
39        &main_c,
40        "extern long fusevm_aot_run_embedded(void);\n\
41         int main(void) { return (int)fusevm_aot_run_embedded(); }\n",
42    )
43    .map_err(|e| e.to_string())?;
44
45    let lib = staticlib_path()?;
46    let mut cmd = std::process::Command::new("cc");
47    cmd.arg(&main_c).arg(&obj).arg(&lib).arg("-o").arg(out);
48    if cfg!(target_os = "macos") {
49        cmd.args([
50            // NOTE: the linker prints a benign "no platform load command found in
51            // <aot>.o, assuming: macOS" — the cranelift-object `.o` emitted by
52            // fusevm::aot has no LC_BUILD_VERSION. It is cosmetic (the executable
53            // links and runs correctly). The real fix is upstream in fusevm::aot
54            // (stamp the Mach-O platform at object emission); a node-js-side
55            // -Wl,-platform_version only introduces conflicting-version warnings,
56            // so we deliberately do NOT pass one here.
57            "-framework",
58            "CoreFoundation",
59            "-framework",
60            "Security",
61            "-liconv",
62            "-lc++",
63        ]);
64    } else {
65        cmd.args(["-lpthread", "-ldl", "-lm", "-lrt"]);
66    }
67    let status = cmd.status().map_err(|e| format!("cc: {e}"))?;
68    if !status.success() {
69        return Err(format!("link failed (cc exit {:?})", status.code()));
70    }
71    Ok(())
72}
73
74/// Emit just the relocatable AOT object (embedding the program image).
75fn emit_object(prog: &Program, obj: &Path) -> Result<(), String> {
76    let mut chunk = prog.main.clone();
77    let image = ProgImage {
78        functions: prog.functions.iter().map(|(_, f)| f.clone()).collect(),
79        tries: prog.tries.clone(),
80    };
81    let json = serde_json::to_string(&image).map_err(|e| e.to_string())?;
82    chunk.names.push(format!("{PROG_IMAGE_TAG}{json}"));
83    fusevm::aot::compile_object(&chunk, obj).map_err(|e| format!("node-js --build: {e}"))
84}
85
86/// Locate `libnodejs.a` (a sibling of the running `node` binary, or
87/// `$NODE_JS_STATICLIB`).
88fn staticlib_path() -> Result<PathBuf, String> {
89    if let Ok(p) = std::env::var("NODE_JS_STATICLIB") {
90        return Ok(PathBuf::from(p));
91    }
92    let exe = std::env::current_exe().map_err(|e| e.to_string())?;
93    let lib = exe.parent().ok_or("no exe dir")?.join("libnodejs.a");
94    if lib.exists() {
95        Ok(lib)
96    } else {
97        Err(format!(
98            "libnodejs.a not found next to {}; build the staticlib or set NODE_JS_STATICLIB",
99            exe.display()
100        ))
101    }
102}
103
104/// The AOT runtime hook: install the node-js builtins + numeric hook and reload
105/// the embedded function/try tables before the main chunk runs. Required link
106/// symbol for a standalone node-js AOT binary.
107///
108/// # Safety
109/// `vm` must be a valid, exclusively-borrowable pointer (fusevm's AOT entry
110/// passes one).
111#[no_mangle]
112pub unsafe extern "C" fn fusevm_aot_register_builtins(vm: *mut VM) {
113    let vm = unsafe { &mut *vm };
114    crate::builtins::install(vm);
115    vm.set_numeric_hook(Arc::new(crate::builtins::numeric_hook));
116    let images: Vec<ProgImage> = vm
117        .chunk
118        .names
119        .iter()
120        .filter_map(|n| n.strip_prefix(PROG_IMAGE_TAG))
121        .filter_map(|j| serde_json::from_str(j).ok())
122        .collect();
123    host::with_host(|h| {
124        for img in images {
125            h.load_program(img.functions, img.tries);
126        }
127    });
128}