Skip to main content

ras/
runtime.rs

1//! Runtime compilation (JIT): MIR to executable memory
2
3use crate::error::RasError;
4use lamina_platform::{TargetArchitecture, TargetOperatingSystem};
5
6#[cfg(windows)]
7mod windows_memory {
8    use std::ffi::c_void;
9
10    pub const MEM_COMMIT: u32 = 0x1000;
11    pub const MEM_RELEASE: u32 = 0x8000;
12    pub const MEM_RESERVE: u32 = 0x2000;
13    pub const PAGE_EXECUTE_READ: u32 = 0x20;
14    pub const PAGE_READWRITE: u32 = 0x04;
15
16    unsafe extern "system" {
17        pub fn VirtualAlloc(
18            address: *mut c_void,
19            size: usize,
20            allocation_type: u32,
21            protect: u32,
22        ) -> *mut c_void;
23        pub fn VirtualFree(address: *mut c_void, size: usize, free_type: u32) -> i32;
24        pub fn VirtualProtect(
25            address: *mut c_void,
26            size: usize,
27            new_protect: u32,
28            old_protect: *mut u32,
29        ) -> i32;
30
31        #[cfg(target_arch = "aarch64")]
32        pub fn FlushInstructionCache(
33            process: *mut c_void,
34            base_address: *const c_void,
35            size: usize,
36        ) -> i32;
37        #[cfg(target_arch = "aarch64")]
38        pub fn GetCurrentProcess() -> *mut c_void;
39    }
40}
41
42#[cfg(feature = "encoder")]
43use crate::assembler::RasAssembler;
44
45#[cfg(feature = "encoder")]
46use lamina_mir::Module as MirModule;
47
48/// Executable memory for JIT-compiled code
49pub struct ExecutableMemory {
50    ptr: *mut u8,
51    size: usize,
52}
53
54impl ExecutableMemory {
55    pub fn allocate_writable(size: usize) -> Result<Self, RasError> {
56        #[cfg(unix)]
57        {
58            use libc::{MAP_ANONYMOUS, MAP_PRIVATE, PROT_READ, PROT_WRITE, mmap};
59            use std::ptr;
60
61            let aligned_size = (size + 4095) & !4095;
62
63            #[cfg(all(target_os = "macos", target_arch = "aarch64"))]
64            const MAP_JIT: libc::c_int = 0x0800;
65
66            #[cfg(all(target_os = "macos", target_arch = "aarch64"))]
67            let map_flags = MAP_ANONYMOUS | MAP_PRIVATE | MAP_JIT;
68
69            #[cfg(not(all(target_os = "macos", target_arch = "aarch64")))]
70            let map_flags = MAP_ANONYMOUS | MAP_PRIVATE;
71
72            let ptr = unsafe {
73                mmap(
74                    ptr::null_mut(),
75                    aligned_size,
76                    PROT_READ | PROT_WRITE,
77                    map_flags,
78                    -1,
79                    0,
80                )
81            };
82
83            if ptr == libc::MAP_FAILED {
84                return Err(RasError::IoError(format!(
85                    "mmap failed (size: {} bytes)",
86                    size
87                )));
88            }
89
90            Ok(Self {
91                ptr: ptr as *mut u8,
92                size: aligned_size,
93            })
94        }
95
96        #[cfg(windows)]
97        {
98            use crate::runtime::windows_memory::{
99                MEM_COMMIT, MEM_RESERVE, PAGE_READWRITE, VirtualAlloc,
100            };
101
102            let ptr = unsafe {
103                VirtualAlloc(
104                    std::ptr::null_mut(),
105                    size,
106                    MEM_COMMIT | MEM_RESERVE,
107                    PAGE_READWRITE,
108                )
109            };
110
111            if ptr.is_null() {
112                return Err(RasError::IoError("VirtualAlloc failed".to_string()));
113            }
114
115            Ok(Self {
116                ptr: ptr as *mut u8,
117                size,
118            })
119        }
120
121        #[cfg(not(any(unix, windows)))]
122        {
123            Err(RasError::UnsupportedTarget(
124                "Executable memory allocation not supported on this platform".to_string(),
125            ))
126        }
127    }
128
129    pub fn make_executable(&mut self) -> Result<(), RasError> {
130        #[cfg(unix)]
131        {
132            use libc::{PROT_EXEC, PROT_READ, mprotect};
133
134            let result = unsafe {
135                mprotect(
136                    self.ptr as *mut libc::c_void,
137                    self.size,
138                    PROT_READ | PROT_EXEC,
139                )
140            };
141
142            if result != 0 {
143                return Err(RasError::IoError("mprotect failed".to_string()));
144            }
145
146            #[cfg(target_arch = "aarch64")]
147            {
148                unsafe {
149                    std::sync::atomic::fence(std::sync::atomic::Ordering::SeqCst);
150                    unsafe extern "C" {
151                        fn __clear_cache(start: *const u8, end: *const u8);
152                    }
153                    __clear_cache(self.ptr, self.ptr.add(self.size));
154                    std::sync::atomic::compiler_fence(std::sync::atomic::Ordering::SeqCst);
155                }
156            }
157
158            Ok(())
159        }
160
161        #[cfg(windows)]
162        {
163            use crate::runtime::windows_memory::{PAGE_EXECUTE_READ, VirtualProtect};
164
165            let mut old_protect = 0;
166            let result = unsafe {
167                VirtualProtect(
168                    self.ptr.cast(),
169                    self.size,
170                    PAGE_EXECUTE_READ,
171                    &mut old_protect,
172                )
173            };
174
175            if result == 0 {
176                return Err(RasError::IoError("VirtualProtect failed".to_string()));
177            }
178
179            // On Windows AArch64, flush the instruction cache so the CPU sees
180            // the newly written code after the page protection change.
181            #[cfg(target_arch = "aarch64")]
182            unsafe {
183                use crate::runtime::windows_memory::{FlushInstructionCache, GetCurrentProcess};
184                FlushInstructionCache(
185                    GetCurrentProcess(),
186                    self.ptr.cast(),
187                    self.size,
188                );
189            }
190
191            Ok(())
192        }
193
194        #[cfg(not(any(unix, windows)))]
195        {
196            Err(RasError::UnsupportedTarget(
197                "Making memory executable not supported".to_string(),
198            ))
199        }
200    }
201
202    pub fn allocate(size: usize) -> Result<Self, RasError> {
203        let mut mem = Self::allocate_writable(size)?;
204        mem.make_executable()?;
205        Ok(mem)
206    }
207
208    pub fn write_code(&mut self, code: &[u8]) -> Result<(), RasError> {
209        if code.len() > self.size {
210            return Err(RasError::IoError("Code too large".to_string()));
211        }
212
213        #[cfg(all(target_os = "macos", target_arch = "aarch64"))]
214        {
215            unsafe {
216                unsafe extern "C" {
217                    fn pthread_jit_write_protect_np(value: libc::c_int);
218                }
219                pthread_jit_write_protect_np(0);
220            }
221        }
222
223        unsafe {
224            std::ptr::copy_nonoverlapping(code.as_ptr(), self.ptr, code.len());
225        }
226
227        #[cfg(all(target_os = "macos", target_arch = "aarch64"))]
228        {
229            unsafe {
230                unsafe extern "C" {
231                    fn sys_icache_invalidate(start: *mut libc::c_void, size: libc::size_t);
232                }
233                sys_icache_invalidate(self.ptr as *mut libc::c_void, code.len());
234            }
235        }
236
237        #[cfg(all(target_os = "macos", target_arch = "aarch64"))]
238        {
239            unsafe {
240                unsafe extern "C" {
241                    fn pthread_jit_write_protect_np(value: libc::c_int);
242                }
243                pthread_jit_write_protect_np(1);
244            }
245        }
246
247        Ok(())
248    }
249
250    pub fn code_start(&self) -> *const u8 {
251        self.ptr
252    }
253
254    /// Interprets the first byte of this region as the entry address for a function pointer.
255    ///
256    /// This is **not** a pointer-to-pointer: the mapping begins with machine code, not a stored
257    /// address. Do not use `*const F` to the entry and then dereference; that would load
258    /// instruction bytes as a pointer.
259    ///
260    /// # Safety
261    ///
262    /// The caller must ensure `F` matches the ABI and signature of the generated code, and that
263    /// this `ExecutableMemory` outlives every call through `F`.
264    pub unsafe fn entry_fn<F: Sized>(&self) -> F {
265        debug_assert_eq!(core::mem::size_of::<F>(), core::mem::size_of::<*mut u8>());
266        unsafe { core::mem::transmute_copy(&self.ptr) }
267    }
268}
269
270impl Drop for ExecutableMemory {
271    fn drop(&mut self) {
272        #[cfg(unix)]
273        {
274            use libc::munmap;
275            unsafe {
276                munmap(self.ptr as *mut libc::c_void, self.size);
277            }
278        }
279
280        #[cfg(windows)]
281        {
282            use crate::runtime::windows_memory::{MEM_RELEASE, VirtualFree};
283            unsafe {
284                VirtualFree(self.ptr.cast(), 0, MEM_RELEASE);
285            }
286        }
287    }
288}
289
290/// Runtime compiler: MIR to executable memory
291pub struct RasRuntime {
292    #[cfg(feature = "encoder")]
293    target_arch: TargetArchitecture,
294    #[cfg(feature = "encoder")]
295    target_os: TargetOperatingSystem,
296}
297
298impl RasRuntime {
299    pub fn new(
300        #[cfg(feature = "encoder")] target_arch: TargetArchitecture,
301        #[cfg(not(feature = "encoder"))] _target_arch: TargetArchitecture,
302        #[cfg(feature = "encoder")] target_os: TargetOperatingSystem,
303        #[cfg(not(feature = "encoder"))] _target_os: TargetOperatingSystem,
304    ) -> Self {
305        Self {
306            #[cfg(feature = "encoder")]
307            target_arch,
308            #[cfg(feature = "encoder")]
309            target_os,
310        }
311    }
312
313    #[cfg(feature = "encoder")]
314    pub fn compile_to_memory(&mut self, module: &MirModule) -> Result<ExecutableMemory, RasError> {
315        let mut assembler = RasAssembler::new(self.target_arch, self.target_os)?;
316        let (code, _) = assembler.compile_mir_to_binary_function(module, None)?;
317
318        let mut mem = ExecutableMemory::allocate_writable(code.len())?;
319        mem.write_code(&code)?;
320        mem.make_executable()?;
321
322        Ok(mem)
323    }
324
325    /// Leaks the executable mapping so the returned pointer stays valid. Prefer
326    /// [`compile_to_memory`](Self::compile_to_memory) and keep the `ExecutableMemory` alive.
327    #[cfg(feature = "encoder")]
328    pub fn compile_function<T>(
329        &mut self,
330        module: &MirModule,
331        _function_name: &str,
332    ) -> Result<unsafe extern "C" fn() -> T, RasError> {
333        let mem = self.compile_to_memory(module)?;
334        let f: unsafe extern "C" fn() -> T = unsafe { core::mem::transmute(mem.ptr) };
335        std::mem::forget(mem);
336        Ok(f)
337    }
338}
339
340#[cfg(test)]
341mod tests {
342    use super::*;
343    use lamina_platform::{TargetArchitecture, TargetOperatingSystem};
344
345    #[test]
346    fn executable_memory_allocate_and_write() {
347        // Allocate a small writable+executable region and verify the written bytes
348        // can be read back before making the region executable.
349        let mut mem = ExecutableMemory::allocate_writable(4096).expect("alloc failed");
350        let code = [0x90u8, 0x90, 0x90, 0xc3]; // three NOPs + ret (x86_64)
351        mem.write_code(&code).expect("write failed");
352        // After making it executable the pointer should remain non-null.
353        mem.make_executable().expect("make_executable failed");
354        assert!(!mem.code_start().is_null());
355    }
356
357    #[test]
358    fn executable_memory_too_large_returns_error() {
359        // Allocation rounds up to a page (4096 bytes). Use exactly one page so
360        // the next allocation produces a 4096-byte region, then try to write 8192.
361        let mem = ExecutableMemory::allocate_writable(4096).expect("alloc failed");
362        let big = vec![0u8; 8192];
363        let result = {
364            let mut m = mem;
365            m.write_code(&big)
366        };
367        assert!(result.is_err(), "writing beyond capacity should fail");
368    }
369
370    #[test]
371    fn ras_runtime_new_does_not_panic() {
372        let _rt = RasRuntime::new(TargetArchitecture::X86_64, TargetOperatingSystem::Linux);
373    }
374}
375
376/// Run JIT output on the **host** CPU (x86_64 or aarch64 only). Skips when the host OS string
377/// is not mapped in `TargetOperatingSystem` (e.g. some embedded targets).
378#[cfg(all(test, feature = "encoder"))]
379mod jit_host_exec_tests {
380    use std::str::FromStr;
381
382    use lamina_mir::block::Block;
383    use lamina_mir::function::{Function, Parameter, Signature};
384    use lamina_mir::instruction::{Immediate, Instruction, IntBinOp, IntCmpOp, Operand};
385    use lamina_mir::module::Module;
386    use lamina_mir::register::{Register, VirtualReg};
387    use lamina_mir::types::{MirType, ScalarType};
388    use lamina_platform::{
389        TargetArchitecture, TargetOperatingSystem, detect_host_architecture_only, detect_host_os,
390    };
391
392    use crate::assembler::core::RasAssembler;
393    use crate::error::RasError;
394    use crate::runtime::ExecutableMemory;
395
396    fn host_jit_pair() -> Option<(TargetArchitecture, TargetOperatingSystem)> {
397        let arch = TargetArchitecture::from_str(detect_host_architecture_only()).ok()?;
398        let os = TargetOperatingSystem::from_str(detect_host_os()).ok()?;
399        match arch {
400            TargetArchitecture::X86_64 | TargetArchitecture::Aarch64 => Some((arch, os)),
401            _ => None,
402        }
403    }
404
405    fn compile_to_executable(
406        arch: TargetArchitecture,
407        os: TargetOperatingSystem,
408        module: &lamina_mir::Module,
409    ) -> Result<ExecutableMemory, RasError> {
410        let mut asm = RasAssembler::new(arch, os)?;
411        let (code, _) = asm.compile_mir_to_binary_function(module, None)?;
412        let mut mem = ExecutableMemory::allocate_writable(code.len())?;
413        mem.write_code(&code)?;
414        mem.make_executable()?;
415        Ok(mem)
416    }
417
418    fn exec_host_i32(module: &Module) -> Option<i32> {
419        let (arch, os) = host_jit_pair()?;
420        let mem = compile_to_executable(arch, os, module).ok()?;
421        let f = unsafe { mem.entry_fn::<extern "C" fn() -> i32>() };
422        Some(f())
423    }
424
425    fn exec_host_i64(module: &Module) -> Option<i64> {
426        let (arch, os) = host_jit_pair()?;
427        let mem = compile_to_executable(arch, os, module).ok()?;
428        let f = unsafe { mem.entry_fn::<extern "C" fn() -> i64>() };
429        Some(f())
430    }
431
432    fn exec_host_i64_binary(module: &Module, a: i64, b: i64) -> Option<i64> {
433        let (arch, os) = host_jit_pair()?;
434        let mem = compile_to_executable(arch, os, module).ok()?;
435        let f = unsafe { mem.entry_fn::<extern "C" fn(i64, i64) -> i64>() };
436        Some(f(a, b))
437    }
438
439    fn ii32(v: i32) -> Operand {
440        Operand::Immediate(Immediate::I32(v))
441    }
442
443    fn ii64(v: i64) -> Operand {
444        Operand::Immediate(Immediate::I64(v))
445    }
446
447    fn module_scalar_binop(scalar: ScalarType, op: IntBinOp, lhs: Operand, rhs: Operand) -> Module {
448        let ty = MirType::Scalar(scalar);
449        let out = Register::Virtual(VirtualReg::gpr(0));
450        let sig = Signature::new("f").with_return(ty.clone());
451        let mut f = Function::new(sig);
452        let mut entry = Block::new("entry");
453        entry.push(Instruction::IntBinary {
454            op,
455            ty: ty.clone(),
456            dst: out.clone(),
457            lhs,
458            rhs,
459        });
460        entry.push(Instruction::Ret {
461            value: Some(Operand::Register(out.clone())),
462        });
463        f.add_block(entry);
464        let mut module = Module::new("host_scalar_binop");
465        module.add_function(f);
466        module
467    }
468
469    fn module_i64_binop_params(op: IntBinOp) -> Module {
470        let i64_ty = MirType::Scalar(ScalarType::I64);
471        let a = Register::Virtual(VirtualReg::gpr(0));
472        let b = Register::Virtual(VirtualReg::gpr(1));
473        let out = Register::Virtual(VirtualReg::gpr(2));
474        let sig = Signature::new("g")
475            .with_params(vec![
476                Parameter::new(a.clone(), i64_ty.clone()),
477                Parameter::new(b.clone(), i64_ty.clone()),
478            ])
479            .with_return(i64_ty.clone());
480        let mut f = Function::new(sig);
481        let mut entry = Block::new("entry");
482        entry.push(Instruction::IntBinary {
483            op,
484            ty: i64_ty.clone(),
485            dst: out.clone(),
486            lhs: Operand::Register(a.clone()),
487            rhs: Operand::Register(b.clone()),
488        });
489        entry.push(Instruction::Ret {
490            value: Some(Operand::Register(out.clone())),
491        });
492        f.add_block(entry);
493        let mut module = Module::new("host_i64_params");
494        module.add_function(f);
495        module
496    }
497
498    fn module_i32_cmp_eq(lhs: Operand, rhs: Operand) -> Module {
499        let i32_ty = MirType::Scalar(ScalarType::I32);
500        let out = Register::Virtual(VirtualReg::gpr(0));
501        let sig = Signature::new("cmp").with_return(i32_ty.clone());
502        let mut f = Function::new(sig);
503        let mut entry = Block::new("entry");
504        entry.push(Instruction::IntCmp {
505            op: IntCmpOp::Eq,
506            ty: i32_ty.clone(),
507            dst: out.clone(),
508            lhs,
509            rhs,
510        });
511        entry.push(Instruction::Ret {
512            value: Some(Operand::Register(out.clone())),
513        });
514        f.add_block(entry);
515        let mut module = Module::new("host_i32_cmp");
516        module.add_function(f);
517        module
518    }
519
520    #[test]
521    fn jit_host_exec_i64_add_immediates() {
522        let Some(got) = exec_host_i64(&module_scalar_binop(
523            ScalarType::I64,
524            IntBinOp::Add,
525            ii64(10),
526            ii64(32),
527        )) else {
528            return;
529        };
530        assert_eq!(got, 42);
531    }
532
533    #[test]
534    fn jit_host_exec_i64_sub_immediates() {
535        let Some(got) = exec_host_i64(&module_scalar_binop(
536            ScalarType::I64,
537            IntBinOp::Sub,
538            ii64(100),
539            ii64(33),
540        )) else {
541            return;
542        };
543        assert_eq!(got, 67);
544    }
545
546    #[test]
547    fn jit_host_exec_i64_mul_immediates() {
548        let Some(got) = exec_host_i64(&module_scalar_binop(
549            ScalarType::I64,
550            IntBinOp::Mul,
551            ii64(6),
552            ii64(7),
553        )) else {
554            return;
555        };
556        assert_eq!(got, 42);
557    }
558
559    #[test]
560    fn jit_host_exec_i64_add_two_arguments() {
561        let Some(got) = exec_host_i64_binary(&module_i64_binop_params(IntBinOp::Add), 7, 35) else {
562            return;
563        };
564        assert_eq!(got, 42);
565    }
566
567    #[test]
568    fn jit_host_exec_i64_mul_two_arguments() {
569        let Some(got) = exec_host_i64_binary(&module_i64_binop_params(IntBinOp::Mul), 6, 7) else {
570            return;
571        };
572        assert_eq!(got, 42);
573    }
574
575    #[test]
576    fn jit_host_exec_i32_sub_immediates() {
577        let Some(got) = exec_host_i32(&module_scalar_binop(
578            ScalarType::I32,
579            IntBinOp::Sub,
580            ii32(50),
581            ii32(8),
582        )) else {
583            return;
584        };
585        assert_eq!(got, 42);
586    }
587
588    #[test]
589    fn jit_host_exec_i32_mul_immediates() {
590        let Some(got) = exec_host_i32(&module_scalar_binop(
591            ScalarType::I32,
592            IntBinOp::Mul,
593            ii32(6),
594            ii32(7),
595        )) else {
596            return;
597        };
598        assert_eq!(got, 42);
599    }
600
601    #[test]
602    fn jit_host_exec_i32_and_or_xor_immediates() {
603        let Some(a) = exec_host_i32(&module_scalar_binop(
604            ScalarType::I32,
605            IntBinOp::And,
606            ii32(0x0F),
607            ii32(0x33),
608        )) else {
609            return;
610        };
611        assert_eq!(a, 3);
612        let Some(o) = exec_host_i32(&module_scalar_binop(
613            ScalarType::I32,
614            IntBinOp::Or,
615            ii32(8),
616            ii32(2),
617        )) else {
618            return;
619        };
620        assert_eq!(o, 10);
621        let Some(x) = exec_host_i32(&module_scalar_binop(
622            ScalarType::I32,
623            IntBinOp::Xor,
624            ii32(15),
625            ii32(5),
626        )) else {
627            return;
628        };
629        assert_eq!(x, 10);
630    }
631
632    #[test]
633    fn jit_host_exec_i32_udiv_immediates() {
634        let Some(got) = exec_host_i32(&module_scalar_binop(
635            ScalarType::I32,
636            IntBinOp::UDiv,
637            ii32(10),
638            ii32(3),
639        )) else {
640            return;
641        };
642        assert_eq!(got, 3);
643    }
644
645    #[test]
646    fn jit_host_exec_i32_sdiv_negative_dividend() {
647        let Some(got) = exec_host_i32(&module_scalar_binop(
648            ScalarType::I32,
649            IntBinOp::SDiv,
650            ii32(-7),
651            ii32(2),
652        )) else {
653            return;
654        };
655        assert_eq!(got, -3);
656    }
657
658    #[test]
659    fn jit_host_exec_i32_urem_srem_immediates() {
660        let Some(u) = exec_host_i32(&module_scalar_binop(
661            ScalarType::I32,
662            IntBinOp::URem,
663            ii32(10),
664            ii32(3),
665        )) else {
666            return;
667        };
668        assert_eq!(u, 1);
669        let Some(s) = exec_host_i32(&module_scalar_binop(
670            ScalarType::I32,
671            IntBinOp::SRem,
672            ii32(-7),
673            ii32(3),
674        )) else {
675            return;
676        };
677        assert_eq!(s, -1);
678    }
679
680    #[test]
681    fn jit_host_exec_i32_shl_immediates() {
682        let Some(got) = exec_host_i32(&module_scalar_binop(
683            ScalarType::I32,
684            IntBinOp::Shl,
685            ii32(3),
686            ii32(4),
687        )) else {
688            return;
689        };
690        assert_eq!(got, 48);
691    }
692
693    #[test]
694    fn jit_host_exec_i32_lshr_ashr_immediates() {
695        let Some(l) = exec_host_i32(&module_scalar_binop(
696            ScalarType::I32,
697            IntBinOp::LShr,
698            ii32(128),
699            ii32(3),
700        )) else {
701            return;
702        };
703        assert_eq!(l, 16);
704        let Some(a) = exec_host_i32(&module_scalar_binop(
705            ScalarType::I32,
706            IntBinOp::AShr,
707            ii32(-16),
708            ii32(2),
709        )) else {
710            return;
711        };
712        assert_eq!(a, -4);
713    }
714
715    #[test]
716    fn jit_host_exec_i32_intcmp_eq_immediates() {
717        let Some(eq) = exec_host_i32(&module_i32_cmp_eq(ii32(9), ii32(9))) else {
718            return;
719        };
720        assert_eq!(eq, 1);
721        let Some(ne) = exec_host_i32(&module_i32_cmp_eq(ii32(2), ii32(3))) else {
722            return;
723        };
724        assert_eq!(ne, 0);
725    }
726}