import { WASI, STDOUT, STDERR } from './constants'
import { MemoryManager } from './MemoryManager'
import { FunctionRegistry } from './FunctionRegistry'
import { FunctionPrototype } from './types/FunctionPrototype'
// @filename: types.d.ts
/**
* Write
* @callback writeCallback
* @param {string} text
*/
/**
* Drain the write if a newline is in the latest test.
* @private
* @param {writeCallback} write A function to write a complete line
* @param {string} prev The previous text
* @param {string} current The latest text
*/
function drainWriter(write, prev, current) {
let text = prev + current
while (text.includes('\n')) {
const [line, rest] = text.split('\n', 2)
write(line)
text = rest
}
return text
}
/**
* An implementation of WASI which supports the minimum required to memory
* allocation, stdio and multi byte (UTF-8) characters.
*/
export class Wasi {
/**
* Create a Wasi class
* @param {Object.<string, string>} env The environment variables
*/
constructor(env) {
this.env = env
/**
* @property {WebAssembly.Instance} [instance] The WebAssembly instance.
*/
this.instance = null
/**
* @property {MemoryManager} [memoryManager] The WebAssembly memory manager
*/
this.memoryManager = null
/**
* @property {FunctionRegistry} [functionRegistry] The function registry
*/
this.functionRegistry = null
/**
* @private
* @property {string} Text sent to stdout before a newline has een received.
*/
this.stdoutText = ''
/**
* @private
* @property {string} Text sent to stderr before a newline has een received.
*/
this.stderrText = ''
}
/**
* Initialize the WASI class with a WebAssembly instance.
* @param {WebAssembly.Instance} instance A WebAssembly instance
*/
init(instance) {
this.instance = instance
this.memoryManager = new MemoryManager(
/** @type {WebAssembly.Memory} */ (instance.exports.memory),
/** @type {malloc} */ (instance.exports.malloc),
/** @type {free} */ (instance.exports.free))
this.functionRegistry = new FunctionRegistry(this.memoryManager)
}
/**
* Register a function
* @param {string|symbol} name The function name
* @param {FunctionPrototype} prototype The function prototype
* @param {wasmCallback} callback The wasm callback
*/
registerFunction (name, prototype, callback) {
this.functionRegistry.registerImplied(name, prototype, callback)
}
/**
* Invoke a function implied by the arguments
* @param {string|symbol} name The function name
* @param {Array<any>} values The values with which to call the function
* @param {object} options Name mangling options
* @returns {*} The return value if any
*/
invokeImpliedFunction (name, values, options) {
const callback = this.functionRegistry.findImplied(name, values, options)
return callback(...values)
}
/**
* Invoke a function given an explicit argument mangle
* @param {string|symbol} name The function name
* @param {Array<any>} values The values with which to call the function
* @param {string} mangledArgs The mangled arguments
* @returns {*} The return value if any
*/
invokeExplicitFunction(name, values, mangledArgs) {
const callback = this.functionRegistry.findExplicit(name, mangledArgs)
return callback(values)
}
/**
* Invoke a function with defaults
* @param {string|symbol} name The function name
* @param {...any} values The function values
* @returns {*}
*/
invoke(name, ...values) {
return this.invokeImpliedFunction(name, values, {})
}
/**
* Get the environment variables.
* @param {number} environ The environment
* @param {number} environBuf The address of the buffer
*/
environ_get(environ, environBuf) {
const encoder = new TextEncoder()
Object.entries(this.env).map(
([key, value]) => `${key}=${value}`
).forEach(envVar => {
this.memoryManager.dataView.setUint32(environ, environBuf, true)
environ += 4
const bytes = encoder.encode(envVar)
const buf = new Uint8Array(this.memoryManager.memory.buffer, environBuf, bytes.length + 1)
environBuf += buf.byteLength
})
return WASI.ESUCCESS
}
/**
* Get the size required to store the environment variables.
* @param {number} environCount The number of environment variables
* @param {number} environBufSize The size of the environment variables buffer
*/
environ_sizes_get(environCount, environBufSize) {
const encoder = new TextEncoder()
const envVars = Object.entries(this.env).map(
([key, value]) => `${key}=${value}`
)
const size = envVars.reduce(
(acc, envVar) => acc + encoder.encode(envVar).byteLength + 1,
0
)
this.memoryManager.dataView.setUint32(environCount, envVars.length, true)
this.memoryManager.dataView.setUint32(environBufSize, size, true)
return WASI.ESUCCESS
}
/**
* This gets called on exit to stop the running program. We don't have
* anything to stop!
* @param {number} rval The return value
*/
proc_exit(rval) {
return WASI.ESUCCESS
}
/**
* Open the file descriptor
* @param {number} fd The file descriptor
*/
fd_close(fd) {
return WASI.ESUCCESS
}
/**
* Seek
* @param {number} fd The file descriptor
* @param {number} offset_low The low offset
* @param {number} offset_high The high offset
* @param {number} whence Whence
* @param {number} newOffset The new offset
*/
fd_seek(fd, offset_low, offset_high, whence, newOffset) {
return WASI.ESUCCESS
}
/**
* Write to a file descriptor
* @param {number} fd The file descriptor
* @param {number} iovs The address of the scatter vector
* @param {number} iovsLen The length of the scatter vector
* @param {number} nwritten The number of items written
*/
fd_write(fd, iovs, iovsLen, nwritten) {
if (!(fd === 1 || fd === 2)) {
return WASI.ERRNO.BADF
}
const buffers = Array.from({ length: iovsLen }, (_, i) => {
const ptr = iovs + i * 8
const buf = this.memoryManager.dataView.getUint32(ptr, true)
const bufLen = this.memoryManager.dataView.getUint32(ptr + 4, true)
return new Uint8Array(this.memoryManager.memory.buffer, buf, bufLen)
})
const textDecoder = new TextDecoder()
let written = 0
let text = ''
buffers.forEach(buf => {
text += textDecoder.decode(buf)
written += buf.byteLength
})
this.memoryManager.dataView.setUint32(nwritten, written, true)
if (fd === STDOUT) {
this.stdoutText = drainWriter(console.log, this.stdoutText, text)
} else if (fd === STDERR) {
this.stderrText = drainWriter(console.error, this.stderrText, text)
}
return WASI.ESUCCESS
}
/**
* Get the status of a file descriptor
* @param {number} fd The file descriptor
* @param {number} stat The status
*/
fd_fdstat_get(fd, stat) {
if (!(fd === 1 || fd === 2)) {
return WASI.ERRNO.BADF
}
if (this.memoryManager == null) {
throw new Error('No memory')
}
this.memoryManager.dataView.setUint8(stat + 0, WASI.FILETYPE.CHARACTER_DEVICE)
this.memoryManager.dataView.setUint32(stat + 2, WASI.FDFLAGS.APPEND, true)
this.memoryManager.dataView.setBigUint64(stat + 8, WASI.RIGHTS.FD.WRITE, true)
this.memoryManager.dataView.setBigUint64(stat + 16, WASI.RIGHTS.FD.WRITE, true)
return WASI.ESUCCESS
}
imports() {
return {
environ_get: (environ, environBuf) => this.environ_get(environ, environBuf),
environ_sizes_get: (environCount, environBufSize) => this.environ_sizes_get(environCount, environBufSize),
proc_exit: rval => this.proc_exit(rval),
fd_close: fd => this.fd_close(fd),
fd_seek: (fd, offset_low, offset_high, whence, newOffset) => this.fd_seek(fd, offset_low, offset_high, whence, newOffset),
fd_write: (fd, iovs, iovsLen, nwritten) => this.fd_write(fd, iovs, iovsLen, nwritten),
fd_fdstat_get: (fd, stat) => this.fd_fdstat_get(fd, stat)
}
}
}