Equana

Back to Tutorials

Custom WASM Modules

Extend Equana with high-performance WebAssembly functions

Run AllReset

WebAssembly (WASM) modules let you extend Equana with custom high-performance functions written in languages like C, C++, or Rust. Upload a compiled WASM binary along with a JSON manifest describing your functions, and call them directly from Equana.

Use cases: Implement performance-critical algorithms, wrap existing C/C++ libraries, or add specialized functions not available in the core library.

How It Works

  1. Write your function in C, C++, or Rust
  2. Compile to WebAssembly (.wasm file)
  3. Create a manifest file (.wasm.json) describing your functions
  4. Upload both files to your workspace
  5. Call your functions from Equana
Code [1]Run
# Workspace file structure:
#
# my_workspace/
#   scripts/
#     notebook.json
#   functions/
#     myfunction.m
#   mymodule.wasm           # Compiled WASM binary
#   mymodule.wasm.json      # Manifest file
#
# The .wasm and .wasm.json files must have matching names.
# Place them in your workspace root or any subdirectory.

Manifest Format

The manifest is a JSON file that describes your WASM module and its exported functions. It must have the same base name as your .wasm file with a .wasm.json extension.

Module Fields

FieldTypeDescriptionRequired
versionnumberSchema version (always 1)Yes
namestringDisplay name for the moduleYes
descriptionstringModule descriptionNo
wasmFilestringWASM filename (defaults to {manifest}.wasm)No
memoryobjectMemory configuration { initial, maximum }No
functionsarrayArray of function definitionsYes

Function Fields

FieldTypeDescription
namestringEquana function name (what users call)
wasmExportstringActual export name in WASM binary
signaturestringDisplay signature (e.g., "y = func(x)")
paramsarrayParameter definitions with name and type
returnsobjectReturn type and optional shape
Code [2]Run
# mymodule.wasm.json - Manifest file structure
#
# {
#   "version": 1,
#   "name": "my_module",
#   "description": "My custom math operations",
#   "functions": [
#     {
#       "name": "scale_array",
#       "wasmExport": "scale_f64",
#       "signature": "y = scale_array(x, factor)",
#       "params": [
#         { "name": "x", "type": "f64[]" },
#         { "name": "factor", "type": "f64" }
#       ],
#       "returns": { "type": "f64[]" }
#     }
#   ]
# }

Type Marshalling

Equana automatically converts between Equana values and WASM types. Here's how types are mapped:

Equana TypeWASM TypeNotes
number (scalar)f6464-bit float
number (scalar)i3232-bit integer (truncated)
NDArrayf64[]Float array (pointer + length)
NDArrayi32[]Integer array (pointer + length)
booleani32true=1, false=0
(no return)voidFunction returns null

Array handling: Arrays are flattened to 1D and passed as a pointer plus length. Use the shape field in returns to specify how the output should be reshaped (e.g., ["input.rows", "input.cols"]).

Scalar Functions

For functions that take and return scalar values, use f64 or i32 types:

Code [3]Run
# Manifest for a scalar function (add two numbers)
#
# {
#   "version": 1,
#   "name": "math_utils",
#   "functions": [
#     {
#       "name": "fast_add",
#       "wasmExport": "add_i32",
#       "signature": "c = fast_add(a, b)",
#       "params": [
#         { "name": "a", "type": "i32" },
#         { "name": "b", "type": "i32" }
#       ],
#       "returns": { "type": "i32" }
#     }
#   ]
# }

Array Functions

For array operations, use f64[] or i32[] types. Specify the output shape to preserve dimensions:

Code [4]Run
# Manifest for array operations that preserve shape
#
# {
#   "version": 1,
#   "name": "array_ops",
#   "functions": [
#     {
#       "name": "array_square",
#       "wasmExport": "square_elements",
#       "signature": "y = array_square(x)",
#       "params": [
#         { "name": "x", "type": "f64[]" }
#       ],
#       "returns": {
#         "type": "f64[]",
#         "shape": ["input.rows", "input.cols"]
#       }
#     }
#   ]
# }
#
# The shape field preserves the input dimensions:
# - "input.rows" uses the first input's row count
# - "input.cols" uses the first input's column count

Shape References

The shape field supports these references:

  • "input.rows" — Row count of first array input
  • "input.cols" — Column count of first array input
  • number — Fixed dimension (e.g., [1, 10])

Writing WASM Code

You can write WASM modules in any language that compiles to WebAssembly. Here are examples in C and Rust:

C Example

Code [5]Run
# Example C source (mymodule.c):
#
# #include <stdint.h>
#
# __attribute__((export_name("add_arrays")))
# void add_arrays(
#     double* a, int32_t len_a,
#     double* b, int32_t len_b,
#     double* out
# ) {
#     for (int i = 0; i < len_a; i++) {
#         out[i] = a[i] + b[i];
#     }
# }
#
# Compile with: clang --target=wasm32 -O2 -nostdlib \
#   -Wl,--no-entry -Wl,--export-all -o mymodule.wasm mymodule.c

Rust Example

Code [6]Run
# Example Rust source (lib.rs):
#
# #[no_mangle]
# pub extern "C" fn scale_f64(
#     ptr: *const f64,
#     len: usize,
#     factor: f64,
#     out_ptr: *mut f64,
# ) {
#     unsafe {
#         for i in 0..len {
#             *out_ptr.add(i) = *ptr.add(i) * factor;
#         }
#     }
# }
#
# Compile with: cargo build --target wasm32-unknown-unknown --release

Memory Configuration

For modules that process large arrays, you can configure memory limits:

Code [7]Run
# Memory configuration in manifest:
#
# {
#   "version": 1,
#   "name": "large_arrays",
#   "memory": {
#     "initial": 256,    # Initial pages (256 * 64KB = 16MB)
#     "maximum": 1024    # Max pages (1024 * 64KB = 64MB)
#   },
#   "functions": [...]
# }
#
# Default: 256 initial pages (16MB), 1024 max (64MB)
# Each page is 64KB. Increase for large array operations.

Complete Example

Here's a complete workflow for creating a custom dot product function:

Code [8]Run
# Complete Example: Custom dot product
#
# 1. Write C code (dot_product.c):
#    double dot_product(double* a, int len_a, double* b, int len_b) {
#        double sum = 0.0;
#        for (int i = 0; i < len_a; i++) sum += a[i] * b[i];
#        return sum;
#    }
#
# 2. Create manifest (dot_product.wasm.json):
#    {
#      "version": 1,
#      "name": "dot_product",
#      "functions": [{
#        "name": "fast_dot",
#        "wasmExport": "dot_product",
#        "signature": "d = fast_dot(a, b)",
#        "params": [
#          { "name": "a", "type": "f64[]" },
#          { "name": "b", "type": "f64[]" }
#        ],
#        "returns": { "type": "f64" }
#      }]
#    }
#
# 3. Upload both files to your workspace
#
# 4. Use in Equana:
#    a = [1, 2, 3, 4, 5];
#    b = [2, 2, 2, 2, 2];
#    result = fast_dot(a, b)   # Returns 30

Tips & Best Practices

  • Use f64 for compatibility (all Equana numbers are doubles)
  • Export a malloc and free function for better memory management
  • Keep function names descriptive but short (they become Equana function names)
  • Test your WASM module locally before uploading
  • For debugging, add a simple test function that returns a known value

Security note: WASM runs in a sandboxed environment and cannot access the filesystem, network, or other system resources. It can only operate on data passed to it through function parameters.

Workbench

Clear
No variables in workbench

Next Steps