npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

wasm-heap-manager

v0.1.0

Published

Provides safe and convenient methods to allocate, read, and write numeric values, strings, and typed arrays, within an ArrayBuffer heap, like the one used within WebAssembly and Emscripten modules.

Downloads

85

Readme

WebAssembly Heap Manager

A TypeScript / JavaScript library that provides safe and convenient methods to read, write, allocate and wrap numeric values, strings, and typed arrays, within the WebAssembly heap, Emscripten heap, or any custom one, defined by:

  • A single ArrayBuffer or SharedArrayBuffer containing the entire heap
  • An allocator function, like alloc(size)
  • A deallocator function, like free(address)

To ensure the ArrayBuffer can be grown or replaced when needed, the manager is initialized with a user-defined callback function getLatestHeap() that should always return the most up-to-date instance of the heap. When the manager detects the current ArrayBuffer has become detached (byteLength of 0), the manager will call this function to retrieve the most up-to-date ArrayBuffer (other polling behaviors can also be configured via the the pollingMode constructor option).

Features

  • Provides operations to read and write various numeric and string types on arbitrary heap locations
  • Creates TypedArray views on arbitrary regions of the heap
  • Creates JavaScript objects that wrap around typed heap references, allowing to safely allocate, read, update and free them
  • Supports all JavaScript typed arrays, including BigInt64Array, BigUint64Array and Uint8ClampedArray
  • Supports thread-safe, atomic reads and writes for all primitive types. Polyfills atomic operations for floating point and clamped array elements
  • Efficiently reads, writes, allocates and wraps ASCII, UTF-8, UTF-16 and UTF-32 strings directly on the heap
  • Supports ArrayBuffer heaps larger than 4 GiB, up to 2^53 - 1 bytes, which is about 8192 TiB (large ArrayBuffer support is already available in Node.js 22+ and latest Firefox)
  • Supports SharedArrayBuffer. Can be used to efficiently share data between different JavaScript threads
  • Provides optional garbage collection by attaching allocated references to an internal FinalizationRegistry bound to the JavaScript runtime's garbage collector
  • Works on all major JavaScript runtimes, including browsers, Node.js, Deno and Bun
  • Optimized for speed and minimal overhead, while balancing for safety and convenience
  • No dependencies

Installation

npm install wasm-heap-manager

Initialization

Wrap the heap of an Emscripten module (either WebAssembly or JavaScript-based)

import { createWasmHeapManager } from 'wasm-heap-manager'

// ...

// Ensure `_malloc` and `_free` are included as exports
// by passing `-s EXPORTED_FUNCTIONS="['_malloc', '_free', ...]"` to `emcc`
// when the code is compiled.
const heapManager = createWasmHeapManager(
	() => emscriptenModule.HEAPU8.buffer
	emscriptenModule._malloc,
	emscriptenModule._free
)

Or equivalent utility method:

import { wrapEmscriptenModuleHeap } from 'wasm-heap-manager'

// ...

const heapManager = wrapEmscriptenModuleHeap(emscriptenModule)

Wrap the heap of a plain WebAssembly module's memory object

import { createWasmHeapManager } from 'wasm-heap-manager'

// ...

const instanceExports = wasmModule.instance.exports

// Assuming `memory`, `malloc` and `free` are valid exports from the module:
const heapManager = createWasmHeapManager(
	() => instanceExports.memory.buffer,
	instanceExports.malloc,
	instanceExports.free
)

Wrap a custom WebAssembly.Memory object, allocator function, and deallocator function

Here's a basic "bump" allocator, for illustration purposes only:

import { createWasmHeapManager } from 'wasm-heap-manager'

const memory = new WebAssembly.Memory({ initial: 1, maximum: 1000 })

// Current allocation address. Is incremented after each allocation.
let currentAddress = 8

function myAllocator(requestedSize: number) {
	// Compute start and end addresses of the newly allocated region
	const startAddress = currentAddress
	let endAddress = currentAddress + Math.ceil(requestedSize)

	// Align end address to 8 byte boundaries, if needed
	endAddress = alignToNextMultiple(endAddress, 8)

	// Grow memory if needed
	const currentCapacity = memory.buffer.byteLength

	if (endAddress > currentCapacity) {
		const additionalRequiredCapacity = endAddress - currentCapacity
		const additionalRequiredPages = Math.ceil(additionalRequiredCapacity / (2 ** 16)) + 1

		memory.grow(additionalRequiredPages)
	}

	// Update current allocation address
	currentAddress = endAddress

	return startAddress
}

function myDeallocator(address: number) {
	// Do nothing. It never frees memory.
}

const heapManager = createWasmHeapManager(
	() => memory.buffer,
	myAllocator,
	myDeallocator
)

Code for utility method alignToNextMultiple:

function alignToNextMultiple(value: number, alignmentConstant: number) {
	// Align end address to the alignment constant, if needed
	const alignmentRemainder = value % alignmentConstant

	if (alignmentRemainder > 0) {
		// Pad end address to next multiple of the alignment constant
		value += alignmentConstant - alignmentRemainder
	}

	return value
}

Reading and writing

Directly reading and writing numeric values on the heap

Reading numeric values:

const int8Value = heapManager.readInt8(address)
const uint8Value = heapManager.readUint8(address)
const clampedUint8Value = heapManager.readClampedUint8(address)
const int16Value = heapManager.readInt16(address)
const uint16Value = heapManager.readUint16(address)
const int32Value = heapManager.readInt32(address)
const uint32Value = heapManager.readUint32(address)
const bigInt64Value = heapManager.readBigInt64(address)
const bigUint64Value = heapManager.readBigUint64(address)
const float32Value = heapManager.readFloat32(address)
const float64Value = heapManager.readFloat64(address)

// Pointer types
const pointer32Value = heapManager.readPointer32(address)
const pointer53Value = heapManager.readPointer53(address)
const pointer64Value = heapManager.readPointer64(address)

// Large unsigned integer extensions (little endian):
const bigUint128Value = heapManager.readBigUint128LE(address)
const bigUint256Value = heapManager.readBigUint256LE(address)

Writing numeric values:

heapManager.writeInt8(address, int8Value)
heapManager.writeUint8(address, uint8Value)
heapManager.writeClampedUint8(address, clampedUint8Value)
heapManager.writeInt16(address, int16Value)
heapManager.writeUint16(address, uint16Value)
heapManager.writeInt32(address, int32Value)
heapManager.writeUint32(address, uint32Value)
heapManager.writeBigInt64(address, bigInt64Value)
heapManager.writeBigUint64(address, bigUint64Value)
heapManager.writeFloat32(address, float32Value)
heapManager.writeFloat64(address, float64Value)

// Pointer types
heapManager.writePointer32(address, pointer32Value)
heapManager.writePointer53(address, pointer53Value)
heapManager.writePointer64(address, pointer64Value)

// Large unsigned integer extensions (little endian):
heapManager.writeBigUint128LE(address, bigUint128Value)
heapManager.writeBigUint256LE(address, bigUint256Value)

Directly reading and writing strings on the heap

Read string:

const stringValue = heapManager.readNullTerminatedAsciiString(address)
const stringValue = heapManager.readNullTerminatedUtf8String(address)
const stringValue = heapManager.readNullTerminatedUtf16String(address)
const stringValue = heapManager.readNullTerminatedUtf32String(address)

Write string:

heapManager.writeNullTerminatedAsciiString(address, stringValue)
heapManager.writeNullTerminatedUtf8String(address, stringValue)
heapManager.writeNullTerminatedUtf16String(address, stringValue)
heapManager.writeNullTerminatedUtf32String(address, stringValue)

Creating TypedArray views of arbitrary heap regions

address is a byte address on the heap. elementCount is an element count for the view, for the particular type that is being viewed

heapManager.viewInt8Array(address, elementCount)
heapManager.viewUint8Array(address, elementCount)
heapManager.viewClampedUint8Array(address, elementCount)
heapManager.viewInt16Array(address, elementCount)
heapManager.viewUint16Array(address, elementCount)
heapManager.viewInt32Array(address, elementCount)
heapManager.viewUint32Array(address, elementCount)
heapManager.viewBigInt64Array(address, elementCount)
heapManager.viewBigUint64Array(address, elementCount)
heapManager.viewFloat32Array(address, elementCount)
heapManager.viewFloat64Array(address, elementCount)

// Pointer views
heapManager.viewPointer32Array(address, elementCount) // identical to viewUint32Array
heapManager.viewPointer64Array(address, elementCount) // identical to viewBigUint64Array

The view is a subarray of the heap's current ArrayBuffer. You can read and write to and from it directly.

Note: the returned subarray should not be used for a long duration as it can become invalid or out-of-date when the underlying ArrayBuffer is detached during a memory resize or similar event. Please ensure you only use the returned typed array for the very immediate term!

Allocating memory

Allocating numeric values on the heap

const int8Ref = heapManager.allocInt8()
const uint8Ref = heapManager.allocUint8()
const clampedUint8Ref = heapManager.allocClampedUint8()
const int16Ref = heapManager.allocInt16()
const uint16Ref = heapManager.allocUint16()
const int32Ref = heapManager.allocInt32()
const uint32Ref = heapManager.allocUint32()
const bigInt64Ref = heapManager.allocBigInt64()
const bigUint64Ref = heapManager.allocBigUint64()
const float32Ref = heapManager.allocFloat32()
const float64Ref = heapManager.allocFloat64()

// Pointer types
const pointer32Ref = heapManager.allocPointer32()
const pointer53Ref = heapManager.allocPointer53()
const pointer64Ref = heapManager.allocPointer64()

The returned reference object has these specialized properties and methods:

  • value: getter/setter for easy access to the value
  • read(): read value
  • readAtomic(): atomically read value
  • write(newValue) write a new value
  • writeAtomic(newValue): atomically write a new value

And these inherited properties and methods:

  • address: heap byte offset of the allocated region
  • allocatedByteSize: allocated size of the reference, in bytes
  • allocatedBytesView: Uint8Array view of the allocated memory region
  • clear(): set allocated memory region to all 0
  • free(): free the memory associated with this reference
  • isFreed: check if reference has been freed (only aware of free called through the reference!)

Allocating strings on the heap

const asciiStringRef = heapManager.allocNullTerminatedAsciiString(elementCount)
const utf8StringRef = heapManager.allocNullTerminatedUtf8String(elementCount)
const utf16StringRef = heapManager.allocNullTerminatedUtf16String(elementCount)
const utf32StringRef = heapManager.allocNullTerminatedUtf32String(elementCount)

elementCount is the total number of encoded elements that would be allocated.

  • For ASCII and UTF-8, each element is a uint8 (1 byte)
  • For UTF-16, each element is a uint16 (2 bytes)
  • For UTF-32, each element is a uint32 (4 bytes)

The returned reference object has these specialized properties and methods:

  • value: getter/setter for easy access to the stored string
  • read(): read value
  • write(newValue): write new value
  • encodedElementsView: a Uint8Array, Uint16Array or Uint32Array subarray of the string's encoded elements, excluding the terminating character
  • encodedElementCount: element count of the stored string, excluding terminating character. Computing this value requires scanning the memory to find the offset of the first 0 element in the allocated region
  • encodedBytesView: a Uint8Array subarray of the string's encoded bytes, excluding terminating character
  • encodedByteLength: byte length of the stored string, excluding terminating character.

And these inherited properties and methods:

  • address: heap byte offset of the allocated region
  • allocatedByteSize: allocated size of the reference, in bytes
  • allocatedBytesView: Uint8Array view of the allocated memory region
  • clear(): set allocated memory region to all 0
  • free(): free the memory associated with this reference
  • isFreed: check if reference has been freed (only applies to calls made through the reference!)
  • bytesPerElement: number of bytes for each encoded elements. 1 for ASCII and UTF-8, 2 for UTF-16 and 4 for UTF-32

Allocating typed arrays on the heap

const int8ArrayRef = heapManager.allocInt8Array(elementCount)
const uint8ArrayRef = heapManager.allocUint8Array(elementCount)
const uint8ClampedArrayRef = heapManager.allocClampedUint8Array(elementCount)
const int16ArrayRef = heapManager.allocInt16Array(elementCount)
const uint16ArrayRef = heapManager.allocUint16Array(elementCount)
const int32ArrayRef = heapManager.allocInt32Array(elementCount)
const uint32ArrayRef = heapManager.allocUint32Array(elementCount)
const bigInt64ArrayRef = heapManager.allocBigInt64Array(elementCount)
const bigUint64ArrayRef = heapManager.allocBigUint64Array(elementCount)
const float32ArrayRef = heapManager.allocFloat32Array(elementCount)
const float64ArrayRef = heapManager.allocFloat64Array(elementCount)

// Pointer types
const pointer32ArrayRef = heapManager.allocPointer32Array(elementCount)
const pointer53ArrayRef = heapManager.allocPointer53Array(elementCount)
const pointer64ArrayRef = heapManager.allocPointer64Array(elementCount)

The returned reference object has these specialized properties and methods:

  • view: subarray for easy access to the reference's memory region
  • readAt(index): read element at index
  • readAtomicAt(index): atomically read element at index
  • writeAt(index, newValue): write element at index
  • writeAtomicAt(index, newValue): atomically write element at index
  • elementCount: element count
  • bytesPerElement: bytes per element

And these inherited properties and methods:

  • address: heap byte offset of the allocated region
  • allocatedByteSize: allocated size of the reference, in bytes
  • allocatedBytesView: Uint8Array view of the allocated memory region
  • clear(): set allocated memory region to all 0
  • free(): free the memory associated with this reference
  • isFreed: check if reference has been freed (only applies to calls made through the reference!)

Wrapping existing data

Wrapping existing numeric values on the heap

For example, pointers returned from WebAssembly methods can be wrapped and used in a safe way.

const int8Ref = heapManager.wrapInt8(address)
const uint8Ref = heapManager.wrapUint8(address)
const clampedUint8Ref = heapManager.wrapClampedUint8(address)
const int16Ref = heapManager.wrapInt16(address)
const uint16Ref = heapManager.wrapUint16(address)
const int32Ref = heapManager.wrapInt32(address)
const uint32Ref = heapManager.wrapUint32(address)
const bigInt64Ref = heapManager.wrapBigInt64(address)
const bigUint64Ref = heapManager.wrapBigUint64(address)
const float32Ref = heapManager.wrapFloat32(address)
const float64Ref = heapManager.wrapFloat64(address)

// Pointer types
const pointer32Ref = heapManager.wrapPointer32(address)
const pointer53Ref = heapManager.wrapPointer53(address)
const pointer64Ref = heapManager.wrapPointer64(address)

Returns the same reference object as the value allocation methods.

Wrapping existing strings on the heap

const stringRef = heapManager.wrapNullTerminatedAsciiString(address)
const stringRef = heapManager.wrapNullTerminatedUtf8String(address)
const stringRef = heapManager.wrapNullTerminatedUtf16String(address)
const stringRef = heapManager.wrapNullTerminatedUtf32String(address)

Returns the same reference object as the string allocation methods.

Wrapping existing typed arrays on the heap

const int8ArrayRef = heapManager.wrapInt8Array(address, elementCount)
const uint8ArrayRef = heapManager.wrapUint8Array(address, elementCount)
const uint8ClampedArrayRef = heapManager.wrapClampedUint8Array(address, elementCount)
const int16ArrayRef = heapManager.wrapInt16Array(address, elementCount)
const uint16ArrayRef = heapManager.wrapUint16Array(address, elementCount)
const int32ArrayRef = heapManager.wrapInt32Array(address, elementCount)
const uint32ArrayRef = heapManager.wrapUint32Array(address, elementCount)
const bigInt64ArrayRef = heapManager.wrapBigInt64Array(address, elementCount)
const bigUint64ArrayRef = heapManager.wrapBigUint64Array(address, elementCount)
const float32ArrayRef = heapManager.wrapFloat32Array(address, elementCount)
const float64ArrayRef = heapManager.wrapFloat64Array(address, elementCount)

// Pointer types
const pointer32ArrayRef = heapManager.wrapPointer32Array(address, elementCount)
const pointer53ArrayRef = heapManager.wrapPointer53Array(address, elementCount)
const pointer64ArrayRef = heapManager.wrapPointer64Array(address, elementCount)

Returns the same reference object as the typed array allocation methods.

Pointer types

  • pointer32 operations (read, write, allocate, wrap), Pointer32Ref and Pointer32ArrayRef are identical to uint32 operations. The distinct naming is meant for increased code safety to ensure pointers are uniquely typed in the user code
  • pointer64 operations (read, write, allocate, wrap), Pointer64Ref and Pointer64ArrayRef are identical to BigUint64 operations. The distinct naming is meant for increased code safety to ensure pointers are uniquely typed in the user code
  • pointer53 operations internally use BigUint64 for storage on the heap, but are implicitly cast to and from numbers. Since JavaScript numbers are limited to a maximum safe integer values of 2^53 - 1, it means the number based pointers can reference up to 8192 TiB, which is sufficient for almost all use cases today

Recommendation: unless you're expecting an extremely large memory capacity, for memory ranges over 2^32 (4 GiB), use pointer53 instead of pointer64. It saves the hassle of converting to and from BigInts and in that way simplifies your code, and could in practice help ensure that larger address spaces are correctly managed, without the extra boilerplate code.

Constructor options

  • clearAllocatedRegions: always clear a region after it is allocated. Defaults to true
  • pollingMode: how often the manager would poll the callback to get the latest ArrayBuffer. Can be set to always (will invoke the callback every time the heap is accessed), whenEmpty (will call the callback when the ArrayBuffer has a byteLength of 0), or never (will never calls the callback - assumes the ArrayBuffer is static and never replaced). Defaults to whenEmpty, which works with the standard behavior of Emscripten heaps and WASM memory objects. If you are using a custom ArrayBuffer object as heap, you may need to set to always (in case it's being replaced) or never (in case it is static)
  • enableGarbageCollection: when true, allocated references would be garbage collected when their reference is garbage collected. This applies only to references created using the allocate..() methods, not wrapped references. In practice, the JavaScript garbage collector may be invoked very infrequently on some runtimes (like Node.js), which may cause a lot of memory to be held. Defaults to false

Future

  • Support for arrays with a custom element size
  • Support for structure data types
  • Support for arrays of structures

License

MIT