wasmer_compiler/
misc.rs

1//! A common functionality used among various compilers.
2
3use core::fmt::Display;
4use std::{collections::HashMap, path::PathBuf};
5
6#[cfg(not(target_arch = "wasm32"))]
7use std::process::{Command, Stdio};
8
9use itertools::Itertools;
10use target_lexicon::Architecture;
11use wasmer_types::{
12    CompileError, FunctionIndex, FunctionType, LocalFunctionIndex, SignatureIndex, Type,
13};
14
15#[cfg(not(target_arch = "wasm32"))]
16use tempfile::NamedTempFile;
17
18/// Represents the kind of compiled function or module, used for debugging and identification
19/// purposes across multiple compiler backends (e.g., LLVM, Cranelift).
20#[derive(Debug, Clone)]
21pub enum CompiledKind {
22    /// A locally-defined function in the Wasm file.
23    Local(LocalFunctionIndex, String),
24    /// A function call trampoline for a given signature.
25    FunctionCallTrampoline(SignatureIndex, FunctionType),
26    /// A dynamic function trampoline for a given imported function.
27    DynamicFunctionTrampoline(FunctionIndex, FunctionType),
28    /// An import function trampoline.
29    ImportFunctionTrampoline(FunctionIndex, FunctionType),
30    /// An entire Wasm module.
31    Module,
32}
33
34/// Converts a slice of `Type` into a string signature, mapping each type to a specific character.
35/// Used to represent function signatures in a compact string form.
36pub fn types_to_signature(types: &[Type]) -> String {
37    let tokens = types
38        .iter()
39        .map(|ty| match ty {
40            Type::I32 => "i",
41            Type::I64 => "I",
42            Type::F32 => "f",
43            Type::F64 => "F",
44            Type::V128 => "v",
45            Type::ExternRef => "e",
46            Type::FuncRef => "r",
47            Type::ExceptionRef => "x",
48        })
49        .collect_vec();
50    // Apparently, LLVM has issues if the filename is too long, thus we compact it.
51    tokens
52        .chunk_by(|a, b| a == b)
53        .map(|chunk| {
54            if chunk.len() >= 8 {
55                format!("{}x{}", chunk.len(), chunk[0])
56            } else {
57                chunk.to_owned().join("")
58            }
59        })
60        .join("")
61}
62
63/// Sanitizes a string so it can be safely used as a filename.
64fn sanitize_filename(name: &str) -> String {
65    name.chars()
66        .map(|c| {
67            if c.is_alphanumeric() || c == '_' || c == '-' {
68                c
69            } else {
70                '_'
71            }
72        })
73        .collect()
74}
75
76/// Converts a kind into a filename, that we will use to dump
77/// the contents of the IR object file to.
78pub fn function_kind_to_filename(kind: &CompiledKind, suffix: &str) -> String {
79    match kind {
80        CompiledKind::Local(local_func_index, name) => {
81            let mut name = sanitize_filename(name);
82
83            // Limit to 255 characters to comply with common filesystem path component restrictions.
84            const PATH_LIMIT: usize = 255;
85
86            if name.len() + suffix.len() > PATH_LIMIT {
87                let id_string = local_func_index.as_u32().to_string();
88                name.truncate(PATH_LIMIT - id_string.len() - suffix.len() - 1);
89                name.push('_');
90                name.push_str(&id_string);
91                name.push_str(suffix);
92            } else {
93                name.push_str(suffix);
94            }
95
96            debug_assert!(name.len() <= PATH_LIMIT);
97            name
98        }
99        CompiledKind::FunctionCallTrampoline(_, func_type) => format!(
100            "trampoline_call_{}_{}{suffix}",
101            types_to_signature(func_type.params()),
102            types_to_signature(func_type.results())
103        ),
104        CompiledKind::DynamicFunctionTrampoline(_, func_type) => format!(
105            "trampoline_dynamic_{}_{}{suffix}",
106            types_to_signature(func_type.params()),
107            types_to_signature(func_type.results())
108        ),
109        CompiledKind::ImportFunctionTrampoline(_, func_type) => format!(
110            "trampoline_import_{}_{}{suffix}",
111            types_to_signature(func_type.params()),
112            types_to_signature(func_type.results())
113        ),
114        CompiledKind::Module => "Module".to_string(),
115    }
116}
117
118/// Extended methods related to a compiled function.
119pub trait CompiledFunctionExt {
120    /// Return a name of the function for linkage purpose.
121    fn linkage_name(&self) -> String;
122
123    /// For serialization purpose, provide an object file name.
124    fn object_filename(&self) -> String {
125        format!("{}.o", self.linkage_name())
126    }
127
128    /// Symbol name holding the trap information for this function.
129    fn traps_name(&self) -> String {
130        format!("{}.traps", self.linkage_name())
131    }
132}
133
134impl CompiledFunctionExt for CompiledKind {
135    fn linkage_name(&self) -> String {
136        match self {
137            Self::Local(index, _) => format!("f{}", index.as_u32()),
138            Self::FunctionCallTrampoline(index, _) => format!("t{}", index.as_u32()),
139            Self::DynamicFunctionTrampoline(index, _) => format!("dt{}", index.as_u32()),
140            Self::ImportFunctionTrampoline(index, _) => format!("i{}", index.as_u32()),
141            Self::Module => "Module".to_string(),
142        }
143    }
144}
145
146/// Saves disassembled assembly code to a file with optional comments at specific offsets.
147///
148/// This function takes raw machine code bytes, disassembles them using `objdump`, and writes
149/// the annotated assembly to a file in the specified debug directory.
150#[cfg(not(target_arch = "wasm32"))]
151pub fn save_assembly_to_file<C: Display>(
152    arch: Architecture,
153    path: PathBuf,
154    body: &[u8],
155    assembly_comments: HashMap<usize, C>,
156) -> Result<(), CompileError> {
157    use std::{fs::File, io::Write};
158    use which::which;
159
160    #[derive(Debug)]
161    struct DecodedInsn<'a> {
162        offset: usize,
163        insn: &'a str,
164    }
165
166    fn parse_instructions(content: &str) -> Result<Vec<DecodedInsn<'_>>, CompileError> {
167        content
168            .lines()
169            .map(|line| line.trim())
170            .skip_while(|l| !l.starts_with("0000000000000000"))
171            .skip(1)
172            .filter(|line| line.trim() != "...")
173            .map(|line| -> Result<DecodedInsn<'_>, CompileError> {
174                let (offset, insn_part) = line.split_once(':').ok_or(CompileError::Codegen(
175                    format!("cannot parse objdump line: '{line}'"),
176                ))?;
177                // instruction content can be empty
178                let insn = insn_part
179                    .trim()
180                    .split_once('\t')
181                    .map_or("", |(_data, insn)| insn)
182                    .trim();
183                Ok(DecodedInsn {
184                    offset: usize::from_str_radix(offset, 16).map_err(|err| {
185                        CompileError::Codegen(format!("hex number expected: {err}"))
186                    })?,
187                    insn,
188                })
189            })
190            .collect()
191    }
192
193    // Note objdump cannot read from stdin.
194    let mut tmpfile = NamedTempFile::new()
195        .map_err(|err| CompileError::Codegen(format!("cannot create temporary file: {err}")))?;
196    tmpfile
197        .write_all(body)
198        .map_err(|err| CompileError::Codegen(format!("assembly dump write failed: {err}")))?;
199    tmpfile
200        .flush()
201        .map_err(|err| CompileError::Codegen(format!("flush failed: {err}")))?;
202
203    let (objdump_arch, objdump_binary) = match arch {
204        Architecture::X86_64 => ("i386:x86-64", "x86_64-linux-gnu-objdump"),
205        Architecture::Aarch64(..) => ("aarch64", "aarch64-linux-gnu-objdump"),
206        Architecture::Riscv64(..) => ("riscv:rv64", "riscv64-linux-gnu-objdump"),
207        _ => {
208            return Err(CompileError::Codegen(
209                "Assembly dumping is not supported for this architecture".to_string(),
210            ));
211        }
212    };
213
214    let bins = [objdump_binary, "objdump"];
215    let objdump_binary = bins.iter().find(|bin| which(bin).is_ok());
216    let Some(objdump_binary) = objdump_binary else {
217        // Objdump is an optional dependency, do not fail if not present.
218        return Ok(());
219    };
220
221    let command = Command::new(objdump_binary)
222        .arg("-b")
223        .arg("binary")
224        .arg("-m")
225        .arg(objdump_arch)
226        .arg("-D")
227        .arg(tmpfile.path())
228        .stdout(Stdio::piped())
229        .stderr(Stdio::null())
230        .spawn();
231
232    let Ok(command) = command else {
233        // The target might not be supported, do not fail in that case.
234        return Ok(());
235    };
236
237    let output = command
238        .wait_with_output()
239        .map_err(|err| CompileError::Codegen(format!("failed to read stdout: {err}")))?;
240    let content = String::from_utf8_lossy(&output.stdout);
241
242    let parsed_instructions = parse_instructions(content.as_ref())?;
243
244    let mut file = File::create(path).map_err(|err| {
245        CompileError::Codegen(format!("debug object file creation failed: {err}"))
246    })?;
247
248    // Dump the instruction annotated with the comments.
249    for insn in parsed_instructions {
250        if let Some(comment) = assembly_comments.get(&insn.offset) {
251            file.write_all(format!("      \t\t;; {comment}\n").as_bytes())
252                .map_err(|err| {
253                    CompileError::Codegen(format!("cannot write content to object file: {err}"))
254                })?;
255        }
256        file.write_all(format!("{:6x}:\t\t{}\n", insn.offset, insn.insn).as_bytes())
257            .map_err(|err| {
258                CompileError::Codegen(format!("cannot write content to object file: {err}"))
259            })?;
260    }
261
262    Ok(())
263}
264
265/// Saves disassembled assembly code to a file with optional comments at specific offsets.
266///
267/// This function takes raw machine code bytes, disassembles them using `objdump`, and writes
268/// the annotated assembly to a file in the specified debug directory.
269#[cfg(target_arch = "wasm32")]
270pub fn save_assembly_to_file<C: Display>(
271    _arch: Architecture,
272    _path: PathBuf,
273    _body: &[u8],
274    _assembly_comments: HashMap<usize, C>,
275) -> Result<(), CompileError> {
276    Ok(())
277}