use core::fmt::Display;
use std::{collections::HashMap, path::PathBuf};
#[cfg(not(target_arch = "wasm32"))]
use std::process::{Command, Stdio};
use itertools::Itertools;
use target_lexicon::Architecture;
use wasmer_types::{CompileError, FunctionType, LocalFunctionIndex, Type};
#[cfg(not(target_arch = "wasm32"))]
use tempfile::NamedTempFile;
#[derive(Debug, Clone)]
pub enum CompiledKind {
Local(LocalFunctionIndex, String),
FunctionCallTrampoline(FunctionType),
DynamicFunctionTrampoline(FunctionType),
Module,
}
pub fn types_to_signature(types: &[Type]) -> String {
let tokens = types
.iter()
.map(|ty| match ty {
Type::I32 => "i",
Type::I64 => "I",
Type::F32 => "f",
Type::F64 => "F",
Type::V128 => "v",
Type::ExternRef => "e",
Type::FuncRef => "r",
Type::ExceptionRef => "x",
})
.collect_vec();
tokens
.chunk_by(|a, b| a == b)
.map(|chunk| {
if chunk.len() >= 8 {
format!("{}x{}", chunk.len(), chunk[0])
} else {
chunk.to_owned().join("")
}
})
.join("")
}
fn sanitize_filename(name: &str) -> String {
name.chars()
.map(|c| {
if c.is_alphanumeric() || c == '_' || c == '-' {
c
} else {
'_'
}
})
.collect()
}
pub fn function_kind_to_filename(kind: &CompiledKind, suffix: &str) -> String {
match kind {
CompiledKind::Local(local_func_index, name) => {
let mut name = sanitize_filename(name);
const PATH_LIMIT: usize = 255;
if name.len() + suffix.len() > PATH_LIMIT {
let id_string = local_func_index.as_u32().to_string();
name.truncate(PATH_LIMIT - id_string.len() - suffix.len() - 1);
name.push('_');
name.push_str(&id_string);
name.push_str(suffix);
} else {
name.push_str(suffix);
}
debug_assert!(name.len() <= PATH_LIMIT);
name
}
CompiledKind::FunctionCallTrampoline(func_type) => format!(
"trampoline_call_{}_{}{suffix}",
types_to_signature(func_type.params()),
types_to_signature(func_type.results())
),
CompiledKind::DynamicFunctionTrampoline(func_type) => format!(
"trampoline_dynamic_{}_{}{suffix}",
types_to_signature(func_type.params()),
types_to_signature(func_type.results())
),
CompiledKind::Module => "module".into(),
}
}
#[cfg(not(target_arch = "wasm32"))]
pub fn save_assembly_to_file<C: Display>(
arch: Architecture,
path: PathBuf,
body: &[u8],
assembly_comments: HashMap<usize, C>,
) -> Result<(), CompileError> {
use std::{fs::File, io::Write};
use which::which;
#[derive(Debug)]
struct DecodedInsn<'a> {
offset: usize,
insn: &'a str,
}
fn parse_instructions(content: &str) -> Result<Vec<DecodedInsn<'_>>, CompileError> {
content
.lines()
.map(|line| line.trim())
.skip_while(|l| !l.starts_with("0000000000000000"))
.skip(1)
.filter(|line| line.trim() != "...")
.map(|line| -> Result<DecodedInsn<'_>, CompileError> {
let (offset, insn_part) = line.split_once(':').ok_or(CompileError::Codegen(
format!("cannot parse objdump line: '{line}'"),
))?;
let insn = insn_part
.trim()
.split_once('\t')
.map_or("", |(_data, insn)| insn)
.trim();
Ok(DecodedInsn {
offset: usize::from_str_radix(offset, 16).map_err(|err| {
CompileError::Codegen(format!("hex number expected: {err}"))
})?,
insn,
})
})
.collect()
}
let mut tmpfile = NamedTempFile::new()
.map_err(|err| CompileError::Codegen(format!("cannot create temporary file: {err}")))?;
tmpfile
.write_all(body)
.map_err(|err| CompileError::Codegen(format!("assembly dump write failed: {err}")))?;
tmpfile
.flush()
.map_err(|err| CompileError::Codegen(format!("flush failed: {err}")))?;
let (objdump_arch, objdump_binary) = match arch {
Architecture::X86_64 => ("i386:x86-64", "x86_64-linux-gnu-objdump"),
Architecture::Aarch64(..) => ("aarch64", "aarch64-linux-gnu-objdump"),
Architecture::Riscv64(..) => ("riscv:rv64", "riscv64-linux-gnu-objdump"),
_ => {
return Err(CompileError::Codegen(
"Assembly dumping is not supported for this architecture".to_string(),
));
}
};
let bins = [objdump_binary, "objdump"];
let objdump_binary = bins.iter().find(|bin| which(bin).is_ok());
let Some(objdump_binary) = objdump_binary else {
return Ok(());
};
let command = Command::new(objdump_binary)
.arg("-b")
.arg("binary")
.arg("-m")
.arg(objdump_arch)
.arg("-D")
.arg(tmpfile.path())
.stdout(Stdio::piped())
.stderr(Stdio::null())
.spawn();
let Ok(command) = command else {
return Ok(());
};
let output = command
.wait_with_output()
.map_err(|err| CompileError::Codegen(format!("failed to read stdout: {err}")))?;
let content = String::from_utf8_lossy(&output.stdout);
let parsed_instructions = parse_instructions(content.as_ref())?;
let mut file = File::create(path).map_err(|err| {
CompileError::Codegen(format!("debug object file creation failed: {err}"))
})?;
for insn in parsed_instructions {
if let Some(comment) = assembly_comments.get(&insn.offset) {
file.write_all(format!(" \t\t;; {comment}\n").as_bytes())
.map_err(|err| {
CompileError::Codegen(format!("cannot write content to object file: {err}"))
})?;
}
file.write_all(format!("{:6x}:\t\t{}\n", insn.offset, insn.insn).as_bytes())
.map_err(|err| {
CompileError::Codegen(format!("cannot write content to object file: {err}"))
})?;
}
Ok(())
}
#[cfg(target_arch = "wasm32")]
pub fn save_assembly_to_file<C: Display>(
_arch: Architecture,
_path: PathBuf,
_body: &[u8],
_assembly_comments: HashMap<usize, C>,
) -> Result<(), CompileError> {
Ok(())
}