| #!/usr/bin/env node |
| /* |
| * Copyright 2016 WebAssembly Community Group participants |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| /* |
| * Simple implmentation of WASI in JS in order to support running of tests |
| * with minimal system dependencies such as the GCC torture tests. |
| * |
| * This script is designed to run under both d8 and nodejs. |
| * |
| * Usage: wasi.js <wasm_binary> |
| */ |
| |
| const PAGE_SIZE = (64 * 1024); |
| let heap_size_bytes = 16 * 1024 * 1024; |
| let heap_size_pages = heap_size_bytes / PAGE_SIZE; |
| let default_memory = new WebAssembly.Memory({initial: heap_size_pages, maximum: heap_size_pages}) |
| let heap; |
| let heap_uint8; |
| let heap_uint16; |
| let heap_uint32; |
| |
| // This is node.js |
| if (typeof process === 'object' && typeof require === 'function') { |
| // Emulate JS shell behavior used below |
| var nodeFS = require('fs'); |
| var nodePath = require('path'); |
| var read = function(file_path) { |
| filename = nodePath['normalize'](file_path); |
| return nodeFS['readFileSync'](filename); |
| } |
| var print = console.log; |
| var arguments = process['argv'].slice(2); |
| var quit = process.exit |
| } |
| |
| // Exceptions |
| function TerminateWasmException(value, code) { |
| this.stack = (new Error()).stack; |
| this.value = value; |
| this.exit_code = code; |
| this.message = 'Terminating WebAssembly'; |
| this.toString = function() { return this.message + ': ' + this.value; }; |
| } |
| |
| function NotYetImplementedException(what) { |
| this.stack = (new Error()).stack; |
| this.message = 'Not yet implemented'; |
| this.what = what; |
| this.toString = function() { return this.message + ': ' + this.what; }; |
| } |
| |
| // Heap access helpers. |
| function setHeap(m) { |
| memory = m |
| heap = m.buffer |
| heap_uint8 = new Uint8Array(heap); |
| heap_uint16 = new Uint16Array(heap); |
| heap_uint32 = new Uint32Array(heap); |
| heap_size_bytes = heap.byteLength; |
| } |
| |
| function checkHeap() { |
| if (heap.byteLength == 0) { |
| setHeap(main_module.exports.memory); |
| } |
| } |
| |
| function readChar(ptr) { |
| return String.fromCharCode(heap_uint8[ptr]); |
| } |
| |
| function readStr(ptr, len = -1) { |
| let str = ''; |
| var end = heap_size_bytes; |
| if (len != -1) |
| end = ptr + len; |
| for (var i = ptr; i < end && heap_uint8[i] != 0; ++i) |
| str += readChar(i); |
| return str; |
| } |
| |
| function writeBuffer(offset, buf) { |
| buf.copy(heap_uint8, offset); |
| } |
| |
| function writeStr(offset, str) { |
| var start = offset; |
| for (var i = 0; i < str.length; i++ ) { |
| write8(offset, str.charCodeAt(i)); |
| offset++; |
| } |
| write8(offset, 0); |
| offset++; |
| return offset - start; |
| } |
| |
| function write8(offset, value) { heap_uint8[offset] = value; } |
| function write16(offset, value) { heap_uint16[offset>>1] = value; } |
| function write32(offset, value) { heap_uint32[offset>>2] = value; } |
| |
| function write64(offset, valueFirst, valueLast) { |
| heap_uint32[(offset+0)>>2] = valueFirst; |
| heap_uint32[(offset+4)>>2] = valueLast; |
| } |
| |
| function read8(offset) { return heap_uint8[offset]; } |
| function read16(offset) { return heap_uint16[offset>>1]; } |
| function read32(offset) { return heap_uint32[offset>>2]; } |
| |
| let DEBUG = false; |
| |
| function dbg(message) { |
| if (DEBUG) |
| print(message); |
| } |
| |
| // WASI implemenation |
| // See: https://github.com/WebAssembly/WASI/blob/master/design/WASI-core.md |
| var wasi_interface = (function() { |
| const STDIN = 0; |
| const STDOUT = 1; |
| const STDERR = 2; |
| const MAXFD = 2; |
| |
| const WASI_ESUCCESS = 0; |
| const WASI_EBADF = 8; |
| const WASI_ENOTSUP = 58; |
| const WASI_EPERM = 63; |
| |
| const WASI_PREOPENTYPE_DIR = 0; |
| |
| const WASI_LOOKUP_SYMLINK_FOLLOW = 0x1; |
| |
| const WASI_FDFLAG_APPEND = 0x0001; |
| const WASI_FDFLAG_DSYNC = 0x0002; |
| const WASI_FDFLAG_NONBLOCK = 0x0004; |
| const WASI_FDFLAG_RSYNC = 0x0008; |
| const WASI_FDFLAG_SYNC = 0x0010; |
| |
| const WASI_RIGHT_FD_DATASYNC = 0x00000001; |
| const WASI_RIGHT_FD_READ = 0x00000002; |
| const WASI_RIGHT_FD_SEEK = 0x00000004; |
| const WASI_RIGHT_PATH_OPEN = 0x00002000; |
| const WASI_RIGHT_PATH_FILESTAT_GET = 0x00040000; |
| const WASI_RIGHT_FD_READDIR = 0x00004000; |
| const WASI_RIGHT_FD_FILESTAT_GET = 0x00200000; |
| const WASI_RIGHT_ALL = 0xffffffff; |
| |
| const WASI_FILETYPE_UNKNOWN = 0; |
| const WASI_FILETYPE_BLOCK_DEVICE = 1; |
| const WASI_FILETYPE_CHARACTER_DEVICE = 2; |
| const WASI_FILETYPE_DIRECTORY = 3; |
| const WASI_FILETYPE_REGULAR_FILE = 4; |
| const WASI_FILETYPE_SOCKET_DGRAM = 5; |
| const WASI_FILETYPE_SOCKET_STREAM = 6; |
| const WASI_FILETYPE_SYMBOLIC_LINK = 7; |
| |
| const WASI_WHENCE_CUR = 0; |
| const WASI_WHENCE_END = 1; |
| const WASI_WHENCE_SET = 2; |
| |
| let env = { |
| USER: 'alice', |
| }; |
| |
| let argv = []; |
| |
| let stdin = (function() { |
| return { |
| flush: function() {} |
| }; |
| })(); |
| |
| let stdout = (function() { |
| let buf = ''; |
| return { |
| type: WASI_FILETYPE_CHARACTER_DEVICE, |
| flags: WASI_FDFLAG_APPEND, |
| write: function(str) { |
| buf += str; |
| if (buf[-1] == '\n') { |
| buf = buf.slice(0, -1); |
| print(buf); |
| buf = ''; |
| } |
| }, |
| flush: function() { |
| if (buf[-1] == '\n') |
| buf = buf.slice(0, -1); |
| print(buf); |
| buf = ''; |
| } |
| } |
| })(); |
| |
| let stderr = (function() { |
| let buf = ''; |
| return { |
| type: WASI_FILETYPE_CHARACTER_DEVICE, |
| flags: WASI_FDFLAG_APPEND, |
| write: function(str) { |
| buf += str; |
| if (buf[-1] == '\n') { |
| buf = buf.slice(0, -1); |
| print(buf); |
| buf = ''; |
| } |
| }, |
| flush: function() { |
| if (buf[-1] == '\n') |
| buf = buf.slice(0, -1); |
| print(buf); |
| buf = ''; |
| } |
| } |
| })(); |
| |
| let rootdir = (function() { |
| return { |
| type: WASI_FILETYPE_DIRECTORY, |
| flags: 0, |
| flush: function() {}, |
| name: "/", |
| rootdir: "/", |
| preopen: true, |
| rights_base: WASI_RIGHT_ALL, |
| rights_inheriting: WASI_RIGHT_ALL, |
| }; |
| })(); |
| |
| let openFile = function(filename) { |
| dbg('openFile: ' + filename); |
| let data = read(filename); |
| let position = 0; |
| let end = data.length; |
| return { |
| read: function(len) { |
| let start = position; |
| let end = Math.min(position + len, data.length); |
| position = end; |
| return data.slice(start, end) |
| }, |
| seek: function(offset, whence) { |
| if (whence == WASI_WHENCE_CUR) { |
| position += offset; |
| } else if (whence == WASI_WHENCE_END) { |
| position += end + offset; |
| } else if (whence == WASI_WHENCE_SET) { |
| position = offset; |
| } |
| if (position > end) { |
| position = end; |
| } else if (position < 0) { |
| position = 0; |
| } |
| return position; |
| }, |
| flush: function() {} |
| }; |
| }; |
| |
| let openFiles = [ |
| stdin, |
| stdout, |
| stderr, |
| rootdir, |
| ]; |
| |
| let nextFD = openFiles.length; |
| |
| function isValidFD(fd) { |
| return openFiles.hasOwnProperty(fd) |
| } |
| |
| function trace(syscall_name, syscall_args) { |
| if (DEBUG) |
| dbg('wasi_snapshot_preview1.' + syscall_name + '(' + Array.from(syscall_args) + ')'); |
| } |
| |
| let module_api = { |
| proc_exit: function(code) { |
| trace('proc_exit', arguments); |
| throw new TerminateWasmException('proc_exit(' + code + ')', code); |
| }, |
| environ_sizes_get: function(environ_count_out_ptr, environ_buf_size_out_ptr) { |
| trace('environ_sizes_get', arguments); |
| checkHeap(); |
| const names = Object.getOwnPropertyNames(env); |
| let total_space = 0; |
| for (const i in names) { |
| let name = names[i]; |
| let value = env[name]; |
| // Format of each env entry is name=value with null terminator. |
| total_space += name.length + value.length + 2; |
| } |
| write64(environ_count_out_ptr, names.length); |
| write64(environ_buf_size_out_ptr, total_space) |
| return WASI_ESUCCESS; |
| }, |
| environ_get: function(environ_pointers_out, environ_out) { |
| trace('environ_get', arguments); |
| let names = Object.getOwnPropertyNames(env); |
| for (const i in names) { |
| write32(environ_pointers_out, environ_out); |
| environ_pointers_out += 4; |
| let name = names[i]; |
| let value = env[name]; |
| let full_string = name + "=" + value; |
| environ_out += writeStr(environ_out, full_string); |
| } |
| write32(environ_pointers_out, 0); |
| return WASI_ESUCCESS; |
| }, |
| args_sizes_get: function(args_count_out_ptr, args_buf_size_out_ptr) { |
| trace('args_sizes_get', arguments); |
| checkHeap(); |
| let total_space = 0; |
| for (const value of argv) { |
| total_space += value.length + 1; |
| } |
| write64(args_count_out_ptr, argv.length); |
| write64(args_buf_size_out_ptr, total_space); |
| dbg(argv); |
| return WASI_ESUCCESS; |
| }, |
| args_get: function(args_pointers_out, args_out) { |
| trace('args_get', arguments); |
| for (const value of argv) { |
| write32(args_pointers_out, args_out); |
| args_pointers_out += 4; |
| args_out += writeStr(args_out, value); |
| } |
| write32(args_pointers_out, 0); |
| return WASI_ESUCCESS; |
| }, |
| fd_pread: function(fd, iovs, iovs_len, offset, nread) { |
| trace('fd_pread', arguments); |
| checkHeap(); |
| if (!isValidFD(fd)) |
| return WASI_EBADF; |
| var file = openFiles[fd]; |
| if (fd.read == undefined) |
| return WASI_EBADF; |
| throw new NotYetImplementedException('fd_pread'); |
| }, |
| fd_prestat_get: function(fd, prestat_ptr) { |
| trace('fd_prestat_get', arguments); |
| checkHeap(); |
| if (!isValidFD(fd)) |
| return WASI_EBADF; |
| var file = openFiles[fd]; |
| if (!file.preopen) |
| return WASI_EBADF; |
| write8(prestat_ptr, WASI_PREOPENTYPE_DIR); |
| write64(prestat_ptr+4, file.name.length); |
| return 0; |
| }, |
| fd_prestat_dir_name: function(fd, path_ptr, path_len) { |
| trace('fd_prestat_dir_name', arguments); |
| if (!isValidFD(fd)) |
| return WASI_EBADF; |
| var file = openFiles[fd]; |
| if (!file.preopen) |
| return WASI_EBADF; |
| write64(path_len, file.name.length); |
| writeStr(path_ptr, file.name); |
| return 0; |
| }, |
| fd_fdstat_get: function(fd, fdstat_ptr) { |
| trace('fd_fdstat_get', arguments); |
| if (!isValidFD(fd)) |
| return WASI_EBADF; |
| var file = openFiles[fd]; |
| write8(fdstat_ptr, file.type); |
| write16(fdstat_ptr+2, file.flags); |
| write64(fdstat_ptr+8, file.rights_base); |
| write64(fdstat_ptr+16, file.rights_inheriting); |
| return WASI_ESUCCESS; |
| }, |
| fd_fdstat_set_flags: function(fd, fdflags) { |
| trace('fd_fdstat_set_flags', arguments); |
| if (!isValidFD(fd)) |
| return WASI_EBADF; |
| return WASI_ESUCCESS; |
| }, |
| fd_read: function(fd, iovs_ptr, iovs_len, nread) { |
| trace('fd_read', arguments); |
| if (!isValidFD(fd)) |
| return WASI_EBADF; |
| var file = openFiles[fd]; |
| if (!file.hasOwnProperty('read')) |
| return WASI_EBADF; |
| checkHeap(); |
| let total = 0; |
| for (let i = 0; i < iovs_len; i++) { |
| let buf = read32(iovs_ptr); iovs_ptr += 4; |
| let len = read32(iovs_ptr); iovs_ptr += 4; |
| let data = file.read(len); |
| if (data.length == 0) { |
| break; |
| } |
| writeBuffer(buf, data); |
| total += data.length; |
| } |
| write32(nread, total); |
| return WASI_ESUCCESS; |
| }, |
| fd_write: function(fd, iovs_ptr, iovs_len, nwritten) { |
| trace('fd_write', arguments); |
| if (!isValidFD(fd)) |
| return WASI_EBADF; |
| var file = openFiles[fd]; |
| if (!file.hasOwnProperty('write')) |
| return WASI_EPERM; |
| checkHeap(); |
| let total = 0; |
| for (let i = 0; i < iovs_len; i++) { |
| let buf = read32(iovs_ptr); iovs_ptr += 4; |
| let len = read32(iovs_ptr); iovs_ptr += 4; |
| file.write(readStr(buf, len)); |
| total += len; |
| } |
| write32(nwritten, total); |
| return WASI_ESUCCESS; |
| }, |
| fd_close: function(fd) { |
| trace('fd_close', arguments); |
| if (!isValidFD(fd)) { |
| return WASI_EBADF; |
| } |
| openFiles[fd].flush(); |
| delete openFiles[fd]; |
| if (fd < nextFD) { |
| nextFD = fd; |
| } |
| return WASI_ESUCCESS; |
| }, |
| fd_seek: function(fd, offset, whence, newoffset_ptr) { |
| trace('fd_seek', arguments); |
| if (!isValidFD(fd)) { |
| return WASI_EBADF; |
| } |
| let file = openFiles[fd]; |
| checkHeap(); |
| let intOffset = parseInt(offset.toString()); |
| let newPos = file.seek(intOffset, whence); |
| write64(newoffset_ptr, newPos); |
| dbg("done seek: " + newPos); |
| return WASI_ESUCCESS; |
| }, |
| path_filestat_get: function(dirfd, lookupflags, path, path_len, buf) { |
| trace('path_filestat_get', arguments); |
| if (!isValidFD(dirfd)) { |
| return WASI_EBADF; |
| } |
| let file = openFiles[dirfd]; |
| if (file != rootdir) { |
| return WASI_EBADF; |
| } |
| let filename = readStr(path, path_len); |
| let stat = nodeFS.statSync(filename); |
| if (stat.isFile()) { |
| write32(buf+16, WASI_FILETYPE_REGULAR_FILE); |
| } else if (stat.isSymbolicLink()) { |
| write32(buf+16, WASI_FILETYPE_SYMBOLIC_LINK); |
| } else if (stat.isDirectory()) { |
| write32(buf+16, WASI_FILETYPE_DIRECTORY); |
| } else if (stat.isCharDevice()) { |
| write32(buf+16, WASI_FILETYPE_CHARACTER_DEVICE); |
| } else if (stat.isBlockDevice()) { |
| write32(buf+16, WASI_FILETYPE_BLOCK_DEVICE); |
| } else { |
| write32(buf+16, WASI_FILETYPE_UNKNOWN); |
| } |
| return WASI_ESUCCESS; |
| }, |
| path_open: function(dirfd, dirflags, path, path_len, oflags, fs_rights_base, fs_rights_inheriting, fs_flags, fd_out) { |
| trace('path_open', arguments); |
| checkHeap(); |
| let filename = readStr(path, path_len); |
| trace('path_open', ['dirfd=' + dirfd, 'path=' + filename, 'flags=' + oflags]); |
| if (!isValidFD(dirfd)) |
| return WASI_EBADF; |
| let file = openFiles[dirfd]; |
| if (file != rootdir) |
| return WASI_EBADF; |
| // TODO(sbc): Implement open flags (e.g. O_CREAT) |
| if (oflags) |
| return WASI_ENOTSUP; |
| if (fs_flags) |
| return WASI_ENOTSUP; |
| let fd = nextFD; |
| filename = file.rootdir + filename; |
| openFiles[fd] = openFile(filename); |
| write32(fd_out, fd); |
| while (openFiles[nextFD] != undefined) |
| nextFD++; |
| return WASI_ESUCCESS; |
| }, |
| path_unlink_file: function(dirfd, path, path_len) { |
| checkHeap(); |
| let filename = readStr(path, path_len); |
| trace('path_unlink_file', ['dirfd=' + dirfd, 'path=' + filename]); |
| let file = openFiles[dirfd]; |
| if (file != rootdir) |
| return WASI_EBADF; |
| filename = file.rootdir + filename; |
| trace('path_unlink_file', ['path=' + filename]); |
| //fs.unlinkSync(filename); |
| return WASI_ENOTSUP; |
| }, |
| path_remove_directory: function(dirfd, path, path_len) { |
| trace('path_remove_directory', ['dirfd=' + dirfd, 'path=' + readStr(path, path_len)]); |
| throw new NotYetImplementedException('path_remove_directory'); |
| }, |
| random_get: function(buf, buf_len) { |
| trace('random_get', arguments); |
| return WASI_ESUCCESS; |
| } |
| } |
| |
| return { |
| onExit: function() { |
| for (let k in openFiles){ |
| if (openFiles.hasOwnProperty(k)) { |
| openFiles[k].flush(); |
| } |
| } |
| }, |
| setArgv: function(new_argv) { |
| argv = new_argv; |
| }, |
| api: module_api |
| }; |
| })(); |
| |
| let ffi = (function() { |
| let env = { |
| memory: default_memory, |
| // Any non-wasi dependencies end up under 'env'. |
| // TODO(sbc): Implement on the wasm side or add to WASI? |
| _Unwind_RaiseException: function() { |
| throw new NotYetImplementedException('_Unwind_RaiseException'); |
| } |
| } |
| return { |
| env: env, |
| wasi_snapshot_preview1: wasi_interface.api |
| }; |
| })(); |
| |
| if (arguments.length < 1) |
| throw new Error('Expected at least one wasm module to load.'); |
| |
| function loadWasm(file_path) { |
| const buf = (typeof readbuffer === 'function') |
| ? new Uint8Array(readbuffer(file_path)) |
| : read(file_path, 'binary'); |
| let instance = new WebAssembly.Instance(new WebAssembly.Module(buf), ffi) |
| if (instance.exports.memory) { |
| setHeap(instance.exports.memory); |
| } else { |
| setHeap(default_memory) |
| } |
| return instance; |
| } |
| |
| let main_module_name = arguments[0]; |
| wasi_interface.setArgv(arguments) |
| |
| main_module = loadWasm(main_module_name); |
| |
| if (!(main_module.exports._start instanceof Function)) |
| throw new Error('_start not found'); |
| |
| try { |
| main_module.exports._start(); |
| wasi_interface.onExit(); |
| print(main_module_name + '::_start returned normally'); |
| } catch (e) { |
| wasi_interface.onExit(); |
| if (e instanceof TerminateWasmException) { |
| print('Program terminated with: ' + e.exit_code); |
| quit(e.exit_code); |
| } else if (e instanceof NotYetImplementedException) { |
| print('NotYetImplemented: ' + e.what); |
| } else if (e instanceof WebAssembly.RuntimeError) { |
| print('Runtime trap: ' + e.message); |
| } else { |
| print('Unknown exception of type `' + typeof(e) + '`: ' + e); |
| } |
| throw e; |
| } |