“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; }
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; }
🌟 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
🎨 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();
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();
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();
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();
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();
📖 Quick Start Guide
Step 1: Import the Library
import { StringPointer, ArrayPointer, NumberPointer, CharPointer, BoolPointer, TypeConverter, } from "wasp-lib"; import type { WASMModule } from "wasp-lib";
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; }
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... }
🔧 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; }
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();
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; }
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());
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; }
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();
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; }
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();
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; }
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());
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 }
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); }
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;
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 };
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; }
🛡️ 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); }
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); }
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); }
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); }
🚀 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(); }
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
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();
📚 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; }
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(); } }
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(); } }
🤝 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
📄 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)