Skip to content

WebAssembly (WASM) Integration

Propa facilitates the integration of WebAssembly (WASM) modules, allowing you to run high-performance code (e.g., written in Rust, C++, Go) directly in the browser. This is particularly useful for computationally intensive tasks.

Propa provides WasmModule and TypedWasmModule classes, along with helper functions loadWasm and loadTypedWasm, to simplify loading and interacting with WASM modules.

Building Your WASM Module

Typically, you'll use tools like wasm-pack for Rust or Emscripten for C/C++ to compile your code to WebAssembly. For this guide, we'll focus on a Rust example using wasm-pack.

1. Rust Project Setup (wasm/Cargo.toml) Ensure wasm-bindgen is a dependency:

toml
[package]
name = "my-wasm-lib"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib"]

[dependencies]
wasm-bindgen = "0.2"

[dependencies.web-sys]
version = "0.3"
features = [
  "console",
]

2. Rust Code (wasm/src/lib.rs)

rust
use wasm_bindgen::prelude::*;

// Optional: For logging from Rust to the browser console
#[wasm_bindgen]
extern "C" {
    #[wasm_bindgen(js_namespace = console)]
    fn log(s: &str);
}

#[wasm_bindgen]
pub fn add(a: i32, b: i32) -> i32 {
    log(&format!("Rust says: Adding {} and {}", a, b));
    a + b
}

#[wasm_bindgen]
pub fn concatenate_strings(s1: &str, s2: &str) -> String {
    format!("{} {}", s1, s2)
}

#[wasm_bindgen]
pub fn sum_array(numbers: &[i32]) -> i32 {
    numbers.iter().sum()
}

3. Build with wasm-pack Navigate to your Rust WASM project directory (wasm/) and run:

bash
wasm-pack build --target web --out-dir pkg

This command generates:

  • A .wasm file.
  • A JavaScript glue file (.js).
  • TypeScript definitions (.d.ts) in the pkg directory.

These generated files allow for type-safe interaction from your TypeScript code.

Integrating WASM in Propa

You can use loadTypedWasm for a type-safe experience, leveraging the TypeScript definitions generated by wasm-pack.

TypeScript Integration (.tsx component):

tsx
import { h, ComponentLifecycle, reactive } from '@salernoelia/propa';

// Import the init function and exported functions/types from your WASM package
// The path '../../wasm/pkg/wasm' assumes your 'pkg' directory is two levels up.
// Adjust the path based on your project structure.
// The '.js' extension might be omitted depending on your module resolver setup.
import initWasmModule, { add, concatenate_strings, sum_array } from '../../wasm/pkg/my_wasm_lib'; // Or ...pkg/wasm if that's the output name

// (Optional but recommended) Define an interface for your WASM module's exports
// This can be useful if you don't import functions directly or for more complex scenarios.
// However, with direct imports from the wasm-pack output, this might be redundant.
interface MyWasmExports {
  add: (a: number, b: number) => number;
  concatenate_strings: (s1: string, s2: string) => string;
  sum_array: (numbers: Int32Array) => number; // Note: wasm-bindgen often maps slices to TypedArrays
}

function WasmCalculator() {
  const result = reactive('Loading WASM...');
  const wasmReady = reactive(false);

  // Store a reference to the typed module if needed, or use imported functions directly
  // let wasmInstance: MyWasmExports | null = null; // Example if not using direct imports

  ComponentLifecycle.onMount(async () => {
    try {
      // Initialize the WASM module.
      // The init function is usually the default export from the JS glue file.
      await initWasmModule(); // This loads and compiles the .wasm file

      // At this point, the imported functions (add, concatenate_strings) are ready to use.
      wasmReady.value = true;
      console.log('WASM module loaded successfully!');

      // Perform calculations using the directly imported WASM functions
      const sum = add(10, 20);
      const combined = concatenate_strings("Hello", "WASM!");
      const arrSum = sum_array(new Int32Array([1, 2, 3, 4]));
      result.value = `Sum: ${sum}, Combined: "${combined}", Array Sum: ${arrSum}`;

    } catch (error) {
      console.error('Failed to load WASM module:', error);
      const errorMessage = error instanceof Error ? error.message : String(error);
      result.value = `Error loading WASM: ${errorMessage}`;
    }
  });

  const performRandomAdd = () => {
    if (wasmReady.value) {
      const num1 = Math.floor(Math.random() * 100);
      const num2 = Math.floor(Math.random() * 100);
      const sum = add(num1, num2); // Use directly imported function
      result.value = `Random sum: ${num1} + ${num2} = ${sum}`;
    } else {
      result.value = 'WASM not ready!';
    }
  };

  return (
    <div>
      <h3>WASM Status: {wasmReady.value ? 'Ready' : 'Loading...'}</h3>
      <p>Result: {result}</p>
      <button onClick={performRandomAdd} disabled={!wasmReady.value}>
        Calculate Random Sum
      </button>
    </div>
  );
}

// To use this component:
// const appRoot = document.getElementById('app');
// if (appRoot) appRoot.appendChild(<WasmCalculator />);

Using loadTypedWasm (Alternative)

If you prefer to use Propa's loadTypedWasm helper (perhaps for modules not generated by wasm-pack or for a more centralized loading pattern), you would do it like this:

tsx
import { h, ComponentLifecycle, reactive, loadTypedWasm } from '@salernoelia/propa';

// Define the interface for your WASM module functions for type safety
interface MyWasmModule {
  add: (a: number, b: number) => number;
  concatenate_strings: (s1: string, s2: string) => string;
  // Add other WASM functions here
}

function WasmCalculatorWithHelper() {
  const result = reactive('Loading WASM...');
  const wasmReady = reactive(false);
  let wasmApi: MyWasmModule | null = null;

  ComponentLifecycle.onMount(async () => {
    try {
      // Path to the JS glue file generated by wasm-pack (or your WASM tool)
      // This path needs to be resolvable by Vite's import mechanism.
      // For wasm-pack, it's typically 'path/to/your/pkg/your_wasm_lib.js'
      const wasmModule = await loadTypedWasm<MyWasmModule>('../../wasm/pkg/my_wasm_lib.js');
      wasmApi = wasmModule.getTypedModule(); // Access the typed functions

      wasmReady.value = true;
      console.log('WASM module loaded successfully using loadTypedWasm!');

      if (wasmApi) {
        const sum = wasmApi.add(10, 20);
        const combined = wasmApi.concatenate_strings("Hello", "Propa!");
        result.value = `Sum: ${sum}, Combined: "${combined}"`;
      }
    } catch (error) {
      console.error('Failed to load WASM module:', error);
      result.value = `Error loading WASM: ${error instanceof Error ? error.message : String(error)}`;
    }
  });

  // ... rest of the component ...
  return (
    <div>
      <h3>WASM Status (Helper): {wasmReady.value ? 'Ready' : 'Loading...'}</h3>
      <p>Result: {result}</p>
      {/* ... buttons ... */}
    </div>
  );
}

Important Considerations for loadTypedWasm and wasm-pack: The wasm-pack generated JS file often exports an init function (usually default export) and named exports for your WASM functions. Propa's loadWasm and loadTypedWasm expect the wasmPath to resolve to a module that, when imported, either directly is the WASM instance or has a default export function that initializes and returns/sets up the WASM instance. The initWasmModule() approach shown first is generally more straightforward with wasm-pack's output as it directly uses the generated JS module.

Propa's WASM integration, especially when combined with tools like wasm-pack, provides a powerful and type-safe way to enhance your web applications with the performance of WebAssembly.

Released under the MIT License.