## InWasm - Inline WebAssembly for Typescript.

InWasm is a small bundler for inline standalone wasm libraries. It compiles and bundles
the wasm source code inplace.

### Installation

```bash
npm install inwasm-runtime      # provides InWasm() and type definitions 
npm install --save-dev inwasm   # provides inwasm cli for compiling
```

## Usage

Example with Typescript:

```typescript
// src/xy.wasm.ts
import { InWasm, OutputMode, OutputType } from 'inwasm-runtime';

const getAdderInstance = InWasm({
  name: 'adder',
  type: OutputType.INSTANCE,
  mode: OutputMode.SYNC,
  srctype: 'C',
  exports: {
    add: (a: number, b: number) => 0
  },
  code: `
    int add(int a, int b) {
      return a + b;
    }`
});
const adderInstance = getAdderInstance(); // optional argument: importObject

// use the wasm instance:
console.log(adderInstance.exports.add(23, 42));
```

Before we can actually use the JS script generated by the TS compiler, we need to
run another build step to also compile the wasm source code:
```bash
# compile wasm source, may pull SDKs (depends on config settings)
inwasm lib/xy.wasm.js
# finally run the module, should output 65
node lib/xy.wasm.js
```
It is important to note, that the compile script will overwrite the original JS script (inplace rewrite).
This is normally not an issue for TS projects (already has separated source files).
If directly applied to JS sources, it will "destroy" your source files - currently you have to do
a manual copy step with all nasty consequences for module imports and such. The inplace rewrite may be lifted
in the future once the story, how to deal with plain JS sources, got sorted out.

The final JS file can be further processed or distributed as any other JS script.


### How does it work internally?

The compile script `inwasm` loads the JS files as normal module (currently no ESM support!)
and catches errors thown by `InWasm` to get a hold of the callstack and the source code positions.
Next it evaluates the provided wasm definition (thats the literal object argument of `InWasm`)
and calls the compiler backend for the `srctype` with the content of `code`.
If the compilation succeeds, the generated wasm binary gets base64 encoded and wrapped
into a runtime definition, which finally replaces the original wasm definition.

At runtime `InWasm` returns a getter for the requested output type and mode. The getter approach helps
to lift the initialization burden of the wasm types from the loading phase (lazy eval).
Furthermore the getter memorizes decoded bytes and wasm modules (act as static singletons).


### What is `InWasm` good for?

**TL;DR:** Tiny standalone wasm helpers down to just one exposed function. Anything bigger - probably not,
unless you love to write tons of glue code yourself.

The main purpose of the library is to provide an easy way for embedding small standalone wasm helpers.
Note that beside the wasm bootstrapping there is no glue code provided, thus it cannot replace more
complex solutions with additional JS bridging output (like emscripten with its runtime extensions,
wasm-bindgen etc).

Due to missing glue code, things are very bare metal and you have to provide access and type wrappers
yourself (e.g. directly reading from wasm memory segments). This "embedded programming style" may sound
like a big drawback, but in fact TS/JS is often fast enough to get most things done in a timely fashion,
and just lacks proper performance at a certain point of some data conversion.
Thats where `InWasm` can provide a much faster drop-in alternative to a pure TS/JS implementation,
normally with much smaller package footprint than a bigger wasm integration. `InWasm` itself is at
~800 Bytes bundled, plus base64 string data of the generated binaries. Of course this further depends on
the amount of your own glue code.

Sidenote: Technically it would be possible to create a similar embedding experience for
bigger wasm integrations. Well, that is questionable for several reasons, e.g. wasm files
will grow really big, and inlining those in JS with base64 is a bad idea. Furthermore it would need
proper interfacing of the additional JS glue code, which is definitely out of scope for this library.

For bigger wasm integrations you are better served with the high level interfaces offered by emscripten
and/or rust with wasm-bindgen.


### Supported Types and Modes

Source Types (`srctype`):
- `'C'` - emscripten C, compiled as standalone wasm
- `'C++'` - emscripten C++, compiled as standalone wasm
- `'Clang-C'` - using clang from emscripten SDK
- `'Clang-C++'` - using clang++ from emscripten SDK
- `'Zig'` - preinstalled or autoinstall, compiled as freestanding
- `'wat'` - compiled with wat2wasm
- `'Rust'` - must be preinstalled currently with `cargo` in PATH
- `'custom'` - any custom build script

Output Types (`type`):
- `BYTES` - Uint8Array (raw wasm bytes), typed as `IWasmBytes<T extends IWasmDefinition>`
- `MODULE` - WebAssembly.Module, typed as `IWasmModule<T extends IWasmDefinition>`
- `INSTANCE` - WebAssembly.Instance, typed as `IWasmInstance<T extends IWasmDefinition>`

Output Modes (`mode`):
- `SYNC` - Getter bootstraps output type synchronously and returns it directly.
- `ASYNC` - Getter bootstraps output type with asynchronous interfaces and returns a promise
  resolving to the output type (e.g. `Promise<IWasmInstance<T>>`).

Note that SYNC output mode works only reliable in NodeJS and a web worker context,
as browsers may restrict synchronous wasm module or instance creation in the main context.


### Types & Type Inference

... WIP, types & inference patterns are still subject to change ...


### `InWasm` Coding Restrictions

Due to the way the compile script `inwasm` works by partial execution and a macro like source code replacement,
there are some coding restrictions:
- In general modules containing `InWasm` calls should be endpoints in the module dependency tree
  (not containing complicated imports itself, no cycling imports). They may not import other modules
  containing `InWasm` calls, or compilation will fail.
- All `InWasm` occurences must execute on import of the JS file. This can easily be achieved by
  always declaring them on top level respectively always reachable from there (also allowed in nested
  non-branching structures). Declaring them behind conditional branching will not work,
  if not all branches containing `InWasm` calls are guaranteed to execute from import.
- `InWasm` may not be indirected, it must be called for every given wasm definition as `InWasm({...})`.
  Doing the following will **not** work: `const a = (def) => InWasm(def); a(def1); a(def2);`.
- The wasm definition argument must be provided as a real object literal `{...}` to `InWasm`,
  e.g. `InWasm({... definition details go here ...})`. Using an object identifier like
  `const def = {...}; InWasm(def);` will not work. While technically not needed, this is currently
  enforced by the compile script to keep proper TS type inference working.


### Config Options

With a file `inwasm.config.cjs` in your project root you can configure some settings of `inwasm`:

```javascript
// default - autoinstall zig and emsdk
// (no config file needed, if you are good to go with these)
module.exports = {
  zig: {
    version: 'master',      // pulled sdk version
    store: 'project'        // where to store: 'project' or 'inwasm' folder
  },
  emsdk: {
    version: 'latest',
    store: 'project'
  },
};
// alternatively with custom paths to preinstalled sdks
module.exports = {
  zig: {
    binary: '/path/to/zig'  // zig binary path (use '$PATH' or 'zig' if in PATH)
  },
  emsdk: {
    path: '/path/to/emsdk'  // emsdk installed elsewhere
  },
};
```
`zig.store` and `emsdk.store` support the following values:
- `'project'` - store SDKs in the package folder (default)
- `'inwasm'` - store SDks in the inwasm package (useful for global installs)
- `'parent:package_name'` - store SDKs in parent package folder (inspects package.json for the name)
- `'path:path_to_folder'` - store SDKs in given folder (relative or absolute)

For on-the-fly config overrides it is possible to use env variables,
where the name of the env variable is derived from the config object keys:
```bash
# config.zig.version --> INWASM_ZIG_VERSION
# config.emsdk.path  --> INWASM_EMSDK_PATH
# and so forth ...

# example: set custom zig binary override
INWASM_ZIG_BINARY=/path/to/zig inwasm lib/*wasm.js
```


### Imports / Exports

Imports and exports can be directly attached to the wasm definition:

```typescript
const importObj = { env: {...} };
InWasm({
  // exports should be inlined for proper type inference
  exports: {
    exportedFunctionName: type_stub_of_function,
    ...
  },
  // imports should be referenced, otherwise object is lost at runtime
  imports: importObj
});
```
Exported function names are used to populate EXPORT directives of the compiler,
where supported (not applied for *srctype* `wat` and `custom`).

Exported values are not used for anything else beside type inference by TS,
thus only need to reflect the proper type inteface, e.g. for an exported function
it is enough to stub a function with the right argument types and return type.

Imported names are not further evaluated in the compiler runners beside a `memory`
entry. Currently only clang exposes an interface to declare imported symbol names
upfront (*--allow-undefined-file* switch), which is not yet applied by `inwasm`.
Thus you have to take care yourself to match expected imports later at runtime.

The `importObj` should not be declared inline, unless you provide a similarly shaped
object at runtime by other means. (Note: every JS declaration within a wasm definition
will be lost at runtime.)

At runtime `importsObj` should be provided as argument to the getter of `IWasmInstance`
or any manual instance creation.

Note on other import/export symbol types than memory or functions - WASM furthermore
allows types of *WebAssembly.Global* and *WebAssembly.Table* to be imported or exported.
Your success here may vary, as some compilers dont support them equally, thus `inwasm`
does not further deal with those.


### Memory Settings

The runtime memory of the wasm module can be configured by applying a `memory` entry either
in `exports` (compiled as exported memory) or in `imports.env` (compiled as imported memory):

- exported memory:
  ```typescript
  InWasm({
    ...
    exports: {
      ...
      memory: WebAssembly.Memory({initial: 1, maximum: 1}),
      ...
    },
    ...
  });
  ```

- imported memory:
  ```typescript
  const importObj = {
    env: {
      ...
      memory: WebAssembly.Memory({initial: 1, maximum: 1}),
      ...
    }
  };

  InWasm({
    ...
    imports: importObj,
    ...
  });
  ```

If no memory setting was given to a wasm definition, the module will default to exported memory
of a certain size for most compilers, unless otherwise stated by other means (e.g. an explicit memory
import directive within the code). Most of the time this will lead to much bigger initial memory allocations
than needed at runtime (up to several MBs depending on compiler), or even wrong maximum limits.
Therefore it is almost always a good idea to declare the memory explicitly to keep the footprint as small
as possible.

Very small wasm functions might not need any memory at all. This edge case can occur for pure reentrant functions,
that dont rely on any outer state (global static data), do no stack or heap interactions and handle all needed data
through arguments and the return value. (Also note that the term "stack" might be misleading with wasm, as the stack
is not used the same way as on other architectures, e.g. wasm-native local variables dont live on that "stack".)
Such a no-memory mode is not directly possible with some compilers, as they assume some sort of memory being attached,
even if unused. It still can be faked with some compilers by importing or exporting a 0-page memory,
which should strip any memory notion from the wasm file, if it is really free of any memory access
(double check the final wat file). Always do proper runtime checks with such an aggressive optimization.

Set stack size to zero:
- emscripten: add switch `'-s TOTAL_STACK=0'`
- clang: add switch `'-Wl,-z,stack-size=0'`
- zig: add switch `'--stack 0'`
- rust: add switch `'-Clink-args="-z stack-size=0"'`

Builtins for `memory.size` and `memory.grow` (useful for writing own allocator):
- emscripten/Clang: `__builtin_wasm_memory_size(0)`, `__builtin_wasm_memory_grow(0, delta)`
- zig: `@wasmMemorySize(0)`, `@wasmMemoryGrow(0, delta)`
- rust: `memory_size(0)`, `memory_grow(0, delta)`

where `delta` is the number of memory pages to be added and `0` the memory identifier
(currently restricted to just one memory).


### WASM features

Due to the way `inwasm` operates your nodejs version of the build process should support any WASM feature
used in your definitions (you can test support with the npm package *wasm-check*).

If you are bound to an older node version or use cutting edge features, you can try to enable missing features
with these node cmdline switches:

- Reference types (--experimental-wasm-anyref)
- BigInt between js and wasm (--experimental-wasm-bigint)
- Bulk memory operations (--experimental-wasm-bulk-memory)
- Exceptions (--experimental-wasm-eh)
- Multi values (--experimental-wasm-mv)
- Tail recursion calls (--experimental-wasm-return-call)
- Saturated (non-trapping) conversions from float to int (--experimental-wasm-sat-f2i-conversions)
- Sign/zero extensions (--experimental-wasm-se)
- SIMD (--experimental-wasm-simd)
- Threads (--experimental-wasm-threads)
- Type reflection (--experimental-wasm-type-reflection)

Currently the compiler runners are not yet fully prepared to apply all known additional wasm features
correctly to all build steps and thus might break for certain non-default features.
Please open an issue if you find something crucial missing.


### Testing with `InWasm`

`InWasm` declarations should not be used directly in mocha tests, as the needed code transformations may not
run on import by `inwasm`, if placed inside of mocha's test suite calls like `describe` or `it`,
thus mocha would always end up with a `must run "inwasm"` error. Also running `inwasm` on such a test file
will create various errors for unresolved mocha defines. (This repo uses a very limited *mocha_shim*
to overcome this, but thats not meant for general purpose testing needs.)

Instead place the `InWasm` definitions into a separate module imported by the tests and run `inwasm`
on the separate modules before calling mocha on the test files.

Isolated testing and debugging of the wasm code itself is currently very limited. A future version
may provide better support, until then use close unit/API testing on TS/JS side. Since things
are currently really bare metal, this might even involve writing your own console logging shims.


### Development

The source repo contains three node package folders:
- `/inwasm` - inwasm package with cli tool and definitions
- `/testproject` - main test package for different compiler runners/SDKs
- `/testproject-esm` - main ESM test package for different compiler runners/SDKs

Since test projects depends on `/inwasm`, initialize in this order:
```bash
git clone https://github.com/jerch/inwasm.git

# setup inwasm first
cd inwasm/inwasm
npm install
npm run tgz

# then the testprojects
cd ../testproject
npm install
cd ../testproject-esm
npm install
```


### State

Beta, tested on recent versions of Linux, macOS and Windows with Node 20, 22 & 24.


### Changelog

- 0.1.2
  - cli: fix glob support
  - runtime: IWasmDefinition inline docs
  - runtime: use Uint8Array.fromBase64()
- 0.1.1
  - fix watch mode
- 0.1.0 - first beta release
  - package upgrades
  - C++ runners
  - ESM support (cli itself is ESM now)
  - parent and path directive for SDKs
  - isolated runtime package
