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