# vasm-jsi

> Empty JSI module for vasm

Latest version **0.1.0** (published 2026-09-24) · MIT license · 0 weekly downloads

## Install

```sh
npm install vasm-jsi
pnpm add vasm-jsi
yarn add vasm-jsi
bun add vasm-jsi
```

## Health

**Score 65/100 (B)** — status: active.

Positive: has types; no vulnerabilities; recently updated; high maintenance score; high quality score.

Warnings: low downloads; no esm support; pre 1.0.

## Facts

| | |
|---|---|
| Version | 0.1.0 |
| Published | 2026-09-24 |
| First published | 2026-09-24 |
| Weekly downloads | 0 |
| License | MIT |
| TypeScript types | bundled |
| Module format | CommonJS |
| Dependencies | 0 |
| Unpacked size | 8.4 MB |
| Known vulnerabilities | 0 |
| Install scripts | no |
| Author | Violet Buse |
| Maintainers | violetbuse |
| Keywords | react-native, expo, vasm-jsi, VasmJsi |

## Links

- npm: https://www.npmjs.com/package/vasm-jsi
- Repository: https://github.com/violetbuse/vasm
- Homepage: https://github.com/violetbuse/vasm#readme
- Issues: https://github.com/violetbuse/vasm/issues
- npm.io page: https://npm.io/package/vasm-jsi

## Recent versions

- 0.1.0 (latest) — 2026-09-24

## README

# vasm-jsi

A WebAssembly engine embedded directly in your Expo/React Native app.
`vasm-jsi` compiles and runs WASM modules in-process via JSI (iOS) and JNI
(Android), backed by [WAMR](https://github.com/wasm-micro-runtime/wasm-micro-runtime)'s
interpreter (no AOT/JIT — this works under iOS's no-runtime-codegen
restriction). On web, it's a thin wrapper around the browser's own
`WebAssembly` object, so the same API works everywhere.

Only numeric value types (`i32`, `i64`, `f32`, `f64`) are supported — no
reference/vector types, no multi-value results yet.

## Prerequisites

WAMR is compiled from source as part of your app's native build, the same
way any Expo/React Native native module with its own C/C++ code is built —
there's no separate manual build step. But it does mean your build
environment needs:

- **iOS**: [CMake](https://cmake.org/) on `PATH` (`brew install cmake`), in
  addition to the Xcode command line tools you already need for any iOS
  build. `pod install` invokes it automatically to compile WAMR into
  `WAMR.xcframework` before Xcode builds anything.
- **Network access during the first native build**, on both platforms: the
  CMake configure step fetches WAMR's `simde` (SIMD-emulation) dependency
  from GitHub via `FetchContent`, and on iOS also clones the
  [`ios-cmake`](https://github.com/leetal/ios-cmake) toolchain the same way.
  Neither is vendored, so a fully offline/sandboxed first build will fail;
  subsequent builds reuse what was already fetched.
- **Android** otherwise needs nothing beyond the standard Android NDK, which
  the Android Gradle Plugin already provisions for any native module.

None of this applies on web — it's a plain wrapper around the browser's own
`WebAssembly`, no native toolchain involved.

## Installation

From an Expo app:

```bash
npx expo install vasm-jsi
```

Expo Go doesn't include this module's native code, so you'll need a
[development build](https://docs.expo.dev/develop/development-builds/introduction/):

```bash
npx expo run:ios
npx expo run:android
```

## Usage

```ts
import VasmJsi from 'vasm-jsi';

// 1. Compile and validate a WASM binary. Exports/imports are reflected
//    eagerly, so module.exports()/module.imports() are cheap to call.
const module = await VasmJsi.compile(wasmBytes); // wasmBytes: Uint8Array

console.log(module.exports());
// [{ name: 'add', params: ['i32', 'i32'], results: ['i32'] }, ...]
console.log(module.imports());
// [{ moduleName: 'env', name: 'log', params: ['i32'], results: [] }, ...]

// 2. Instantiate it. Pass an imports object if the module declares any
//    host-function imports (shaped like WebAssembly.Instance's own).
const instance = module.instantiate({
  env: {
    log: (value) => console.log('from wasm:', value),
  },
});

// 3. Call an exported function.
const result = instance.call('add', [1, 2]);
console.log(result); // { hasResult: true, kind: 'i32', value: 3 }

// ...or off the JS thread, for anything that isn't a quick, small call:
const asyncResult = await instance.callAsync('add', [1, 2]);

// 4. Read/write the instance's linear memory.
const bytes = instance.readMemory(0, 16);
instance.writeMemory(0, new Uint8Array([1, 2, 3, 4]));

// 5. Release native resources immediately instead of waiting for GC.
instance.release();
module.release();
```

### API

- **`VasmJsi.compile(bytes: Uint8Array): Promise<VasmModule>`**
  Compiles and validates a WASM binary. Rejects if the bytes aren't a valid
  module.

- **`VasmModule`**
  - `exports(): VasmExport[]` — the module's callable exports.
  - `imports(): VasmImport[]` — the module's declared host-function imports
    (`{ moduleName, name, params, results }`).
  - `instantiate(imports?: VasmImportObject): VasmInstance` — allocates the
    instance's linear memory and execution environment. `imports` must
    supply a function for every entry in `imports()` (extra keys are
    ignored); omit it for a module with no imports. Different instances of
    the same module may be given different bindings.
  - `release(): void` — frees the underlying native module immediately.

- **`VasmInstance`**
  - `call(name: string, args: number[]): VasmCallResult` — calls an exported
    function synchronously on the JS thread.
  - `callAsync(name: string, args: number[]): Promise<VasmCallResult>` —
    same, but runs off the JS thread. Prefer this for anything beyond a
    quick, small call.
  - `readMemory(offset: number, length: number): Uint8Array`
  - `writeMemory(offset: number, bytes: Uint8Array): void`
  - `release(): void` — frees the underlying native instance immediately.

- **Host functions** (`VasmHostFunction = (...args: number[]) => number | void`)
  Called with plain numbers, on whatever thread the triggering `call`/
  `callAsync` is on — for `callAsync` that's off the JS thread, so a host
  function still safely touches JS state but isn't ordered relative to other
  JS execution the way a normal synchronous callback would be. A thrown
  error (or a non-numeric return where a number is expected) surfaces to the
  wasm caller as a trap, and the triggering `call`/`callAsync` rejects.

### Numeric conventions

All call arguments/results cross the JS boundary as plain `number`s
regardless of the WASM value's actual type. For `i64`, this means values
beyond ±2^53 lose precision — there's no BigInt marshaling in this API yet.

### React hooks

`useVasmModule`/`useVasmInstance` wrap the compile/instantiate lifecycle for
use in a component, handling loading/error state and releasing native
resources automatically on unmount or when their input changes:

```tsx
import { useVasmModule, useVasmInstance } from 'vasm-jsi';

function Calculator({ wasmBytes }: { wasmBytes: Uint8Array }) {
  const { module, status: moduleStatus, error: moduleError } = useVasmModule(wasmBytes);
  const { instance, status: instanceStatus } = useVasmInstance(module, {
    env: { log: (value) => console.log('from wasm:', value) },
  });

  if (moduleStatus === 'error') return <Text>Failed to compile: {moduleError!.message}</Text>;
  if (instanceStatus !== 'ready') return <Text>Loading…</Text>;

  const result = instance!.call('add', [1, 2]);
  return <Text>1 + 2 = {result.value}</Text>;
}
```

- **`useVasmModule(bytes: Uint8Array | null | undefined)`** — compiles
  `bytes`, returning `{ module, status, error }` where `status` is
  `'idle' | 'loading' | 'ready' | 'error'`. `bytes` should be a stable
  reference (`useMemo`, module scope, etc.) — a fresh `Uint8Array` every
  render recompiles every render. Pass `null`/`undefined` when there's
  nothing to compile yet.
- **`useVasmInstance(module, imports?)`** — instantiates a `VasmModule` (from
  `useVasmModule` or otherwise), returning
  `{ instance, status, error }` where `status` is
  `'idle' | 'instantiating' | 'ready' | 'error'`. `imports` is read fresh on
  every render without forcing a re-instantiate — host functions always call
  through to the latest closures, but the (comparatively expensive)
  `instantiate()` call only happens once per `module`. The *set* of imports
  (their `moduleName`/`name` keys) is expected to stay fixed across renders,
  since that's really a property of the compiled module; remount (e.g. via a
  `key`) if it needs to change.

Both hooks release their native resource (module/instance) automatically on
unmount and whenever their input identity changes, so you don't need to call
`release()` yourself when using them.

## Development

This package is part of the `vasm` npm workspace; see the repository's
top-level `CLAUDE.md` for the full workspace layout, native build details,
and how the standalone C++ engine tests fit in. From this directory:

```bash
npm run build   # tsc build of the TS API surface
npm run test     # jest (jest-expo preset)
npm run lint      # eslint src/
```

The engine core itself (`ios/common/cpp/VasmEngine.{h,cpp}`) has its own
platform-independent C++ test suite, runnable without Xcode/Gradle/Metro:

```bash
./scripts/test-engine.sh              # host build (requires cmake, wabt)
./scripts/test-engine-android.sh      # cross-compiled, run on a connected device/emulator
```

---
_Source: https://npm.io/package/vasm-jsi · Machine-readable twin of the npm.io package page. Health data is recomputed on every publish._
