@stdlib/wasm
Version:
WebAssembly.
474 lines (437 loc) • 16.9 kB
JavaScript
/**
* @license Apache-2.0
*
* Copyright (c) 2024 The Stdlib Authors.
*
* 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.
*/
/* eslint-disable no-restricted-syntax, no-invalid-this, no-underscore-dangle */
'use strict';
// MODULES //
var isNonNegativeInteger = require( '@stdlib/assert/is-nonnegative-integer' ).isPrimitive;
var isCollection = require( '@stdlib/assert/is-collection' );
var isArrayBuffer = require( '@stdlib/assert/is-arraybuffer' );
var isFunction = require( '@stdlib/assert/is-function' );
var setReadOnly = require( '@stdlib/utils/define-nonenumerable-read-only-property' );
var setReadOnlyAccessor = require( '@stdlib/utils/define-nonenumerable-read-only-accessor' );
var hasWebAssemblySupport = require( '@stdlib/assert/has-wasm-support' );
var objectAssign = require( '@stdlib/object/assign' );
var Uint8Array = require( '@stdlib/array/uint8' );
var DataView = require( '@stdlib/array/dataview' );
var bytesPerElement = require( '@stdlib/ndarray/base/bytes-per-element' );
var ceil = require( '@stdlib/math/base/special/ceil' );
var writeDataView = require( '@stdlib/strided/base/write-dataview' ).ndarray;
var readDataView = require( '@stdlib/strided/base/read-dataview' ).ndarray;
var dtype = require( '@stdlib/array/dtype' );
var dtype2wasm = require( './../../base/dtype2wasm' );
var format = require( '@stdlib/string/format' );
// VARIABLES //
var HAS_WASM_SUPPORT = hasWebAssemblySupport();
var PAGE_SIZE = 64 * 1024; // 64KiB = size of 1 page // TODO: consider moving to `@stdlib/constants/wasm/page-size`
// MAIN //
/**
* WebAssembly module wrapper constructor.
*
* @constructor
* @param {(Uint8Array|ArrayBuffer)} binary - WebAssembly binary code
* @param {(Object|null)} memory - WebAssembly memory instance
* @param {Object} [imports] - WebAssembly module import object
* @throws {Error} environment does not support WebAssembly
* @returns {WasmModule} wrapper instance
*/
function WasmModule( binary, memory, imports ) {
if ( !( this instanceof WasmModule ) ) {
if ( arguments.length > 2 ) {
return new WasmModule( binary, memory, imports );
}
return new WasmModule( binary, memory, {} );
}
if ( !HAS_WASM_SUPPORT ) {
throw new Error( 'unexpected error. Environment does not support WebAssembly.' );
}
if ( isArrayBuffer( binary ) ) {
binary = new Uint8Array( binary );
}
this._binary = binary;
this._module = null;
this._instance = null;
this._imports = imports;
if ( memory ) {
this._memory = memory;
this._view = new DataView( memory.buffer );
this._buffer = new Uint8Array( memory.buffer );
} else {
this._memory = null;
this._view = null;
this._buffer = null;
}
return this;
}
/**
* Returns WebAssembly binary code.
*
* @name binary
* @memberof WasmModule.prototype
* @readonly
* @type {Uint8Array}
*/
setReadOnlyAccessor( WasmModule.prototype, 'binary', function get() {
return this._binary.slice();
});
/**
* Returns WebAssembly memory.
*
* @name memory
* @memberof WasmModule.prototype
* @readonly
* @type {(Object|null)}
*/
setReadOnlyAccessor( WasmModule.prototype, 'memory', function get() {
return this._memory;
});
/**
* Returns a WebAssembly memory buffer as a `Uint8Array`.
*
* @name buffer
* @memberof WasmModule.prototype
* @readonly
* @type {(Uint8Array|null)}
*/
setReadOnlyAccessor( WasmModule.prototype, 'buffer', function get() {
return this._buffer;
});
/**
* Returns a WebAssembly memory buffer as a `DataView`.
*
* @name view
* @memberof WasmModule.prototype
* @readonly
* @type {(DataView|null)}
*/
setReadOnlyAccessor( WasmModule.prototype, 'view', function get() {
return this._view;
});
/**
* Returns "raw" WebAssembly module exports.
*
* @name exports
* @memberof WasmModule.prototype
* @readonly
* @type {(Object|null)}
*/
setReadOnlyAccessor( WasmModule.prototype, 'exports', function get() {
if ( !this._instance ) {
return null;
}
return objectAssign( {}, this._instance.exports );
});
/**
* Asynchronously initializes a WebAssembly module instance.
*
* @name initialize
* @memberof WasmModule.prototype
* @readonly
* @type {Function}
* @returns {Promise} promise which resolves upon initializing a WebAssembly module instance
*/
setReadOnly( WasmModule.prototype, 'initialize', function initialize() {
var self;
var fcn;
self = this;
if ( this._instance ) {
fcn = returnInstance;
} else {
fcn = instantiate;
}
// FIXME: replace with `@stdlib/promise/ctor`
return new Promise( fcn ); // eslint-disable-line stdlib/require-globals
/**
* Returns a WebAssembly instance.
*
* @private
* @param {Function} resolve - callback to invoke upon fulfilling a promise
* @param {Function} reject - callback to invoke upon rejecting a promise
* @returns {void}
*/
function returnInstance( resolve ) {
resolve( self );
}
/**
* Instantiates a WebAssembly instance.
*
* @private
* @param {Function} resolve - callback to invoke upon fulfilling a promise
* @param {Function} reject - callback to invoke upon rejecting a promise
* @returns {void}
*/
function instantiate( resolve, reject ) {
var p = WebAssembly.instantiate( self._binary, self._imports ); // TODO: replace with `@stdlib/wasm/instantiate`
p.then( onResolve, onReject );
/**
* Callback invoked upon fulfilling a promise.
*
* @private
* @param {Object} module - WebAssembly module
* @param {Object} instance - WebAssembly instance
* @returns {void}
*/
function onResolve( module, instance ) {
self._module = module;
self._instance = instance;
resolve( self );
}
/**
* Callback invoked upon rejecting a promise.
*
* @private
* @param {*} reason - reason for rejecting a promise
*/
function onReject( reason ) {
reject( reason );
}
}
});
/**
* Asynchronously initializes a WebAssembly module instance.
*
* @name initializeAsync
* @memberof WasmModule.prototype
* @readonly
* @type {Function}
* @param {Callback} clbk - callback to invoke upon initializing a WebAssembly module instance
* @throws {TypeError} must provide a callback function
* @returns {void}
*/
setReadOnly( WasmModule.prototype, 'initializeAsync', function initializeAsync( clbk ) {
var self;
var p;
if ( !isFunction( clbk ) ) {
throw new TypeError( format( 'invalid argument. Must provide a function. Value: `%s`.', clbk ) );
}
if ( this._instance ) {
return clbk( null, this );
}
self = this;
p = WebAssembly.instantiate( this._binary, this._imports ); // TODO: replace with `@stdlib/wasm/instantiate`
p.then( onResolve, onReject );
/**
* Callback invoked upon fulfilling a promise.
*
* @private
* @param {Object} module - WebAssembly module
* @param {Object} instance - WebAssembly instance
* @returns {void}
*/
function onResolve( module, instance ) {
self._module = module;
self._instance = instance;
clbk( null, self );
}
/**
* Callback invoked upon rejecting a promise.
*
* @private
* @param {*} reason - reason for rejecting a promise
*/
function onReject( reason ) {
clbk( reason );
}
});
/**
* Synchronously initializes a WebAssembly module instance.
*
* ## Notes
*
* - In web browsers, JavaScript engines may raise an exception when attempting to synchronously compile large WebAssembly binaries due to concerns about blocking the main thread. Hence, for large binaries (e.g., >4KiB), consider using asynchronous initialization methods.
*
* @name initializeSync
* @memberof WasmModule.prototype
* @readonly
* @type {Function}
* @returns {WasmModule} wrapper instance
*/
setReadOnly( WasmModule.prototype, 'initializeSync', function initializeSync() {
if ( this._instance ) {
return this;
}
this._module = new WebAssembly.Module( this._binary ); // TODO: replace with `@stdlib/wasm/module`
this._instance = new WebAssembly.Instance( this._module, this._imports ); // TODO: replace with `@stdlib/wasm/instantiate`
return this;
});
/**
* Reallocates the underlying WebAssembly memory instance to a specified number of bytes.
*
* ## Notes
*
* - WebAssembly memory can only **grow**, not shrink. Hence, if provided a number of bytes which is less than or equal to the size of the current memory, the function does nothing.
* - When non-shared memory is resized, the underlying the `ArrayBuffer` is detached, consequently invalidating any associated typed array views. Before resizing non-shared memory, ensure that associated typed array views no longer need byte access and can be garbage collected.
*
* @name realloc
* @memberof WasmModule.prototype
* @readonly
* @type {Function}
* @param {NonNegativeInteger} nbytes - memory size (in bytes)
* @throws {TypeError} must provide a nonnegative integer
* @returns {boolean} boolean indicating whether the resize operation was successful
*/
setReadOnly( WasmModule.prototype, 'realloc', function realloc( nbytes ) {
var size;
if ( !isNonNegativeInteger( nbytes ) ) {
throw new TypeError( format( 'invalid argument. Must provide a nonnegative integer. Value: `%s`.', nbytes ) );
}
if ( this._memory === null ) {
return false;
}
size = ceil( ( nbytes-this._buffer.length ) / PAGE_SIZE );
if ( size > 0 ) {
try {
this._memory.grow( size );
} catch ( err ) { // eslint-disable-line no-unused-vars
return false;
}
this._view = new DataView( this._memory.buffer );
this._buffer = new Uint8Array( this._memory.buffer );
return true;
}
return false;
});
/**
* Returns a boolean indicating whether the underlying WebAssembly memory instance has the capacity to store a provided list of values starting from a specified byte offset.
*
* @name hasCapacity
* @memberof WasmModule.prototype
* @readonly
* @type {Function}
* @param {NonNegativeInteger} byteOffset - byte offset at which to start writing values
* @param {Collection} values - input array containing values to write
* @throws {TypeError} first argument must be a nonnegative integer
* @throws {TypeError} second argument must be a collection
* @returns {boolean} boolean indicating whether the underlying WebAssembly memory instance has enough capacity
*/
setReadOnly( WasmModule.prototype, 'hasCapacity', function hasCapacity( byteOffset, values ) {
var nb;
if ( !isNonNegativeInteger( byteOffset ) ) {
throw new TypeError( format( 'invalid argument. First argument must be a nonnegative integer. Value: `%s`.', byteOffset ) );
}
if ( !isCollection( values ) ) {
throw new TypeError( format( 'invalid argument. Second argument must be an array-like object. Value: `%s`.', values ) );
}
if ( this._memory === null ) {
return false;
}
nb = bytesPerElement( dtype2wasm( dtype( values ) || 'generic' ) );
if ( (values.length*nb)+byteOffset > this._buffer.length ) {
return false;
}
return true;
});
/**
* Returns a boolean indicating whether a provided list of values is a view of the underlying memory of the WebAssembly module.
*
* @name isView
* @memberof WasmModule.prototype
* @readonly
* @type {Function}
* @param {Collection} values - input array
* @throws {TypeError} must provide a collection
* @returns {boolean} boolean indicating whether the list is a memory view
*/
setReadOnly( WasmModule.prototype, 'isView', function isView( values ) {
if ( !isCollection( values ) ) {
throw new TypeError( format( 'invalid argument. Must provide an array-like object. Value: `%s`.', values ) );
}
if ( this._memory && values.buffer ) {
return ( values.buffer === this._memory.buffer );
}
return false;
});
/**
* Writes values to the underlying WebAssembly memory instance.
*
* ## Notes
*
* - The function infers element size (i.e., number of bytes per element) from the data type of the input array. For example, if provided a `Float32Array`, the function writes each element as a single-precision floating-point number to the underlying WebAssembly memory instance.
* - In order to write elements as a different data type, you need to perform an explicit cast **before** calling this method. For example, in order to write single-precision floating-point numbers contained in a `Float32Array` as signed 32-bit integers, you must first convert the `Float32Array` to an `Int32Array` before passing the values to this method.
* - If provided an array having an unknown or "generic" data type, elements are written as double-precision floating-point numbers.
*
* @name write
* @memberof WasmModule.prototype
* @readonly
* @type {Function}
* @param {NonNegativeInteger} byteOffset - byte offset at which to start writing values
* @param {Collection} values - input array containing values to write
* @throws {TypeError} first argument must be a nonnegative integer
* @throws {TypeError} second argument must be a collection
* @throws {Error} unable to perform write operation as the module is not bound to a memory instance
* @throws {RangeError} insufficient memory
* @returns {WasmModule} wrapper instance
*/
setReadOnly( WasmModule.prototype, 'write', function write( byteOffset, values ) {
var nb;
if ( this._memory === null ) {
throw new Error( 'invalid invocation. Unable to perform write operation, as the WebAssembly module is not bound to an underlying WebAssembly memory instance.' );
}
if ( !isNonNegativeInteger( byteOffset ) ) {
throw new TypeError( format( 'invalid argument. First argument must be a nonnegative integer. Value: `%s`.', byteOffset ) );
}
if ( !isCollection( values ) ) {
throw new TypeError( format( 'invalid argument. Second argument must be an array-like object. Value: `%s`.', values ) );
}
nb = bytesPerElement( dtype2wasm( dtype( values ) || 'generic' ) );
if ( (values.length*nb)+byteOffset > this._buffer.length ) {
throw new RangeError( 'invalid argument. Second argument is incompatible with the specified byte offset and available memory. Resize the underlying memory instance in order to accommodate the list of provided values.' );
}
writeDataView( values.length, values, 1, 0, this._view, nb, byteOffset, true ); // eslint-disable-line max-len
return this;
});
/**
* Reads values from the underlying WebAssembly memory instance.
*
* ## Notes
*
* - The function infers element size (i.e., number of bytes per element) from the data type of the output array. For example, if provided a `Float32Array`, the function reads each element as a single-precision floating-point number from the underlying WebAssembly memory instance.
* - In order to read elements as a different data type, you need to perform an explicit cast **after** calling this method. For example, in order to read single-precision floating-point numbers contained in a `Float32Array` as signed 32-bit integers, you must convert the `Float32Array` to an `Int32Array` after reading memory values using this method.
* - If provided an output array having an unknown or "generic" data type, elements are read as double-precision floating-point numbers.
*
* @name read
* @memberof WasmModule.prototype
* @readonly
* @type {Function}
* @param {NonNegativeInteger} byteOffset - byte offset at which to start reading values
* @param {Collection} out - output array for storing read values
* @throws {TypeError} first argument must be a nonnegative integer
* @throws {TypeError} second argument must be a collection
* @throws {Error} unable to perform read operation as the module is not bound to a memory instance
* @throws {RangeError} output array exceeds module memory
* @returns {WasmModule} wrapper instance
*/
setReadOnly( WasmModule.prototype, 'read', function write( byteOffset, out ) {
var nb;
if ( this._memory === null ) {
throw new Error( 'invalid invocation. Unable to perform read operation, as the WebAssembly module is not bound to an underlying WebAssembly memory instance.' );
}
if ( !isNonNegativeInteger( byteOffset ) ) {
throw new TypeError( format( 'invalid argument. First argument must be a nonnegative integer. Value: `%s`.', byteOffset ) );
}
if ( !isCollection( out ) ) {
throw new TypeError( format( 'invalid argument. Second argument must be an array-like object. Value: `%s`.', out ) );
}
nb = bytesPerElement( dtype2wasm( dtype( out ) || 'generic' ) );
if ( (out.length*nb)+byteOffset > this._buffer.length ) {
throw new RangeError( 'invalid argument. Second argument is incompatible with the specified byte offset and available memory. Not enough values to fill the provided output array.' );
}
readDataView( out.length, this._view, nb, byteOffset, out, 1, 0, true );
return this;
});
// EXPORTS //
module.exports = WasmModule;