DEV Community

Cover image for Introducing wasp-lib: A TypeScript Library for Simplified WebAssembly Memory Management
Pt. Prashant tripathi
Pt. Prashant tripathi

Posted on • Originally published at github.com

Introducing wasp-lib: A TypeScript Library for Simplified WebAssembly Memory Management

“Hexagon speed, wasp-lib precision, powered by WASM.”

A zero-dependency TypeScript library for seamless, type-safe interaction with Emscripten-generated WebAssembly memory.

🎯 What is wasp-lib?

🐝 W.A.S.P. stands for Web Assembly Safe Pointers

wasp-lib is a powerful TypeScript library that bridges the gap between
JavaScript and WebAssembly memory management. It transforms complex, error-prone
manual memory operations into simple, type-safe method calls, making WebAssembly
integration as easy as working with native JavaScript objects.

The Problem

Working with WebAssembly memory directly is challenging:

  • Memory Leaks: Forgetting to call _free() leads to memory leaks
  • Type Safety: No compile-time guarantees about data types
  • Boilerplate Code: Repetitive allocation/deallocation patterns
  • Error Prone: Manual pointer arithmetic and buffer management

The Solution

wasp-lib provides intuitive wrapper classes that:

  • Automatically manage memory allocation and deallocation
  • Ensure type safety with TypeScript generics
  • Eliminate boilerplate with simple, chainable APIs
  • Prevent memory leaks with built-in cleanup mechanisms

Before vs After

Before wasp-lib 😰

// Manual memory management - error-prone and verbose! function processData(wasm: any, numbers: number[]) { // Allocate memory manually const arraySize = numbers.length * 4; // 4 bytes per i32 const arrayPtr = wasm._malloc(arraySize); // Copy data byte by byte for (let i = 0; i < numbers.length; i++) { wasm.setValue(arrayPtr + i * 4, numbers[i], "i32"); } // Call WASM function const sum = wasm._sum_array(arrayPtr, numbers.length); // Read result and manually free memory wasm._free(arrayPtr); // Easy to forget! return sum; } 
Enter fullscreen mode Exit fullscreen mode

After wasp-lib 🎉

// Clean, type-safe, automatic cleanup! function processData(wasm: WASMModule, numbers: number[]) { const arrayPtr = ArrayPointer.from(wasm, "i32", numbers.length, numbers); const sum = wasm._sum_array(arrayPtr.ptr, numbers.length); arrayPtr.free(); // Or use readAndFree() for automatic cleanup return sum; } 
Enter fullscreen mode Exit fullscreen mode

🌟 Key Features

  • 🔒 Type-Safe Memory Operations: Full TypeScript support with generic types
  • 🧹 Automatic Memory Management: Built-in allocation, deallocation, and cleanup
  • 🎯 Intuitive Pointer Abstractions: High-level classes for all data types
  • 📦 Zero Dependencies: Lightweight with no external dependencies
  • Emscripten-Optimized: Designed specifically for Emscripten-generated modules
  • 🧪 Battle-Tested: Comprehensive test suite with 100% coverage
  • 📚 Rich Documentation: Auto-generated API docs with examples
  • 🛡️ Memory Safety: Built-in bounds checking and validation

🚀 Installation

# npm npm install wasp-lib # yarn yarn add wasp-lib # pnpm pnpm add wasp-lib # bun bun add wasp-lib 
Enter fullscreen mode Exit fullscreen mode

🎨 Use Cases

1. Image Processing

// Process image pixel data in WebAssembly const pixels = new Uint8Array(width * height * 4); const pixelPtr = ArrayPointer.from(wasm, "i8", pixels.length, [...pixels]); wasm._apply_filter(pixelPtr.ptr, width, height); const processedPixels = pixelPtr.readAndFree(); 
Enter fullscreen mode Exit fullscreen mode

2. Mathematical Computations

// High-performance matrix operations const matrix = [ [1, 2], [3, 4], [5, 6], ]; const flatMatrix = matrix.flat(); const matrixPtr = ArrayPointer.from( wasm, "double", flatMatrix.length, flatMatrix ); const determinant = wasm._calculate_determinant(matrixPtr.ptr, 3, 2); matrixPtr.free(); 
Enter fullscreen mode Exit fullscreen mode

3. String Processing

// Natural language processing const text = "Hello, WebAssembly world!"; const textPtr = StringPointer.from(wasm, text.length + 100, text); wasm._analyze_sentiment(textPtr.ptr); const analysis = textPtr.readAndFree(); 
Enter fullscreen mode Exit fullscreen mode

4. Game Development

// Game entity positions const positions = [ { x: 10.5, y: 20.3, z: 0.0 }, { x: 15.2, y: 18.7, z: 5.5 }, ]; const flatPositions = positions.flatMap(p => [p.x, p.y, p.z]); const posPtr = ArrayPointer.from( wasm, "float", flatPositions.length, flatPositions ); wasm._update_physics(posPtr.ptr, positions.length); const updatedPositions = posPtr.readAndFree(); 
Enter fullscreen mode Exit fullscreen mode

5. Scientific Computing

// Signal processing const signal = new Array(1024).fill(0).map((_, i) => Math.sin(i * 0.1)); const signalPtr = ArrayPointer.from(wasm, "double", signal.length, signal); wasm._fft_transform(signalPtr.ptr, signal.length); const spectrum = signalPtr.readAndFree(); 
Enter fullscreen mode Exit fullscreen mode

📖 Quick Start Guide

Step 1: Import the Library

import { StringPointer, ArrayPointer, NumberPointer, CharPointer, BoolPointer, TypeConverter, } from "wasp-lib"; import type { WASMModule } from "wasp-lib"; 
Enter fullscreen mode Exit fullscreen mode

Step 2: Initialize Your WASM Module

// Assuming you have a WASM module generated by Emscripten import Module from "./your-wasm-module.js"; async function initWasm() { const wasm: WASMModule = await Module(); return wasm; } 
Enter fullscreen mode Exit fullscreen mode

Step 3: Use Pointer Classes

async function example() { const wasm = await initWasm(); // String operations const greeting = StringPointer.from(wasm, 50, "Hello"); wasm._process_string(greeting.ptr); console.log(greeting.readAndFree()); // "Hello World!" (modified by WASM) // Array operations const numbers = [1, 2, 3, 4, 5]; const arrayPtr = ArrayPointer.from(wasm, "i32", numbers.length, numbers); const sum = wasm._sum_array(arrayPtr.ptr, numbers.length); arrayPtr.free(); console.log(sum); // 15 // Number operations const valuePtr = NumberPointer.from(wasm, "double", 3.14159); wasm._square_value(valuePtr.ptr); console.log(valuePtr.readAndFree()); // 9.869... } 
Enter fullscreen mode Exit fullscreen mode

🔧 Complete API Reference

Core Classes

StringPointer

Manages C-style null-terminated strings in WASM memory.

class StringPointer extends BasePointer<string> { // Static factory methods static from( wasm: WASMModule, length: number, input?: string ): StringPointer; static alloc(wasm: WASMModule, length: number): StringPointer; // Instance methods write(input: string): void; read(): string; readAndFree(): string; free(): void; // Properties readonly ptr: number; readonly length: number; readonly isValid: boolean; } 
Enter fullscreen mode Exit fullscreen mode

Methods:

  • from(wasm, length, input?) - Create from JavaScript string with specified buffer size
  • alloc(wasm, length) - Allocate empty buffer of specified length
  • write(input) - Write new string content (must fit in allocated buffer)
  • read() - Read string as JavaScript string
  • readAndFree() - Read then immediately free memory

Example:

// Create with initial content const strPtr = StringPointer.from(wasm, 100, "Initial text"); // Modify content strPtr.write("New content"); // Read current content const content = strPtr.read(); // "New content" // Clean up strPtr.free(); // One-shot operation const result = StringPointer.from(wasm, 50, "temp").readAndFree(); 
Enter fullscreen mode Exit fullscreen mode

NumberPointer<T>

Type-safe wrapper for single numeric values.

class NumberPointer<T extends C_NumberType> extends BasePointer< number | bigint > { // Static factory methods static from<T>( wasm: WASMModule, type: T, input: TypedValue<T> ): NumberPointer<T>; static alloc<T>(wasm: WASMModule, type: T): NumberPointer<T>; // Instance methods write(value: TypedValue<T>): void; read(): TypedValue<T>; readAndFree(): TypedValue<T>; // Properties readonly type: T; } 
Enter fullscreen mode Exit fullscreen mode

Supported Types:

  • 'i8' - 8-bit signed integer (-128 to 127)
  • 'i16' - 16-bit signed integer (-32,768 to 32,767)
  • 'i32' - 32-bit signed integer (-2,147,483,648 to 2,147,483,647)
  • 'i64' - 64-bit signed integer (uses BigInt)
  • 'float' - 32-bit floating point
  • 'double' - 64-bit floating point

Example:

// Integer types const intPtr = NumberPointer.from(wasm, "i32", 42); const bigIntPtr = NumberPointer.from(wasm, "i64", 9007199254740991n); // Floating point types const floatPtr = NumberPointer.from(wasm, "float", 3.14); const doublePtr = NumberPointer.from(wasm, "double", 2.718281828); // Modify values intPtr.write(84); floatPtr.write(2.71); // Read values (correct TypeScript types) const intValue: number = intPtr.read(); // 84 const bigIntValue: bigint = bigIntPtr.read(); // 9007199254740991n const floatValue: number = floatPtr.read(); // 2.71 // Clean up [intPtr, bigIntPtr, floatPtr, doublePtr].forEach(ptr => ptr.free()); 
Enter fullscreen mode Exit fullscreen mode

ArrayPointer<T, N>

Type-safe wrapper for numeric arrays with fixed-length support.

class ArrayPointer< T extends C_NumberType, N extends number = number, > extends BasePointer<FixedLengthArray<TypedValue<T>, N>> { // Static factory methods static from<T, N>( wasm: WASMModule, type: T, length: N, input?: TypedValue<T>[] ): ArrayPointer<T, N>; static alloc<T, N>( wasm: WASMModule, type: T, length: N ): ArrayPointer<T, N>; // Instance methods write(values: TypedValue<T>[]): void; add(index: number, value: TypedValue<T>): void; read(): FixedLengthArray<TypedValue<T>, N>; readAndFree(): FixedLengthArray<TypedValue<T>, N>; // Properties readonly type: T; readonly length: N; } 
Enter fullscreen mode Exit fullscreen mode

Example:

// Create from existing array const numbers = [1.1, 2.2, 3.3, 4.4, 5.5]; const arrayPtr = ArrayPointer.from(wasm, "double", numbers.length, numbers); // Allocate empty array const emptyPtr = ArrayPointer.alloc(wasm, "i32", 10); // Modify individual elements arrayPtr.add(0, 10.5); // Set first element to 10.5 arrayPtr.add(4, 99.9); // Set last element to 99.9 // Write entire array emptyPtr.write([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); // Read array (returns fixed-length array type) const result = arrayPtr.read(); // FixedLengthArray<number, 5> console.log(result); // [10.5, 2.2, 3.3, 4.4, 99.9] // Bounds checking try { arrayPtr.add(10, 42); // Throws: out of bounds } catch (error) { console.error(error.message); // "Out-of-bounds access: tried to write at index 10 in array of length 5" } arrayPtr.free(); emptyPtr.free(); 
Enter fullscreen mode Exit fullscreen mode

CharPointer

Wrapper for single character values.

class CharPointer extends BasePointer<string> { // Static factory methods static from(wasm: WASMModule, input: string): CharPointer; static alloc(wasm: WASMModule): CharPointer; // Instance methods write(input: string): void; read(): string; readAndFree(): string; } 
Enter fullscreen mode Exit fullscreen mode

Example:

// Create from character const charPtr = CharPointer.from(wasm, "A"); // Modify character charPtr.write("Z"); // Read character const char = charPtr.read(); // 'Z' // Validation try { CharPointer.from(wasm, "AB"); // Throws: must be exactly one character } catch (error) { console.error(error.message); } charPtr.free(); 
Enter fullscreen mode Exit fullscreen mode

BoolPointer

Wrapper for boolean values (stored as C integers: 0 or 1).

class BoolPointer extends BasePointer<boolean> { // Static factory methods static from(wasm: WASMModule, value: boolean): BoolPointer; static alloc(wasm: WASMModule): BoolPointer; // Instance methods write(value: boolean): void; read(): boolean; readAndFree(): boolean; } 
Enter fullscreen mode Exit fullscreen mode

Example:

// Create from boolean const boolPtr = BoolPointer.from(wasm, true); // Toggle value boolPtr.write(false); // Read value const isTrue = boolPtr.read(); // false // Allocate with default false const defaultPtr = BoolPointer.alloc(wasm); console.log(defaultPtr.read()); // false [boolPtr, defaultPtr].forEach(ptr => ptr.free()); 
Enter fullscreen mode Exit fullscreen mode

Utility Classes

TypeConverter

Static utility class for type conversions between JavaScript and C types.

class TypeConverter { // Boolean conversions static boolToC(value: boolean): C_BoolType; // JS boolean → C boolean (0|1) static boolFromC(value: C_BoolType): boolean; // C boolean → JS boolean // Character conversions static charToC(char: string): C_CharType; // Single char → ASCII code static charFromC(code: C_CharType): string; // ASCII code → Single char // Type validation static validateNumberType(type: string): boolean; // Validate supported type static getTypeSize(type: C_NumberType): number; // Get type size in bytes } 
Enter fullscreen mode Exit fullscreen mode

Example:

// Boolean conversions const cTrue = TypeConverter.boolToC(true); // 1 const cFalse = TypeConverter.boolToC(false); // 0 const jsTrue = TypeConverter.boolFromC(1); // true const jsFalse = TypeConverter.boolFromC(0); // false // Character conversions const asciiA = TypeConverter.charToC("A"); // 65 const charFromAscii = TypeConverter.charFromC(65); // 'A' // Type validation const isValid = TypeConverter.validateNumberType("i32"); // true const size = TypeConverter.getTypeSize("double"); // 8 // Error handling try { TypeConverter.charToC("AB"); // Throws: must be exactly one character TypeConverter.charFromC(300); // Throws: invalid ASCII code TypeConverter.getTypeSize("invalid"); // Throws: unsupported type } catch (error) { console.error(error.message); } 
Enter fullscreen mode Exit fullscreen mode

Type Definitions

Core Types

// Supported C numeric types type C_NumberType = "i8" | "i16" | "i32" | "i64" | "float" | "double"; // C boolean type (0 or 1) type C_BoolType = 0 | 1; // C character type (ASCII code 0-255) type C_CharType = number; // Type constants const C_TRUE: C_BoolType = 1; const C_FALSE: C_BoolType = 0; 
Enter fullscreen mode Exit fullscreen mode

Size Constants

// Size in bytes for each numeric type const C_TYPE_SIZES: Record<C_NumberType, number> = { i8: 1, // 8-bit integer i16: 2, // 16-bit integer i32: 4, // 32-bit integer i64: 8, // 64-bit integer float: 4, // 32-bit float double: 8, // 64-bit double }; 
Enter fullscreen mode Exit fullscreen mode

WASM Module Interface

interface WASMModule extends EmscriptenModule { // Memory management _malloc(size: number): number; _free(ptr: number): void; // Value operations setValue(ptr: number, value: number | bigint, type: string): void; getValue(ptr: number, type: string): number | bigint; // String operations stringToUTF8(str: string, outPtr: number, maxBytesToWrite: number): void; UTF8ToString(ptr: number): string; lengthBytesUTF8(str: string): number; // Function wrapping cwrap(ident: string, returnType: string, argTypes: string[]): Function; // File system (if enabled) FS: typeof FS; } 
Enter fullscreen mode Exit fullscreen mode

🛡️ Error Handling

wasp-lib provides comprehensive error handling with descriptive messages:

Memory Safety

// Automatic validation const ptr = StringPointer.from(wasm, 10, "test"); ptr.free(); try { ptr.read(); // Throws: "Cannot operate on freed or invalid pointer" } catch (error) { console.error(error.message); } 
Enter fullscreen mode Exit fullscreen mode

Bounds Checking

// Array bounds validation const arrayPtr = ArrayPointer.alloc(wasm, "i32", 5); try { arrayPtr.add(10, 42); // Throws: "Out-of-bounds access: tried to write at index 10 in array of length 5" } catch (error) { console.error(error.message); } 
Enter fullscreen mode Exit fullscreen mode

Type Validation

// Input validation try { CharPointer.from(wasm, "AB"); // Throws: "Input must be exactly one character" TypeConverter.charFromC(300); // Throws: "Invalid ASCII code: 300. Must be 0-255" // @ts-expect-error TypeConverter.getTypeSize("invalid"); // Throws: "Unsupported number type: invalid" } catch (error) { console.error(error.message); } 
Enter fullscreen mode Exit fullscreen mode

Buffer Overflow Protection

// String length validation const strPtr = StringPointer.alloc(wasm, 10); try { strPtr.write("This string is way too long for the buffer"); // Throws: "String length exceeds buffer size" } catch (error) { console.error(error.message); } 
Enter fullscreen mode Exit fullscreen mode

🚀 Performance Tips

1. Reuse Pointers When Possible

// Good: Reuse pointer for multiple operations const arrayPtr = ArrayPointer.alloc(wasm, "double", 1000); for (let i = 0; i < iterations; i++) { // Modify array data arrayPtr.write(newData); wasm._process_array(arrayPtr.ptr, 1000); } arrayPtr.free(); // Avoid: Creating new pointers in loops for (let i = 0; i < iterations; i++) { const arrayPtr = ArrayPointer.from(wasm, "double", 1000, newData); // Expensive wasm._process_array(arrayPtr.ptr, 1000); arrayPtr.free(); } 
Enter fullscreen mode Exit fullscreen mode

2. Use Appropriate Buffer Sizes

// Good: Right-sized buffer const strPtr = StringPointer.from(wasm, input.length + 10, input); // Small overhead // Avoid: Oversized buffers const strPtr = StringPointer.from(wasm, 10000, input); // Wastes memory 
Enter fullscreen mode Exit fullscreen mode

3. Batch Operations

// Good: Process arrays in batches const batchSize = 1000; const arrayPtr = ArrayPointer.alloc(wasm, "float", batchSize); for (let i = 0; i < data.length; i += batchSize) { const batch = data.slice(i, i + batchSize); arrayPtr.write(batch); wasm._process_batch(arrayPtr.ptr, batch.length); } arrayPtr.free(); 
Enter fullscreen mode Exit fullscreen mode

📚 Advanced Examples

Image Processing Pipeline

async function processImage(imageData: ImageData, wasm: WASMModule) { // Convert ImageData to array const pixels = Array.from(imageData.data); // Create WASM array pointer const pixelPtr = ArrayPointer.from(wasm, "i8", pixels.length, pixels); // Apply multiple filters wasm._blur_filter(pixelPtr.ptr, imageData.width, imageData.height, 3); wasm._sharpen_filter(pixelPtr.ptr, imageData.width, imageData.height); wasm._color_correction( pixelPtr.ptr, imageData.width, imageData.height, 1.2 ); // Get processed data const processedPixels = pixelPtr.readAndFree(); // Convert back to ImageData const processedImageData = new ImageData( new Uint8ClampedArray(processedPixels), imageData.width, imageData.height ); return processedImageData; } 
Enter fullscreen mode Exit fullscreen mode

Scientific Computing

class Matrix { private data: ArrayPointer<"double", number>; constructor( private wasm: WASMModule, private rows: number, private cols: number, initialData?: number[] ) { this.data = ArrayPointer.from( wasm, "double", rows * cols, initialData || new Array(rows * cols).fill(0) ); } multiply(other: Matrix): Matrix { if (this.cols !== other.rows) { throw new Error( "Matrix dimensions incompatible for multiplication" ); } const result = new Matrix(this.wasm, this.rows, other.cols); this.wasm._matrix_multiply( this.data.ptr, other.data.ptr, result.data.ptr, this.rows, this.cols, other.cols ); return result; } transpose(): Matrix { const result = new Matrix(this.wasm, this.cols, this.rows); this.wasm._matrix_transpose( this.data.ptr, result.data.ptr, this.rows, this.cols ); return result; } toArray(): number[] { return [...this.data.read()]; } free(): void { this.data.free(); } } 
Enter fullscreen mode Exit fullscreen mode

Audio Processing

class AudioProcessor { private wasm: WASMModule; private bufferSize: number; private leftChannel: ArrayPointer<"float", number>; private rightChannel: ArrayPointer<"float", number>; constructor(wasm: WASMModule, bufferSize: number) { this.wasm = wasm; this.bufferSize = bufferSize; this.leftChannel = ArrayPointer.alloc(wasm, "float", bufferSize); this.rightChannel = ArrayPointer.alloc(wasm, "float", bufferSize); } processAudio( leftSamples: Float32Array, rightSamples: Float32Array ): { left: Float32Array; right: Float32Array } { // Copy samples to WASM memory this.leftChannel.write([...leftSamples]); this.rightChannel.write([...rightSamples]); // Apply effects this.wasm._apply_reverb( this.leftChannel.ptr, this.rightChannel.ptr, this.bufferSize ); this.wasm._apply_eq( this.leftChannel.ptr, this.rightChannel.ptr, this.bufferSize ); this.wasm._apply_compressor( this.leftChannel.ptr, this.rightChannel.ptr, this.bufferSize ); // Get processed samples const processedLeft = new Float32Array(this.leftChannel.read()); const processedRight = new Float32Array(this.rightChannel.read()); return { left: processedLeft, right: processedRight }; } free(): void { this.leftChannel.free(); this.rightChannel.free(); } } 
Enter fullscreen mode Exit fullscreen mode

🤝 Contributing

We welcome contributions! Please see our Contributing Guide
for details.

Development Setup

# Clone repository git clone https://github.com/ptprashanttripathi/wasp-lib.git cd wasp-lib # Install dependencies npm install # Build TypeScript npm run build # Build WASM test module npm run build:wasm # Run tests npm test # Generate documentation npm run build:docs 
Enter fullscreen mode Exit fullscreen mode

📄 License

This project is MIT licensed.

🙏 Acknowledgments

  • Emscripten Team - For making WebAssembly accessible
  • TypeScript Team - For excellent type system support
  • WebAssembly Community - For pushing the boundaries of web performance

Made with ❤️ by Pt. Prashant Tripathi

⭐ Star this repo if you find it helpful!

Top comments (0)