Commit | Line | Data |
---|---|---|
89149437 JM |
1 | #!/usr/bin/env node |
2 | ||
3 | // Copyright Joel Martin | |
4 | // License MIT | |
5 | ||
6 | const { promisify } = require('util') | |
7 | const fs = require('fs') | |
8 | const readFile = promisify(fs.readFile) | |
9 | const assert = require('assert') | |
10 | const { TextDecoder, TextEncoder } = require('text-encoding') | |
11 | const node_readline = require('./node_readline.js') | |
12 | ||
13 | assert('WebAssembly' in global, 'WebAssembly not detected') | |
14 | ||
15 | // | |
16 | // Memory interaction utilities | |
17 | // | |
18 | ||
19 | // Convert node Buffer to Uint8Array | |
20 | function toUint8Array(buf) { | |
21 | let u = new Uint8Array(buf.length) | |
22 | for (let i = 0; i < buf.length; ++i) { | |
23 | u[i] = buf[i] | |
24 | } | |
25 | return u | |
26 | } | |
27 | ||
28 | // Read null terminated string out of webassembly memory | |
29 | function get_string(memory, addr) { | |
30 | //console.warn("get_string:", addr) | |
31 | let u8 = new Uint8Array(memory.buffer, addr) | |
32 | let length = u8.findIndex(e => e == 0) | |
33 | let bytes = new Uint8Array(memory.buffer, addr, length) | |
34 | let str = new TextDecoder('utf8').decode(bytes) | |
35 | return str | |
36 | } | |
37 | ||
38 | // Write null terminated string into webassembly memory | |
39 | function put_string(memory, addr, str, max_length) { | |
40 | let buf8 = new Uint8Array(memory.buffer, addr) | |
41 | ||
42 | let bytes = new TextEncoder('utf8').encode(str) | |
43 | if (max_length && bytes.length > max_length) { | |
44 | bytes = bytes.slice(0, max_length) | |
45 | } | |
46 | ||
47 | buf8.set(bytes, 0) | |
48 | buf8[bytes.length] = 0 // null terminator | |
49 | return bytes.length+1 | |
50 | } | |
51 | ||
52 | // Put argv structure at beginning of memory | |
53 | function marshal_argv(memory, offset, args) { | |
54 | let view = new DataView(memory.buffer, offset) | |
55 | let buf8 = new Uint8Array(memory.buffer, offset) | |
56 | ||
57 | let stringStart = (args.length + 1) * 4 | |
58 | for (let i = 0; i < args.length; i++) { | |
59 | let len = put_string(memory, stringStart, args[i]) | |
60 | view.setUint32(i*4, stringStart, true) | |
61 | stringStart = stringStart + len | |
62 | } | |
63 | view.setUint32(args.length*4, 0, true) | |
64 | return offset + stringStart // start of free memory | |
65 | } | |
66 | ||
67 | // Based on: | |
68 | // https://gist.github.com/kripken/59c67556dc03bb6d57052fedef1e61ab | |
69 | ||
70 | // Loads a WebAssembly dynamic library, returns a promise. | |
71 | async function loadWebAssembly(filename, args) { | |
72 | // Fetch the file and compile it | |
73 | const wasm_str = await readFile(filename) | |
74 | const wasm_bin = toUint8Array(wasm_str) | |
75 | const module = await WebAssembly.compile(wasm_bin) | |
76 | let memory = new WebAssembly.Memory({ initial: 256 }) | |
77 | // Core imports | |
76adfab9 | 78 | function printline(addr, stream) { |
89149437 JM |
79 | console.log(get_string(memory, addr).replace(/\n$/, '')) |
80 | } | |
81 | ||
82 | // Returns addr on success and -1 on failure | |
83 | // Truncates to max_length | |
84 | function readline(prompt, addr, max_length) { | |
85 | let line = node_readline.readline(get_string(memory, prompt)) | |
86 | if (line === null) { return 0 } | |
87 | put_string(memory, addr, line, max_length) | |
88 | return 1 | |
89 | } | |
90 | ||
91 | function read_file(path_addr, buf) { | |
92 | let path = get_string(memory, path_addr) | |
93 | let contents = fs.readFileSync(path, 'utf8') | |
94 | return put_string(memory, buf, contents) | |
95 | } | |
96 | ||
6a51946b | 97 | function get_time_ms() { |
266391c5 JM |
98 | // subtract 30 years to make sure it fits into i32 without |
99 | // wrapping or becoming negative | |
100 | return (new Date()).getTime() - 0x38640900 | |
76adfab9 JM |
101 | } |
102 | ||
89149437 JM |
103 | // Marshal arguments |
104 | const memoryStart = 0 | |
105 | let memoryBase = marshal_argv(memory, memoryStart, args) | |
106 | memoryBase = memoryBase + (8 - (memoryBase % 8)) | |
107 | ||
108 | // Create the imports for the module, including the | |
109 | // standard dynamic library imports | |
110 | imports = {} | |
111 | imports.env = {} | |
112 | imports.env.exit = process.exit | |
76adfab9 | 113 | imports.env.printline = printline |
89149437 | 114 | imports.env.readline = readline |
89149437 | 115 | imports.env.read_file = read_file |
6a51946b JM |
116 | imports.env.get_time_ms = get_time_ms |
117 | ||
6a51946b JM |
118 | imports.env.stdout = 0 |
119 | imports.env.fputs = printline | |
89149437 JM |
120 | |
121 | imports.env.memory = memory | |
122 | imports.env.memoryBase = memoryBase | |
123 | imports.env.table = new WebAssembly.Table({ initial: 0, element: 'anyfunc' }) | |
124 | imports.env.tableBase = imports.env.tableBase || 0 | |
125 | // Create the instance. | |
126 | return [new WebAssembly.Instance(module, imports), args.length, 0] | |
127 | } | |
128 | ||
129 | async function main() { | |
130 | assert(process.argv.length >= 3, | |
131 | 'Usage: ./run.js prog.wasm [ARGS...]') | |
132 | ||
133 | const wasm = process.argv[2] | |
134 | const args = process.argv.slice(2) | |
135 | const [instance, argc, argv] = await loadWebAssembly(wasm, args) | |
136 | ||
137 | let exports = instance.exports | |
138 | assert(exports, 'no exports found') | |
139 | assert('_main' in exports, '_main not found in wasm module exports') | |
140 | if ('__post_instantiate' in exports) { | |
141 | //console.warn('calling exports.__post_instantiate()') | |
142 | exports['__post_instantiate']() | |
143 | } | |
144 | //console.warn(`calling exports._main(${argc}, ${argv})`) | |
145 | let start = new Date() | |
146 | let res = exports['_main'](argc, argv) | |
147 | let end = new Date() | |
148 | //console.warn('runtime: ' + (end-start) + 'ms') | |
149 | process.exit(res) | |
150 | } | |
151 | ||
152 | if (module.parent) { | |
153 | module.exports.loadWebAssembly = loadWebAssembly | |
154 | } else { | |
155 | main() | |
156 | } |