Skip to main content

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    /// Symbol name holding the trap information for this function.
124    fn traps_name(&self) -> String {
125        format!("{}.traps", self.linkage_name())
126    }
127}
128
129impl CompiledFunctionExt for CompiledKind {
130    fn linkage_name(&self) -> String {
131        match self {
132            Self::Local(index, _) => format!("f{}", index.as_u32()),
133            Self::FunctionCallTrampoline(index, _) => format!("t{}", index.as_u32()),
134            Self::DynamicFunctionTrampoline(index, _) => format!("dt{}", index.as_u32()),
135            Self::ImportFunctionTrampoline(index, _) => format!("i{}", index.as_u32()),
136            Self::Module => "Module".to_string(),
137        }
138    }
139}
140
141/// Saves disassembled assembly code to a file with optional comments at specific offsets.
142///
143/// This function takes raw machine code bytes, disassembles them using `objdump`, and writes
144/// the annotated assembly to a file in the specified debug directory.
145#[cfg(not(target_arch = "wasm32"))]
146pub fn save_assembly_to_file<C: Display>(
147    arch: Architecture,
148    path: PathBuf,
149    body: &[u8],
150    assembly_comments: HashMap<usize, C>,
151) -> Result<(), CompileError> {
152    use std::{fs::File, io::Write};
153    use which::which;
154
155    #[derive(Debug)]
156    struct DecodedInsn<'a> {
157        offset: usize,
158        insn: &'a str,
159    }
160
161    fn parse_instructions(content: &str) -> Result<Vec<DecodedInsn<'_>>, CompileError> {
162        content
163            .lines()
164            .map(|line| line.trim())
165            .skip_while(|l| !l.starts_with("0000000000000000"))
166            .skip(1)
167            .filter(|line| line.trim() != "...")
168            .map(|line| -> Result<DecodedInsn<'_>, CompileError> {
169                let (offset, insn_part) = line.split_once(':').ok_or(CompileError::Codegen(
170                    format!("cannot parse objdump line: '{line}'"),
171                ))?;
172                // instruction content can be empty
173                let insn = insn_part
174                    .trim()
175                    .split_once('\t')
176                    .map_or("", |(_data, insn)| insn)
177                    .trim();
178                Ok(DecodedInsn {
179                    offset: usize::from_str_radix(offset, 16).map_err(|err| {
180                        CompileError::Codegen(format!("hex number expected: {err}"))
181                    })?,
182                    insn,
183                })
184            })
185            .collect()
186    }
187
188    // Note objdump cannot read from stdin.
189    let mut tmpfile = NamedTempFile::new()
190        .map_err(|err| CompileError::Codegen(format!("cannot create temporary file: {err}")))?;
191    tmpfile
192        .write_all(body)
193        .map_err(|err| CompileError::Codegen(format!("assembly dump write failed: {err}")))?;
194    tmpfile
195        .flush()
196        .map_err(|err| CompileError::Codegen(format!("flush failed: {err}")))?;
197
198    let (objdump_arch, objdump_binary) = match arch {
199        Architecture::X86_64 => ("i386:x86-64", "x86_64-linux-gnu-objdump"),
200        Architecture::Aarch64(..) => ("aarch64", "aarch64-linux-gnu-objdump"),
201        Architecture::Riscv64(..) => ("riscv:rv64", "riscv64-linux-gnu-objdump"),
202        _ => {
203            return Err(CompileError::Codegen(
204                "Assembly dumping is not supported for this architecture".to_string(),
205            ));
206        }
207    };
208
209    let bins = [objdump_binary, "objdump"];
210    let objdump_binary = bins.iter().find(|bin| which(bin).is_ok());
211    let Some(objdump_binary) = objdump_binary else {
212        // Objdump is an optional dependency, do not fail if not present.
213        return Ok(());
214    };
215
216    let command = Command::new(objdump_binary)
217        .arg("-b")
218        .arg("binary")
219        .arg("-m")
220        .arg(objdump_arch)
221        .arg("-D")
222        .arg(tmpfile.path())
223        .stdout(Stdio::piped())
224        .stderr(Stdio::null())
225        .spawn();
226
227    let Ok(command) = command else {
228        // The target might not be supported, do not fail in that case.
229        return Ok(());
230    };
231
232    let output = command
233        .wait_with_output()
234        .map_err(|err| CompileError::Codegen(format!("failed to read stdout: {err}")))?;
235    let content = String::from_utf8_lossy(&output.stdout);
236
237    let parsed_instructions = parse_instructions(content.as_ref())?;
238
239    let mut file = File::create(path).map_err(|err| {
240        CompileError::Codegen(format!("debug object file creation failed: {err}"))
241    })?;
242
243    // Dump the instruction annotated with the comments.
244    for insn in parsed_instructions {
245        if let Some(comment) = assembly_comments.get(&insn.offset) {
246            file.write_all(format!("      \t\t;; {comment}\n").as_bytes())
247                .map_err(|err| {
248                    CompileError::Codegen(format!("cannot write content to object file: {err}"))
249                })?;
250        }
251        file.write_all(format!("{:6x}:\t\t{}\n", insn.offset, insn.insn).as_bytes())
252            .map_err(|err| {
253                CompileError::Codegen(format!("cannot write content to object file: {err}"))
254            })?;
255    }
256
257    Ok(())
258}
259
260/// Saves disassembled assembly code to a file with optional comments at specific offsets.
261///
262/// This function takes raw machine code bytes, disassembles them using `objdump`, and writes
263/// the annotated assembly to a file in the specified debug directory.
264#[cfg(target_arch = "wasm32")]
265pub fn save_assembly_to_file<C: Display>(
266    _arch: Architecture,
267    _path: PathBuf,
268    _body: &[u8],
269    _assembly_comments: HashMap<usize, C>,
270) -> Result<(), CompileError> {
271    Ok(())
272}