use std::path::{Path, PathBuf};
use std::process::Command;
use inkwell::AddressSpace;
use inkwell::OptimizationLevel;
use inkwell::context::Context;
use inkwell::module::{Linkage, Module};
use inkwell::targets::{
CodeModel, FileType, InitializationConfig, RelocMode, Target, TargetMachine, TargetTriple,
};
use inkwell::values::FunctionValue;
use crate::error::CodegenError;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct AotKernelHandle {
pub abi_version: u32,
pub process_count: u32,
}
pub struct AotBackend {
target_machine: TargetMachine,
}
impl AotBackend {
pub fn for_host() -> Result<Self, CodegenError> {
Target::initialize_native(&InitializationConfig::default())
.map_err(|e| CodegenError::ObjectEmission(format!("native target init: {e}")))?;
let triple = TargetMachine::get_default_triple();
let target = Target::from_triple(&triple).map_err(|e| {
CodegenError::ObjectEmission(format!(
"failed to resolve a Target for host triple {:?}: {e}",
triple.as_str()
))
})?;
if !target.has_target_machine() {
return Err(CodegenError::ObjectEmission(format!(
"CG-140: host target {:?} has no TargetMachine (codegen backend) registered \
in this pinned llvm-sys build -- object emission is impossible (a JIT-only \
or reduced target-list prebuilt?)",
triple.as_str()
)));
}
let cpu = TargetMachine::get_host_cpu_name();
let features = TargetMachine::get_host_cpu_features();
let target_machine = target
.create_target_machine(
&triple,
cpu.to_str().unwrap_or(""),
features.to_str().unwrap_or(""),
OptimizationLevel::Default,
RelocMode::PIC,
CodeModel::Default,
)
.ok_or_else(|| {
CodegenError::ObjectEmission(format!(
"Target::create_target_machine returned None for host triple {:?}",
triple.as_str()
))
})?;
verify_target_machine_emits_objects(&target_machine)?;
Ok(AotBackend { target_machine })
}
pub fn triple(&self) -> TargetTriple {
self.target_machine.get_triple()
}
pub fn emit_object(&self, module: &Module, path: &Path) -> Result<(), CodegenError> {
self.target_machine
.write_to_file(module, FileType::Object, path)
.map_err(|e| CodegenError::ObjectEmission(format!("{path:?}: {e}")))
}
pub fn link(
&self,
objects: &[PathBuf],
runtime_rlib: &Path,
out_so: &Path,
) -> Result<AotKernelHandle, CodegenError> {
self.link_exporting(objects, runtime_rlib, out_so, None)
}
pub fn link_exporting(
&self,
objects: &[PathBuf],
runtime_rlib: &Path,
out_so: &Path,
exports: Option<&[&str]>,
) -> Result<AotKernelHandle, CodegenError> {
if objects.is_empty() {
return Err(CodegenError::Link(
"link: at least one object file is required".to_string(),
));
}
let mut cmd = Command::new("cc");
cmd.arg("-shared").arg("-fPIC").arg("-o").arg(out_so);
cmd.arg("-Wl,-Bsymbolic");
cmd.arg("-Wl,-z,lazy");
let script = exports.map(|_| out_so.with_extension("map"));
if let (Some(path), Some(patterns)) = (&script, exports) {
let mut text = String::from("{\n global:\n");
for p in patterns {
text.push_str(&format!(" {p};\n"));
}
text.push_str(" local:\n *;\n};\n");
std::fs::write(path, text).map_err(|e| {
CodegenError::Link(format!("cannot write the version script {path:?}: {e}"))
})?;
cmd.arg(format!("-Wl,--version-script={}", path.display()));
}
for obj in objects {
cmd.arg(obj);
}
cmd.arg("-Wl,--whole-archive")
.arg(runtime_rlib)
.arg("-Wl,--no-whole-archive");
cmd.arg("-lpthread").arg("-ldl").arg("-lm");
let output = cmd
.output()
.map_err(|e| CodegenError::Link(format!("failed to spawn `cc`: {e}")))?;
if let Some(path) = &script {
let _ = std::fs::remove_file(path);
}
if !output.status.success() {
return Err(CodegenError::Link(format!(
"`cc -shared` exited with {:?}: {}",
output.status.code(),
String::from_utf8_lossy(&output.stderr)
)));
}
let lib = unsafe { libloading::Library::new(out_so) }
.map_err(|e| CodegenError::Load(format!("dlopen {}: {e}", out_so.display())))?;
let abi_version: u32 = unsafe {
let sym: libloading::Symbol<extern "C" fn() -> u32> = lib
.get(sv_sim_abi::version::SIM_ABI_VERSION_SYMBOL.as_bytes())
.map_err(|e| CodegenError::Load(format!("dlsym sim_abi_version: {e}")))?;
sym()
};
let process_count: u32 = unsafe {
let sym: libloading::Symbol<extern "C" fn(sv_sim_abi::SimHandle) -> u32> = lib
.get(sv_sim_abi::entry::SIM_PROCESS_COUNT_SYMBOL.as_bytes())
.map_err(|e| CodegenError::Load(format!("dlsym sim_process_count: {e}")))?;
sym(sv_sim_abi::SimHandle(std::ptr::null_mut()))
};
Ok(AotKernelHandle {
abi_version,
process_count,
})
}
pub fn link_exe(
&self,
objects: &[PathBuf],
runtime_rlib: &Path,
out_exe: &Path,
) -> Result<(), CodegenError> {
if objects.is_empty() {
return Err(CodegenError::Link(
"link_exe: at least one object file is required".to_string(),
));
}
let mut cmd = Command::new("cc");
cmd.arg("-o").arg(out_exe);
for obj in objects {
cmd.arg(obj);
}
cmd.arg("-Wl,--whole-archive")
.arg(runtime_rlib)
.arg("-Wl,--no-whole-archive");
cmd.arg("-Wl,--export-dynamic");
cmd.arg("-lpthread").arg("-ldl").arg("-lm");
let output = cmd
.output()
.map_err(|e| CodegenError::Link(format!("failed to spawn `cc`: {e}")))?;
if !output.status.success() {
return Err(CodegenError::Link(format!(
"`cc` exited with {:?}: {}",
output.status.code(),
String::from_utf8_lossy(&output.stderr)
)));
}
let bytes = std::fs::read(out_exe)
.map_err(|e| CodegenError::Load(format!("reading {}: {e}", out_exe.display())))?;
if !elf_header_is_a_valid_executable(&bytes) {
return Err(CodegenError::Load(format!(
"{} was linked but its ELF header is not a valid ET_EXEC/ET_DYN executable",
out_exe.display()
)));
}
Ok(())
}
}
fn elf_header_is_a_valid_executable(bytes: &[u8]) -> bool {
const ET_EXEC: u16 = 2;
const ET_DYN: u16 = 3;
bytes.len() >= 18
&& bytes[0..4] == [0x7f, b'E', b'L', b'F']
&& bytes[4] == 2 && bytes[5] == 1 && matches!(u16::from_le_bytes([bytes[16], bytes[17]]), ET_EXEC | ET_DYN)
}
fn verify_target_machine_emits_objects(tm: &TargetMachine) -> Result<(), CodegenError> {
let ctx = Context::create();
let probe_module = ctx.create_module("cg140_probe");
let void_fn_ty = ctx.void_type().fn_type(&[], false);
let f = probe_module.add_function("cg140_probe_fn", void_fn_ty, None);
let builder = ctx.create_builder();
let entry = ctx.append_basic_block(f, "entry");
builder.position_at_end(entry);
builder.build_return(None).map_err(|e| {
CodegenError::ObjectEmission(format!(
"CG-140 probe module failed to build its own trivial body: {e}"
))
})?;
probe_module.verify().map_err(|e| {
CodegenError::ObjectEmission(format!(
"CG-140 probe module unexpectedly failed LLVM verify: {e}"
))
})?;
tm.write_to_memory_buffer(&probe_module, FileType::Object)
.map(|_buf| ())
.map_err(|e| {
CodegenError::ObjectEmission(format!(
"CG-140: this build's pinned llvm-sys/LLVM prebuilt does not actually support \
object-file emission for host triple {:?} (a JIT-only or reduced target-list \
build?): {e}",
tm.get_triple().as_str()
))
})
}
pub fn emit_process_count_fn<'ctx>(
ctx: &'ctx Context,
module: &Module<'ctx>,
count: u32,
) -> FunctionValue<'ctx> {
let ptr_ty = ctx.ptr_type(AddressSpace::default());
let i32_ty = ctx.i32_type();
let fn_ty = i32_ty.fn_type(&[ptr_ty.into()], false);
let f = module.add_function(
sv_sim_abi::entry::SIM_PROCESS_COUNT_SYMBOL,
fn_ty,
Some(Linkage::External),
);
let entry = ctx.append_basic_block(f, "entry");
let builder = ctx.create_builder();
builder.position_at_end(entry);
let ret = i32_ty.const_int(u64::from(count), false);
builder
.build_return(Some(&ret))
.expect("a single `ret i32 <const>` in a fresh entry block always builds");
f
}
pub const DESIGN_IMAGE_PTR_SYMBOL: &str = "sv_sim_design_image_ptr";
pub const DESIGN_IMAGE_LEN_SYMBOL: &str = "sv_sim_design_image_len";
pub const SYMBOL_TABLE_SYMBOL: &str = "SV_SIM_SYMBOL_TABLE";
pub const SYMBOL_COUNT_SYMBOL: &str = "SV_SIM_SYMBOL_COUNT";
pub const AOT_REGISTER_SYMBOL: &str = "sv_sim_aot_register";
pub fn emit_image_globals<'ctx>(
ctx: &'ctx Context,
module: &Module<'ctx>,
image_bytes: &[u8],
symbols: &[String],
) -> Result<(), crate::error::LoweringError> {
let ptr_ty = ctx.ptr_type(AddressSpace::default());
let i32_ty = ctx.i32_type();
let i64_ty = ctx.i64_type();
let builder = ctx.create_builder();
let fail = |m: String| crate::error::LoweringError {
message: m,
at: "aot::emit_image_globals".to_string(),
};
let data = ctx.const_string(image_bytes, false);
let image_global = module.add_global(data.get_type(), None, "SV_SIM_DESIGN_IMAGE");
image_global.set_initializer(&data);
image_global.set_constant(true);
image_global.set_linkage(Linkage::Internal);
let image_ptr = image_global.as_pointer_value();
let ptr_fn = module.add_function(
DESIGN_IMAGE_PTR_SYMBOL,
ptr_ty.fn_type(&[], false),
Some(Linkage::External),
);
builder.position_at_end(ctx.append_basic_block(ptr_fn, "entry"));
builder
.build_return(Some(&image_ptr))
.map_err(|e| fail(e.to_string()))?;
let len_fn = module.add_function(
DESIGN_IMAGE_LEN_SYMBOL,
i64_ty.fn_type(&[], false),
Some(Linkage::External),
);
builder.position_at_end(ctx.append_basic_block(len_fn, "entry"));
builder
.build_return(Some(&i64_ty.const_int(image_bytes.len() as u64, false)))
.map_err(|e| fail(e.to_string()))?;
let mut entries = Vec::with_capacity(symbols.len());
for name in symbols {
let f = module.get_function(name).ok_or_else(|| {
fail(format!(
"the image names kernel `{name}`, which was not lowered"
))
})?;
entries.push(f.as_global_value().as_pointer_value());
}
let table = ptr_ty.const_array(&entries);
let table_global = module.add_global(table.get_type(), None, SYMBOL_TABLE_SYMBOL);
table_global.set_initializer(&table);
table_global.set_constant(true);
table_global.set_linkage(Linkage::External);
let count_global = module.add_global(i32_ty, None, SYMBOL_COUNT_SYMBOL);
count_global.set_initializer(&i32_ty.const_int(symbols.len() as u64, false));
count_global.set_constant(true);
count_global.set_linkage(Linkage::External);
let register = module.add_function(
AOT_REGISTER_SYMBOL,
ctx.void_type().fn_type(
&[ptr_ty.into(), i64_ty.into(), ptr_ty.into(), i32_ty.into()],
false,
),
Some(Linkage::External),
);
let ctor = module.add_function(
"sv_sim_aot_ctor",
ctx.void_type().fn_type(&[], false),
Some(Linkage::Internal),
);
builder.position_at_end(ctx.append_basic_block(ctor, "entry"));
builder
.build_call(
register,
&[
image_ptr.into(),
i64_ty.const_int(image_bytes.len() as u64, false).into(),
table_global.as_pointer_value().into(),
i32_ty.const_int(symbols.len() as u64, false).into(),
],
"",
)
.map_err(|e| fail(e.to_string()))?;
builder
.build_return(None)
.map_err(|e| fail(e.to_string()))?;
let entry_ty = ctx.struct_type(&[i32_ty.into(), ptr_ty.into(), ptr_ty.into()], false);
let entry = ctx.const_struct(
&[
i32_ty.const_int(65535, false).into(),
ctor.as_global_value().as_pointer_value().into(),
ptr_ty.const_null().into(),
],
false,
);
let ctors = entry_ty.const_array(&[entry]);
let ctors_global = module.add_global(ctors.get_type(), None, "llvm.global_ctors");
ctors_global.set_initializer(&ctors);
ctors_global.set_linkage(Linkage::Appending);
Ok(())
}
pub fn emit_main_fn<'ctx>(ctx: &'ctx Context, module: &Module<'ctx>) -> FunctionValue<'ctx> {
let ptr_ty = ctx.ptr_type(AddressSpace::default());
let i32_ty = ctx.i32_type();
let aot_main = module.add_function(
"sv_sim_aot_main",
i32_ty.fn_type(&[i32_ty.into(), ptr_ty.into()], false),
Some(Linkage::External),
);
let main_fn = module.add_function(
"main",
i32_ty.fn_type(&[i32_ty.into(), ptr_ty.into()], false),
Some(Linkage::External),
);
let builder = ctx.create_builder();
let entry = ctx.append_basic_block(main_fn, "entry");
builder.position_at_end(entry);
let argc = main_fn.get_nth_param(0).expect("main has an argc param");
let argv = main_fn.get_nth_param(1).expect("main has an argv param");
let ret = builder
.build_call(aot_main, &[argc.into(), argv.into()], "code")
.expect("a two-argument call to an external fn of matching type always builds")
.try_as_basic_value()
.unwrap_basic();
builder
.build_return(Some(&ret))
.expect("returning the call's own i32 result always builds");
main_fn
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn for_host_runs_the_cg140_object_emission_check() {
AotBackend::for_host()
.expect("AotBackend::for_host must succeed and must have verified real object emission on this host (CG-140)");
}
#[test]
fn link_with_no_objects_is_a_loud_error_not_a_panic_or_silent_noop() {
let backend = AotBackend::for_host().expect("AotBackend::for_host");
let dir = std::env::temp_dir();
let err = backend
.link(
&[],
&dir.join("cg063_link_empty_probe.rlib"),
&dir.join("cg063_link_empty_probe.so"),
)
.expect_err("link with zero objects must be an Err, never Ok or a panic");
let msg = err.to_string();
assert!(
msg.contains("at least one object file"),
"error message should name the actual problem, got: {msg}"
);
}
}