use std::fs;
use std::io;
use std::path::{Path, PathBuf};
use std::process::Command;
use object::write::{Object, Symbol, SymbolSection};
use object::{Architecture, BinaryFormat, Endianness, SectionKind, SymbolKind, SymbolScope};
use luna_core::compiler::compile_chunk;
use luna_core::frontend::parser::parse;
use luna_core::jit::aot_meta::{
AotTraceMetaHeader, PerExitInlineEntry, PerExitTagsEntry, encode_meta_blob, pack_exit_tag,
pack_tag_res_kind,
};
use luna_core::jit::trace_types::{CompileOptions, CompiledTrace, TraceRecord};
use luna_core::runtime::Heap;
use luna_core::runtime::Value;
use luna_core::version::LuaVersion;
use luna_core::vm::dump;
use crate::{BYTECODE_END_SYMBOL, BYTECODE_SECTION_NAME, BYTECODE_START_SYMBOL};
#[derive(Debug)]
pub enum AotError {
Io(io::Error),
Syntax(String),
Object(String),
Link(String),
UnsupportedTarget(String),
}
impl std::fmt::Display for AotError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
AotError::Io(e) => write!(f, "io error: {e}"),
AotError::Syntax(msg) => write!(f, "syntax error: {msg}"),
AotError::Object(msg) => write!(f, "object-file emission failed: {msg}"),
AotError::Link(msg) => write!(f, "linker failed: {msg}"),
AotError::UnsupportedTarget(t) => {
write!(f, "unsupported target triple in scaffold session: {t}")
}
}
}
}
impl std::error::Error for AotError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
AotError::Io(e) => Some(e),
_ => None,
}
}
}
impl From<io::Error> for AotError {
fn from(e: io::Error) -> Self {
AotError::Io(e)
}
}
pub fn embed_bytecode(
source_path: &Path,
out_path: &Path,
target_triple: Option<&str>,
version: LuaVersion,
) -> Result<(), AotError> {
if let Some(t) = target_triple
&& t != host_triple()
{
return Err(AotError::UnsupportedTarget(t.to_string()));
}
let src = fs::read(source_path)?;
let ast = parse(&src, version).map_err(|e| {
AotError::Syntax(format!(
"{}:{}: {}",
source_path.display(),
e.line,
String::from_utf8_lossy(&e.msg)
))
})?;
let mut heap = Heap::new();
let chunk_name = source_path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("aot-chunk")
.as_bytes()
.to_vec();
let proto = compile_chunk(&ast, version, &chunk_name, &mut heap).map_err(|e| {
AotError::Syntax(format!(
"{}:{}: {}",
source_path.display(),
e.line,
String::from_utf8_lossy(&e.msg)
))
})?;
let dump_bytes = dump::dump(&proto, false, version);
let workdir = out_path
.parent()
.map(Path::to_path_buf)
.unwrap_or_else(|| PathBuf::from("."));
let stem = out_path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("luna_aot");
let bytecode_obj_path = workdir.join(format!("{stem}.luna_bytecode.o"));
let stub_obj_path = workdir.join(format!("{stem}.luna_stub.o"));
write_bytecode_object(&dump_bytes, &bytecode_obj_path)?;
write_scaffold_entry_object(&stub_obj_path)?;
link_with_cc(&[&bytecode_obj_path, &stub_obj_path], out_path)?;
Ok(())
}
fn write_bytecode_object(bytecode: &[u8], out: &Path) -> Result<(), AotError> {
let (format, arch, endian) = host_object_target();
let mut obj = Object::new(format, arch, endian);
let section_id = obj.add_section(
Vec::new(),
BYTECODE_SECTION_NAME.as_bytes().to_vec(),
SectionKind::ReadOnlyData,
);
let _start_offset = obj.append_section_data(section_id, bytecode, 1);
let _ = format; let start_name = BYTECODE_START_SYMBOL.to_string();
let end_name = BYTECODE_END_SYMBOL.to_string();
let _start_sym = obj.add_symbol(Symbol {
name: start_name.into_bytes(),
value: 0,
size: 0,
kind: SymbolKind::Data,
scope: SymbolScope::Dynamic,
weak: false,
section: SymbolSection::Section(section_id),
flags: object::SymbolFlags::None,
});
let _end_sym = obj.add_symbol(Symbol {
name: end_name.into_bytes(),
value: bytecode.len() as u64,
size: 0,
kind: SymbolKind::Data,
scope: SymbolScope::Dynamic,
weak: false,
section: SymbolSection::Section(section_id),
flags: object::SymbolFlags::None,
});
let bytes = obj
.write()
.map_err(|e| AotError::Object(format!("Object::write: {e}")))?;
fs::write(out, bytes)?;
Ok(())
}
fn write_scaffold_entry_object(out: &Path) -> Result<(), AotError> {
let c_src = format!(
r#"#include <stdio.h>
#include <stdint.h>
extern uint8_t __luna_bytecode_start[];
extern uint8_t __luna_bytecode_end[];
int main(int argc, char **argv) {{
size_t len = (size_t)(__luna_bytecode_end - __luna_bytecode_start);
fprintf(stderr,
"luna-aot scaffold: embedded bytecode length = %zu bytes (section %s)\n"
" (interp dispatch wiring is a follow-up session)\n",
len, "{section}");
(void)argc; (void)argv;
return 0;
}}
"#,
section = BYTECODE_SECTION_NAME
);
let mut c_path = out.to_path_buf();
c_path.set_extension("c");
fs::write(&c_path, c_src)?;
let cc = std::env::var("CC").unwrap_or_else(|_| "cc".into());
let status = Command::new(&cc)
.arg("-c")
.arg(&c_path)
.arg("-o")
.arg(out)
.output()
.map_err(|e| AotError::Link(format!("spawn {cc}: {e}")))?;
if !status.status.success() {
return Err(AotError::Link(format!(
"cc -c {} failed (exit {:?}):\n{}",
c_path.display(),
status.status.code(),
String::from_utf8_lossy(&status.stderr)
)));
}
Ok(())
}
fn link_with_cc(objects: &[&Path], out_path: &Path) -> Result<(), AotError> {
let cc = std::env::var("CC").unwrap_or_else(|_| "cc".into());
let mut cmd = Command::new(&cc);
for obj in objects {
cmd.arg(obj);
}
cmd.arg("-o").arg(out_path);
let output = cmd
.output()
.map_err(|e| AotError::Link(format!("spawn {cc}: {e}")))?;
if !output.status.success() {
return Err(AotError::Link(format!(
"cc link failed (exit {:?}):\n{}",
output.status.code(),
String::from_utf8_lossy(&output.stderr)
)));
}
Ok(())
}
pub fn compile_and_link(
source_path: &Path,
out_path: &Path,
target_triple: Option<&str>,
version: LuaVersion,
) -> Result<(), AotError> {
let target = match target_triple {
Some(t) => TargetSpec::from_triple(t)?,
None => TargetSpec::host(),
};
let dump_bytes = compile_to_dump(source_path, version)?;
let workdir = out_path
.parent()
.map(Path::to_path_buf)
.unwrap_or_else(|| PathBuf::from("."));
let stem = out_path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("luna_aot");
let bytecode_obj_path = workdir.join(format!("{stem}.luna_bytecode.o"));
let cmain_obj_path = workdir.join(format!("{stem}.luna_cmain.o"));
write_bytecode_object_for(&dump_bytes, &bytecode_obj_path, &target)?;
write_aot_cmain_object_for(&cmain_obj_path, &target)?;
let traces_obj_path = {
let path = workdir.join(format!("{stem}.luna_traces.o"));
match harvest_and_emit_aot_traces(&dump_bytes, version, &path, &target)? {
HarvestedTraces::None => None,
HarvestedTraces::Some => Some(path),
}
};
let staticlib = build_runtime_helpers_staticlib(target.triple_for_cargo())?;
link_aot_binary_for(
&bytecode_obj_path,
&cmain_obj_path,
traces_obj_path.as_deref(),
&staticlib,
out_path,
&target,
)?;
Ok(())
}
enum HarvestedTraces {
None,
Some,
}
pub fn compile_and_link_host(
source_path: &Path,
out_path: &Path,
version: LuaVersion,
) -> Result<(), AotError> {
compile_and_link(source_path, out_path, None, version)
}
fn compile_to_dump(source_path: &Path, version: LuaVersion) -> Result<Vec<u8>, AotError> {
let src = fs::read(source_path)?;
let ast = parse(&src, version).map_err(|e| {
AotError::Syntax(format!(
"{}:{}: {}",
source_path.display(),
e.line,
String::from_utf8_lossy(&e.msg)
))
})?;
let mut heap = Heap::new();
let chunk_name = source_path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("aot-chunk")
.as_bytes()
.to_vec();
let proto = compile_chunk(&ast, version, &chunk_name, &mut heap).map_err(|e| {
AotError::Syntax(format!(
"{}:{}: {}",
source_path.display(),
e.line,
String::from_utf8_lossy(&e.msg)
))
})?;
Ok(dump::dump(&proto, false, version))
}
fn build_runtime_helpers_staticlib(target_triple: Option<&str>) -> Result<PathBuf, AotError> {
if target_triple.is_none() {
if let Ok(prebuilt) = std::env::var("LUNA_AOT_RUNTIME_HELPERS_STATICLIB") {
let p = PathBuf::from(prebuilt);
if !p.exists() {
return Err(AotError::Link(format!(
"LUNA_AOT_RUNTIME_HELPERS_STATICLIB points at {} but the file does not exist",
p.display()
)));
}
return Ok(p);
}
}
use std::sync::Mutex;
static BUILD_LOCK: Mutex<()> = Mutex::new(());
let _guard = BUILD_LOCK.lock().unwrap_or_else(|p| p.into_inner());
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").map_err(|_| {
AotError::Link(match target_triple {
None => "CARGO_MANIFEST_DIR not set — cannot locate workspace to \
build luna-runtime-helpers. Set \
LUNA_AOT_RUNTIME_HELPERS_STATICLIB to a prebuilt \
staticlib to bypass."
.to_string(),
Some(t) => format!(
"CARGO_MANIFEST_DIR not set — cannot locate workspace to build \
luna-runtime-helpers for target {t}. Cross-compilation \
requires running luna-aot from inside its workspace (e.g. \
`cargo run -p luna-aot -- compile ...`), because the \
staticlib must be built for the target ABI. \
LUNA_AOT_RUNTIME_HELPERS_STATICLIB does NOT apply here — it \
is honoured for host builds only, so that a cross link is \
never handed a staticlib built for the wrong ABI."
),
})
})?;
let workspace_root = PathBuf::from(&manifest_dir)
.parent()
.and_then(Path::parent)
.map(Path::to_path_buf)
.ok_or_else(|| {
AotError::Link(format!(
"could not derive workspace root from CARGO_MANIFEST_DIR={manifest_dir}"
))
})?;
let cargo = std::env::var("CARGO").unwrap_or_else(|_| "cargo".into());
let mut cmd = Command::new(&cargo);
cmd.current_dir(&workspace_root)
.arg("build")
.arg("-p")
.arg("luna-runtime-helpers")
.arg("--profile=release-aot-helpers")
.env_remove("RUSTFLAGS");
if let Some(t) = target_triple {
cmd.arg("--target").arg(t);
}
let output = cmd
.output()
.map_err(|e| AotError::Link(format!("spawn cargo: {e}")))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
let hint = if let Some(t) = target_triple {
if stderr.contains("can't find crate for `std`")
|| stderr.contains("the `std` crate is not available")
|| stderr.contains("target may not be installed")
{
format!(
"\nhint: cross-compiling to {t} requires the rust-std component — \
run `rustup target add {t}` and retry."
)
} else {
String::new()
}
} else {
String::new()
};
return Err(AotError::Link(format!(
"cargo build -p luna-runtime-helpers{target_suffix} failed (exit {:?}):\nstdout:\n{}\nstderr:\n{}{hint}",
output.status.code(),
String::from_utf8_lossy(&output.stdout),
stderr,
target_suffix = target_triple
.map(|t| format!(" --target={t}"))
.unwrap_or_default(),
)));
}
let mut staticlib = workspace_root.join("target");
if let Some(t) = target_triple {
staticlib.push(t);
}
staticlib.push("release-aot-helpers");
let unix_name = "libluna_runtime_helpers.a";
let windows_name = "luna_runtime_helpers.lib";
let unix_path = staticlib.join(unix_name);
let windows_path = staticlib.join(windows_name);
if unix_path.exists() {
Ok(unix_path)
} else if windows_path.exists() {
Ok(windows_path)
} else {
Err(AotError::Link(format!(
"cargo build succeeded but neither {} nor {} exist",
unix_path.display(),
windows_path.display(),
)))
}
}
fn host_object_target() -> (BinaryFormat, Architecture, Endianness) {
let format = BinaryFormat::native_object();
let arch = if cfg!(target_arch = "x86_64") {
Architecture::X86_64
} else if cfg!(target_arch = "aarch64") {
Architecture::Aarch64
} else if cfg!(target_arch = "x86") {
Architecture::I386
} else if cfg!(target_arch = "riscv64") {
Architecture::Riscv64
} else {
Architecture::Unknown
};
let endian = Endianness::Little;
(format, arch, endian)
}
fn host_triple() -> &'static str {
if cfg!(all(target_arch = "aarch64", target_os = "macos")) {
"aarch64-apple-darwin"
} else if cfg!(all(target_arch = "x86_64", target_os = "macos")) {
"x86_64-apple-darwin"
} else if cfg!(all(target_arch = "x86_64", target_os = "linux")) {
"x86_64-unknown-linux-gnu"
} else if cfg!(all(target_arch = "aarch64", target_os = "linux")) {
"aarch64-unknown-linux-gnu"
} else if cfg!(all(target_arch = "x86_64", target_os = "windows")) {
"x86_64-pc-windows-msvc"
} else {
"unknown"
}
}
#[derive(Debug, Clone)]
pub struct TargetSpec {
pub triple: String,
pub is_host: bool,
pub format: BinaryFormat,
pub arch: Architecture,
pub endian: Endianness,
pub os: TargetOs,
pub libc: TargetLibc,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TargetOs {
MacOs,
Linux,
Windows,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TargetLibc {
Default,
Musl,
MinGw,
}
impl TargetSpec {
pub fn host() -> Self {
let (format, arch, endian) = host_object_target();
let triple = host_triple().to_string();
let os = if cfg!(target_os = "macos") {
TargetOs::MacOs
} else if cfg!(target_os = "linux") {
TargetOs::Linux
} else if cfg!(target_os = "windows") {
TargetOs::Windows
} else {
TargetOs::Linux
};
let libc = if cfg!(target_env = "musl") {
TargetLibc::Musl
} else if cfg!(all(target_os = "windows", target_env = "gnu")) {
TargetLibc::MinGw
} else {
TargetLibc::Default
};
TargetSpec {
triple,
is_host: true,
format,
arch,
endian,
os,
libc,
}
}
pub fn from_triple(triple: &str) -> Result<Self, AotError> {
if triple == host_triple() {
return Ok(Self::host());
}
let parts: Vec<&str> = triple.split('-').collect();
if parts.len() < 3 {
return Err(AotError::UnsupportedTarget(format!(
"triple {triple:?} has fewer than 3 components (expected arch-vendor-os[-env])"
)));
}
let arch_str = parts[0];
let os_str = parts[2];
let env_str = parts.get(3).copied().unwrap_or("");
let arch = match arch_str {
"x86_64" => Architecture::X86_64,
"aarch64" => Architecture::Aarch64,
"i686" | "i586" | "x86" => Architecture::I386,
"riscv64" | "riscv64gc" => Architecture::Riscv64,
other => {
return Err(AotError::UnsupportedTarget(format!(
"arch component {other:?} of triple {triple:?} not in tier 1/2 set \
(supported: x86_64, aarch64, i686, riscv64)"
)));
}
};
let (os, format, libc) = match os_str {
"darwin" => (TargetOs::MacOs, BinaryFormat::MachO, TargetLibc::Default),
"linux" => {
let libc = if env_str.contains("musl") {
TargetLibc::Musl
} else {
TargetLibc::Default
};
(TargetOs::Linux, BinaryFormat::Elf, libc)
}
"windows" => {
let libc = if env_str == "gnu" {
TargetLibc::MinGw
} else {
TargetLibc::Default
};
(TargetOs::Windows, BinaryFormat::Coff, libc)
}
other => {
return Err(AotError::UnsupportedTarget(format!(
"os component {other:?} of triple {triple:?} not in tier 1/2 set \
(supported: darwin, linux, windows)"
)));
}
};
Ok(TargetSpec {
triple: triple.to_string(),
is_host: false,
format,
arch,
endian: Endianness::Little,
os,
libc,
})
}
pub fn triple_for_cargo(&self) -> Option<&str> {
if self.is_host {
None
} else {
Some(&self.triple)
}
}
pub fn is_msvc(&self) -> bool {
self.os == TargetOs::Windows && self.libc == TargetLibc::Default
}
fn msvc_cc_command(&self) -> Option<Command> {
if let Ok(cc) = std::env::var("CC") {
return Some(Command::new(cc));
}
for candidate in &["clang-cl", "cl.exe", "cl"] {
if which_on_path(candidate) {
return Some(Command::new(candidate));
}
}
None
}
fn msvc_link_command(&self) -> Option<Command> {
if let Ok(ld) = std::env::var("LD") {
return Some(Command::new(ld));
}
for candidate in &["lld-link", "link.exe", "link"] {
if which_on_path(candidate) {
return Some(Command::new(candidate));
}
}
None
}
pub fn cc_command(&self) -> Command {
if let Ok(cc) = std::env::var("CC") {
return Command::new(cc);
}
if self.is_host {
return Command::new("cc");
}
let cross_candidates: &[&str] = match (self.os, self.arch, self.libc) {
(TargetOs::Linux, Architecture::Aarch64, TargetLibc::Default) => {
&["aarch64-linux-gnu-gcc"]
}
(TargetOs::Linux, Architecture::X86_64, TargetLibc::Default) => {
&["x86_64-linux-gnu-gcc"]
}
(TargetOs::Linux, Architecture::Aarch64, TargetLibc::Musl) => {
&["aarch64-linux-musl-gcc", "musl-gcc"]
}
(TargetOs::Linux, Architecture::X86_64, TargetLibc::Musl) => {
&["x86_64-linux-musl-gcc", "musl-gcc"]
}
(TargetOs::Windows, Architecture::X86_64, TargetLibc::MinGw) => {
&["x86_64-w64-mingw32-gcc"]
}
(TargetOs::Windows, Architecture::I386, TargetLibc::MinGw) => &["i686-w64-mingw32-gcc"],
_ => &[],
};
for candidate in cross_candidates {
if which_on_path(candidate) {
return Command::new(candidate);
}
}
if self.os == TargetOs::MacOs {
let mut cmd = Command::new("cc");
cmd.arg("-target").arg(&self.triple);
return cmd;
}
let mut cmd = Command::new("cc");
cmd.arg("-target").arg(&self.triple);
cmd
}
pub fn cranelift_isa_builder(&self) -> Result<cranelift_codegen::isa::Builder, AotError> {
use std::str::FromStr;
if self.is_host {
return cranelift_native::builder().map_err(|e| {
AotError::Object(format!(
"cranelift_native::builder for host triple {}: {e}",
self.triple
))
});
}
let triple = target_lexicon::Triple::from_str(&self.triple).map_err(|e| {
AotError::Object(format!(
"target_lexicon could not parse triple {:?}: {e}",
self.triple
))
})?;
cranelift_codegen::isa::lookup(triple).map_err(|e| {
AotError::Object(format!(
"cranelift_codegen::isa::lookup for triple {}: {e:?} \
(re-build luna-aot with `cranelift-codegen` feature \
`all-arch` or the per-arch feature for {})",
self.triple, self.triple,
))
})
}
}
fn which_on_path(binary: &str) -> bool {
if let Ok(path) = std::env::var("PATH") {
for dir in path.split(if cfg!(windows) { ';' } else { ':' }) {
if dir.is_empty() {
continue;
}
let candidate = std::path::Path::new(dir).join(binary);
if candidate.exists() {
return true;
}
}
}
false
}
fn write_bytecode_object_for(
bytecode: &[u8],
out: &Path,
target: &TargetSpec,
) -> Result<(), AotError> {
let mut obj = Object::new(target.format, target.arch, target.endian);
let section_id = obj.add_section(
Vec::new(),
BYTECODE_SECTION_NAME.as_bytes().to_vec(),
SectionKind::ReadOnlyData,
);
let _start_offset = obj.append_section_data(section_id, bytecode, 1);
let start_name = BYTECODE_START_SYMBOL.to_string();
let end_name = BYTECODE_END_SYMBOL.to_string();
let _start_sym = obj.add_symbol(Symbol {
name: start_name.into_bytes(),
value: 0,
size: 0,
kind: SymbolKind::Data,
scope: SymbolScope::Dynamic,
weak: false,
section: SymbolSection::Section(section_id),
flags: object::SymbolFlags::None,
});
let _end_sym = obj.add_symbol(Symbol {
name: end_name.into_bytes(),
value: bytecode.len() as u64,
size: 0,
kind: SymbolKind::Data,
scope: SymbolScope::Dynamic,
weak: false,
section: SymbolSection::Section(section_id),
flags: object::SymbolFlags::None,
});
let bytes = obj
.write()
.map_err(|e| AotError::Object(format!("Object::write: {e}")))?;
fs::write(out, bytes)?;
Ok(())
}
fn write_aot_cmain_object_for(out: &Path, target: &TargetSpec) -> Result<(), AotError> {
let placeholder = match target.os {
TargetOs::MacOs => {
"__attribute__((used, section(\"__DATA,luna_strkey_idx\"), aligned(8)))\n\
static const char luna_strkey_idx_placeholder[16] = {0};\n\
__attribute__((used, section(\"__DATA,luna_trace_meta\"), aligned(8)))\n\
static const char luna_trace_meta_placeholder[48] = {0};\n\
__attribute__((used, section(\"__DATA,luna_inline_chnx\"), aligned(8)))\n\
static const char luna_inline_chnx_placeholder[16] = {0};\n"
}
TargetOs::Linux => {
"__attribute__((used, section(\"luna_strkey_idx\"), aligned(8)))\n\
static const char luna_strkey_idx_placeholder[16] = {0};\n\
__attribute__((used, section(\"luna_trace_meta\"), aligned(8)))\n\
static const char luna_trace_meta_placeholder[48] = {0};\n\
__attribute__((used, section(\"luna_inline_chnx\"), aligned(8)))\n\
static const char luna_inline_chnx_placeholder[16] = {0};\n"
}
TargetOs::Windows if target.is_msvc() => {
"#pragma section(\".lt_skix\", read)\n\
__declspec(allocate(\".lt_skix\")) __declspec(align(8))\n\
static const char luna_strkey_idx_placeholder[16] = {0};\n\
#pragma section(\".lt_meta\", read)\n\
__declspec(allocate(\".lt_meta\")) __declspec(align(8))\n\
static const char luna_trace_meta_placeholder[48] = {0};\n\
#pragma section(\".lt_chai\", read)\n\
__declspec(allocate(\".lt_chai\")) __declspec(align(8))\n\
static const char luna_inline_chnx_placeholder[16] = {0};\n"
}
TargetOs::Windows => {
"__attribute__((used, section(\".lt_skix\"), aligned(8)))\n\
static const char luna_strkey_idx_placeholder[16] = {0};\n\
__attribute__((used, section(\".lt_meta\"), aligned(8)))\n\
static const char luna_trace_meta_placeholder[48] = {0};\n\
__attribute__((used, section(\".lt_chai\"), aligned(8)))\n\
static const char luna_inline_chnx_placeholder[16] = {0};\n"
}
};
let c_src = format!(
r#"#include <stddef.h>
#include <stdint.h>
extern uint8_t __luna_bytecode_start[];
extern uint8_t __luna_bytecode_end[];
extern int luna_aot_run(const uint8_t *bytecode, size_t len);
{placeholder}
int main(int argc, char **argv) {{
(void)argc; (void)argv;
size_t len = (size_t)(__luna_bytecode_end - __luna_bytecode_start);
return luna_aot_run(__luna_bytecode_start, len);
}}
"#
);
let mut c_path = out.to_path_buf();
c_path.set_extension("c");
fs::write(&c_path, c_src)?;
let mut cmd = if target.is_msvc() {
let Some(mut cl) = target.msvc_cc_command() else {
return Err(AotError::Link(format!(
"MSVC C compiler not on PATH for target {} — install one of: \
(a) `clang-cl` via LLVM (`brew install llvm` on macOS; \
`apt install clang` on Linux), or (b) Visual Studio Build \
Tools 2022 (`cl.exe`, Windows host only — invoke luna-aot \
from a Developer Command Prompt). Override with `CC=...` \
to point at a custom driver.",
target.triple
)));
};
cl.arg("/c");
cl.arg(&c_path);
cl.arg(format!("/Fo:{}", out.display()));
cl.arg("/nologo");
if !cfg!(target_os = "windows") {
cl.arg(format!("--target={}", target.triple));
}
cl
} else {
let mut cmd = target.cc_command();
cmd.arg("-c").arg(&c_path).arg("-o").arg(out);
cmd
};
let status = cmd
.output()
.map_err(|e| AotError::Link(format!("spawn cc for target {}: {e}", target.triple)))?;
if !status.status.success() {
return Err(AotError::Link(format!(
"cc -c {} (target {}) failed (exit {:?}):\n{}",
c_path.display(),
target.triple,
status.status.code(),
String::from_utf8_lossy(&status.stderr)
)));
}
Ok(())
}
fn link_aot_binary_for(
bytecode_obj: &Path,
cmain_obj: &Path,
traces_obj: Option<&Path>,
staticlib: &Path,
out_path: &Path,
target: &TargetSpec,
) -> Result<(), AotError> {
if target.is_msvc() {
return link_aot_binary_msvc(
bytecode_obj,
cmain_obj,
traces_obj,
staticlib,
out_path,
target,
);
}
let mut cmd = target.cc_command();
cmd.arg(cmain_obj).arg(bytecode_obj);
if let Some(traces) = traces_obj {
cmd.arg(traces);
}
cmd.arg(staticlib);
match target.os {
TargetOs::MacOs => {
cmd.args(["-framework", "CoreFoundation"]);
cmd.args(["-framework", "Security"]);
cmd.arg("-liconv");
}
TargetOs::Linux => {
cmd.arg("-lpthread");
cmd.arg("-ldl");
cmd.arg("-lm");
if target.libc != TargetLibc::Musl {
cmd.arg("-lrt");
cmd.arg("-lgcc_s");
cmd.arg("-lutil");
}
}
TargetOs::Windows => {
cmd.arg("-luserenv");
cmd.arg("-lkernel32");
cmd.arg("-lws2_32");
cmd.arg("-lbcrypt");
cmd.arg("-ladvapi32");
cmd.arg("-lntdll");
}
}
cmd.arg("-o").arg(out_path);
let output = cmd
.output()
.map_err(|e| AotError::Link(format!("spawn cc for target {}: {e}", target.triple)))?;
if !output.status.success() {
return Err(AotError::Link(format!(
"cc link failed (target {}, exit {:?}):\ncommand: {:?}\nstderr:\n{}",
target.triple,
output.status.code(),
cmd,
String::from_utf8_lossy(&output.stderr),
)));
}
Ok(())
}
fn link_aot_binary_msvc(
bytecode_obj: &Path,
cmain_obj: &Path,
traces_obj: Option<&Path>,
staticlib: &Path,
out_path: &Path,
target: &TargetSpec,
) -> Result<(), AotError> {
let Some(mut cmd) = target.msvc_link_command() else {
return Err(AotError::Link(format!(
"MSVC linker (lld-link / link.exe) not on PATH for target {} — \
install one of: (a) LLVM (`brew install llvm` on macOS; \
`apt install lld` on Linux) which ships `lld-link`, or \
(b) Visual Studio Build Tools 2022 (`link.exe`, Windows host \
only — invoke luna-aot from a Developer Command Prompt so \
`PATH` + `LIB` are set). Override with `LD=...` to point at \
a custom linker.",
target.triple
)));
};
cmd.arg("/NOLOGO");
cmd.arg("/SUBSYSTEM:CONSOLE");
let machine = match target.arch {
Architecture::X86_64 => "X64",
Architecture::I386 => "X86",
Architecture::Aarch64 => "ARM64",
_ => {
return Err(AotError::Link(format!(
"MSVC link: unsupported arch {:?} for target {} (supported: \
x86_64, i686, aarch64)",
target.arch, target.triple
)));
}
};
cmd.arg(format!("/MACHINE:{machine}"));
cmd.arg(format!("/OUT:{}", out_path.display()));
cmd.arg(cmain_obj).arg(bytecode_obj);
if let Some(traces) = traces_obj {
cmd.arg(traces);
}
cmd.arg(staticlib);
for lib in &[
"bcrypt.lib",
"userenv.lib",
"ws2_32.lib",
"advapi32.lib",
"ntdll.lib",
"kernel32.lib",
"legacy_stdio_definitions.lib",
"ucrt.lib",
"vcruntime.lib",
"msvcrt.lib",
] {
cmd.arg(lib);
}
let output = cmd.output().map_err(|e| {
AotError::Link(format!(
"spawn MSVC linker for target {}: {e}",
target.triple
))
})?;
if !output.status.success() {
return Err(AotError::Link(format!(
"MSVC link failed (target {}, exit {:?}):\ncommand: {:?}\n\
stdout:\n{}\nstderr:\n{}",
target.triple,
output.status.code(),
cmd,
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr),
)));
}
Ok(())
}
struct RecordingTraceCompiler {
inner: luna_jit::jit_backend::CraneliftBackend,
}
thread_local! {
static AOT_CAPTURED_RECORDS: std::cell::RefCell<Vec<([u8; 16], u32, TraceRecord)>> =
const { std::cell::RefCell::new(Vec::new()) };
}
impl luna_core::jit::TraceCompiler for RecordingTraceCompiler {
fn try_compile_trace(
&self,
storage: &mut dyn luna_core::jit::JitStorage,
record: &TraceRecord,
opts: CompileOptions,
) -> Option<CompiledTrace> {
let hash = record.head_proto.stable_hash();
AOT_CAPTURED_RECORDS.with(|cell| {
cell.borrow_mut()
.push((hash, record.head_pc, record.clone()));
});
self.inner.try_compile_trace(storage, record, opts)
}
fn last_compile_checkpoint(&self) -> &'static str {
self.inner.last_compile_checkpoint()
}
}
fn harvest_and_emit_aot_traces(
dump_bytes: &[u8],
version: LuaVersion,
out: &Path,
target: &TargetSpec,
) -> Result<HarvestedTraces, AotError> {
use cranelift_codegen::settings::{self, Configurable};
use cranelift_module::{DataDescription, Linkage, Module, default_libcall_names};
use cranelift_object::{ObjectBuilder, ObjectModule};
AOT_CAPTURED_RECORDS.with(|cell| cell.borrow_mut().clear());
let mut vm = luna_jit::new_with_jit(version);
vm.install_jit_backend(
luna_jit::jit_backend::CraneliftBackend,
RecordingTraceCompiler {
inner: luna_jit::jit_backend::CraneliftBackend,
},
);
vm.set_trace_jit_enabled(true);
vm.set_jit_enabled(false);
vm.set_bytecode_loading(true);
let closure = match vm.load(dump_bytes, b"=embedded-aot-warmup") {
Ok(c) => c,
Err(_) => return Ok(HarvestedTraces::None),
};
let _ = vm.call_value(Value::Closure(closure), &[]);
let captured: Vec<([u8; 16], u32, TraceRecord)> =
AOT_CAPTURED_RECORDS.with(|cell| std::mem::take(&mut *cell.borrow_mut()));
let probe_on = std::env::var_os("LUNA_AOT_HARVEST_PROBE").is_some();
if probe_on {
eprintln!("luna-aot harvest: captured {} TraceRecords", captured.len());
}
if captured.is_empty() {
return Ok(HarvestedTraces::None);
}
let mut installable: Vec<(
usize,
[u8; 16],
u32,
TraceRecord,
luna_core::jit::send_compat::TArc<CompiledTrace>,
)> = Vec::new();
let mut filter_stats = (0usize, 0usize, 0usize, 0usize);
for (i, (hash, head_pc, record)) in captured.into_iter().enumerate() {
let proto = record.head_proto;
let traces_ref = proto.traces.borrow();
let Some(ct) = traces_ref.iter().find(|c| c.head_pc == head_pc).cloned() else {
filter_stats.0 += 1;
continue;
};
drop(traces_ref);
if !ct.dispatchable {
filter_stats.1 += 1;
continue;
}
if !ct.per_exit_inline.is_empty() {
filter_stats.2 += 1;
}
if !ct.per_exit_tags.is_empty() {
filter_stats.3 += 1;
}
installable.push((i, hash, head_pc, record, ct));
}
if probe_on {
eprintln!(
"luna-aot harvest: installable={}, filtered: no_ct={}, undispatchable={}, accepted_with_per_exit_inline={}, accepted_with_per_exit_tags={}",
installable.len(),
filter_stats.0,
filter_stats.1,
filter_stats.2,
filter_stats.3,
);
}
if installable.is_empty() {
return Ok(HarvestedTraces::None);
}
let isa = {
let mut flag_builder = settings::builder();
flag_builder
.set("use_colocated_libcalls", "false")
.expect("flag");
flag_builder.set("is_pic", "true").expect("flag");
flag_builder.set("opt_level", "speed").expect("flag");
target
.cranelift_isa_builder()?
.finish(settings::Flags::new(flag_builder))
.map_err(|e| {
AotError::Object(format!(
"Cranelift ISA finish for target {}: {e}",
target.triple
))
})?
};
let object_builder = ObjectBuilder::new(isa, "luna_aot_traces", default_libcall_names())
.map_err(|e| AotError::Object(format!("ObjectBuilder: {e}")))?;
let mut module = ObjectModule::new(object_builder);
let mut blob_payload: Vec<u8> = Vec::new();
let mut per_trace_meta: Vec<(String, u32, u32)> = Vec::new();
for (idx, _hash, _head_pc, record, ct) in installable.iter() {
let fn_name = format!("luna_aot_trace_{idx:08x}");
let opts = CompileOptions {
internal_loop: true,
pre53: matches!(version, LuaVersion::Lua51 | LuaVersion::Lua52),
aot: true,
};
let lower_res = luna_jit::jit_backend::trace::lower_trace_into_named(
&mut module,
record,
opts,
Some(&fn_name),
);
if lower_res.is_none() {
if probe_on {
eprintln!(
"luna-aot harvest: AOT lower bailed for trace idx={idx} head_pc={}",
ct.head_pc
);
}
continue;
}
let entry_tags_vec: Vec<u8> = ct.entry_tags.iter().copied().collect();
let exit_tags_vec: Vec<u8> = ct.exit_tags.iter().copied().map(pack_exit_tag).collect();
let per_exit_tags_entries: Vec<PerExitTagsEntry> = ct
.per_exit_tags
.iter()
.map(|(cont_pc, tags)| PerExitTagsEntry {
cont_pc: *cont_pc,
tags_packed: tags.iter().copied().map(pack_exit_tag).collect(),
})
.collect();
let header = AotTraceMetaHeader {
magic: luna_core::jit::aot_meta::AOT_META_MAGIC,
version: luna_core::jit::aot_meta::AOT_META_VERSION,
head_pc: ct.head_pc,
n_ops: ct.n_ops,
window_size: ct.window_size,
dispatchable: u8::from(ct.dispatchable),
tag_res_kind: pack_tag_res_kind(ct.global_tag_res_kind),
entry_tags_len: entry_tags_vec.len() as u16,
exit_tags_len: exit_tags_vec.len() as u32,
};
let per_exit_inline_entries: Vec<PerExitInlineEntry> = ct
.per_exit_inline
.iter()
.map(PerExitInlineEntry::from_inline_side_exit)
.collect();
let blob = encode_meta_blob(
&header,
&entry_tags_vec,
&exit_tags_vec,
&per_exit_tags_entries,
&per_exit_inline_entries,
);
let blob_offset = blob_payload.len() as u32;
let blob_len = blob.len() as u32;
blob_payload.extend_from_slice(&blob);
per_trace_meta.push((fn_name, blob_offset, blob_len));
}
if per_trace_meta.is_empty() {
return Ok(HarvestedTraces::None);
}
let blob_data_id = module
.declare_data("__luna_trace_blob_combined", Linkage::Local, false, false)
.map_err(|e| AotError::Object(format!("declare_data blob: {e}")))?;
{
let mut desc = DataDescription::new();
desc.define(blob_payload.into_boxed_slice());
let (blob_seg, blob_sect) = if cfg!(target_os = "windows") {
("", ".lt_blob")
} else {
("__DATA", "luna_trace_blob")
};
desc.set_segment_section(blob_seg, blob_sect);
module
.define_data(blob_data_id, &desc)
.map_err(|e| AotError::Object(format!("define_data blob: {e}")))?;
}
for (idx, (fn_name, blob_offset, blob_len)) in per_trace_meta.iter().enumerate() {
let hash = installable[idx].1;
let head_pc = installable[idx].2;
let entry_data_id = module
.declare_data(
&format!("__luna_trace_meta_entry_{idx:08x}"),
Linkage::Local,
false,
false,
)
.map_err(|e| AotError::Object(format!("declare_data meta entry: {e}")))?;
let mut desc = DataDescription::new();
let mut payload = [0u8; 48];
payload[0..16].copy_from_slice(&hash);
payload[16..20].copy_from_slice(&head_pc.to_le_bytes());
payload[40..44].copy_from_slice(&blob_len.to_le_bytes());
desc.define(Box::new(payload));
let (meta_seg, meta_sect) = if cfg!(target_os = "windows") {
("", ".lt_meta")
} else {
("__DATA", "luna_trace_meta")
};
desc.set_segment_section(meta_seg, meta_sect);
desc.set_align(8);
let trace_fn_sig = {
let mut sig = module.make_signature();
sig.params.push(cranelift_codegen::ir::AbiParam::new(
cranelift_codegen::ir::types::I64,
));
sig.returns.push(cranelift_codegen::ir::AbiParam::new(
cranelift_codegen::ir::types::I64,
));
sig
};
let fn_id = module
.declare_function(fn_name, Linkage::Import, &trace_fn_sig)
.map_err(|e| AotError::Object(format!("declare_function for reloc: {e}")))?;
let fn_gv = module.declare_func_in_data(fn_id, &mut desc);
desc.write_function_addr(24, fn_gv);
let blob_gv = module.declare_data_in_data(blob_data_id, &mut desc);
desc.write_data_addr(32, blob_gv, *blob_offset as i64);
module
.define_data(entry_data_id, &desc)
.map_err(|e| AotError::Object(format!("define_data meta entry: {e}")))?;
}
let product = module.finish();
let bytes = product
.emit()
.map_err(|e| AotError::Object(format!("ObjectProduct::emit: {e}")))?;
fs::write(out, &bytes)?;
Ok(HarvestedTraces::Some)
}