wasmtime_explorer/
lib.rs

1use anyhow::Result;
2use capstone::arch::BuildsCapstone;
3use serde_derive::Serialize;
4use std::{
5    fs::File,
6    io::{read_to_string, Write},
7    path::Path,
8    str::FromStr,
9};
10use wasmtime_environ::demangle_function_name;
11
12pub fn generate(
13    config: &wasmtime::Config,
14    target: Option<&str>,
15    clif_dir: Option<&Path>,
16    wasm: &[u8],
17    dest: &mut dyn Write,
18) -> Result<()> {
19    let target = match target {
20        None => target_lexicon::Triple::host(),
21        Some(target) => target_lexicon::Triple::from_str(target)?,
22    };
23
24    let wat = annotate_wat(wasm)?;
25    let wat_json = serde_json::to_string(&wat)?;
26    let asm = annotate_asm(config, &target, wasm)?;
27    let asm_json = serde_json::to_string(&asm)?;
28    let clif_json = clif_dir
29        .map::<anyhow::Result<String>, _>(|clif_dir| {
30            let clif = annotate_clif(clif_dir, &asm)?;
31            Ok(serde_json::to_string(&clif)?)
32        })
33        .transpose()?;
34
35    let index_css = include_str!("./index.css");
36    let index_js = include_str!("./index.js");
37
38    write!(
39        dest,
40        r#"
41<!DOCTYPE html>
42<html>
43  <head>
44    <title>Wasmtime Compiler Explorer</title>
45    <style>
46      {index_css}
47    </style>
48  </head>
49  <body class="hbox">
50    <pre id="wat"></pre>
51        "#
52    )?;
53    if clif_json.is_some() {
54        write!(dest, r#"<div id="clif"></div>"#)?;
55    }
56    write!(
57        dest,
58        r#"
59    <div id="asm"></div>
60    <script>
61      window.WAT = {wat_json};
62        "#
63    )?;
64    if let Some(clif_json) = clif_json {
65        write!(
66            dest,
67            r#"
68          window.CLIF = {clif_json};
69            "#
70        )?;
71    }
72    write!(
73        dest,
74        r#"
75      window.ASM = {asm_json};
76    </script>
77    <script>
78      {index_js}
79    </script>
80  </body>
81</html>
82        "#
83    )?;
84    Ok(())
85}
86
87#[derive(Serialize, Clone, Copy, Debug)]
88struct WasmOffset(u32);
89
90#[derive(Serialize, Debug)]
91struct AnnotatedWat {
92    chunks: Vec<AnnotatedWatChunk>,
93}
94
95#[derive(Serialize, Debug)]
96struct AnnotatedWatChunk {
97    wasm_offset: Option<WasmOffset>,
98    wat: String,
99}
100
101fn annotate_wat(wasm: &[u8]) -> Result<AnnotatedWat> {
102    let printer = wasmprinter::Config::new();
103    let mut storage = String::new();
104    let chunks = printer
105        .offsets_and_lines(wasm, &mut storage)?
106        .map(|(offset, wat)| AnnotatedWatChunk {
107            wasm_offset: offset.map(|o| WasmOffset(u32::try_from(o).unwrap())),
108            wat: wat.to_string(),
109        })
110        .collect();
111    Ok(AnnotatedWat { chunks })
112}
113
114#[derive(Serialize, Debug)]
115struct AnnotatedAsm {
116    functions: Vec<AnnotatedFunction>,
117}
118
119#[derive(Serialize, Debug)]
120struct AnnotatedFunction {
121    func_index: u32,
122    name: Option<String>,
123    demangled_name: Option<String>,
124    instructions: Vec<AnnotatedInstruction>,
125}
126
127#[derive(Serialize, Debug)]
128struct AnnotatedInstruction {
129    wasm_offset: Option<WasmOffset>,
130    address: u32,
131    bytes: Vec<u8>,
132    mnemonic: Option<String>,
133    operands: Option<String>,
134}
135
136fn annotate_asm(
137    config: &wasmtime::Config,
138    target: &target_lexicon::Triple,
139    wasm: &[u8],
140) -> Result<AnnotatedAsm> {
141    let engine = wasmtime::Engine::new(config)?;
142    let module = wasmtime::Module::new(&engine, wasm)?;
143
144    let text = module.text();
145    let address_map: Vec<_> = module
146        .address_map()
147        .ok_or_else(|| anyhow::anyhow!("address maps must be enabled in the config"))?
148        .collect();
149
150    let mut address_map_iter = address_map.into_iter().peekable();
151    let mut current_entry = address_map_iter.next();
152    let mut wasm_offset_for_address = |start: usize, address: u32| -> Option<WasmOffset> {
153        // Consume any entries that happened before the current function for the
154        // first instruction.
155        while current_entry.map_or(false, |cur| cur.0 < start) {
156            current_entry = address_map_iter.next();
157        }
158
159        // Next advance the address map up to the current `address` specified,
160        // including it.
161        while address_map_iter.peek().map_or(false, |next_entry| {
162            u32::try_from(next_entry.0).unwrap() <= address
163        }) {
164            current_entry = address_map_iter.next();
165        }
166        current_entry.and_then(|entry| entry.1.map(WasmOffset))
167    };
168
169    let functions = module
170        .functions()
171        .map(|function| {
172            let body = &text[function.offset..][..function.len];
173
174            let mut cs = match target.architecture {
175                target_lexicon::Architecture::Aarch64(_) => capstone::Capstone::new()
176                    .arm64()
177                    .mode(capstone::arch::arm64::ArchMode::Arm)
178                    .build()
179                    .map_err(|e| anyhow::anyhow!("{e}"))?,
180                target_lexicon::Architecture::Riscv64(_) => capstone::Capstone::new()
181                    .riscv()
182                    .mode(capstone::arch::riscv::ArchMode::RiscV64)
183                    .build()
184                    .map_err(|e| anyhow::anyhow!("{e}"))?,
185                target_lexicon::Architecture::S390x => capstone::Capstone::new()
186                    .sysz()
187                    .mode(capstone::arch::sysz::ArchMode::Default)
188                    .build()
189                    .map_err(|e| anyhow::anyhow!("{e}"))?,
190                target_lexicon::Architecture::X86_64 => capstone::Capstone::new()
191                    .x86()
192                    .mode(capstone::arch::x86::ArchMode::Mode64)
193                    .build()
194                    .map_err(|e| anyhow::anyhow!("{e}"))?,
195                _ => anyhow::bail!("Unsupported target: {target}"),
196            };
197
198            // This tells capstone to skip over anything that looks like data,
199            // such as inline constant pools and things like that. This also
200            // additionally is required to skip over trapping instructions on
201            // AArch64.
202            cs.set_skipdata(true).unwrap();
203
204            let instructions = cs
205                .disasm_all(body, function.offset as u64)
206                .map_err(|e| anyhow::anyhow!("{e}"))?;
207            let instructions = instructions
208                .iter()
209                .map(|inst| {
210                    let address = u32::try_from(inst.address()).unwrap();
211                    let wasm_offset = wasm_offset_for_address(function.offset, address);
212                    Ok(AnnotatedInstruction {
213                        wasm_offset,
214                        address,
215                        bytes: inst.bytes().to_vec(),
216                        mnemonic: inst.mnemonic().map(ToString::to_string),
217                        operands: inst.op_str().map(ToString::to_string),
218                    })
219                })
220                .collect::<Result<Vec<_>>>()?;
221
222            let demangled_name = if let Some(name) = &function.name {
223                let mut demangled = String::new();
224                if demangle_function_name(&mut demangled, &name).is_ok() {
225                    Some(demangled)
226                } else {
227                    None
228                }
229            } else {
230                None
231            };
232
233            Ok(AnnotatedFunction {
234                func_index: function.index.as_u32(),
235                name: function.name,
236                demangled_name,
237                instructions,
238            })
239        })
240        .collect::<Result<Vec<_>>>()?;
241
242    Ok(AnnotatedAsm { functions })
243}
244
245#[derive(Serialize, Debug)]
246struct AnnotatedClif {
247    functions: Vec<AnnotatedClifFunction>,
248}
249
250#[derive(Serialize, Debug)]
251struct AnnotatedClifFunction {
252    func_index: u32,
253    name: Option<String>,
254    demangled_name: Option<String>,
255    instructions: Vec<AnnotatedClifInstruction>,
256}
257
258#[derive(Serialize, Debug)]
259struct AnnotatedClifInstruction {
260    wasm_offset: Option<WasmOffset>,
261    clif: String,
262}
263
264fn annotate_clif(clif_dir: &Path, asm: &AnnotatedAsm) -> Result<AnnotatedClif> {
265    let mut clif = AnnotatedClif {
266        functions: Vec::new(),
267    };
268    for function in &asm.functions {
269        let function_path = clif_dir.join(format!("wasm_func_{}.clif", function.func_index));
270        if !function_path.exists() {
271            continue;
272        }
273        let mut clif_function = AnnotatedClifFunction {
274            func_index: function.func_index,
275            name: function.name.clone(),
276            demangled_name: function.demangled_name.clone(),
277            instructions: Vec::new(),
278        };
279        let file = File::open(&function_path)?;
280        for mut line in read_to_string(file)?.lines() {
281            if line.is_empty() {
282                continue;
283            }
284            let mut wasm_offset = None;
285            if line.starts_with('@') {
286                wasm_offset = Some(WasmOffset(u32::from_str_radix(&line[1..5], 16)?));
287                line = &line[28..];
288            } else if line.starts_with("     ") {
289                line = &line[28..];
290            }
291            clif_function.instructions.push(AnnotatedClifInstruction {
292                wasm_offset,
293                clif: line.to_string(),
294            });
295        }
296        clif.functions.push(clif_function);
297    }
298    Ok(clif)
299}