Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
kateinoigakukun committed May 31, 2024
0 parents commit 6e9b3f6
Show file tree
Hide file tree
Showing 9 changed files with 674 additions and 0 deletions.
16 changes: 16 additions & 0 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
on:
push:
branches: [main]
pull_request:
branches: [main]

jobs:
test:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '22'
- run: sudo apt install -y wabt
- run: npm test
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/test/testsuite
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2024 Yuta Saito

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
81 changes: 81 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# wasm-imports-parser

A simple parser for WebAssembly imports with [WebAssembly Type Reflection JS API](https://github.com/WebAssembly/js-types/blob/main/proposals/js-types/Overview.md) compatibility.

Typically useful for constructing shared memory with a limit requested by imports of a WebAssembly module.

## Installation

```
npm install wasm-imports-parser
```

## Example


```js
import { parseImports } from 'wasm-imports-parser';

const moduleBytes = new Uint8Array([
0x00, 0x61, 0x73, 0x6d, // magic number
0x01, 0x00, 0x00, 0x00, // version
// import section with one import
0x02, // section code
0x06, // section length
0x01, // number of imports
0x00, // module name length
0x00, // field name length
0x02, // import kind: memory
0x00, // limits flags
0x01, // initial pages: 1
]);
const imports = parseImports(moduleBytes);
console.log(imports);
// > [
// > {
// > module: '',
// > name: '',
// > kind: 'memory',
// > type: { minimum: 1, shared: false, index: 'i32' }
// > }
// > ]
```

## As a polyfill for [WebAssembly Type Reflection JS API](https://github.com/WebAssembly/js-types/blob/main/proposals/js-types/Overview.md)

This parser can be used as a polyfill for the WebAssembly Type Reflection JS API.

```js
import { polyfill } from 'wasm-imports-parser/polyfill.js';

const WebAssembly = polyfill(globalThis.WebAssembly);

const moduleBytes = new Uint8Array([
0x00, 0x61, 0x73, 0x6d, // magic number
0x01, 0x00, 0x00, 0x00, // version
// import section with one import
0x02, // section code
0x06, // section length
0x01, // number of imports
0x00, // module name length
0x00, // field name length
0x02, // import kind: memory
0x00, // limits flags
0x01, // initial pages: 1
]);
const module = await WebAssembly.compile(moduleBytes);
const imports = WebAssembly.Module.imports(module);
console.log(imports);
// > [
// > {
// > module: '',
// > name: '',
// > kind: 'memory',
// > type: { minimum: 1, shared: false, index: 'i32' }
// > }
// > ]
```

## License

This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
251 changes: 251 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
/**
* @typedef {"i32" | "i64" | "f32" | "f64" | "funcref" | "externref"} ValueType
* @typedef { { parameters: ValueType[], results: ValueType[] } } FunctionType
* @typedef { { element: "funcref" | "externref", minimum: number, maximum?: number } } TableType
* @typedef { { minimum: number, maximum?: number, shared: boolean, index: "i32" | "i64" } } MemoryType
* @typedef { { value: ValueType, mutable: boolean } } GlobalType
*/

/**
* Parse a WebAssembly module bytes and return the imports entries.
*
* @param {BufferSource} moduleBytes - The WebAssembly module bytes.
* @returns { (
* { module: string, name: string } & (
* { kind: "function", type: FunctionType } |
* { kind: "table", type: TableType } |
* { kind: "memory", type: MemoryType } |
* { kind: "global", type: GlobalType }
* )
* )[] } - The imports entries.
* @throws {Error} - If the module bytes are invalid.
*
* @example
* import { parseImports } from "wasm-imports-parser";
*
* function mockImports(imports) {
* let mock = {};
* for (const imp of imports) {
* let value;
* switch (imp.kind) {
* case "table":
* value = new WebAssembly.Table(imp.type);
* break;
* case "memory":
* value = new WebAssembly.Memory(imp.type);
* break;
* case "global":
* value = new WebAssembly.Global(imp.type, undefined);
* break;
* case "function":
* value = () => { throw "unimplemented" };
* break;
* }
* if (! (imp.module in mock)) mock[imp.module] = {};
* mock[imp.module][imp.name] = value;
* }
* return mock;
* }
*
* const imports = parseImports(moduleBytes);
* const importObject = mockImports(imports);
* const { instance } = await WebAssembly.instantiate(moduleBytes, importObject);
*/
export function parseImports(moduleBytes) {
const parseState = new ParseState(moduleBytes);
parseMagicNumber(parseState);
parseVersion(parseState);

const types = [];
const imports = [];

while (parseState.hasMoreBytes()) {
const sectionId = parseState.readByte();
const sectionSize = parseState.readUnsignedLEB128();
switch (sectionId) {
case 1: {
// Type section
const typeCount = parseState.readUnsignedLEB128();
for (let i = 0; i < typeCount; i++) {
types.push(parseFunctionType(parseState));
}
break;
}
case 2: {
// Ok, found import section
const importCount = parseState.readUnsignedLEB128();
for (let i = 0; i < importCount; i++) {
const module = parseState.readName();
const name = parseState.readName();
const type = parseState.readByte();
switch (type) {
case 0x00:
const index = parseState.readUnsignedLEB128();
imports.push({ module, name, kind: "function", type: types[index] });
break;
case 0x01:
imports.push({ module, name, kind: "table", type: parseTableType(parseState) });
break;
case 0x02:
imports.push({ module, name, kind: "memory", type: parseLimits(parseState) });
break;
case 0x03:
imports.push({ module, name, kind: "global", type: parseGlobalType(parseState) });
break;
default:
throw new Error(`Unknown import descriptor type ${type}`);
}
}
// Skip the rest of the module
return imports;
}
default: {
parseState.skipBytes(sectionSize);
break;
}
}
}
return [];
}

class ParseState {
constructor(moduleBytes) {
this.moduleBytes = moduleBytes;
this.offset = 0;
this.textDecoder = new TextDecoder("utf-8");
}

hasMoreBytes() {
return this.offset < this.moduleBytes.length;
}

readByte() {
return this.moduleBytes[this.offset++];
}

skipBytes(count) {
this.offset += count;
}

/// Read unsigned LEB128 integer
readUnsignedLEB128() {
let result = 0;
let shift = 0;
let byte;
do {
byte = this.readByte();
result |= (byte & 0x7F) << shift;
shift += 7;
} while (byte & 0x80);
return result;
}

readName() {
const nameLength = this.readUnsignedLEB128();
const nameBytes = this.moduleBytes.slice(this.offset, this.offset + nameLength);
const name = this.textDecoder.decode(nameBytes);
this.offset += nameLength;
return name;
}

assertBytes(expected) {
const baseOffset = this.offset;
const expectedLength = expected.length;
for (let i = 0; i < expectedLength; i++) {
if (this.moduleBytes[baseOffset + i] !== expected[i]) {
throw new Error(`Expected ${expected} at offset ${baseOffset}`);
}
}
this.offset += expectedLength;
}
}

function parseMagicNumber(parseState) {
const expected = [0x00, 0x61, 0x73, 0x6D];
parseState.assertBytes(expected);
}

function parseVersion(parseState) {
const expected = [0x01, 0x00, 0x00, 0x00];
parseState.assertBytes(expected);
}

function parseTableType(parseState) {
const elementType = parseState.readByte();
let element;
switch (elementType) {
case 0x70:
element = "funcref";
break;
case 0x6F:
element = "externref";
break;
default:
throw new Error(`Unknown table element type ${elementType}`);
}
const { minimum, maximum } = parseLimits(parseState);
if (maximum) {
return { element, minimum, maximum };
} else {
return { element, minimum };
}
}

function parseLimits(parseState) {
const flags = parseState.readByte();
const minimum = parseState.readUnsignedLEB128();
const hasMaximum = flags & 1;
const shared = (flags & 2) !== 0;
const isMemory64 = (flags & 4) !== 0;
const index = isMemory64 ? "i64" : "i32";
if (hasMaximum) {
const maximum = parseState.readUnsignedLEB128();
return { minimum, shared, index, maximum };
} else {
return { minimum, shared, index };
}
}

function parseGlobalType(parseState) {
const value = parseValueType(parseState);
const mutable = parseState.readByte() === 1;
return { value, mutable };
}

function parseValueType(parseState) {
const type = parseState.readByte();
switch (type) {
case 0x7F:
return "i32";
case 0x7E:
return "i64";
case 0x7D:
return "f32";
case 0x7C:
return "f64";
case 0x70:
return "funcref";
case 0x6f:
return "externref";
default:
throw new Error(`Unknown value type ${type}`);
}
}

function parseFunctionType(parseState) {
const form = parseState.readByte();
if (form !== 0x60) {
throw new Error(`Expected function type form 0x60, got ${form}`);
}
const parameters = [];
const parameterCount = parseState.readUnsignedLEB128();
for (let i = 0; i < parameterCount; i++) {
parameters.push(parseValueType(parseState));
}
const results = [];
const resultCount = parseState.readUnsignedLEB128();
for (let i = 0; i < resultCount; i++) {
results.push(parseValueType(parseState));
}
return { parameters, results };
}
13 changes: 13 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"name": "wasm-imports-parser",
"version": "1.0.0",
"description": "A simple parser for WebAssembly imports",
"main": "index.js",
"type": "module",
"scripts": {
"test": "node --experimental-wasm-type-reflection test/check.mjs && node test/check.mjs"
},
"keywords": [],
"author": "SwiftWasm Team",
"license": "MIT"
}
Loading

0 comments on commit 6e9b3f6

Please sign in to comment.