Skip to main content

svod_runtime/
llvm.rs

1//! LLVM JIT compilation via external clang + ELF loader.
2//!
3//! Compiles LLVM IR text via `clang -x ir -c -O2` stdin→stdout and loads the
4//! resulting object via the shared JIT ELF loader. No linked LLVM required.
5
6use crate::Result;
7use crate::dispatch::KernelCif;
8use tracing::debug;
9
10/// LLVM JIT-compiled kernel using external clang + mmap ELF loader.
11pub struct LlvmKernel {
12    _mmap: memmap2::MmapMut,
13    fn_ptr: *const (),
14    entry_point: String,
15    name: String,
16    var_names: Vec<String>,
17    cif: KernelCif,
18}
19
20// SAFETY: Function pointer points to read-only compiled code in mmap'd memory.
21// Multiple threads can call it concurrently.
22unsafe impl Send for LlvmKernel {}
23unsafe impl Sync for LlvmKernel {}
24
25impl LlvmKernel {
26    /// Compile LLVM IR text to executable code via external clang.
27    pub fn compile_ir(
28        ir: &str,
29        entry_point: impl Into<String>,
30        name: impl Into<String>,
31        var_names: Vec<String>,
32        buf_count: usize,
33    ) -> Result<Self> {
34        let entry_point = entry_point.into();
35        let name = name.into();
36
37        debug!(kernel.name = %name, ir.length = ir.len(), "Compiling LLVM IR via external clang");
38
39        if let Ok(dir) = std::env::var("SVOD_DUMP_LLVM_IR") {
40            let path = std::path::Path::new(&dir).join(format!("{name}.ll"));
41            let _ = std::fs::create_dir_all(&dir);
42            let _ = std::fs::write(&path, ir);
43        }
44
45        if let Ok(dir) = std::env::var("SVOD_DUMP_POST_O2_IR") {
46            // Run the same `-O2 -funroll-loops -fvectorize -fslp-vectorize`
47            // pipeline as the JIT compile but emit textual LLVM IR instead
48            // of an object file. Writes `<dir>/<name>.post.ll`.
49            let _ = std::fs::create_dir_all(&dir);
50            if let Some(post_ir) = compile_ir_to_post_o2_text(ir) {
51                let path = std::path::Path::new(&dir).join(format!("{name}.post.ll"));
52                let _ = std::fs::write(&path, post_ir);
53            }
54        }
55
56        let obj = compile_ir_to_object(ir)?;
57        let (fn_ptr, mmap) = crate::jit_loader::jit_load(&obj, &entry_point)?;
58        let cif = KernelCif::new(buf_count + var_names.len());
59
60        debug!(kernel.name = %name, "LLVM kernel compiled and loaded");
61
62        Ok(Self { _mmap: mmap, fn_ptr, entry_point, name, var_names, cif })
63    }
64
65    /// Compile a RenderedKernel from the codegen crate.
66    pub fn compile(kernel: &svod_codegen::RenderedKernel) -> Result<Self> {
67        Self::compile_ir(&kernel.code, &kernel.name, &kernel.name, kernel.var_names.clone(), kernel.buffer_args.len())
68    }
69
70    pub fn var_names(&self) -> &[String] {
71        &self.var_names
72    }
73
74    pub fn fn_ptr(&self) -> *const () {
75        self.fn_ptr
76    }
77
78    pub fn name(&self) -> &str {
79        &self.name
80    }
81
82    /// Execute the kernel with buffer pointers and variable values.
83    ///
84    /// # Safety
85    ///
86    /// Caller must ensure buffer pointers are valid/aligned and `vals` length
87    /// matches `var_names`.
88    pub unsafe fn execute_with_vals(&self, buffers: &[*mut u8], vals: &[i64]) -> Result<()> {
89        debug!(
90            kernel.entry_point = %self.entry_point,
91            kernel.num_buffers = buffers.len(),
92            kernel.num_vals = vals.len(),
93            "Executing LLVM kernel"
94        );
95
96        unsafe { self.cif.dispatch(self.fn_ptr, buffers, vals, None) };
97
98        Ok(())
99    }
100
101    pub(crate) fn cif(&self) -> &KernelCif {
102        &self.cif
103    }
104}
105
106/// Compile LLVM IR text to a relocatable object via `clang -x ir`.
107///
108/// Uses `--target=<arch>-none-unknown-elf` to produce a relocatable ELF object
109/// (same as the C path in jit_loader), so the JIT ELF loader can handle
110/// relocations consistently.
111fn compile_ir_to_object(ir: &str) -> Result<Vec<u8>> {
112    use std::io::Write;
113    use std::process::{Command, Stdio};
114
115    let target = crate::jit_loader::elf_target_triple();
116
117    let mut args = vec![
118        "-x",
119        "ir",
120        "-c",
121        "-O2",
122        "-march=native",
123        "-fPIC",
124        "-fno-math-errno",
125        "-fno-stack-protector",
126        "-funroll-loops",
127        "-fvectorize",
128        "-fslp-vectorize",
129    ];
130    args.push(&target);
131    args.extend_from_slice(crate::jit_loader::platform_clang_flags());
132    args.extend_from_slice(&["-", "-o", "-"]);
133
134    let mut child = Command::new("clang")
135        .args(&args)
136        .stdin(Stdio::piped())
137        .stdout(Stdio::piped())
138        .stderr(Stdio::piped())
139        .spawn()
140        .map_err(|e| crate::Error::JitCompilation {
141            reason: format!("Failed to spawn clang: {e}. Is clang installed?"),
142        })?;
143
144    child
145        .stdin
146        .take()
147        .expect("stdin was piped")
148        .write_all(ir.as_bytes())
149        .map_err(|e| crate::Error::JitCompilation { reason: format!("Failed to write IR to clang stdin: {e}") })?;
150
151    let output = child
152        .wait_with_output()
153        .map_err(|e| crate::Error::JitCompilation { reason: format!("Failed to wait for clang: {e}") })?;
154
155    if !output.status.success() {
156        let stderr = String::from_utf8_lossy(&output.stderr);
157        return Err(crate::Error::JitCompilation { reason: format!("clang IR compilation failed:\n{stderr}") });
158    }
159
160    if output.stdout.is_empty() {
161        return Err(crate::Error::JitCompilation { reason: "clang produced empty output from IR".to_string() });
162    }
163
164    Ok(output.stdout)
165}
166
167/// Run the same `-O2` LLVM pass pipeline as the JIT compile but emit
168/// textual LLVM IR. Returns `None` on compile failure (silent — this
169/// is a diagnostic-only path, never load-bearing).
170fn compile_ir_to_post_o2_text(ir: &str) -> Option<String> {
171    use std::io::Write;
172    use std::process::{Command, Stdio};
173
174    let mut args = vec![
175        "-x",
176        "ir",
177        "-S",
178        "-emit-llvm",
179        "-O2",
180        "-march=native",
181        "-fno-math-errno",
182        "-funroll-loops",
183        "-fvectorize",
184        "-fslp-vectorize",
185    ];
186    args.extend_from_slice(crate::jit_loader::platform_clang_flags());
187    args.extend_from_slice(&["-", "-o", "-"]);
188
189    let mut child = Command::new("clang")
190        .args(&args)
191        .stdin(Stdio::piped())
192        .stdout(Stdio::piped())
193        .stderr(Stdio::piped())
194        .spawn()
195        .ok()?;
196    child.stdin.take()?.write_all(ir.as_bytes()).ok()?;
197    let output = child.wait_with_output().ok()?;
198    if !output.status.success() {
199        return None;
200    }
201    String::from_utf8(output.stdout).ok()
202}
203
204#[cfg(test)]
205#[path = "test/unit/llvm.rs"]
206mod tests;