Skip to content

Commit

Permalink
Introduce the wasmtime-explorer crate
Browse files Browse the repository at this point in the history
This implements Godbolt Compiler Explorer-like functionality for Wasmtime and
Cranelift. Given a Wasm module, it compiles the module to native code and then
writes a standalone HTML file that gives a split pane view between the WAT and
ASM disassemblies.
  • Loading branch information
fitzgen committed Mar 11, 2023
1 parent 83f21e7 commit 643cdd0
Show file tree
Hide file tree
Showing 11 changed files with 546 additions and 8 deletions.
18 changes: 16 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 7 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand All @@ -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" }

Expand Down Expand Up @@ -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"] }
Expand Down Expand Up @@ -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" }
Expand Down Expand Up @@ -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]
Expand Down
18 changes: 18 additions & 0 deletions crates/explorer/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"] }
8 changes: 8 additions & 0 deletions crates/explorer/src/.eslintrc.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
root: true

env:
browser: true
es2022: true

extends:
- "eslint:recommended"
26 changes: 26 additions & 0 deletions crates/explorer/src/index.css
Original file line number Diff line number Diff line change
@@ -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;
}
238 changes: 238 additions & 0 deletions crates/explorer/src/index.js
Original file line number Diff line number Diff line change
@@ -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);
}
Loading

0 comments on commit 643cdd0

Please sign in to comment.