DrSensor / nusa

incremental runtime that bring both simplicity and power into webdev (buildless, cross-language, data-driven)
MIT License
4 stars 0 forks source link

Support WebAssembly #5

Open DrSensor opened 1 year ago

DrSensor commented 1 year ago

prototype (stalled)

Warning: may not relevant anymore see comment at the bottom for the current approach

Unlike Javascript module that export class, setter in WebAssembly doesn't automatically update html attribute. You need to specify which bounded property to update.

//! lib.zig -> script.wasm

/// update property which bounded to several attributes 
extern fn update([]const u8) void; // πŸ‘ˆπŸ‘‡
/// most likely explicit update may not needed since
/// the update are automatic after event handler is executed

/// get instance id number
/// throw error if not an instance (i.e called inside static method)
extern fn lc() u8; // link count
// extern lc: u8 // according to spec, wasm support global value import

var count: [4]u8 = undefined; // limit instance to 4
export fn @"get count"() u8 {
    return count[lc()];
}
export fn @"set count"(value: u8) void {
    count[lc()] = value;
    update("count");
}

export fn constructor() { // called every time when <script> attached into <render-scope>
  count[lc()] = 0;
}

export fn static() { // called only once
  // do something when the wasm module loaded
}

var static_count: u8 = 0;
export fn @"static get count"() u8 {
    return static_count;
}
export fn @"static set count"(value: u8) void {
    static_count = value;
    update("static count");
}

export fn increment() void {
    count[lc()] += 1;
    @"set count"(count);

    static_count += 1;
    @"static set count"(static_count);
}

Note that the setter not really necessary in this example. You can rid them and just call update function. I use setter just for demonstration that this feature indeed support a setter.


<!-- page.html -->
<render-scope>
<button onclick:="increment">


---
> **πŸ€” Food for Thought**
> Maybe consider storing the property, getter/setter operation, and others as ArrayBuffer for maximum flexibility
> (inspired from [WASM-4 Memory Map](https://wasm4.org/docs/reference/memory))
DrSensor commented 1 year ago

V8 now support calling Promise in WebAssembly (behind feature flag)

DrSensor commented 1 year ago

TBD

import { Attribute, Bind, ColonFor } from "../query.ts";
import bind from "./common.ts";

const cache: Record<string, Module> = {};
let count = 0;

export default (attrs: Attribute[]) => async (path: string) => {
  let linkCount = count;
  (await source).url;
  const module = await WebAssembly.instantiateStreaming(source, {
    env: { lc: () => linkCount },
  });
  count++;
};

/** WebAssembly module **instance** */
interface Module {
  /** instantiate module */
  ["constructor"](): void;
  /** instance accessor for accessing instance value */
  [key: `get ${string}`]: () => unknown;
  /** instance accessor for mutating instance value */
  [key: `set ${string}`]: (value: unknown) => unknown;
  /** instance method that might access/mutate instance and/or global value */
  [key: string]: (...args: unknown[]) => unknown;

  /** static block which run only once */
  // ["static"](): void; // wasm $startFunction serve the same purpose
  /** static accessor for accessing global value */
  [key: `static get ${string}`]: () => unknown;
  /** static accessor for mutating global value */
  [key: `static set ${string}`]: (value: unknown) => unknown;
  /** static method that can't access/mutate instance value */
  [key: `static ${string}`]: (...args: unknown[]) => unknown;
}
DrSensor commented 1 year ago

Some references

DrSensor commented 1 year ago

Data Type compatibility across languages

I'm thinking to use Apache Arrow format to solve this. Also, ecmascript data type still fully supported and may automatically inferred when the module is in JavaScript, but only partially (and no infer value) when in WebAssembly. By using this columnar format, it's possible for the module to run on multiple threads whether it's main, worker, or even gpu.

In javascript it might look like

import { Map, Set, Date, ... } from "nusa/std/type"
import { Utf8, Float16, Date64, ... } from "nusa/arrow/type"

Another approach is to use objectbuffer by Bnaya Peretz

I'm in favor of Apache Arrow because the interoperability between languages are more consistent. For example, objectbuffer use ES string which is UTF-16 while most programming languages use UTF-8.

DrSensor commented 1 year ago

15x Faster TypedArrays: Vector Addition in WebAssembly @ 154GB/s (see wasmblr benchmark code)

DrSensor commented 1 year ago

Arquero, a JavaScript library for query processing and transformation of Apache Arrow columns

DrSensor commented 1 year ago

Based on wasm component model and the output of wit-bindgen, the size of <link href=module.wasm> can be reduced by splitting arrow data type into different components/chunks and import it when <render-scope> is visible based on the declared imports in module.wasm.

So which language should be used for implementing that?

C23 - βœ… It support wasm multi-value return - βœ”οΈ Can manipulate wasm table (only clang 18 (unstable), stable release (clang 16) doesn't have that yet πŸ₯²) - ❌ Can't import wasm table - ❌ Can't import mutable-globals
Rust - βœ… It support wasm multi-value return - ❌ Can't manipulate wasm table - ❌ Can't import wasm table - ❌ Can't import mutable-globals
Odin TBD most likely same limitation as Rust πŸ€”
Zig - ❌ Doesn't support wasm multi-value return - ❌ Can't manipulate wasm table - ❌ Can't import wasm table - ❌ Can't import mutable-globals
AssemblyScript - ❌ Doesn't support wasm multi-value return - ❌ Can't manipulate wasm table - ❌ Can't import wasm table - βœ… Can import mutable-globals
DrSensor commented 10 months ago

I've written some of [component].wasm in frankenstein Rust πŸ˜‚


What the heck is frankenstein Rust?

It's Rust without Cargo that use voodoo technique like manually linking to hand-knitted assembly code and using bunch compiler flags or tools other than rustc


Seems I will continue to go on this path πŸ§Ÿβ€β™‚οΈ

DrSensor commented 10 months ago

Since not all wasm language can import a function with multi-value return, I've added a fallback function with cABI prefix which pack all return values into 64bit single value. For example,

(import "nusa" "accessor" (func $a (result i32 i32)))

the fallback would be

(import "nusa" "cABIaccessor" (func $a (result i64)))

The language binding must use cABI* function instead of normal one only if they can't produce wasm binary with multivalue return.