diff --git a/Cargo.lock b/Cargo.lock index 53af2084f5..f61028eb3e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3533,6 +3533,7 @@ dependencies = [ "wasmtime-component-util", "wasmtime-cranelift", "wasmtime-environ", + "wasmtime-explorer", "wasmtime-runtime", "wasmtime-wasi", "wasmtime-wasi-crypto", @@ -3647,6 +3648,19 @@ dependencies = [ "wat", ] +[[package]] +name = "wasmtime-explorer" +version = "8.0.0" +dependencies = [ + "anyhow", + "capstone", + "serde", + "serde_json", + "target-lexicon", + "wasmprinter", + "wasmtime", +] + [[package]] name = "wasmtime-fiber" version = "8.0.0" diff --git a/Cargo.toml b/Cargo.toml index 8b22fbfb3d..f2634cffc9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,6 +27,7 @@ wasmtime-cache = { workspace = true } wasmtime-cli-flags = { workspace = true } wasmtime-cranelift = { workspace = true } wasmtime-environ = { workspace = true } +wasmtime-explorer = { workspace = true } wasmtime-wast = { workspace = true } wasmtime-wasi = { workspace = true, features = ["exit"] } wasmtime-wasi-crypto = { workspace = true, optional = true } @@ -39,8 +40,8 @@ humantime = "2.0.0" once_cell = { workspace = true } listenfd = "1.0.0" wat = { workspace = true } -serde = "1.0.94" -serde_json = "1.0.26" +serde = { workspace = true } +serde_json = { workspace = true } wasmparser = { workspace = true } wasm-coredump-builder = { version = "0.1.11" } @@ -70,8 +71,8 @@ component-macro-test = { path = "crates/misc/component-macro-test" } component-test-util = { workspace = true } bstr = "0.2.17" libc = "0.2.60" -serde = "1.0" -serde_json = "1.0" +serde = { workspace = true } +serde_json = { workspace = true } [target.'cfg(windows)'.dev-dependencies] windows-sys = { workspace = true, features = ["Win32_System_Memory"] } @@ -120,6 +121,7 @@ wasmtime-cli-flags = { path = "crates/cli-flags", version = "=8.0.0" } wasmtime-cranelift = { path = "crates/cranelift", version = "=8.0.0" } wasmtime-cranelift-shared = { path = "crates/cranelift-shared", version = "=8.0.0" } wasmtime-environ = { path = "crates/environ", version = "=8.0.0" } +wasmtime-explorer = { path = "crates/explorer", version = "=8.0.0" } wasmtime-fiber = { path = "crates/fiber", version = "=8.0.0" } wasmtime-types = { path = "crates/types", version = "8.0.0" } wasmtime-jit = { path = "crates/jit", version = "=8.0.0" } @@ -196,6 +198,7 @@ heck = "0.4" similar = "2.1.0" toml = "0.5.9" serde = "1.0.94" +serde_json = "1.0.80" glob = "0.3.0" [features] diff --git a/crates/explorer/Cargo.toml b/crates/explorer/Cargo.toml new file mode 100644 index 0000000000..59eff18c90 --- /dev/null +++ b/crates/explorer/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "wasmtime-explorer" +authors.workspace = true +description = "Compiler explorer for Wasmtime and Cranelift" +documentation = "https://docs.rs/wasmtime-explorer/" +edition.workspace = true +license = "Apache-2.0 WITH LLVM-exception" +repository = "https://github.com/bytecodealliance/wasmtime" +version.workspace = true + +[dependencies] +anyhow = { workspace = true } +capstone = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +target-lexicon = { workspace = true } +wasmprinter = { workspace = true } +wasmtime = { workspace = true, features = ["cranelift"] } diff --git a/crates/explorer/src/.eslintrc.yml b/crates/explorer/src/.eslintrc.yml new file mode 100644 index 0000000000..6bd1f3477c --- /dev/null +++ b/crates/explorer/src/.eslintrc.yml @@ -0,0 +1,8 @@ +root: true + +env: + browser: true + es2022: true + +extends: + - "eslint:recommended" diff --git a/crates/explorer/src/index.css b/crates/explorer/src/index.css new file mode 100644 index 0000000000..ba23a1fbe7 --- /dev/null +++ b/crates/explorer/src/index.css @@ -0,0 +1,26 @@ +* { + margin: 0; + padding: 0; +} + +.hbox { + display: flex; + flex-direction: row; +} + +html, body { + width: 100%; + height: 100%; +} + +#wat { + width: 50%; + height: 100%; + overflow: scroll; +} + +#asm { + width: 50%; + height: 100%; + overflow: scroll; +} diff --git a/crates/explorer/src/index.js b/crates/explorer/src/index.js new file mode 100644 index 0000000000..afcfac9e5b --- /dev/null +++ b/crates/explorer/src/index.js @@ -0,0 +1,238 @@ +/*** State *********************************************************************/ + +class State { + constructor(wat, asm) { + this.wat = wat; + this.asm = asm; + } +} + +const state = window.STATE = new State(window.WAT, window.ASM); + +/*** Hues for Offsets **********************************************************/ + +const hues = [ + 80, + 160, + 240, + 320, + 40, + 120, + 200, + 280, + 20, + 100, + 180, + 260, + 340, + 60, + 140, + 220, + 300, +]; + +const nextHue = (function () { + let i = 0; + return () => { + return hues[++i % hues.length]; + }; +}()); + +// NB: don't just assign hues based on something simple like `hues[offset % +// hues.length]` since that can suffer from bias due to certain alignments +// happening more or less frequently. +const offsetToHue = new Map(); + +// Get the hue for the given offset, or assign it a new one if it doesn't have +// one already. +const hueForOffset = offset => { + if (offsetToHue.has(offset)) { + return offsetToHue.get(offset); + } else { + let hue = nextHue(); + offsetToHue.set(offset, hue); + return hue; + } +}; + +// Get the hue for the given offset, only if the offset has already been +// assigned a hue. +const existingHueForOffset = offset => { + return offsetToHue.get(offset); +}; + +// Get WAT chunk elements by Wasm offset. +const watByOffset = new Map(); + +// Get asm instruction elements by Wasm offset. +const asmByOffset = new Map(); + +// Get all (WAT chunk or asm instruction) elements by offset. +const anyByOffset = new Map(); + +const addWatElem = (offset, elem) => { + if (!watByOffset.has(offset)) { + watByOffset.set(offset, []); + } + watByOffset.get(offset).push(elem); + + if (!anyByOffset.has(offset)) { + anyByOffset.set(offset, []); + } + anyByOffset.get(offset).push(elem); +}; + +const addAsmElem = (offset, elem) => { + if (!asmByOffset.has(offset)) { + asmByOffset.set(offset, []); + } + asmByOffset.get(offset).push(elem); + + if (!anyByOffset.has(offset)) { + anyByOffset.set(offset, []); + } + anyByOffset.get(offset).push(elem); +}; + +/*** Event Handlers ************************************************************/ + +const watElem = document.getElementById("wat"); +watElem.addEventListener("click", event => { + if (event.target.dataset.wasmOffset == null) { + return; + } + + const offset = parseInt(event.target.dataset.wasmOffset); + if (!asmByOffset.get(offset)) { + return; + } + + const firstAsmElem = asmByOffset.get(offset)[0]; + firstAsmElem.scrollIntoView({ + behavior: "smooth", + block: "center", + inline: "nearest", + }); +}, { passive: true }); + +const asmElem = document.getElementById("asm"); +asmElem.addEventListener("click", event => { + if (event.target.dataset.wasmOffset == null) { + return; + } + + const offset = parseInt(event.target.dataset.wasmOffset); + if (!watByOffset.get(offset)) { + return; + } + + const firstWatElem = watByOffset.get(offset)[0]; + firstWatElem.scrollIntoView({ + behavior: "smooth", + block: "center", + inline: "nearest", + }); +}, { passive: true }); + +const onMouseEnter = event => { + if (event.target.dataset.wasmOffset == null) { + return; + } + + const offset = parseInt(event.target.dataset.wasmOffset); + const hue = hueForOffset(offset); + for (const elem of anyByOffset.get(offset)) { + elem.style.backgroundColor = `hsl(${hue} 75% 80%)`; + } +}; + +const onMouseLeave = event => { + if (event.target.dataset.wasmOffset == null) { + return; + } + + const offset = parseInt(event.target.dataset.wasmOffset); + const hue = hueForOffset(offset); + for (const elem of anyByOffset.get(offset)) { + elem.style.backgroundColor = `hsl(${hue} 50% 95%)`; + } +}; + +/*** Rendering *****************************************************************/ + +const repeat = (s, n) => { + return s.repeat(n >= 0 ? n : 0); +}; + +const renderAddress = addr => { + let hex = addr.toString(16); + return repeat("0", 8 - hex.length) + hex; +}; + +const renderBytes = bytes => { + let s = ""; + for (let i = 0; i < bytes.length; i++) { + if (i != 0) { + s += " "; + } + const hexByte = bytes[i].toString(16); + s += hexByte.length == 2 ? hexByte : "0" + hexByte; + } + return s + repeat(" ", 30 - s.length); +}; + +const renderInst = (mnemonic, operands) => { + if (operands.length == 0) { + return mnemonic; + } else { + return mnemonic + " " + operands; + } +}; + +// Render the ASM. + +let nthFunc = 0; +for (const func of state.asm.functions) { + const funcElem = document.createElement("div"); + + const funcHeader = document.createElement("h3"); + funcHeader.textContent = `Defined Function ${nthFunc}`; + funcElem.appendChild(funcHeader); + + const bodyElem = document.createElement("pre"); + for (const inst of func.instructions) { + const instElem = document.createElement("span"); + instElem.textContent = `${renderAddress(inst.address)} ${renderBytes(inst.bytes)} ${renderInst(inst.mnemonic, inst.operands)}\n`; + if (inst.wasm_offset != null) { + instElem.setAttribute("data-wasm-offset", inst.wasm_offset); + const hue = hueForOffset(inst.wasm_offset); + instElem.style.backgroundColor = `hsl(${hue} 50% 90%)`; + instElem.addEventListener("mouseenter", onMouseEnter); + instElem.addEventListener("mouseleave", onMouseLeave); + addAsmElem(inst.wasm_offset, instElem); + } + bodyElem.appendChild(instElem); + } + funcElem.appendChild(bodyElem); + + asmElem.appendChild(funcElem); + nthFunc++; +} + +// Render the WAT. + +for (const chunk of state.wat.chunks) { + const chunkElem = document.createElement("span"); + if (chunk.wasm_offset != null) { + chunkElem.dataset.wasmOffset = chunk.wasm_offset; + const hue = existingHueForOffset(chunk.wasm_offset); + if (hue) { + chunkElem.style.backgroundColor = `hsl(${hue} 50% 95%)`; + chunkElem.addEventListener("mouseenter", onMouseEnter); + chunkElem.addEventListener("mouseleave", onMouseLeave); + addWatElem(chunk.wasm_offset, chunkElem); + } + } + chunkElem.textContent = chunk.wat; + watElem.appendChild(chunkElem); +} diff --git a/crates/explorer/src/lib.rs b/crates/explorer/src/lib.rs new file mode 100644 index 0000000000..23c8cf28c8 --- /dev/null +++ b/crates/explorer/src/lib.rs @@ -0,0 +1,175 @@ +use anyhow::Result; +use capstone::arch::BuildsCapstone; +use serde::Serialize; +use std::{io::Write, str::FromStr}; + +pub fn generate( + config: &wasmtime::Config, + target: Option<&str>, + wasm: &[u8], + dest: &mut dyn Write, +) -> Result<()> { + let target = match target { + None => target_lexicon::Triple::host(), + Some(target) => target_lexicon::Triple::from_str(target)?, + }; + + let wat = annotate_wat(wasm)?; + let wat_json = serde_json::to_string(&wat)?; + let asm = annotate_asm(config, &target, wasm)?; + let asm_json = serde_json::to_string(&asm)?; + + let index_css = include_str!("./index.css"); + let index_js = include_str!("./index.js"); + + write!( + dest, + r#" + + +
+