Skip to main content

relon_codegen_llvm/
wasm_link.rs

1//! S3.X wasm32 link step: turn an LLVM-emitted **relocatable** wasm
2//! object (`\0asm` with a `linking` custom section, undefined symbols,
3//! no exports / no memory) into an **instantiable** wasm module.
4//!
5//! `LlvmAotEvaluator::emit_object_for_target(.., CodegenTarget::Wasm32)`
6//! writes a relocatable object — the LLVM WebAssembly backend emits the
7//! same object-file shape `clang -c --target=wasm32` produces. wasmtime
8//! cannot instantiate that directly; it needs the linker pass that
9//! materialises the `memory`, the `globals` (stack pointer), and the
10//! function `export`s. We shell out to `wasm-ld` for that, mirroring how
11//! a `clang --target=wasm32` toolchain finishes the build.
12//!
13//! `wasm-ld` is the LLVM linker shipped with the `lld` package; we probe
14//! the common binary names (`wasm-ld`, `wasm-ld-NN`). The relocatable
15//! wasm object format is stable across recent LLVM majors, so a system
16//! `wasm-ld-17` happily links an LLVM-18-emitted object.
17
18use std::path::Path;
19use std::process::Command;
20
21use crate::error::LlvmError;
22
23/// Candidate `wasm-ld` binary names, most-specific first. The LLVM-18
24/// build the emitter uses doesn't ship `wasm-ld` in `/usr/lib/llvm-18`
25/// on every distro, but the wasm object format is forward/back-compatible
26/// across these majors for the link step.
27const WASM_LD_CANDIDATES: &[&str] = &[
28    "wasm-ld",
29    "wasm-ld-18",
30    "wasm-ld-17",
31    "wasm-ld-19",
32    "wasm-ld-16",
33];
34
35/// Locate a usable `wasm-ld` on `PATH`. Returns the binary name (for
36/// `Command::new`) or `None` when no candidate responds to `--version`.
37pub fn find_wasm_ld() -> Option<String> {
38    for name in WASM_LD_CANDIDATES {
39        let ok = Command::new(name)
40            .arg("--version")
41            .output()
42            .map(|o| o.status.success())
43            .unwrap_or(false);
44        if ok {
45            return Some((*name).to_string());
46        }
47    }
48    None
49}
50
51/// Link a relocatable wasm object (`obj_path`) into an instantiable
52/// wasm module written to `out_path`, exporting `entry_symbol` and the
53/// linear `memory`.
54///
55/// Flags:
56/// - `--no-entry`: there is no `_start` / `main`; the module is a
57///   library whose entry is the exported relon symbol.
58/// - `--export=<entry_symbol>` + `--export=__heap_base`: surface the
59///   relon entry (and the heap base, useful for the buffer-arena
60///   handshake) to the host.
61/// - `--allow-undefined`: tolerate unresolved imports (e.g. a future
62///   WASI host fn) — they become wasm `import`s the host satisfies.
63/// - `--export-memory` (implicit default) yields the `memory` export
64///   wasmtime reads for the arena handshake.
65pub fn link_wasm_object(
66    obj_path: &Path,
67    out_path: &Path,
68    entry_symbol: &str,
69) -> Result<(), LlvmError> {
70    let ld = find_wasm_ld().ok_or_else(|| {
71        LlvmError::Codegen(
72            "wasm-ld not found on PATH (install `lld` / `wasm-ld`); required to link the \
73             relocatable wasm32 object into an instantiable module"
74                .into(),
75        )
76    })?;
77    let output = Command::new(&ld)
78        .arg("--no-entry")
79        .arg("--allow-undefined")
80        .arg(format!("--export={entry_symbol}"))
81        // `__heap_base` is a synthetic global the linker emits marking
82        // the first byte past the static data; the buffer-arena
83        // handshake lays its `ArenaState` + arena there. Harmless export
84        // for the fast path (no consumer reads it).
85        .arg("--export=__heap_base")
86        .arg(obj_path)
87        .arg("-o")
88        .arg(out_path)
89        .output()
90        .map_err(|e| LlvmError::Codegen(format!("spawn {ld}: {e}")))?;
91    if !output.status.success() {
92        return Err(LlvmError::Codegen(format!(
93            "{ld} failed ({}):\n{}",
94            output.status,
95            String::from_utf8_lossy(&output.stderr)
96        )));
97    }
98    Ok(())
99}