Skip to content

Instantly share code, notes, and snippets.

@bhelx
Last active January 27, 2024 04:15
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save bhelx/c3533d6e1d0ec6aa71da2fae51f09a2b to your computer and use it in GitHub Desktop.
Save bhelx/c3533d6e1d0ec6aa71da2fae51f09a2b to your computer and use it in GitHub Desktop.
Just enough js code to parse and run a simple Wasm file. Very naive and just using this to learn the spec. don't use in a real system. `wat2wasm test.wat && node wam.js ./test.wasm`
(module
(func
(export "add41")
(param $n i32)
(result i32)
i32.const 41
local.get $n
i32.add
)
)
const {
decodeUInt32,
MAX_NUMBER_OF_BYTE_U32,
} = require("@webassemblyjs/leb128");
const fs = require("fs");
class Parser {
MAGIC_BYTES = Buffer.from([0x00, 0x61, 0x73, 0x6d]);
MOD_VERSION = Buffer.from([0x01, 0x00, 0x00, 0x00]);
constructor(data) {
this.data = data;
this.index = 0;
}
peekBytes(n) {
return this.data.subarray(this.index, this.index + n);
}
consumeBytes(n) {
let result = this.peekBytes(n)
this.seek(n)
return result
}
consumeByte() {
const result = this.data[this.index];
this.seek(1)
return result
}
seek(n) {
if (this.index + n > this.data.length)
throw Error("Tring to seek past EOF");
this.index += n;
}
endOfFile() {
return this.index >= this.data.length;
}
consumeU32() {
const bytes = this.peekBytes(MAX_NUMBER_OF_BYTE_U32);
const result = decodeUInt32(bytes);
this.seek(result.nextIndex)
return result.value
}
parseModule() {
const magic = this.consumeBytes(4);
if (!magic.equals(this.MAGIC_BYTES)) {
throw Error("Magic bytes not found: " + magic);
}
const version = this.consumeBytes(4);
if (!version.equals(this.MOD_VERSION)) {
throw Error(`Module version did not magtch : ${version}`);
}
const sections = [];
while (!this.endOfFile()) {
const section = this.parseSection();
sections.push(section);
}
return {
type: "Module",
sections,
};
}
parseSection() {
const id = this.consumeByte();
let sectionSize = this.consumeU32();
switch (id) {
case 0x01:
return this.parseTypeSection(sectionSize);
case 0x03:
return this.parseFunctionSection(sectionSize);
case 0x07:
return this.parseExportSection(sectionSize);
case 0x0A:
return this.parseCodeSection(sectionSize);
default:
throw Error("Unknown section id: " + id);
}
}
parseTypeSection(sectionSize) {
var numTypes = this.consumeU32();
let funcTypes = [];
for (var i = 0; i < numTypes; i++) {
var headerByte = this.consumeU32();
if (headerByte != 0x60) {
throw Error("Expected 0x60 header byte got " + headerByte);
}
var numFuncElements = this.consumeU32();
const funcSig = {
params: [],
returns: [],
};
// params
for (var j = 0; j < numFuncElements; j++) {
var valueType = this.consumeU32();
funcSig.params.push(valueType);
}
var numFuncElements = this.consumeU32();
// returns
for (var j = 0; j < numFuncElements; j++) {
var valueType = this.consumeU32();
funcSig.returns.push(valueType);
}
funcTypes.push({
funcSig,
});
}
return {
type: "TypeSection",
funcTypes,
};
}
parseFunctionSection(sectionSize) {
this.seek(sectionSize);
return {
type: "FunctionSection",
};
}
parseExportSection(sectionSize) {
var numExports = this.consumeU32();
const exports = [];
for (let i = 0; i < numExports; i++) {
var strLen = this.consumeU32();
const bytes = this.consumeBytes(strLen);
const name = new TextDecoder().decode(bytes);
const type = this.consumeByte();
const idx = this.consumeByte();
exports.push({
name,
desc: {
type,
idx,
},
});
}
return {
type: "ExportSection",
exports,
};
}
parseCodeSection(sectionSize) {
var numCodes = this.consumeU32();
const functions = [];
for (let i = 0; i < numCodes; i++) {
var codeSize = this.consumeU32();
var funcIndex = this.consumeU32();
const end = this.index + codeSize - 1;
const instructions = [];
while (this.index < end) {
const instr = { opcode: this.consumeByte(), operands: [] };
switch (instr.opcode) {
case 0x20:
instr.name = "local.get";
instr.operands.push(this.consumeByte());
break;
case 0x41:
instr.name = "i32.const";
instr.operands.push(this.consumeByte());
break;
case 0x6a:
instr.name = "i32.add";
break;
case 0x0b:
instr.name = "end";
break;
default:
throw Error("Opcode not supported yet: " + instr.opcode);
}
instructions.push(instr);
}
functions.push({
funcIndex,
instructions,
});
}
return {
type: "CodeSection",
functions,
};
}
}
class WamMachine {
constructor() {
this.stack = [];
}
run(code, args) {
let ret = undefined;
code.forEach((c) => {
switch (c.opcode) {
case 0x20:
this.stack.push(args[c.operands[0]]);
break;
case 0x41:
this.stack.push(c.operands[0]);
break;
case 0x6a:
let a = this.stack.pop();
let b = this.stack.pop();
this.stack.push(a + b);
break;
case 0x0b:
ret = this.stack.pop();
break;
}
});
return ret;
}
}
class WamModule {
constructor(file) {
const data = fs.readFileSync(file);
this.module = new Parser(data).parseModule();
this.machine = new WamMachine();
this.exports = {};
const exportSec = this.module.sections.find(
(s) => s.type === "ExportSection"
);
const codeSec = this.module.sections.find((s) => s.type === "CodeSection");
//const typeSec = this.module.sections.find(s => s.type === "TypeSection")
exportSec.exports.forEach((fe) => {
const instructions = codeSec.functions[fe.desc.idx].instructions;
//const funcType = typSec.funcTypes[fe.desc.type]
this.exports[fe.name] = (...args) => {
return this.machine.run(instructions, args);
};
});
}
}
const mod = new WamModule(process.argv[2]);
const add41 = mod.exports.add41;
console.log(add41(1)); // prints 42
@mhmd-azeez
Copy link

test.wasm:

00 61 73 6d
01 00 00 00
01 06 01 60
01 7f 01 7f
03 02 01 00
07 09 01 05
61 64 64 34
31 00 00 0a
09 01 07 00
41 29 20 00
6a 0b

layout:

00000000: 00 61 73 6d       # wasm magic number: .asm
00000004: 01 00 00 00       # version

00000008: 01                # section "type" (id = 1)
00000009: 06                # section size = 6 bytes
0000000a: 01                # number of types = 1
0000000b: 60                # first type = function (60)
0000000c: 01                # number of parameters = 1
0000000d: 7f                # first parameter type = i32
0000000e: 01                # number of return types
0000000f: 7f                # first return type = i32

00000010: 03                # section "function" (id = 3)
00000011: 02                # section size = 2 bytes
00000012: 01                # number of fuctions = 1
00000013: 00                # index of the function in the code section = 0

00000014: 07                # section "export" (id = 7)
00000015: 09                # section size = 9 bytes
00000016: 01                # number of exports = 1
00000017: 05                # length of the export name = 5 bytes
00000018: 61 64 64 34 31    # name of the first export = add41
0000001d: 00                # export kind = 0 = function
0000001e: 00                # index of the exported function in the code section = 0

0000001f: 0a                # section "code" (id = 10)
00000020: 09                # section size = 9 bytes
00000021: 01                # number of functions = 1
00000022: 07                # function body size = 7 bytes
00000023: 00                # number of local declarations = 0

00000024: 41                # instruction i32.const   
00000025: 29                # `i32` literal = 42
00000026: 20                # instruction `local.get x`
00000027: 00                # index local = 0 = first parameter
00000028: 6a                # instruction `i32.add`
00000029: 0b                # instruction `end`

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment