Last active
January 27, 2024 04:15
-
-
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`
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(module | |
(func | |
(export "add41") | |
(param $n i32) | |
(result i32) | |
i32.const 41 | |
local.get $n | |
i32.add | |
) | |
) | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
test.wasm:
layout: