├── examples ├── host-typescript │ ├── index.ts │ ├── __tests__ │ │ ├── builds.ts │ │ ├── example.bench.ts │ │ └── example.test.ts │ └── host.ts ├── wasm-rust │ ├── rust-toolchain.toml │ ├── shared │ │ ├── Cargo.toml │ │ └── lib.rs │ ├── Cargo.toml │ ├── api_zaw │ │ ├── Cargo.toml │ │ └── lib.rs │ ├── api_bindgen │ │ ├── Cargo.toml │ │ ├── index.ts │ │ └── lib.rs │ ├── __tests__ │ │ └── bindgen.test.ts │ └── Cargo.lock ├── wasm-zig │ ├── build.zig.zon │ ├── build.zig │ └── main.zig └── utils │ └── index.ts ├── implementations ├── host-typescript │ ├── src │ │ ├── types.ts │ │ ├── index.ts │ │ ├── constants.ts │ │ ├── binding.ts │ │ ├── interop.ts │ │ └── conduit.ts │ ├── vitest.config.ts │ ├── tsup.config.ts │ ├── .npmignore │ ├── tsconfig.json │ ├── package.json │ └── README.md ├── wasm-rust │ ├── interop │ │ ├── externs.rs │ │ ├── log.rs │ │ ├── mod.rs │ │ └── error.rs │ ├── Cargo.lock │ ├── Cargo.toml │ ├── lib.rs │ ├── README.md │ └── conduit │ │ └── mod.rs └── wasm-zig │ ├── src │ ├── conduit.zig │ ├── interop │ │ ├── externs.zig │ │ ├── log.zig │ │ ├── stack.zig │ │ └── error.zig │ ├── zaw.zig │ ├── simd.zig │ ├── interop.zig │ └── conduit │ │ └── conduit.zig │ ├── build.zig.zon │ └── build.zig ├── .gitignore ├── scripts ├── build-all.sh ├── test-rust.sh ├── test-zig.sh ├── build-ts.sh ├── build-zig.sh ├── build-rust.sh └── install.sh ├── .prettierrc.js ├── .github └── workflows │ ├── ci-build-zig.yml │ ├── ci-build-rust.yml │ └── zig-release.yml ├── test-gen ├── generate.ts ├── types.ts └── languages │ ├── zig.ts │ ├── typescript.ts │ └── rust.ts ├── package.json ├── docs ├── contributing.md ├── getting-started.md ├── protocol-interop.md ├── protocol-conduit.md └── benchmarks.md ├── README.md └── LICENSE /examples/host-typescript/index.ts: -------------------------------------------------------------------------------- 1 | export * from './host' 2 | -------------------------------------------------------------------------------- /implementations/host-typescript/src/types.ts: -------------------------------------------------------------------------------- 1 | export type ZawReturn = 0 | 1 2 | -------------------------------------------------------------------------------- /implementations/wasm-rust/interop/externs.rs: -------------------------------------------------------------------------------- 1 | extern "C" { 2 | pub fn hostLog(); 3 | } 4 | -------------------------------------------------------------------------------- /implementations/host-typescript/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './interop' 2 | export * from './types' 3 | -------------------------------------------------------------------------------- /examples/wasm-rust/rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "stable" 3 | targets = ["wasm32-unknown-unknown"] 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Node 2 | node_modules 3 | dist/ 4 | 5 | # Zig 6 | .zig-cache 7 | zig-out 8 | 9 | # Rust 10 | target 11 | pkg 12 | -------------------------------------------------------------------------------- /scripts/build-all.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e; 4 | cd "$(dirname $(realpath $0))"; 5 | 6 | ./build-zig.sh 7 | ./build-rust.sh 8 | ./build-ts.sh 9 | -------------------------------------------------------------------------------- /scripts/test-rust.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e; 4 | ROOT="$(dirname $(realpath $0))/.."; 5 | 6 | cd $ROOT/implementations/wasm-rust 7 | cargo test 8 | -------------------------------------------------------------------------------- /scripts/test-zig.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e; 4 | ROOT="$(dirname $(realpath $0))/.."; 5 | 6 | cd $ROOT/implementations/wasm-zig 7 | zig build test --summary all 8 | -------------------------------------------------------------------------------- /implementations/wasm-zig/src/conduit.zig: -------------------------------------------------------------------------------- 1 | const conduit = @import("./conduit/conduit.zig"); 2 | 3 | pub const Writer = conduit.Writer; 4 | pub const Reader = conduit.Reader; 5 | -------------------------------------------------------------------------------- /implementations/host-typescript/src/constants.ts: -------------------------------------------------------------------------------- 1 | export const MAX_LOG_SIZE = 1024 2 | export const MAX_ERROR_SIZE = 256 3 | export const PAGE_SIZE = 65536 4 | export const DEFAULT_INITIAL_PAGES = 17 5 | -------------------------------------------------------------------------------- /scripts/build-ts.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e; 4 | cd "$(dirname $(realpath $0))/.."; 5 | 6 | npm i --prefix implementations/host-typescript 7 | npm run build --prefix implementations/host-typescript 8 | -------------------------------------------------------------------------------- /examples/wasm-rust/shared/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "shared" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [lib] 7 | crate-type = ["rlib"] 8 | path = "./lib.rs" 9 | 10 | [dependencies] 11 | -------------------------------------------------------------------------------- /implementations/host-typescript/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config' 2 | 3 | export default defineConfig({ 4 | test: { 5 | environment: 'node', 6 | globals: true, 7 | }, 8 | }) -------------------------------------------------------------------------------- /implementations/wasm-rust/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "zaw" 7 | version = "0.0.3" 8 | -------------------------------------------------------------------------------- /implementations/wasm-zig/build.zig.zon: -------------------------------------------------------------------------------- 1 | .{ 2 | .name = .zaw, 3 | .version = "0.0.1", 4 | .fingerprint = 0x14a08aed3971cc86, 5 | .minimum_zig_version = "0.15.0", 6 | .paths = .{ "build.zig", "build.zig.zon", "src/" }, 7 | } 8 | -------------------------------------------------------------------------------- /scripts/build-zig.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e; 4 | ROOT="$(dirname $(realpath $0))/.."; 5 | 6 | cd $ROOT/examples/wasm-zig 7 | zig build -Doptimize=ReleaseFast --summary all 8 | wasm2wat ./zig-out/bin/main.wasm > ./zig-out/bin/main.wat || true 9 | -------------------------------------------------------------------------------- /examples/wasm-zig/build.zig.zon: -------------------------------------------------------------------------------- 1 | .{ 2 | .name = .wasm_zig_example, 3 | .version = "0.0.1", 4 | .fingerprint = 0xd2241bda131682f2, 5 | .minimum_zig_version = "0.15.0", 6 | .paths = .{ "build.zig", "build.zig.zon", "main.zig" }, 7 | } 8 | -------------------------------------------------------------------------------- /examples/wasm-rust/Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "api_bindgen", 4 | "api_zaw", 5 | "shared" 6 | ] 7 | resolver = "2" 8 | 9 | [profile.release] 10 | opt-level = 3 11 | lto = true 12 | codegen-units = 1 13 | panic = "abort" 14 | -------------------------------------------------------------------------------- /examples/wasm-rust/api_zaw/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "api_zaw" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [lib] 7 | crate-type = ["cdylib"] 8 | path = "./lib.rs" 9 | 10 | [dependencies] 11 | zaw = { path = "../../../implementations/wasm-rust" } 12 | shared = { path = "../shared" } 13 | 14 | -------------------------------------------------------------------------------- /implementations/host-typescript/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup' 2 | 3 | export default defineConfig({ 4 | entry: ['src/index.ts'], 5 | format: ['cjs', 'esm'], 6 | dts: true, 7 | clean: true, 8 | sourcemap: true, 9 | minify: false, 10 | splitting: false, 11 | treeshake: true, 12 | }) -------------------------------------------------------------------------------- /implementations/host-typescript/.npmignore: -------------------------------------------------------------------------------- 1 | # Source files 2 | *.ts 3 | !dist/**/*.d.ts 4 | 5 | # Config files 6 | tsconfig.json 7 | tsup.config.ts 8 | vitest.config.ts 9 | 10 | # Development 11 | node_modules/ 12 | .DS_Store 13 | *.log 14 | .npm 15 | 16 | # Tests 17 | *.test.ts 18 | **/*.test.ts 19 | 20 | # Other 21 | *.tsbuildinfo -------------------------------------------------------------------------------- /implementations/wasm-zig/src/interop/externs.zig: -------------------------------------------------------------------------------- 1 | const builtin = @import("builtin"); 2 | 3 | // Allow no-op non-externs in native mode to enable IDE test runs 4 | const impl = if (builtin.target.cpu.arch == .wasm32) struct { 5 | pub extern fn hostLog() void; 6 | } else struct { 7 | pub fn hostLog() void {} 8 | }; 9 | 10 | pub const hostLog = impl.hostLog; 11 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | semi: false, 3 | singleQuote: true, 4 | tabWidth: 2, 5 | bracketSpacing: true, 6 | bracketSameLine: false, 7 | trailingComma: 'all', 8 | arrowParens: 'avoid', 9 | endOfLine: 'lf', 10 | printWidth: 140, 11 | quoteProps: 'as-needed', 12 | jsxSingleQuote: false, 13 | proseWrap: 'preserve', 14 | htmlWhitespaceSensitivity: 'css', 15 | } 16 | -------------------------------------------------------------------------------- /examples/host-typescript/__tests__/builds.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import path from 'path' 3 | 4 | const EXAMPLES_DIR = path.join(__dirname, '../../') 5 | 6 | export const builds = { 7 | zig: fs.readFileSync(path.join(EXAMPLES_DIR, 'wasm-zig/zig-out/bin/main.wasm')), 8 | rust: fs.readFileSync(path.join(EXAMPLES_DIR, 'wasm-rust/target/wasm32-unknown-unknown/release/api_zaw.wasm')), 9 | } 10 | -------------------------------------------------------------------------------- /.github/workflows/ci-build-zig.yml: -------------------------------------------------------------------------------- 1 | name: Build Zig 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | 7 | pull_request: 8 | branches: [main] 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v4 16 | - uses: mlugg/setup-zig@v2 17 | with: 18 | version: 0.15.2 19 | - run: ./scripts/build-zig.sh 20 | - run: ./scripts/test-zig.sh 21 | -------------------------------------------------------------------------------- /examples/wasm-rust/api_bindgen/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "wasm_api_bindgen" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [lib] 7 | crate-type = ["cdylib"] 8 | path = "./lib.rs" 9 | 10 | [dependencies] 11 | wasm-bindgen = "0.2" 12 | js-sys = "0.3" 13 | shared = { path = "../shared" } 14 | 15 | [dependencies.web-sys] 16 | version = "0.3" 17 | features = ["console"] 18 | 19 | [package.metadata.wasm-pack.profile.release] 20 | wasm-opt = ["-O4", "--enable-simd"] 21 | -------------------------------------------------------------------------------- /scripts/build-rust.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e; 4 | ROOT="$(dirname $(realpath $0))/.."; 5 | 6 | cd $ROOT/examples/wasm-rust/api_bindgen 7 | wasm-pack build --target web 8 | wasm2wat ./pkg/wasm_api_bindgen_bg.wasm >./pkg/wasm_api_bindgen_bg.wat || true 9 | 10 | cd $ROOT/examples/wasm-rust/api_zaw 11 | RUSTFLAGS="-C target-feature=+simd128 -C link-arg=--import-memory -C link-arg=--export-memory" cargo build --target wasm32-unknown-unknown --release 12 | 13 | cd $ROOT/examples/wasm-rust/target/wasm32-unknown-unknown/release 14 | wasm2wat api_zaw.wasm > api_zaw.wat || true 15 | -------------------------------------------------------------------------------- /implementations/host-typescript/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "ESNext", 5 | "moduleResolution": "bundler", 6 | "allowSyntheticDefaultImports": true, 7 | "esModuleInterop": true, 8 | "allowJs": true, 9 | "strict": true, 10 | "noEmit": true, 11 | "declaration": true, 12 | "outDir": "dist", 13 | "skipLibCheck": true, 14 | "forceConsistentCasingInFileNames": true 15 | }, 16 | "include": [ 17 | "src/**/*.ts" 18 | ], 19 | "exclude": [ 20 | "node_modules", 21 | "dist" 22 | ] 23 | } -------------------------------------------------------------------------------- /test-gen/generate.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import path from 'path' 3 | import { zigGenerator } from './languages/zig' 4 | import { testCases } from './test-cases' 5 | import { typescriptGenerator } from './languages/typescript' 6 | import { rustGenerator } from './languages/rust' 7 | 8 | for (const generator of [zigGenerator, typescriptGenerator, rustGenerator]) { 9 | const filename = path.join(__dirname, '../', generator.outputFile) 10 | 11 | console.log(`Generating ${filename}`) 12 | 13 | const testFile = generator.generateTestFile(testCases) 14 | 15 | fs.writeFileSync(filename, testFile, 'utf-8') 16 | } 17 | -------------------------------------------------------------------------------- /implementations/wasm-rust/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "zaw" 3 | version = "0.0.3" 4 | edition = "2021" 5 | description = "Zero-allocation WebAssembly communication protocol for Rust" 6 | license = "Apache-2.0" 7 | repository = "https://github.com/stylearcade/zaw" 8 | homepage = "https://github.com/stylearcade/zaw" 9 | documentation = "https://docs.rs/zaw" 10 | keywords = ["webassembly", "wasm", "zero-allocation", "performance", "interop"] 11 | categories = ["api-bindings", "wasm", "no-std"] 12 | readme = "README.md" 13 | authors = ["tristanhoy"] 14 | 15 | [lib] 16 | crate-type = ["rlib", "cdylib"] 17 | path = "lib.rs" 18 | 19 | [dependencies] 20 | -------------------------------------------------------------------------------- /implementations/wasm-rust/interop/log.rs: -------------------------------------------------------------------------------- 1 | use super::externs; 2 | 3 | static mut LOG_STORAGE: [u8; 2048] = [0; 2048]; 4 | 5 | #[allow(static_mut_refs)] 6 | pub fn get_log_ptr() -> i32 { 7 | unsafe { LOG_STORAGE.as_ptr() as i32 } 8 | } 9 | 10 | #[allow(static_mut_refs)] 11 | pub fn log(msg: &str) { 12 | unsafe { 13 | let bytes = msg.as_bytes(); 14 | let to_copy = bytes.len().min(LOG_STORAGE.len() - 1); // Leave space for null terminator 15 | 16 | LOG_STORAGE[..to_copy].copy_from_slice(&bytes[..to_copy]); 17 | LOG_STORAGE[to_copy] = 0; // Null terminate 18 | 19 | externs::hostLog(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.github/workflows/ci-build-rust.yml: -------------------------------------------------------------------------------- 1 | name: Build Rust 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | 7 | pull_request: 8 | branches: [main] 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v4 16 | - uses: actions-rs/toolchain@v1 17 | with: 18 | toolchain: stable 19 | target: wasm32-unknown-unknown 20 | override: true 21 | - run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh 22 | - run: sudo apt install -y wabt 23 | - run: ./scripts/build-rust.sh 24 | - run: ./scripts/test-rust.sh 25 | -------------------------------------------------------------------------------- /implementations/wasm-zig/src/interop/log.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const externs = @import("externs.zig"); 3 | 4 | var logStorage = [_]u8{0} ** 2048; 5 | 6 | pub fn getLogPtr() i32 { 7 | return @intCast(@intFromPtr(&logStorage)); 8 | } 9 | 10 | pub fn log(msg: []const u8) void { 11 | const len = @min(msg.len, logStorage.len - 1); 12 | @memcpy(logStorage[0..len], msg[0..len]); 13 | logStorage[len] = 0; 14 | externs.hostLog(); 15 | } 16 | 17 | pub fn logf(comptime fmt: []const u8, args: anytype) void { 18 | const data = std.fmt.bufPrint(logStorage[0..], fmt, args) catch unreachable; 19 | logStorage[data.len] = 0; 20 | externs.hostLog(); 21 | } 22 | -------------------------------------------------------------------------------- /test-gen/types.ts: -------------------------------------------------------------------------------- 1 | // Type definitions for the template generator system 2 | export type DataType = 'Uint8' | 'Uint32' | 'Int32' | 'Float32' | 'Float64' 3 | 4 | export type Operation = 5 | | { 6 | type: 'write' | 'init' 7 | dataType: DataType 8 | value: number 9 | } 10 | | { 11 | type: 'initArray' | 'initElements' | 'copyArray' | 'copyElements' 12 | dataType: DataType 13 | value: number[] 14 | } 15 | 16 | export type TestCase = { 17 | name: string 18 | operations: Operation[] 19 | expectation: number[] 20 | } 21 | 22 | export type TestGenerator = { 23 | outputFile: string 24 | generateTestFile: (testCases: TestCase[]) => string 25 | } 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "zaw", 3 | "version": "0.0.1", 4 | "description": "", 5 | "scripts": { 6 | "build": "./scripts/build-all.sh", 7 | "bench-ts": "vitest bench --run", 8 | "install": "./scripts/install.sh", 9 | "test-ts": "vitest run --mode=test", 10 | "test-zig": "./scripts/test-zig.sh", 11 | "test-rust": "./scripts/test-rust.sh", 12 | "generate-tests": "tsx ./test-gen/generate.ts" 13 | }, 14 | "author": "tristanhoy", 15 | "license": "ISC", 16 | "dependencies": { 17 | "zaw": "^0.0.3" 18 | }, 19 | "devDependencies": { 20 | "@types/node": "22.15.30", 21 | "tsx": "4.19.4", 22 | "typescript": "^5.0.0", 23 | "vitest": "3.1.3" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /implementations/wasm-zig/build.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | pub fn build(b: *std.Build) !void { 4 | const target = b.standardTargetOptions(.{}); 5 | const optimize = b.standardOptimizeOption(.{}); 6 | 7 | _ = b.addModule("zaw", .{ 8 | .root_source_file = b.path("src/zaw.zig"), 9 | .target = target, 10 | .optimize = optimize, 11 | }); 12 | 13 | const test_step = b.step("test", "Run unit tests"); 14 | 15 | const unit_tests = b.addTest(.{ .root_module = b.createModule(.{ 16 | .root_source_file = b.path("src/conduit/conduit.test.zig"), 17 | .target = target, 18 | }) }); 19 | 20 | const run_unit_tests = b.addRunArtifact(unit_tests); 21 | 22 | test_step.dependOn(&run_unit_tests.step); 23 | } 24 | -------------------------------------------------------------------------------- /implementations/wasm-zig/src/zaw.zig: -------------------------------------------------------------------------------- 1 | pub const conduit = @import("conduit.zig"); 2 | pub const interop = @import("interop.zig"); 3 | pub const simd = @import("simd.zig"); 4 | 5 | /// Sets up all required WASM exports for the zaw interop layer. 6 | /// Call this in a comptime block to export the required functions. 7 | pub inline fn setupInterop() void { 8 | comptime { 9 | @export(&interop.getErrorPtr, .{ .name = "getErrorPtr", .linkage = .strong }); 10 | @export(&interop.getLogPtr, .{ .name = "getLogPtr", .linkage = .strong }); 11 | @export(&interop.allocateInputChannel, .{ .name = "allocateInputChannel", .linkage = .strong }); 12 | @export(&interop.allocateOutputChannel, .{ .name = "allocateOutputChannel", .linkage = .strong }); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /examples/wasm-rust/api_bindgen/index.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import path from 'path' 3 | import init, { xor_array_i32, sum_array_f64, multiply_4x4_f32 } from './pkg/wasm_api_bindgen' 4 | 5 | export type Module = { 6 | xorInt32Array: (values: Int32Array) => number 7 | sumFloat64Array: (values: Float64Array) => number 8 | multiply4x4Float32: (left: Float32Array, right: Float32Array) => Float32Array 9 | } 10 | 11 | export async function initRustBindgen(): Promise { 12 | const buffer = fs.readFileSync(path.join(__dirname, './pkg/wasm_api_bindgen_bg.wasm')) 13 | 14 | await init({ module_or_path: buffer }) 15 | 16 | return { 17 | xorInt32Array: xor_array_i32, 18 | sumFloat64Array: sum_array_f64, 19 | multiply4x4Float32: multiply_4x4_f32, 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /implementations/wasm-zig/src/interop/stack.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const builtin = @import("builtin"); 3 | 4 | var stack: [20]std.builtin.SourceLocation = undefined; 5 | var stackPos: u31 = stack.len; 6 | 7 | pub fn get() []std.builtin.SourceLocation { 8 | return stack[stackPos..]; 9 | } 10 | 11 | pub fn push(src: std.builtin.SourceLocation) void { 12 | if (builtin.mode == .Debug) { 13 | if (stackPos > 0) { 14 | stackPos -= 1; 15 | stack[stackPos] = src; 16 | } 17 | } 18 | } 19 | 20 | pub fn pop() void { 21 | if (builtin.mode == .Debug) { 22 | stackPos += 1; 23 | } 24 | } 25 | 26 | pub fn entry(src: std.builtin.SourceLocation) void { 27 | if (builtin.mode == .Debug) { 28 | stackPos = stack.len - 1; 29 | stack[stackPos] = src; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /.github/workflows/zig-release.yml: -------------------------------------------------------------------------------- 1 | name: Zig Package Release 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | zig-release: 9 | if: startsWith(github.ref, 'refs/tags/zig-v') 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | 14 | - name: Create Zig package 15 | run: | 16 | mkdir zig-package 17 | cp -r implementations/wasm-zig/* zig-package/ 18 | cd zig-package 19 | tar --transform='s|^\./||' -czf ../zaw-wasm.tar.gz . 20 | 21 | - name: Upload to release 22 | uses: actions/upload-release-asset@v1 23 | env: 24 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 25 | with: 26 | upload_url: ${{ github.event.release.upload_url }} 27 | asset_path: ./zaw-wasm.tar.gz 28 | asset_name: zaw-wasm.tar.gz 29 | asset_content_type: application/gzip 30 | -------------------------------------------------------------------------------- /examples/wasm-rust/api_bindgen/lib.rs: -------------------------------------------------------------------------------- 1 | use wasm_bindgen::prelude::*; 2 | use js_sys::Int32Array; 3 | use js_sys::Float32Array; 4 | use js_sys::Float64Array; 5 | 6 | #[wasm_bindgen] 7 | pub fn xor_array_i32(values: &Int32Array) -> i32 { 8 | let vec = values.to_vec(); 9 | 10 | return shared::xor_array_i32(&vec); 11 | } 12 | 13 | #[wasm_bindgen] 14 | pub fn sum_array_f64(values: &Float64Array) -> f64 { 15 | let vec = values.to_vec(); 16 | 17 | return shared::sum_array_f64(&vec); 18 | } 19 | 20 | #[wasm_bindgen] 21 | pub fn multiply_4x4_f32( 22 | a_matrices: &Float32Array, 23 | b_matrices: &Float32Array 24 | ) -> Result { 25 | let a_data: Vec = a_matrices.to_vec(); 26 | let b_data: Vec = b_matrices.to_vec(); 27 | let mut results = vec![0.0f32; a_data.len()]; 28 | 29 | shared::multiply_4x4_f32(&a_data, &b_data, &mut results); 30 | 31 | Ok(Float32Array::from(&results[..])) 32 | } 33 | -------------------------------------------------------------------------------- /examples/wasm-zig/build.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const Builder = std.Build; 3 | 4 | pub fn build(b: *Builder) void { 5 | const target = b.resolveTargetQuery(.{ 6 | .cpu_arch = .wasm32, 7 | .cpu_features_add = std.Target.wasm.featureSet(&.{.simd128}), 8 | .os_tag = .freestanding, 9 | }); 10 | const optimize = b.standardOptimizeOption(.{}); 11 | 12 | const exe = b.addExecutable(.{ 13 | .name = "main", 14 | .root_module = b.createModule(.{ 15 | .root_source_file = b.path("./main.zig"), 16 | .target = target, 17 | .optimize = optimize, 18 | }), 19 | .version = .{ .major = 0, .minor = 0, .patch = 1 }, 20 | }); 21 | 22 | exe.root_module.addImport("zaw", b.addModule("zaw", .{ .root_source_file = b.path("../../implementations/wasm-zig/src/zaw.zig") })); 23 | 24 | // 25 | exe.global_base = 6560; 26 | exe.entry = .disabled; 27 | exe.rdynamic = true; 28 | exe.import_memory = true; 29 | exe.stack_size = std.wasm.page_size; 30 | 31 | b.installArtifact(exe); 32 | } 33 | -------------------------------------------------------------------------------- /docs/contributing.md: -------------------------------------------------------------------------------- 1 | # Contribution Guide 2 | 3 | ## Pull Requests 4 | 5 | - Any PR that expands or updates benchmarks should include updated results in [docs/benchmarks.md](benchmarks.md). 6 | - These must be run on a `c8g` class EC2 instance 7 | - If an appropraite EC2 instance is not available to you, please indicate that a benchmark update is required. 8 | 9 | ## Adding a new WASM implementation 10 | 11 | Start by reviewing `/wasm-zig` and `/examples/wasm-zig`. 12 | 13 | Once you're ready to implement: 14 | 15 | 1. Add your implementation to a new top-level folder `/wasm-` 16 | 2. Add a new example in `/examples//` 17 | 3. Include a `/examples//build.sh` script that compiles a wasm file (should not be committed to source) 18 | 4. Update `/examples/host-typescript/__tests__/builds.ts` to point to the generated wasm file 19 | 5. Add a test generation file in `test-gen/languages/.ts` to generate a full [conduit](conduit.md) test suite 20 | 6. Add a new "bare-bones" example to `/docs/getting-started.md` 21 | 22 | ## Adding a new Host implementation 23 | 24 | At the moment we're not accepting new host implementations - the best way to contribute here is to review our typescript host implementation and provide feedback on the API. 25 | -------------------------------------------------------------------------------- /implementations/wasm-zig/src/simd.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const assert = std.debug.assert; 3 | 4 | // Fixed-lane SIMD operations optimized for WASM 5 | 6 | const WASM_SIMD_BYTES = 128 / 8; 7 | 8 | pub fn getLanes(comptime T: type) comptime_int { 9 | const size = @sizeOf(T); 10 | 11 | switch (size) { 12 | 1, 2, 4, 8 => { 13 | return WASM_SIMD_BYTES / size; 14 | }, 15 | else => { 16 | @compileError("incompatible element type"); 17 | }, 18 | } 19 | } 20 | 21 | pub fn Vec(comptime T: type) type { 22 | return @Vector(getLanes(T), T); 23 | } 24 | 25 | pub fn initVec(comptime T: type) Vec(T) { 26 | switch (getLanes(T)) { 27 | 2 => return .{ 0, 0 }, 28 | 4 => return .{ 0, 0, 0, 0 }, 29 | 8 => return .{ 0, 0, 0, 0, 0, 0, 0, 0 }, 30 | 16 => return .{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, 31 | else => @compileError("incompatible element type"), 32 | } 33 | } 34 | 35 | pub fn sliceToVec(comptime T: type, slice: []T) Vec(T) { 36 | const lanes = getLanes(T); 37 | 38 | assert(slice.len >= lanes); 39 | 40 | const ptr: *const [lanes]T = @ptrCast(slice.ptr); 41 | const arr: [lanes]T = ptr.*; 42 | 43 | return arr; 44 | } 45 | -------------------------------------------------------------------------------- /implementations/wasm-rust/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod conduit; 2 | pub mod interop; 3 | 4 | /// Sets up all required WASM exports for the zaw interop layer. 5 | /// 6 | /// This macro generates the four required exports: 7 | /// - getErrorPtr: Access to error message buffer 8 | /// - getLogPtr: Access to log message buffer 9 | /// - allocateInputChannel: Allocate shared memory for JS→WASM communication 10 | /// - allocateOutputChannel: Allocate shared memory for WASM→JS communication 11 | /// 12 | /// Usage: 13 | /// ```rust 14 | /// zaw::setup_interop!(); 15 | /// ``` 16 | #[macro_export] 17 | macro_rules! setup_interop { 18 | () => { 19 | #[no_mangle] 20 | pub extern "C" fn getErrorPtr() -> i32 { 21 | $crate::interop::error::get_error_ptr() 22 | } 23 | 24 | #[no_mangle] 25 | pub extern "C" fn getLogPtr() -> i32 { 26 | $crate::interop::log::get_log_ptr() 27 | } 28 | 29 | #[no_mangle] 30 | pub extern "C" fn allocateInputChannel(size: i32) -> i32 { 31 | $crate::interop::allocate_input_channel(size) 32 | } 33 | 34 | #[no_mangle] 35 | pub extern "C" fn allocateOutputChannel(size: i32) -> i32 { 36 | $crate::interop::allocate_output_channel(size) 37 | } 38 | }; 39 | } 40 | -------------------------------------------------------------------------------- /examples/utils/index.ts: -------------------------------------------------------------------------------- 1 | export function multiply4x4Float32(a: Float32Array, b: Float32Array): Float32Array { 2 | const c = new Float32Array(16) 3 | 4 | c[0] = a[0] * b[0] + a[1] * b[4] + a[2] * b[8] + a[3] * b[12] 5 | c[1] = a[0] * b[1] + a[1] * b[5] + a[2] * b[9] + a[3] * b[13] 6 | c[2] = a[0] * b[2] + a[1] * b[6] + a[2] * b[10] + a[3] * b[14] 7 | c[3] = a[0] * b[3] + a[1] * b[7] + a[2] * b[11] + a[3] * b[15] 8 | 9 | c[4] = a[4] * b[0] + a[5] * b[4] + a[6] * b[8] + a[7] * b[12] 10 | c[5] = a[4] * b[1] + a[5] * b[5] + a[6] * b[9] + a[7] * b[13] 11 | c[6] = a[4] * b[2] + a[5] * b[6] + a[6] * b[10] + a[7] * b[14] 12 | c[7] = a[4] * b[3] + a[5] * b[7] + a[6] * b[11] + a[7] * b[15] 13 | 14 | c[8] = a[8] * b[0] + a[9] * b[4] + a[10] * b[8] + a[11] * b[12] 15 | c[9] = a[8] * b[1] + a[9] * b[5] + a[10] * b[9] + a[11] * b[13] 16 | c[10] = a[8] * b[2] + a[9] * b[6] + a[10] * b[10] + a[11] * b[14] 17 | c[11] = a[8] * b[3] + a[9] * b[7] + a[10] * b[11] + a[11] * b[15] 18 | 19 | c[12] = a[12] * b[0] + a[13] * b[4] + a[14] * b[8] + a[15] * b[12] 20 | c[13] = a[12] * b[1] + a[13] * b[5] + a[14] * b[9] + a[15] * b[13] 21 | c[14] = a[12] * b[2] + a[13] * b[6] + a[14] * b[10] + a[15] * b[14] 22 | c[15] = a[12] * b[3] + a[13] * b[7] + a[14] * b[11] + a[15] * b[15] 23 | 24 | return c 25 | } 26 | -------------------------------------------------------------------------------- /implementations/host-typescript/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "zaw", 3 | "version": "0.0.4", 4 | "description": "Zero-allocation WebAssembly communication protocol", 5 | "main": "dist/index.js", 6 | "module": "dist/index.mjs", 7 | "types": "dist/index.d.ts", 8 | "exports": { 9 | ".": { 10 | "types": "./dist/index.d.ts", 11 | "import": "./dist/index.mjs", 12 | "require": "./dist/index.js" 13 | } 14 | }, 15 | "files": [ 16 | "dist" 17 | ], 18 | "scripts": { 19 | "build": "tsc && tsup", 20 | "dev": "tsup --watch", 21 | "test": "vitest", 22 | "prepublishOnly": "npm run build" 23 | }, 24 | "devDependencies": { 25 | "tsup": "^8.0.0", 26 | "vitest": "^1.0.0" 27 | }, 28 | "peerDependencies": { 29 | "typescript": "^5.0.0" 30 | }, 31 | "keywords": [ 32 | "webassembly", 33 | "wasm", 34 | "zero-allocation", 35 | "performance", 36 | "interop", 37 | "buffer", 38 | "protocol" 39 | ], 40 | "author": "tristanhoy", 41 | "license": "Apache-2.0", 42 | "repository": { 43 | "type": "git", 44 | "url": "git+https://github.com/stylearcade/zaw.git", 45 | "directory": "implementations/host-typescript" 46 | }, 47 | "homepage": "https://github.com/stylearcade/zaw#readme", 48 | "bugs": { 49 | "url": "https://github.com/stylearcade/zaw/issues" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /implementations/wasm-zig/src/interop.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | const conduit = @import("./conduit.zig"); 4 | 5 | pub const Error = @import("./interop/error.zig"); 6 | pub const OK = Error.OK; 7 | 8 | pub const Stack = @import("./interop/stack.zig"); 9 | 10 | const logModule = @import("./interop/log.zig"); 11 | pub const log = logModule.log; 12 | pub const logf = logModule.logf; 13 | 14 | var input: conduit.Reader = undefined; 15 | var output: conduit.Writer = undefined; 16 | 17 | pub fn getErrorPtr() callconv(.c) i32 { 18 | return Error.getErrorPtr(); 19 | } 20 | 21 | pub fn getLogPtr() callconv(.c) i32 { 22 | return logModule.getLogPtr(); 23 | } 24 | 25 | pub fn allocateInputChannel(sizeInBytes: i32) callconv(.c) i32 { 26 | const sizeInU64s = @divExact(@as(usize, @intCast(sizeInBytes)), 8); 27 | const storage = std.heap.wasm_allocator.alloc(u64, sizeInU64s) catch @panic("Failed to allocate input channel storage"); 28 | const pointer: i32 = @intCast(@intFromPtr(storage.ptr)); 29 | 30 | input = conduit.Reader.from(storage); 31 | 32 | return pointer; 33 | } 34 | 35 | pub fn allocateOutputChannel(sizeInBytes: i32) callconv(.c) i32 { 36 | const sizeInU64s = @divExact(@as(usize, @intCast(sizeInBytes)), 8); 37 | const storage = std.heap.wasm_allocator.alloc(u64, sizeInU64s) catch @panic("Failed to allocate output channel storage"); 38 | const pointer: i32 = @intCast(@intFromPtr(storage.ptr)); 39 | 40 | output = conduit.Writer.from(storage); 41 | 42 | return pointer; 43 | } 44 | 45 | pub fn getInput() conduit.Reader { 46 | input.reset(); 47 | 48 | return input; 49 | } 50 | 51 | pub fn getOutput() conduit.Writer { 52 | output.reset(); 53 | 54 | return output; 55 | } 56 | -------------------------------------------------------------------------------- /scripts/install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | # Install Rust 5 | if ! command -v rustup &> /dev/null; then 6 | echo "Installing rust..." 7 | curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh 8 | fi 9 | 10 | echo "Installing/updating Rust stable toolchain..." 11 | rustup toolchain install stable 12 | rustup target add wasm32-unknown-unknown 13 | 14 | if ! command -v wasm-pack &> /dev/null; then 15 | echo "Installing wasm-pack..." 16 | curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh 17 | fi 18 | 19 | # Install Zig 0.15 20 | if command -v zig &> /dev/null; then 21 | zig_output=$(zig version 2>&1 || true) 22 | 23 | if echo "$zig_output" | grep -qi "no build.zig"; then 24 | echo "Found anyzig" 25 | else 26 | zig_version=$(echo "$zig_output" | cut -d. -f1-2) 27 | if [ "$zig_version" == "0.15" ]; then 28 | echo "Found zig 0.15" 29 | else 30 | echo "❌ Detected zig version $zig_version (required: 0.15)" 31 | echo "Please uninstall your current zig version before proceeding." 32 | echo "Once uninstalled, re-run this script and it will install anyzig in its place." 33 | exit 1 34 | fi 35 | fi 36 | else 37 | echo "Installing anyzig..." 38 | 39 | if uname | grep -q Darwin; then 40 | sudo brew tap anyzig/tap 41 | sudo brew install anyzig 42 | else 43 | sudo curl -L https://github.com/marler8997/anyzig/releases/latest/download/anyzig-x86_64-linux.tar.gz \ 44 | | sudo tar xz -C /usr/local/bin 45 | fi 46 | fi 47 | 48 | # Install WABT 49 | if ! command -v wat2wasm &> /dev/null; then 50 | echo "Installing wabt..." 51 | if uname | grep -q Darwin; then 52 | brew install wabt 53 | else 54 | sudo apt install -y wabt 55 | fi 56 | fi 57 | -------------------------------------------------------------------------------- /examples/wasm-rust/__tests__/bindgen.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest' 2 | import { initRustBindgen } from '../api_bindgen' 3 | import { multiply4x4Float32 } from '../../utils/index.ts' 4 | 5 | describe('Typescript example host', async () => { 6 | const wasm = await initRustBindgen() 7 | 8 | for (const size of [2, 10, 100, 1000, 10000]) { 9 | describe(`XOR Int32Array @ ${size} elements`, () => { 10 | it('should have correct result', () => { 11 | const values = new Int32Array(size).map(() => (Math.random() * 0x100000000) | 0) 12 | 13 | const expectation = values.reduce((a, v) => a ^ v) 14 | 15 | const result = wasm.xorInt32Array(values) 16 | expect(result).to.equal(expectation) 17 | }) 18 | }) 19 | } 20 | 21 | for (const size of [2, 10, 100, 1000, 10000]) { 22 | describe(`Sum Float64Array @ ${size} elements`, () => { 23 | it('should have correct result', () => { 24 | const values = new Float64Array(size).map(() => Math.random()) 25 | 26 | const expectation = values.reduce((a, v) => a + v) 27 | 28 | const result = wasm.sumFloat64Array(values) 29 | expect(result).to.be.closeTo(expectation, 1e-9) 30 | }) 31 | }) 32 | } 33 | 34 | for (const batchSize of [1, 10, 100, 1000]) { 35 | describe(`4x4 Float32 Matrix Multiplication, batch size ${batchSize}`, () => { 36 | it('should have correct result', () => { 37 | const width = 16 * batchSize 38 | const left = new Float32Array(width).map(() => Math.random()) 39 | const right = new Float32Array(width).map(() => Math.random()) 40 | 41 | const expectation = new Float32Array(width) 42 | 43 | for (let i = 0; i < width; i += 16) { 44 | expectation.set(multiply4x4Float32(left.slice(i, i + 16), right.slice(i, i + 16)), i) 45 | } 46 | 47 | const result = wasm.multiply4x4Float32(left, right) 48 | }) 49 | }) 50 | } 51 | }) 52 | -------------------------------------------------------------------------------- /examples/wasm-rust/api_zaw/lib.rs: -------------------------------------------------------------------------------- 1 | use zaw::interop; 2 | use zaw::interop::{Error, OK}; 3 | use shared; 4 | 5 | // Setup all required WASM interop exports 6 | zaw::setup_interop!(); 7 | 8 | #[no_mangle] 9 | pub extern "C" fn throwErrorWithStack() -> i32 { 10 | fn inner() -> Result<(), Error> { 11 | Err(zaw::zaw_error!("Example error message with data: {} {} {}", 1, 2, 3)) 12 | } 13 | 14 | interop::error::handle(inner) 15 | } 16 | 17 | #[no_mangle] 18 | pub extern "C" fn usefulPanic() -> i32 { 19 | zaw::zaw_panic!("Example useful panic message with data: {:?}", "test"); 20 | } 21 | 22 | #[no_mangle] 23 | pub extern "C" fn echo() -> i32 { 24 | let input = interop::get_input(); 25 | 26 | let msg = input.read_array_u8(); 27 | let msg_str = std::str::from_utf8(msg).unwrap_or(""); 28 | 29 | interop::log(&format!("{} from rust", msg_str)); 30 | 31 | OK 32 | } 33 | 34 | #[no_mangle] 35 | pub extern "C" fn xorInt32Array() -> i32 { 36 | let input = interop::get_input(); 37 | let output = interop::get_output(); 38 | 39 | let values = input.read_array_i32(); 40 | let result = shared::xor_array_i32(&values); 41 | 42 | output.write_i32(result); 43 | 44 | OK 45 | } 46 | 47 | #[no_mangle] 48 | pub extern "C" fn sumFloat64Array() -> i32 { 49 | let input = interop::get_input(); 50 | let output = interop::get_output(); 51 | 52 | let values = input.read_array_f64(); 53 | let result = shared::sum_array_f64(&values); 54 | 55 | output.write_f64(result); 56 | 57 | OK 58 | } 59 | 60 | #[no_mangle] 61 | pub extern "C" fn multiply4x4Float32() -> i32 { 62 | let input = interop::get_input(); 63 | let output = interop::get_output(); 64 | 65 | let a_matrices = input.read_array_f32(); 66 | let b_matrices = input.read_array_f32(); 67 | let mut result_matrices = output.init_array_f32(a_matrices.len() as u32); 68 | 69 | shared::multiply_4x4_f32(&a_matrices, &b_matrices, &mut result_matrices); 70 | 71 | OK 72 | } 73 | -------------------------------------------------------------------------------- /implementations/host-typescript/src/binding.ts: -------------------------------------------------------------------------------- 1 | import { Reader, Writer } from './conduit' 2 | import { ZawReturn } from './types' 3 | 4 | type Generator = ( 5 | func: () => ZawReturn, 6 | write: (input: Writer, ...args: Args) => void, 7 | read: (output: Reader, ...args: Args) => Result, 8 | getInput: () => Writer, 9 | getOutput: () => Reader, 10 | handleError: (func: () => ZawReturn) => void, 11 | ) => (...args: Args) => Result 12 | 13 | const cache: Record = {} 14 | 15 | function createGenerator(inputArgCount: number, outputArgCount: number): Generator { 16 | const allArgs = Array.from({ length: inputArgCount }, (_, i) => `arg${i}`) 17 | 18 | const writeArgs = ['input', ...allArgs] 19 | const readArgs = ['output', ...allArgs.slice(0, outputArgCount)] 20 | 21 | const body = `return function(${allArgs.join(', ')}) { 22 | const input = getInput() 23 | 24 | write(${writeArgs.join(', ')}) 25 | 26 | handleError(func) 27 | 28 | const output = getOutput() 29 | 30 | return read(${readArgs.join(', ')}) 31 | }` 32 | 33 | return new Function('func', 'write', 'read', 'getInput', 'getOutput', 'handleError', body) as Generator 34 | } 35 | 36 | export function generateBinding( 37 | func: () => ZawReturn, 38 | write: (input: Writer, ...args: Args) => void, 39 | read: (output: Reader, ...args: Args) => Result, 40 | getInput: () => Writer, 41 | getOutput: () => Reader, 42 | handleError: (func: () => ZawReturn) => void, 43 | ): (...args: Args) => Result { 44 | const inputArgCount = Math.max(write.length - 1, 0) 45 | const outputArgCount = Math.max(read.length - 1, 0) 46 | const cacheKey = `${inputArgCount}_${outputArgCount}` 47 | 48 | let generator = cache[cacheKey] 49 | 50 | if (generator === undefined) { 51 | generator = cache[cacheKey] = createGenerator(inputArgCount, outputArgCount) 52 | } 53 | 54 | return generator(func, write, read, getInput, getOutput, handleError) 55 | } 56 | -------------------------------------------------------------------------------- /implementations/wasm-rust/interop/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::conduit::{Reader, Writer}; 2 | 3 | pub mod error; 4 | pub mod log; 5 | pub mod externs; 6 | 7 | pub use log::{log}; 8 | pub use error::{Error, OK}; 9 | 10 | static mut INPUT: Option> = None; 11 | static mut OUTPUT: Option> = None; 12 | 13 | #[allow(static_mut_refs)] 14 | pub fn get_input() -> &'static mut Reader<'static> { 15 | unsafe { 16 | let reader = INPUT.as_mut().expect("Input channel not initialized"); 17 | 18 | reader.reset(); 19 | 20 | reader 21 | } 22 | } 23 | 24 | #[allow(static_mut_refs)] 25 | pub fn get_output() -> &'static mut Writer<'static> { 26 | unsafe { 27 | let writer = OUTPUT.as_mut().expect("Output channel not initialized"); 28 | 29 | writer.reset(); 30 | 31 | writer 32 | } 33 | } 34 | 35 | fn allocate_buffer(size_in_bytes: i32) -> (*mut u64, usize) { 36 | use std::alloc::{alloc, Layout}; 37 | 38 | let size_in_u64s = (size_in_bytes as usize) / 8; 39 | // Use 16-byte alignment for optimal SIMD performance 40 | let layout = Layout::from_size_align(size_in_bytes as usize, 16).unwrap(); 41 | 42 | unsafe { 43 | let ptr = alloc(layout) as *mut u64; 44 | if ptr.is_null() { 45 | panic!("Failed to allocate channel storage"); 46 | } 47 | 48 | // Touch the memory to force WASM runtime to allocate pages 49 | std::ptr::write_bytes(ptr, 0, size_in_u64s); 50 | 51 | (ptr, size_in_u64s) 52 | } 53 | } 54 | 55 | pub fn allocate_input_channel(size_in_bytes: i32) -> i32 { 56 | let (ptr, size_in_u64s) = allocate_buffer(size_in_bytes); 57 | 58 | unsafe { 59 | let slice = std::slice::from_raw_parts_mut(ptr, size_in_u64s); 60 | INPUT = Some(Reader::from(slice)); 61 | ptr as i32 62 | } 63 | } 64 | 65 | pub fn allocate_output_channel(size_in_bytes: i32) -> i32 { 66 | let (ptr, size_in_u64s) = allocate_buffer(size_in_bytes); 67 | 68 | unsafe { 69 | let slice = std::slice::from_raw_parts_mut(ptr, size_in_u64s); 70 | OUTPUT = Some(Writer::from(slice)); 71 | ptr as i32 72 | } 73 | } 74 | 75 | -------------------------------------------------------------------------------- /examples/host-typescript/host.ts: -------------------------------------------------------------------------------- 1 | import { createInstance, ZawReturn } from '../../implementations/host-typescript/src/index' 2 | 3 | type ExampleExports = { 4 | throwErrorWithStack: () => ZawReturn 5 | usefulPanic: () => ZawReturn 6 | echo: () => ZawReturn 7 | xorInt32Array: () => ZawReturn 8 | sumFloat64Array: () => ZawReturn 9 | multiply4x4Float32: () => ZawReturn 10 | } 11 | 12 | export type ExampleAPI = { 13 | throwErrorWithStack: () => void 14 | usefulPanic: () => void 15 | echo: (msg: string) => string 16 | xorInt32Array: (values: Int32Array) => number 17 | sumFloat64Array: (values: Float64Array) => number 18 | multiply4x4Float32: (left: Float32Array, right: Float32Array) => Float32Array 19 | } 20 | 21 | export async function initExample(wasmBuffer: Buffer): Promise { 22 | let lastLogMsg: string 23 | 24 | const instance = await createInstance(wasmBuffer, { 25 | inputChannelSize: 1_000_000, 26 | outputChannelSize: 1_000_000, 27 | log: message => { 28 | // used to check log is correctly implemented 29 | lastLogMsg = message 30 | console.log(message) 31 | }, 32 | }) 33 | 34 | return { 35 | throwErrorWithStack: instance.bind( 36 | instance.exports.throwErrorWithStack, 37 | () => {}, 38 | () => {}, 39 | ), 40 | usefulPanic: instance.bind( 41 | instance.exports.usefulPanic, 42 | () => {}, 43 | () => {}, 44 | ), 45 | echo: instance.bind( 46 | instance.exports.echo, 47 | (input, msg) => input.writeUtf8String(msg), 48 | output => lastLogMsg, 49 | ), 50 | xorInt32Array: instance.bind( 51 | instance.exports.xorInt32Array, 52 | (input, values) => input.copyInt32Array(values), 53 | output => output.readInt32(), 54 | ), 55 | sumFloat64Array: instance.bind( 56 | instance.exports.sumFloat64Array, 57 | (input, values) => input.copyFloat64Array(values), 58 | output => output.readFloat64(), 59 | ), 60 | multiply4x4Float32: instance.bind( 61 | instance.exports.multiply4x4Float32, 62 | (input, left, right) => { 63 | input.copyFloat32Array(left) 64 | input.copyFloat32Array(right) 65 | }, 66 | output => output.readFloat32Array(), 67 | ), 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /implementations/wasm-rust/interop/error.rs: -------------------------------------------------------------------------------- 1 | static mut ERR_STORAGE: [u8; 256] = [0; 256]; 2 | 3 | pub const OK: i32 = 0; 4 | pub const ERROR: i32 = 1; 5 | 6 | #[derive(Debug)] 7 | pub struct Error { 8 | message: String, 9 | } 10 | 11 | impl Error { 12 | pub fn new(message: String) -> Self { 13 | Self { message } 14 | } 15 | } 16 | 17 | impl std::fmt::Display for Error { 18 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 19 | write!(f, "{}", self.message) 20 | } 21 | } 22 | 23 | impl std::error::Error for Error {} 24 | 25 | pub type Result = std::result::Result; 26 | 27 | #[allow(static_mut_refs)] 28 | pub fn get_error_ptr() -> i32 { 29 | unsafe { ERR_STORAGE.as_ptr() as i32 } 30 | } 31 | 32 | #[allow(static_mut_refs)] 33 | pub fn write_error_to_storage(msg: &str) { 34 | unsafe { 35 | let bytes = msg.as_bytes(); 36 | let to_copy = bytes.len().min(ERR_STORAGE.len() - 1); 37 | ERR_STORAGE[..to_copy].copy_from_slice(&bytes[..to_copy]); 38 | ERR_STORAGE[to_copy] = 0; // Null terminate 39 | } 40 | } 41 | 42 | pub fn handle(func: F) -> i32 43 | where 44 | F: FnOnce() -> Result<()> 45 | { 46 | match func() { 47 | Ok(()) => OK, 48 | Err(err) => { 49 | write_error_to_storage(&err.to_string()); 50 | ERROR 51 | } 52 | } 53 | } 54 | 55 | // Error creation with location info for compatibility with tests 56 | #[macro_export] 57 | macro_rules! zaw_error { 58 | ($msg:expr) => { 59 | $crate::interop::error::Error::new(format!("{}\n at {}:{}", $msg, file!(), line!())) 60 | }; 61 | ($fmt:expr, $($arg:tt)*) => { 62 | $crate::interop::error::Error::new(format!("{}\n at {}:{}", format!($fmt, $($arg)*), file!(), line!())) 63 | }; 64 | } 65 | 66 | #[macro_export] 67 | macro_rules! zaw_panic { 68 | ($msg:expr) => {{ 69 | let msg = format!("{}\n at {}:{}", $msg, file!(), line!()); 70 | $crate::interop::error::write_error_to_storage(&msg); 71 | panic!("{}", msg) 72 | }}; 73 | ($fmt:expr, $($arg:tt)*) => {{ 74 | let msg = format!("{}\n at {}:{}", format!($fmt, $($arg)*), file!(), line!()); 75 | $crate::interop::error::write_error_to_storage(&msg); 76 | panic!("{}", msg) 77 | }}; 78 | } 79 | -------------------------------------------------------------------------------- /examples/host-typescript/__tests__/example.bench.ts: -------------------------------------------------------------------------------- 1 | import os from 'os' 2 | import { bench, describe } from 'vitest' 3 | import { builds } from './builds' 4 | import { initExample } from '../host' 5 | import { initRustBindgen } from '../../wasm-rust/api_bindgen' 6 | import { multiply4x4Float32 } from '../../utils' 7 | 8 | console.log(`--COPY OUTPUT FROM BELOW THIS LINE INTO benchmarks.md---\n\nRunning on ${os.cpus()[0].model}`) 9 | 10 | describe('Typescript example host', async () => { 11 | const zig = await initExample(builds.zig) 12 | const rust = await initExample(builds.rust) 13 | const rustBindgen = await initRustBindgen() 14 | 15 | for (const size of [10, 100, 1_000, 10_000, 100_000]) { 16 | describe(`XOR Int32Array @ ${size} elements`, () => { 17 | const values = new Int32Array(size).map(() => (Math.random() * 0x100000000) | 0) 18 | 19 | bench('js', () => { 20 | let total = 0 21 | 22 | for (let i = values.length; i-- > 0; ) { 23 | total ^= values[i] 24 | } 25 | }) 26 | 27 | bench('zig', () => { 28 | zig.xorInt32Array(values) 29 | }) 30 | 31 | bench('rust', () => { 32 | rust.xorInt32Array(values) 33 | }) 34 | 35 | bench('rust-bindgen', () => { 36 | rustBindgen.xorInt32Array(values) 37 | }) 38 | }) 39 | } 40 | 41 | for (const size of [10, 100, 1_000, 10_000, 100_000]) { 42 | describe(`Sum Float64Array @ ${size} elements`, () => { 43 | const values = new Float64Array(size).map(() => Math.random()) 44 | 45 | bench('js', () => { 46 | let total = 0 47 | 48 | for (let i = values.length; i-- > 0; ) { 49 | total += values[i] 50 | } 51 | }) 52 | 53 | bench('zig', () => { 54 | zig.sumFloat64Array(values) 55 | }) 56 | 57 | bench('rust', () => { 58 | rust.sumFloat64Array(values) 59 | }) 60 | 61 | bench('rust-bindgen', () => { 62 | rustBindgen.sumFloat64Array(values) 63 | }) 64 | }) 65 | } 66 | 67 | for (const batchSize of [1, 10, 100, 1000]) { 68 | describe(`4x4 Float32 Matrix Multiplication, batch size ${batchSize}`, () => { 69 | const width = 16 * batchSize 70 | const left = new Float32Array(width).map(() => Math.random()) 71 | const right = new Float32Array(width).map(() => Math.random()) 72 | 73 | bench('js', () => { 74 | for (let i = 0; i < width; i += 16) { 75 | multiply4x4Float32(left.slice(i, i + 16), right.slice(i, i + 16)) 76 | } 77 | }) 78 | 79 | bench('zig', () => { 80 | zig.multiply4x4Float32(left, right) 81 | }) 82 | 83 | bench('rust', () => { 84 | rust.multiply4x4Float32(left, right) 85 | }) 86 | 87 | bench('rust-bindgen', () => { 88 | rustBindgen.multiply4x4Float32(left, right) 89 | }) 90 | }) 91 | } 92 | }) 93 | -------------------------------------------------------------------------------- /implementations/wasm-zig/src/interop/error.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const builtin = @import("builtin"); 3 | const Stack = @import("./stack.zig"); 4 | 5 | var errStorage = [_]u8{0} ** 256; 6 | 7 | pub const OK: i32 = 0; 8 | pub const ERROR: i32 = 1; 9 | 10 | pub fn getErrorPtr() i32 { 11 | return @intCast(@intFromPtr(&errStorage)); 12 | } 13 | 14 | var cursor: usize = 0; 15 | 16 | fn _startWriting() void { 17 | cursor = 0; // first position holds length 18 | } 19 | 20 | fn _writeFormat(comptime fmt: []const u8, args: anytype) void { 21 | Stack.push(@src()); 22 | defer Stack.pop(); 23 | 24 | const data = std.fmt.bufPrint(errStorage[cursor..], fmt, args) catch unreachable; 25 | 26 | cursor += data.len; 27 | 28 | if (cursor < errStorage.len) { 29 | errStorage[cursor] = 0; 30 | } 31 | } 32 | 33 | fn _writeMessage(msg: []const u8) void { 34 | _writeFormat("{s}", .{msg}); 35 | } 36 | 37 | fn _writeSrc(src: std.builtin.SourceLocation) void { 38 | _writeFormat("\n at {s}:{d}", .{ src.file, src.line }); 39 | } 40 | 41 | fn _writeStack() void { 42 | for (Stack.get()) |stackSrc| { 43 | _writeSrc(stackSrc); 44 | } 45 | } 46 | 47 | fn writeMessage(src: std.builtin.SourceLocation, msg: []const u8) void { 48 | _startWriting(); 49 | _writeMessage(msg); 50 | _writeSrc(src); 51 | _writeStack(); 52 | } 53 | 54 | fn writeFormat(src: std.builtin.SourceLocation, comptime fmt: []const u8, args: anytype) void { 55 | _startWriting(); 56 | _writeFormat(fmt, args); 57 | _writeSrc(src); 58 | _writeStack(); 59 | } 60 | 61 | fn writeError(src: std.builtin.SourceLocation, err: anyerror) void { 62 | _startWriting(); 63 | _writeFormat("{s}", .{@errorName(err)}); 64 | _writeSrc(src); 65 | _writeStack(); 66 | } 67 | 68 | pub fn handlePanic(msg: []const u8, addr: ?usize) noreturn { 69 | _ = addr; 70 | _startWriting(); 71 | _writeMessage(msg); 72 | _writeStack(); 73 | @trap(); 74 | } 75 | 76 | pub fn fromMessage(src: std.builtin.SourceLocation, comptime msg: []const u8) anyerror { 77 | writeMessage(src, msg); 78 | 79 | return error.Custom; 80 | } 81 | 82 | pub fn fromFormat(src: std.builtin.SourceLocation, comptime fmt: []const u8, args: anytype) anyerror { 83 | writeFormat(src, fmt, args); 84 | 85 | return error.Custom; 86 | } 87 | 88 | pub fn fromAny(src: std.builtin.SourceLocation, err: anyerror) anyerror { 89 | writeError(src, err); 90 | 91 | return error.Custom; 92 | } 93 | 94 | pub fn serialize(src: std.builtin.SourceLocation, result: anyerror) i32 { 95 | if (result != error.Custom) { 96 | writeError(src, result); 97 | } 98 | 99 | return ERROR; 100 | } 101 | 102 | pub fn serializeMessage(src: std.builtin.SourceLocation, comptime msg: []const u8) i32 { 103 | return serialize(src, fromMessage(src, msg)); 104 | } 105 | 106 | pub fn serializeFormat(src: std.builtin.SourceLocation, comptime fmt: []const u8, args: anytype) i32 { 107 | return serialize(src, fromFormat(src, fmt, args)); 108 | } 109 | 110 | pub fn handle(func: fn () anyerror!void) i32 { 111 | func() catch |err| return serialize(@src(), err); 112 | 113 | return OK; 114 | } 115 | 116 | pub fn panicFormat(src: std.builtin.SourceLocation, comptime fmt: []const u8, args: anytype) void { 117 | writeFormat(src, fmt, args); 118 | @trap(); 119 | } 120 | 121 | pub fn panic(src: std.builtin.SourceLocation, comptime msg: []const u8) void { 122 | panicFormat(src, msg, .{}); 123 | } 124 | 125 | pub fn assert(src: std.builtin.SourceLocation, value: bool, comptime fmt: []const u8, args: anytype) void { 126 | if (!value) { 127 | if (builtin.mode == .Debug) { 128 | panicFormat(src, fmt, args); 129 | } else { 130 | @trap(); 131 | } 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /implementations/wasm-rust/README.md: -------------------------------------------------------------------------------- 1 | # `zaw` 2 | 3 | ## Zero Allocation WASM @ Style Arcade 4 | 5 | The purpose of `zaw` is to make it easier to achieve the original promise of WebAssembly: 6 | 7 | **High-performance, low-overhead acceleration for targeted code - without rewriting your entire application.** 8 | 9 | ### 🎯 The upshot 10 | 11 | With `zaw`, you'll be able to offload individual algorithms, rather than entire modules, and keep your WebAssembly code lean and simple - truly unlocking the original vision of the WebAssembly founding team. 12 | 13 | ### 🚀 Performance 14 | 15 | **Up to 7x faster than pure JavaScript and 2.5x faster than wasm-bindgen for XOR Int32Array Bench** 16 | 17 | | Element Count | Winner | vs `zaw` | vs `js` | vs `wasm-bindgen` | 18 | | ------------- | ------ | ----------- | ----------- | ----------------- | 19 | | 10 | `js` | 2.0x faster | - | 4.0x faster | 20 | | 100 | `zaw` | - | 1.2x faster | 2.0x faster | 21 | | 1,000 | `zaw` | - | 5.5x faster | 2.6x faster | 22 | | 10,000 | `zaw` | - | 9.9x faster | 2.6x faster | 23 | | 100,000 | `zaw` | - | 9.7x faster | 2.5x faster | 24 | 25 | ### 📦 Installation 26 | 27 | ```bash 28 | # Typescript 29 | npm install zaw 30 | 31 | # Rust 32 | cargo add zaw 33 | ``` 34 | 35 | Or you can just fork [zaw-starter-rust](https://github.com/stylearcade/zaw-starter-rust). 36 | 37 | ### 🔥 Quick Start 38 | 39 | Here's how to sum an array of Float64s using `zaw`. 40 | 41 | This won't actually be fast; check out the [example implementations](https://github.com/stylearcade/zaw/examples) to see what this looks like with full SIMD & batching. 42 | 43 | #### Host Implementation 44 | 45 | ##### Typescript 46 | 47 | ```typescript 48 | import { createInstance } from 'zaw' 49 | 50 | // Low-level WASM API 51 | type WasmExports = { 52 | sumFloat64Array: () => 0 | 1 // 0 = OK, 1 = Error 53 | } 54 | 55 | // High-level API with bindings 56 | type WasmApi = { 57 | sumFloat64Array: (values: Float64Array) => number 58 | } 59 | 60 | export async function initWasmApi(wasmBuffer): Promise { 61 | const instance = await createInstance(wasmBuffer, { 62 | // Reserve 1kb for both input and output channels 63 | inputChannelSize: 1_000, 64 | outputChannelSize: 1_000, 65 | }) 66 | 67 | return { 68 | sumFloat64Array: instance.bind( 69 | // The exported function to bind to 70 | instance.exports.sumFloat64Array, 71 | 72 | // Input binding: copy values into WASM (zero allocation) 73 | (input, values) => input.copyFloat64Array(values), 74 | 75 | // Output binding: read the sum from the output channel 76 | output => output.readFloat64(), 77 | ), 78 | } 79 | } 80 | 81 | // Load your WASM module 82 | const api = await initWasmApi(wasmBuffer) 83 | const numbers = new Float64Array([1.5, 2.3, 3.7, 4.1]) 84 | const sum = api.sumFloat64Array(numbers) 85 | console.log('Sum:', sum) // 9.5 86 | ``` 87 | 88 | #### WASM Implementation 89 | 90 | ```rust 91 | use zaw::interop; 92 | use zaw::interop::error::{Error, OK}; 93 | 94 | // Setup all required WASM interop exports 95 | zaw::setup_interop!(); 96 | 97 | #[no_mangle] 98 | pub extern "C" fn sumFloat64Array() -> i32 { 99 | let input = interop::get_input(); // Get shared input buffer 100 | let output = interop::get_output(); // Get shared output buffer 101 | 102 | let values = input.read_array_f64(); // Read array from JS 103 | 104 | let mut total = 0.0; 105 | for value in values { 106 | total += value; // Simple sum (in reality, use SIMD) 107 | } 108 | 109 | output.write_f64(total); // Write result back to JS 110 | 111 | return OK; 112 | } 113 | ``` 114 | 115 | #### Error Handling 116 | 117 | ```rust 118 | #[no_mangle] 119 | pub extern "C" myFunction() -> i32 { 120 | fn inner() => Result<(), Error> { 121 | // Your logic here 122 | } 123 | 124 | // Will serialize error and return to host (or just return OK) 125 | interop::error::handle(inner) 126 | } 127 | ``` 128 | -------------------------------------------------------------------------------- /docs/getting-started.md: -------------------------------------------------------------------------------- 1 | ### 📦 Installation 2 | 3 | ```bash 4 | # Typescript 5 | npm install zaw 6 | 7 | # Zig 8 | zig fetch https://github.com/stylearcade/zaw/releases/download/zig-v0.0.1/zaw-wasm.tar.gz 9 | 10 | ## build.zig 11 | exe.root_module.addImport("zaw", b.dependency("zaw", .{ 12 | .target = target, 13 | .optimize = optimize, 14 | }).module("zaw")); 15 | 16 | # Rust 17 | 18 | cargo add zaw 19 | ``` 20 | 21 | Or you can just fork [zaw-starter-zig](https://github.com/stylearcade/zaw-starter-zig) or [zaw-starter-rust](https://github.com/stylearcade/zaw-starter-rust). 22 | 23 | ### 🔥 Quick Start 24 | 25 | Here's how to sum an array of Float64s using `zaw`. 26 | 27 | This won't actually be fast; check out the [example implementations](https://github.com/stylearcade/zaw/examples) to see what this looks like with full SIMD & batching. 28 | 29 | #### Host Implementation 30 | 31 | ##### Typescript 32 | 33 | ```typescript 34 | import { createInstance } from 'zaw' 35 | 36 | // Low-level WASM API 37 | type WasmExports = { 38 | sumFloat64Array: () => 0 | 1 // 0 = OK, 1 = Error 39 | } 40 | 41 | // High-level API with bindings 42 | type WasmApi = { 43 | sumFloat64Array: (values: Float64Array) => number 44 | } 45 | 46 | export async function initWasmApi(wasmBuffer): Promise { 47 | const instance = await createInstance(wasmBuffer, { 48 | // Reserve 1kb for both input and output channels 49 | inputChannelSize: 1_000, 50 | outputChannelSize: 1_000, 51 | }) 52 | 53 | return { 54 | sumFloat64Array: instance.bind( 55 | // The exported function to bind to 56 | instance.exports.sumFloat64Array, 57 | 58 | // Input binding: copy values into WASM (zero allocation) 59 | (input, values) => input.copyFloat64Array(values), 60 | 61 | // Output binding: read the sum from the output channel 62 | output => output.readFloat64(), 63 | ), 64 | } 65 | } 66 | 67 | // Load your WASM module 68 | const api = await initWasmApi(wasmBuffer) 69 | const numbers = new Float64Array([1.5, 2.3, 3.7, 4.1]) 70 | const sum = api.sumFloat64Array(numbers) 71 | console.log('Sum:', sum) // 9.5 72 | ``` 73 | 74 | #### WASM Implementation 75 | 76 | ##### Zig 77 | 78 | ```zig 79 | const zaw = @import("zaw"); 80 | 81 | const interop = zaw.interop; 82 | const OK = interop.OK; 83 | 84 | // Setup all required WASM interop exports 85 | comptime { 86 | zaw.setupInterop(); 87 | } 88 | 89 | export fn sumFloat64Array() i32 { 90 | var input = interop.getInput() // Get shared input buffer 91 | var output = interop.getOutput() // Get shared output buffer 92 | 93 | const values = input.readArray(f64) // Read array from JS 94 | 95 | var total: f64 = 0 96 | for (values) |x| total += x // Simple sum (in reality, use SIMD) 97 | 98 | output.write(f64, total) // Write result back to JS 99 | return OK 100 | } 101 | ``` 102 | 103 | ##### Rust 104 | 105 | ```rust 106 | use zaw::interop; 107 | use zaw::interop::error::{Error, OK}; 108 | 109 | // Setup all required WASM interop exports 110 | zaw::setup_interop!(); 111 | 112 | #[no_mangle] 113 | pub extern "C" fn sumFloat64Array() -> i32 { 114 | let input = interop::get_input(); // Get shared input buffer 115 | let output = interop::get_output(); // Get shared output buffer 116 | 117 | let values = input.read_array_f64(); // Read array from JS 118 | 119 | let mut total = 0.0; 120 | for value in values { 121 | total += value; // Simple sum (in reality, use SIMD) 122 | } 123 | 124 | output.write_f64(total); // Write result back to JS 125 | 126 | return OK; 127 | } 128 | ``` 129 | 130 | #### Error Handling 131 | 132 | ##### Zig 133 | 134 | ```zig 135 | fn myFunction_inner() !void { 136 | // Your logic here 137 | } 138 | 139 | export fn myFunction() i32 { 140 | return Error.handle(myFunction_inner); 141 | } 142 | ``` 143 | 144 | ##### Rust 145 | 146 | ```rust 147 | #[no_mangle] 148 | pub extern "C" myFunction() -> i32 { 149 | fn inner() => Result<(), Error> { 150 | // Your logic here 151 | } 152 | 153 | // Will serialize error and return to host (or just return OK) 154 | interop::error::handle(inner) 155 | } 156 | ``` 157 | -------------------------------------------------------------------------------- /examples/host-typescript/__tests__/example.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest' 2 | import { builds } from './builds' 3 | import { initExample } from '../host' 4 | import { multiply4x4Float32 } from '../../utils/index.ts' 5 | 6 | describe('Typescript example host', async () => { 7 | const zig = await initExample(builds.zig) 8 | const rust = await initExample(builds.rust) 9 | 10 | const implementations = { 11 | zig, 12 | rust, 13 | } 14 | 15 | for (const size of [2, 10, 100, 1000, 10000]) { 16 | describe(`XOR Int32Array @ ${size} elements`, () => { 17 | const values = new Int32Array(size).map(() => (Math.random() * 0x100000000) | 0) 18 | 19 | const expectation = values.reduce((a, v) => a ^ v) 20 | 21 | for (const [name, impl] of Object.entries(implementations)) { 22 | describe(name, () => { 23 | it('should have correct result', () => { 24 | const result = impl.xorInt32Array(values) 25 | 26 | expect(result).to.equal(expectation) 27 | }) 28 | }) 29 | } 30 | }) 31 | } 32 | 33 | for (const size of [2, 10, 100, 1000, 10000]) { 34 | describe(`Sum Float64Array @ ${size} elements`, () => { 35 | const values = new Float64Array(size).map(() => Math.random()) 36 | 37 | const expectation = values.reduce((a, v) => a + v) 38 | 39 | for (const [name, impl] of Object.entries(implementations)) { 40 | describe(name, () => { 41 | it('should have correct result', () => { 42 | const result = impl.sumFloat64Array(values) 43 | 44 | expect(result).to.be.closeTo(expectation, 1e-9) 45 | }) 46 | }) 47 | } 48 | }) 49 | } 50 | 51 | for (const size of [1, 10, 100, 1000]) { 52 | describe(`Multiply 4x4 Float32 Matrices @ ${size} matrices`, () => { 53 | const left = new Float32Array(size * 16).map(() => Math.random()) 54 | const right = new Float32Array(size * 16).map(() => Math.random()) 55 | const width = 16 * size 56 | 57 | const expectation = new Float32Array(width) 58 | 59 | for (let i = 0; i < width; i += 16) { 60 | expectation.set(multiply4x4Float32(left.slice(i, i + 16), right.slice(i, i + 16)), i) 61 | } 62 | 63 | for (const [name, impl] of Object.entries(implementations)) { 64 | describe(name, () => { 65 | it('should match expected output', () => { 66 | const result = impl.multiply4x4Float32(left, right) 67 | 68 | expect(result.length).to.equal(expectation.length) 69 | for (let i = 0; i < result.length; i++) { 70 | expect(result[i]).to.be.closeTo(expectation[i], 1e-5) 71 | } 72 | }) 73 | }) 74 | } 75 | }) 76 | } 77 | 78 | describe('Error messages', () => { 79 | describe('zig', () => { 80 | it('should include an error message', () => { 81 | expect(() => zig.throwErrorWithStack()).throws('error message') 82 | }) 83 | it('should throw a zig stack trace', () => { 84 | expect(() => zig.throwErrorWithStack()).throws('main.zig') 85 | }) 86 | }) 87 | describe('rust', () => { 88 | it('should include an error message', () => { 89 | expect(() => rust.throwErrorWithStack()).throws('error message') 90 | }) 91 | it('should throw a rust stack trace', () => { 92 | expect(() => rust.throwErrorWithStack()).throws('lib.rs') 93 | }) 94 | }) 95 | }) 96 | 97 | describe('Panic messages', () => { 98 | describe('zig', () => { 99 | it('should include a useful error message', () => { 100 | expect(() => zig.usefulPanic()).throws('useful panic message') 101 | }) 102 | it('should throw a zig stack trace', () => { 103 | expect(() => zig.usefulPanic()).throws('main.zig') 104 | }) 105 | }) 106 | 107 | describe('rust', () => { 108 | it('should include a useful error message', () => { 109 | expect(() => rust.usefulPanic()).throws('useful panic message') 110 | }) 111 | it('should throw a rust stack trace', () => { 112 | expect(() => rust.usefulPanic()).throws('lib.rs') 113 | }) 114 | }) 115 | }) 116 | 117 | describe('echo', () => { 118 | describe('zig', () => { 119 | it('should echo back', () => { 120 | const result = zig.echo('test message') 121 | 122 | expect(result).to.equal('test message from zig') 123 | }) 124 | }) 125 | 126 | describe('rust', () => { 127 | it('should echo back', () => { 128 | const result = rust.echo('test message') 129 | 130 | expect(result).to.equal('test message from rust') 131 | }) 132 | }) 133 | }) 134 | }) 135 | -------------------------------------------------------------------------------- /implementations/host-typescript/src/interop.ts: -------------------------------------------------------------------------------- 1 | import { Reader, Writer } from './conduit' 2 | import { DEFAULT_INITIAL_PAGES, MAX_ERROR_SIZE, MAX_LOG_SIZE } from './constants' 3 | import { generateBinding } from './binding' 4 | import { ZawReturn } from './types' 5 | 6 | export type InstanceOptions = { 7 | inputChannelSize: number 8 | outputChannelSize: number 9 | initialMemoryPages?: number 10 | log?: (message: string) => void 11 | } 12 | 13 | export type ExportBase = Record number> & { 14 | getLogPtr: () => number 15 | getErrorPtr: () => number 16 | allocateInputChannel: (sizeInBytes: number) => number 17 | allocateOutputChannel: (sizeInBytes: number) => number 18 | } 19 | 20 | export type BindingFactory = ( 21 | func: () => ZawReturn, 22 | write: (input: Writer, ...args: Args) => void, 23 | read: (output: Reader, ...args: Args) => Result, 24 | ) => (...args: Args) => Result 25 | 26 | export type Instance> = { 27 | getMemory: () => ArrayBuffer 28 | getBytes: () => Uint8ClampedArray 29 | exports: ExportBase & T 30 | createView: (init: (buffer: ArrayBuffer) => T) => () => T 31 | getInput: () => Writer 32 | getOutput: () => Reader 33 | handleError: (func: () => number) => void 34 | getSize: () => number 35 | bind: BindingFactory 36 | } 37 | 38 | export async function createInstance>( 39 | wasmBuffer: Buffer, 40 | options: InstanceOptions, 41 | ): Promise> { 42 | const { inputChannelSize, outputChannelSize, initialMemoryPages = DEFAULT_INITIAL_PAGES, log = console.log.bind(console) } = options 43 | const memory = new WebAssembly.Memory({ initial: initialMemoryPages }) 44 | 45 | const imports = { 46 | env: { 47 | memory, 48 | hostLog: () => { 49 | hostLog() // has to be hoisted 50 | }, 51 | }, 52 | } 53 | 54 | const { instance } = await WebAssembly.instantiate(wasmBuffer, imports) 55 | 56 | const exports = instance.exports as ExportBase & T 57 | 58 | const createView = (createFunc: (buffer: ArrayBuffer) => T): (() => T) => { 59 | let buffer: ArrayBuffer 60 | let instance: T 61 | 62 | return () => { 63 | if (instance === undefined || memory.buffer !== buffer) { 64 | buffer = memory.buffer 65 | instance = createFunc(buffer) 66 | } 67 | 68 | return instance 69 | } 70 | } 71 | 72 | const logPtr = exports.getLogPtr() 73 | const errPtr = exports.getErrorPtr() 74 | const inputPtr = exports.allocateInputChannel(inputChannelSize) 75 | const outputPtr = exports.allocateOutputChannel(outputChannelSize) 76 | 77 | const getBytes = createView(buffer => new Uint8ClampedArray(buffer)) 78 | const getLogData = createView(buffer => new Uint8ClampedArray(buffer, logPtr, MAX_LOG_SIZE)) 79 | const getErrorData = createView(buffer => new Uint8ClampedArray(buffer, errPtr, MAX_ERROR_SIZE)) 80 | const getInputChannel = createView(buffer => new Writer(buffer, inputPtr, inputChannelSize)) 81 | const getOutputChannel = createView(buffer => new Reader(buffer, outputPtr, outputChannelSize)) 82 | 83 | const hostLog = (): void => { 84 | const data = getLogData() 85 | const length = data.indexOf(0) 86 | const message = Buffer.from(data.subarray(0, length)).toString('utf8') 87 | 88 | log(message) 89 | } 90 | 91 | const throwWasmError = (e?: Error): void => { 92 | const data = getErrorData() 93 | const length = data.indexOf(0) 94 | 95 | if (length > 0) { 96 | const message = Buffer.from(data.subarray(0, length)).toString('utf8') 97 | 98 | throw Error(message) 99 | } else if (e !== undefined) { 100 | throw e 101 | } else { 102 | throw Error('Unknown error') 103 | } 104 | } 105 | 106 | const handleError = (func: () => number): void => { 107 | let result 108 | 109 | try { 110 | result = func() 111 | } catch (e) { 112 | throwWasmError(e as Error) 113 | } 114 | 115 | if (result !== 0) { 116 | throwWasmError() 117 | } 118 | } 119 | 120 | const getInput = (): Writer => { 121 | const input = getInputChannel() 122 | 123 | input.reset() 124 | 125 | return input 126 | } 127 | 128 | const getOutput = (): Reader => { 129 | const input = getOutputChannel() 130 | 131 | input.reset() 132 | 133 | return input 134 | } 135 | 136 | const bind: BindingFactory = ( 137 | func: () => ZawReturn, 138 | write: (input: Writer, ...args: T) => void, 139 | read: (output: Reader, ...args: T) => R, 140 | ) => generateBinding(func, write, read, getInput, getOutput, handleError) 141 | 142 | return { 143 | exports, 144 | getMemory: () => memory.buffer, 145 | getSize: () => memory.buffer.byteLength, 146 | createView, 147 | getBytes, 148 | getInput, 149 | getOutput, 150 | handleError, 151 | bind, 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /examples/wasm-rust/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "api_zaw" 7 | version = "0.1.0" 8 | dependencies = [ 9 | "shared", 10 | "zaw", 11 | ] 12 | 13 | [[package]] 14 | name = "bumpalo" 15 | version = "3.18.1" 16 | source = "registry+https://github.com/rust-lang/crates.io-index" 17 | checksum = "793db76d6187cd04dff33004d8e6c9cc4e05cd330500379d2394209271b4aeee" 18 | 19 | [[package]] 20 | name = "cfg-if" 21 | version = "1.0.0" 22 | source = "registry+https://github.com/rust-lang/crates.io-index" 23 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 24 | 25 | [[package]] 26 | name = "js-sys" 27 | version = "0.3.77" 28 | source = "registry+https://github.com/rust-lang/crates.io-index" 29 | checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" 30 | dependencies = [ 31 | "once_cell", 32 | "wasm-bindgen", 33 | ] 34 | 35 | [[package]] 36 | name = "log" 37 | version = "0.4.27" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" 40 | 41 | [[package]] 42 | name = "once_cell" 43 | version = "1.21.3" 44 | source = "registry+https://github.com/rust-lang/crates.io-index" 45 | checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" 46 | 47 | [[package]] 48 | name = "proc-macro2" 49 | version = "1.0.95" 50 | source = "registry+https://github.com/rust-lang/crates.io-index" 51 | checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" 52 | dependencies = [ 53 | "unicode-ident", 54 | ] 55 | 56 | [[package]] 57 | name = "quote" 58 | version = "1.0.40" 59 | source = "registry+https://github.com/rust-lang/crates.io-index" 60 | checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" 61 | dependencies = [ 62 | "proc-macro2", 63 | ] 64 | 65 | [[package]] 66 | name = "rustversion" 67 | version = "1.0.21" 68 | source = "registry+https://github.com/rust-lang/crates.io-index" 69 | checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" 70 | 71 | [[package]] 72 | name = "shared" 73 | version = "0.1.0" 74 | 75 | [[package]] 76 | name = "syn" 77 | version = "2.0.101" 78 | source = "registry+https://github.com/rust-lang/crates.io-index" 79 | checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" 80 | dependencies = [ 81 | "proc-macro2", 82 | "quote", 83 | "unicode-ident", 84 | ] 85 | 86 | [[package]] 87 | name = "unicode-ident" 88 | version = "1.0.18" 89 | source = "registry+https://github.com/rust-lang/crates.io-index" 90 | checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" 91 | 92 | [[package]] 93 | name = "wasm-bindgen" 94 | version = "0.2.100" 95 | source = "registry+https://github.com/rust-lang/crates.io-index" 96 | checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" 97 | dependencies = [ 98 | "cfg-if", 99 | "once_cell", 100 | "rustversion", 101 | "wasm-bindgen-macro", 102 | ] 103 | 104 | [[package]] 105 | name = "wasm-bindgen-backend" 106 | version = "0.2.100" 107 | source = "registry+https://github.com/rust-lang/crates.io-index" 108 | checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" 109 | dependencies = [ 110 | "bumpalo", 111 | "log", 112 | "proc-macro2", 113 | "quote", 114 | "syn", 115 | "wasm-bindgen-shared", 116 | ] 117 | 118 | [[package]] 119 | name = "wasm-bindgen-macro" 120 | version = "0.2.100" 121 | source = "registry+https://github.com/rust-lang/crates.io-index" 122 | checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" 123 | dependencies = [ 124 | "quote", 125 | "wasm-bindgen-macro-support", 126 | ] 127 | 128 | [[package]] 129 | name = "wasm-bindgen-macro-support" 130 | version = "0.2.100" 131 | source = "registry+https://github.com/rust-lang/crates.io-index" 132 | checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" 133 | dependencies = [ 134 | "proc-macro2", 135 | "quote", 136 | "syn", 137 | "wasm-bindgen-backend", 138 | "wasm-bindgen-shared", 139 | ] 140 | 141 | [[package]] 142 | name = "wasm-bindgen-shared" 143 | version = "0.2.100" 144 | source = "registry+https://github.com/rust-lang/crates.io-index" 145 | checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" 146 | dependencies = [ 147 | "unicode-ident", 148 | ] 149 | 150 | [[package]] 151 | name = "wasm_api_bindgen" 152 | version = "0.1.0" 153 | dependencies = [ 154 | "js-sys", 155 | "shared", 156 | "wasm-bindgen", 157 | "web-sys", 158 | ] 159 | 160 | [[package]] 161 | name = "web-sys" 162 | version = "0.3.77" 163 | source = "registry+https://github.com/rust-lang/crates.io-index" 164 | checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" 165 | dependencies = [ 166 | "js-sys", 167 | "wasm-bindgen", 168 | ] 169 | 170 | [[package]] 171 | name = "zaw" 172 | version = "0.0.3" 173 | -------------------------------------------------------------------------------- /test-gen/languages/zig.ts: -------------------------------------------------------------------------------- 1 | import { TestCase, Operation, TestGenerator, DataType } from '../types' 2 | 3 | type ZigDataType = 'u8' | 'u32' | 'i32' | 'f32' | 'f64' 4 | 5 | const dataTypeMap: Record = { 6 | Uint8: 'u8', 7 | Uint32: 'u32', 8 | Int32: 'i32', 9 | Float32: 'f32', 10 | Float64: 'f64', 11 | } 12 | 13 | // Format a value for Zig based on its type 14 | function formatValue(value: number, dataType: ZigDataType): string { 15 | if (dataType === 'f32' || dataType === 'f64') { 16 | // Special case: handle negative zero 17 | if (Object.is(value, -0)) { 18 | return '-0.0' 19 | } 20 | 21 | // Special case: float literals in Zig need decimal point 22 | if (Number.isInteger(value) && !String(value).includes('e')) { 23 | return `${value}.0` 24 | } 25 | } 26 | 27 | // Special case: large u32 values need explicit type annotation 28 | if (dataType === 'u32' && value > 0x7fffffff) { 29 | return `@as(u32, 0x${value.toString(16)})` 30 | } 31 | 32 | // Default case: just convert to string 33 | return `${value}` 34 | } 35 | 36 | // Generate a single operation for Zig 37 | function generateWriteOperation(op: Operation, index: number): string[] { 38 | const dataType = dataTypeMap[op.dataType] 39 | 40 | switch (op.type) { 41 | case 'write': 42 | return [`writer.write(${dataType}, ${formatValue(op.value, dataType)});`] 43 | 44 | case 'init': 45 | return [`const ptr${index} = writer.init(${dataType});`, `ptr${index}.* = ${formatValue(op.value, dataType)};`] 46 | 47 | case 'copyArray': 48 | case 'copyElements': 49 | return [ 50 | `var arr${index} = [_]${dataType}{${op.value.map(v => formatValue(v, dataType)).join(', ')}};`, 51 | `writer.${op.type}(${dataType}, &arr${index});`, 52 | ] 53 | 54 | case 'initArray': 55 | case 'initElements': 56 | return [ 57 | `const arr${index} = writer.${op.type}(${dataType}, ${op.value.length});`, 58 | ...op.value.map((v, i) => `arr${index}[${i}] = ${formatValue(v, dataType)};`), 59 | ] 60 | } 61 | } 62 | 63 | function generateReadOperation(op: Operation, index: number): string[] { 64 | const dataType = dataTypeMap[op.dataType] 65 | 66 | switch (op.type) { 67 | case 'write': 68 | case 'init': 69 | return [`try expectEqual(${formatValue(op.value, dataType)}, reader.read(${dataType}));`] 70 | 71 | case 'copyArray': 72 | return [`try expectEqualSlices(${dataType}, &arr${index}, reader.readArray(${dataType}));`] 73 | 74 | case 'copyElements': 75 | return [`try expectEqualSlices(${dataType}, &arr${index}, reader.readElements(${dataType}, ${op.value.length}));`] 76 | 77 | case 'initArray': 78 | return [`try expectEqualSlices(${dataType}, arr${index}, reader.readArray(${dataType}));`] 79 | 80 | case 'initElements': 81 | return [`try expectEqualSlices(${dataType}, arr${index}, reader.readElements(${dataType}, ${op.value.length}));`] 82 | } 83 | } 84 | 85 | // Generate a complete Zig test 86 | function generateTestCase(testCase: TestCase): string { 87 | const indent = (lines: string[]): string => { 88 | return ` ${lines.join('\n ')}\n` 89 | } 90 | 91 | let code = `test "${testCase.name}" {\n` 92 | 93 | code += indent(['var storage = [_]u64{0} ** 32;', 'var writer = Writer.from(&storage);', 'var reader = Reader.from(&storage);']) 94 | 95 | code += '\n' 96 | 97 | // Generate operations 98 | for (let i = 0; i < testCase.operations.length; i++) { 99 | code += indent(generateWriteOperation(testCase.operations[i], i)) 100 | } 101 | 102 | code += '\n' 103 | 104 | // Verify buffer contents 105 | code += indent([ 106 | `const expected = [_]u8{ ${testCase.expectation.join(', ')} };`, 107 | // `try expectEqual(expected.len, writer.channel.offset(u8));`, 108 | `try expectEqualSlices(u8, &expected, writer.channel.storage(u8)[0..${testCase.expectation.length}]);`, 109 | ]) 110 | 111 | code += '\n' 112 | 113 | for (let i = 0; i < testCase.operations.length; i++) { 114 | code += indent(generateReadOperation(testCase.operations[i], i)) 115 | } 116 | 117 | code += `}` 118 | 119 | return code 120 | } 121 | 122 | // Generate all Zig tests 123 | function generateTestFile(testCases: TestCase[]): string { 124 | const parts: string[] = [ 125 | `const std = @import("std"); 126 | const testing = std.testing; 127 | const expect = testing.expect; 128 | const expectEqual = testing.expectEqual; 129 | const expectEqualSlices = testing.expectEqualSlices; 130 | const conduit = @import("conduit.zig"); 131 | const Writer = conduit.Writer; 132 | const Reader = conduit.Reader; 133 | `, 134 | ] 135 | 136 | for (const testCase of testCases) { 137 | parts.push(generateTestCase(testCase)) 138 | } 139 | 140 | return parts.join('\n\n') 141 | } 142 | 143 | export const zigGenerator: TestGenerator = { 144 | outputFile: 'implementations/wasm-zig/src/conduit/conduit.test.zig', 145 | generateTestFile, 146 | } 147 | -------------------------------------------------------------------------------- /implementations/host-typescript/README.md: -------------------------------------------------------------------------------- 1 | # `zaw` 2 | 3 | ## Zero Allocation WASM @ Style Arcade 4 | 5 | The purpose of `zaw` is to make it easier to achieve the original promise of WebAssembly: 6 | 7 | **High-performance, low-overhead acceleration for targeted code - without rewriting your entire application.** 8 | 9 | ### 🎯 The upshot 10 | 11 | With `zaw`, you'll be able to offload individual algorithms, rather than entire modules, and keep your WebAssembly code lean and simple - truly unlocking the original vision of the WebAssembly founding team. 12 | 13 | ### 🚀 Performance 14 | 15 | **Up to 7x faster than pure JavaScript and 2.5x faster than wasm-bindgen for XOR Int32Array Bench** 16 | 17 | | Element Count | Winner | vs `zaw` | vs `js` | vs `wasm-bindgen` | 18 | | ------------- | ------ | ----------- | ----------- | ----------------- | 19 | | 10 | `js` | 1.9x faster | - | 4.2x faster | 20 | | 100 | `zaw` | - | 1.4x faster | 2.2x faster | 21 | | 1,000 | `zaw` | - | 5.6x faster | 2.5x faster | 22 | | 10,000 | `zaw` | - | 7.1x faster | 2.3x faster | 23 | | 100,000 | `zaw` | - | 7.1x faster | 2.4x faster | 24 | 25 | ### 📦 Installation 26 | 27 | ```bash 28 | npm install zaw 29 | ``` 30 | 31 | ### 🔥 Quick Start 32 | 33 | Here's how to sum an array of Float64s using `zaw`. 34 | 35 | This won't actually be fast; check out the [example implementations](https://github.com/stylearcade/zaw/examples) to see what this looks like with full SIMD & batching. 36 | 37 | #### Host Implementation 38 | 39 | ##### Typescript 40 | 41 | ```typescript 42 | import { createInstance } from 'zaw' 43 | 44 | // Low-level WASM API 45 | type WasmExports = { 46 | sumFloat64Array: () => 0 | 1 // 0 = OK, 1 = Error 47 | } 48 | 49 | // High-level API with bindings 50 | type WasmApi = { 51 | sumFloat64Array: (values: Float64Array) => number 52 | } 53 | 54 | export async function initWasmApi(wasmBuffer): Promise { 55 | const instance = await createInstance(wasmBuffer, { 56 | // Reserve 1kb for both input and output channels 57 | inputChannelSize: 1_000, 58 | outputChannelSize: 1_000, 59 | }) 60 | 61 | return { 62 | sumFloat64Array: instance.bind( 63 | // The exported function to bind to 64 | instance.exports.sumFloat64Array, 65 | 66 | // Input binding: copy values into WASM (zero allocation) 67 | (input, values) => input.copyFloat64Array(values), 68 | 69 | // Output binding: read the sum from the output channel 70 | output => output.readFloat64(), 71 | ), 72 | } 73 | } 74 | 75 | // Load your WASM module 76 | const api = await initWasmApi(wasmBuffer) 77 | const numbers = new Float64Array([1.5, 2.3, 3.7, 4.1]) 78 | const sum = api.sumFloat64Array(numbers) 79 | console.log('Sum:', sum) // 9.5 80 | ``` 81 | 82 | #### WASM Implementation 83 | 84 | ##### Zig 85 | 86 | ```zig 87 | const zaw = @import("zaw"); 88 | 89 | const interop = zaw.interop; 90 | const OK = interop.OK; 91 | 92 | // Setup all required WASM interop exports 93 | comptime { 94 | zaw.setupInterop(); 95 | } 96 | 97 | export fn sumFloat64Array() i32 { 98 | var input = interop.getInput() // Get shared input buffer 99 | var output = interop.getOutput() // Get shared output buffer 100 | 101 | const values = input.readArray(f64) // Read array from JS 102 | 103 | var total: f64 = 0 104 | for (values) |x| total += x // Simple sum (in reality, use SIMD) 105 | 106 | output.write(f64, total) // Write result back to JS 107 | return OK 108 | } 109 | ``` 110 | 111 | ##### Rust 112 | 113 | ```rust 114 | use zaw::interop; 115 | use zaw::interop::error::{Error, OK}; 116 | 117 | // Setup all required WASM interop exports 118 | zaw::setup_interop!(); 119 | 120 | #[no_mangle] 121 | pub extern "C" fn sumFloat64Array() -> i32 { 122 | let input = interop::get_input(); // Get shared input buffer 123 | let output = interop::get_output(); // Get shared output buffer 124 | 125 | let values = input.read_array_f64(); // Read array from JS 126 | 127 | let mut total = 0.0; 128 | for value in values { 129 | total += value; // Simple sum (in reality, use SIMD) 130 | } 131 | 132 | output.write_f64(total); // Write result back to JS 133 | 134 | return OK; 135 | } 136 | ``` 137 | 138 | #### Error Handling 139 | 140 | ##### Zig 141 | 142 | ```zig 143 | fn myFunction_inner() !void { 144 | // Your logic here 145 | } 146 | 147 | export fn myFunction() i32 { 148 | return Error.handle(myFunction_inner); 149 | } 150 | ``` 151 | 152 | ##### Rust 153 | 154 | ```rust 155 | #[no_mangle] 156 | pub extern "C" myFunction() -> i32 { 157 | fn inner() => Result<(), Error> { 158 | // Your logic here 159 | } 160 | 161 | // Will serialize error and return to host (or just return OK) 162 | interop::error::handle(inner) 163 | } 164 | ``` 165 | -------------------------------------------------------------------------------- /docs/protocol-interop.md: -------------------------------------------------------------------------------- 1 | # `zaw::interop` WASM <> Host Interop Protocol 2 | 3 | ## 1. Overview 4 | 5 | This document specifies how the [Fixed-Buffer Channel](channel.md) can be incorporated into a WASM module, in addition to "batteries-included" features for logging and error handling. 6 | 7 | ## 2. Conformance 8 | 9 | Implementations **MUST**: 10 | 11 | 1. Use the Fixed‑Buffer Channel memory layout (8‑byte aligned buffer) for both input and output channels, allocating separate regions for each. 12 | 13 | 2. Export the Protocol Methods (Section 4). 14 | 15 | 3. Reserve two static regions in linear memory for error and log message.: 16 | 17 | 4. Follow the error/log workflow defined in Section 6. 18 | 19 | ## 3. Terminology 20 | 21 | - **Module**: The WebAssembly module implementing accelerated logic. 22 | - **Host**: The environment initializing and interacting with the Module. 23 | - **Input Channel**: The shared memory region used to transfer method arguments from the WASM Host to the WASM Module, using a Channel Writer on the Host side and a Channel Reader on the Module side. 24 | - **Output Channel**: The shared memory region used to transfer return data from the WASM Module to the WASM Host, using a Channel Writer on the Module side and a Channel Reader on the Host side. 25 | - **Error Region**: A static, 256-byte region of Module memory for null-terminated error messages. 26 | - **Log Region**: A static, 1024-byte region of Module memory for null-terminated log messages. 27 | 28 | ## 4. Protocol Methods (WASM Exports) 29 | 30 | | Method Signature | Details | 31 | | ---------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | 32 | | `allocateInputChannel(sizeInBytes: Int32) => Int32` | - Allocates a block of memory of the desired size
- Constructs a Channel Reader
- Returns an integer pointer to the allocated memory region | 33 | | `allocateOutputChannel(sizeInBytes: Int32) => Int32` | - Allocates a block of memory of the desired size
- Constructs a Channel Writer
- Returns an integer pointer to the allocated memory region | 34 | | `getErrorPtr() => Int32` | - Returns a pointer to a 256-byte, static region in memory used to store null-terminated error messages | 35 | | `getLogPtr() => Int32` | - Returns a pointer to a 1024-byte, static region in memory used to store null-terminated log messages | 36 | 37 | ## 5. WASM Host Requirements 38 | 39 | 1. **Initialization**: During startup, hosts **MUST**: 40 | - Call `allocateInputChannel` with the desired size, retain the returned pointer, slice a buffer view from the WASM memory and construct a Channel Writer. 41 | - Call `allocateOutputChannel` with the desired size and retain the returned pointer, slice a buffer view from the WASM memory and construct a Channel Writer. 42 | 2. **Channel Binding**: hosts **MUST** maintain freshly bound Channels: 43 | - If `memory.grow()` is invoked, the backing storage of any Channel Readers or Writers will become stale, however the pointers returned by `allocateInputChannel` and `allocateOutputChannel` will remain valid due to the lineary memory model of WebAssembly 44 | - In this scenario, hosts **MUST** rebind any buffer views (using the previously returned pointers) and replace or refresh any Channels with the new buffer views. 45 | - Hosts **MUST NOT** call `allocateInputChannel` or `allocateOutputChannel` again. 46 | 3. **Calling conventions**: 47 | - Before invoking a module function, the host must fetch a fresh `Channel Writer` and write any input arguments 48 | - After execution, check the return code: 49 | - `0` (`OK`): Fetch a fresh `Channel Reader` and read output data from output channel. 50 | - `1` (`ERROR`): Call `getErrorPtr()` and read a null‑terminated string to retrieve the error message; propagate or throw. 51 | - If execution fails (`panic`): 52 | - Call `getErrorPtr()` and read a null‑terminated string to retrieve the error message; propagate or throw. 53 | 4. **Import definitions**: 54 | - Expose a `hostLog()` external method that uses `getLogPtr()` to read a null-terminated log message and print to console / stdout 55 | 56 | ## 6. WASM Module Logging & Error Flows 57 | 58 | 1. **On Success**: 59 | 60 | - Module writes 0 on the host-visible return. 61 | 62 | - Error Region is set to a single null byte ("\0"). 63 | 64 | 2. **On Error**: 65 | 66 | - Module writes 1 on return. 67 | 68 | - Module writes a null‑terminated error message into the Error Region. 69 | 70 | 3. **On Panic**: 71 | 72 | - Module traps or returns a special panic code (e.g., 2), then writes message in Error Region. 73 | 74 | 4. **Logging**: 75 | 76 | - Module may log messages at any time by writing into the Log Region and then invoking the imported `log()` hook. 77 | -------------------------------------------------------------------------------- /examples/wasm-zig/main.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | const zaw = @import("zaw"); 4 | 5 | const interop = zaw.interop; 6 | const Error = interop.Error; 7 | const OK = interop.OK; 8 | const Stack = interop.Stack; 9 | 10 | const simd = zaw.simd; 11 | const Vec = simd.Vec; 12 | 13 | // Setup all required WASM interop exports 14 | comptime { 15 | zaw.setupInterop(); 16 | } 17 | 18 | fn _throwErrorWithStack() !void { 19 | Stack.push(@src()); 20 | defer Stack.pop(); 21 | 22 | return Error.fromFormat(@src(), "Example error message with data: {d} {d} {d}", .{ 1, 2, 3 }); 23 | } 24 | 25 | export fn throwErrorWithStack() i32 { 26 | Stack.entry(@src()); 27 | defer Stack.pop(); 28 | 29 | return Error.handle(_throwErrorWithStack); 30 | } 31 | 32 | export fn usefulPanic() i32 { 33 | Stack.entry(@src()); 34 | defer Stack.pop(); 35 | 36 | Error.panicFormat(@src(), "Example useful panic message with data: {any}", .{ .key = "value" }); 37 | 38 | return OK; 39 | } 40 | 41 | export fn echo() i32 { 42 | var input = interop.getInput(); 43 | 44 | const msg = input.readArray(u8); 45 | 46 | interop.logf("{s} from zig", .{msg}); 47 | 48 | return OK; 49 | } 50 | 51 | export fn xorInt32Array() i32 { 52 | var input = interop.getInput(); 53 | var output = interop.getOutput(); 54 | 55 | const values = input.readArray(i32); 56 | const len = values.len; 57 | const lanes = simd.getLanes(i32); 58 | const batchSize = lanes * 4; 59 | 60 | var acc: [4]Vec(i32) = .{ 61 | simd.initVec(i32), 62 | simd.initVec(i32), 63 | simd.initVec(i32), 64 | simd.initVec(i32), 65 | }; 66 | var i: usize = 0; 67 | 68 | while (i + batchSize <= len) : (i += batchSize) { 69 | const offset = values[i..]; 70 | 71 | acc[0] ^= simd.sliceToVec(i32, offset); 72 | acc[1] ^= simd.sliceToVec(i32, offset[lanes..]); 73 | acc[2] ^= simd.sliceToVec(i32, offset[lanes * 2 ..]); 74 | acc[3] ^= simd.sliceToVec(i32, offset[lanes * 3 ..]); 75 | } 76 | 77 | while (i + lanes <= len) : (i += lanes) { 78 | acc[0] ^= simd.sliceToVec(i32, values[i..]); 79 | } 80 | 81 | var total: i32 = 0; 82 | 83 | for (0..lanes) |x| total ^= acc[0][x] ^ acc[1][x] ^ acc[2][x] ^ acc[3][x]; 84 | 85 | if (i < len) { 86 | for (values[i..len]) |x| total ^= x; 87 | } 88 | 89 | output.write(i32, total); 90 | 91 | return OK; 92 | } 93 | 94 | export fn sumFloat64Array() i32 { 95 | var input = interop.getInput(); 96 | var output = interop.getOutput(); 97 | 98 | const values = input.readArray(f64); 99 | const len = values.len; 100 | const lanes = simd.getLanes(f64); 101 | const batchSize = lanes * 4; 102 | 103 | var acc: [4]Vec(f64) = .{ 104 | simd.initVec(f64), 105 | simd.initVec(f64), 106 | simd.initVec(f64), 107 | simd.initVec(f64), 108 | }; 109 | var i: usize = 0; 110 | 111 | while (i + batchSize <= len) : (i += batchSize) { 112 | const offset = values[i..]; 113 | 114 | acc[0] += simd.sliceToVec(f64, offset); 115 | acc[1] += simd.sliceToVec(f64, offset[lanes..]); 116 | acc[2] += simd.sliceToVec(f64, offset[lanes * 2 ..]); 117 | acc[3] += simd.sliceToVec(f64, offset[lanes * 3 ..]); 118 | } 119 | 120 | while (i + lanes <= len) : (i += lanes) { 121 | acc[0] += simd.sliceToVec(f64, values[i..]); 122 | } 123 | 124 | var total: f64 = 0; 125 | 126 | for (0..lanes) |x| total += acc[0][x] + acc[1][x] + acc[2][x] + acc[3][x]; 127 | 128 | if (i < len) { 129 | for (values[i..len]) |x| total += x; 130 | } 131 | 132 | output.write(f64, total); 133 | 134 | return OK; 135 | } 136 | 137 | fn multiply4x4Float32Single(a: []const f32, b: []const f32, result: []f32) void { 138 | // Load B matrix columns as SIMD vectors 139 | const b_col0 = simd.Vec(f32){ b[0], b[1], b[2], b[3] }; 140 | const b_col1 = simd.Vec(f32){ b[4], b[5], b[6], b[7] }; 141 | const b_col2 = simd.Vec(f32){ b[8], b[9], b[10], b[11] }; 142 | const b_col3 = simd.Vec(f32){ b[12], b[13], b[14], b[15] }; 143 | 144 | // Process each row of A matrix using comptime loop unrolling 145 | inline for (0..4) |row| { 146 | const base_idx = row * 4; 147 | 148 | // Create splat vectors for each element in the row 149 | const a0_splat: simd.Vec(f32) = @splat(a[base_idx + 0]); 150 | const a1_splat: simd.Vec(f32) = @splat(a[base_idx + 1]); 151 | const a2_splat: simd.Vec(f32) = @splat(a[base_idx + 2]); 152 | const a3_splat: simd.Vec(f32) = @splat(a[base_idx + 3]); 153 | 154 | // Compute the matrix multiplication for this row 155 | const row_result = a0_splat * b_col0 + a1_splat * b_col1 + a2_splat * b_col2 + a3_splat * b_col3; 156 | 157 | // Store the results 158 | inline for (0..4) |col| { 159 | result[base_idx + col] = row_result[col]; 160 | } 161 | } 162 | } 163 | 164 | export fn multiply4x4Float32() i32 { 165 | var input = interop.getInput(); 166 | var output = interop.getOutput(); 167 | 168 | const a_matrices = input.readArray(f32); 169 | const b_matrices = input.readArray(f32); 170 | const num_matrices = a_matrices.len / 16; 171 | 172 | var result_matrices = output.initArray(f32, a_matrices.len); 173 | 174 | for (0..num_matrices) |i| { 175 | const start_idx = i * 16; 176 | const end_idx = start_idx + 16; 177 | 178 | const a_matrix = a_matrices[start_idx..end_idx]; 179 | const b_matrix = b_matrices[start_idx..end_idx]; 180 | const r_matrix = result_matrices[start_idx..end_idx]; 181 | 182 | multiply4x4Float32Single(a_matrix, b_matrix, r_matrix); 183 | } 184 | 185 | return OK; 186 | } 187 | -------------------------------------------------------------------------------- /test-gen/languages/typescript.ts: -------------------------------------------------------------------------------- 1 | import { TestCase, Operation, TestGenerator, DataType } from '../types' 2 | 3 | // Generates a number that can survive a float64 => float32 => float64 conversion 4 | function formatFloat32(value: number): number { 5 | const buffer = new ArrayBuffer(4) 6 | const view = new DataView(buffer) 7 | view.setFloat32(0, value, true) 8 | return view.getFloat32(0, true) 9 | } 10 | 11 | // Format a value for TypeScript based on its type 12 | function formatValue(value: number, dataType: DataType): string { 13 | // Special case: handle negative zero 14 | if ((dataType === 'Float32' || dataType === 'Float64') && Object.is(value, -0)) { 15 | return '-0' 16 | } 17 | 18 | if (dataType === 'Float32') return JSON.stringify(formatFloat32(value)) 19 | 20 | // Special case: large integers as hex 21 | if ((dataType === 'Uint32' || dataType === 'Int32') && Math.abs(value) > 0x1000000) { 22 | if (value < 0) { 23 | // For negative values, don't use hex notation as it causes syntax errors 24 | return String(value) 25 | } 26 | return `0x${value.toString(16)}` 27 | } 28 | 29 | // Default case: just convert to JSON string (handles special numbers correctly) 30 | return JSON.stringify(value) 31 | } 32 | 33 | // Generate a single write operation for TypeScript 34 | function generateWriteOperation(op: Operation, index: number): string[] { 35 | const dataType = op.dataType 36 | 37 | switch (op.type) { 38 | case 'write': 39 | return [`writer.write${dataType}(${formatValue(op.value, dataType)});`] 40 | 41 | case 'init': 42 | return [`const ptr${index} = writer.init${dataType}();`, `ptr${index}(${formatValue(op.value, dataType)});`] 43 | 44 | case 'copyArray': 45 | return [`writer.copy${dataType}Array([${op.value.map(v => formatValue(v, dataType)).join(', ')}]);`] 46 | 47 | case 'copyElements': 48 | return [`writer.copy${dataType}Elements([${op.value.map(v => formatValue(v, dataType)).join(', ')}]);`] 49 | 50 | case 'initArray': 51 | return [ 52 | `const arr${index} = writer.init${dataType}Array(${op.value.length});`, 53 | ...op.value.map((v, i) => `arr${index}[${i}] = ${formatValue(v, dataType)};`), 54 | ] 55 | 56 | case 'initElements': 57 | return [ 58 | `const arr${index} = writer.init${dataType}Elements(${op.value.length});`, 59 | ...op.value.map((v, i) => `arr${index}[${i}] = ${formatValue(v, dataType)};`), 60 | ] 61 | } 62 | } 63 | 64 | // Generate a read operation for TypeScript 65 | function generateReadOperation(op: Operation, index: number): string[] { 66 | const dataType = op.dataType 67 | 68 | switch (op.type) { 69 | case 'write': 70 | case 'init': 71 | return [`expect(reader.read${dataType}()).toEqual(${formatValue(op.value, dataType)});`] 72 | 73 | case 'copyArray': 74 | case 'initArray': 75 | return [ 76 | `const readArr${index} = reader.read${dataType}Array();`, 77 | `expect(readArr${index}.length).toEqual(${op.value.length});`, 78 | ...op.value.map((v, i) => `expect(readArr${index}[${i}]).toEqual(${formatValue(v, dataType)});`), 79 | ] 80 | 81 | case 'copyElements': 82 | case 'initElements': 83 | return [ 84 | `const readElems${index} = reader.read${dataType}Elements(${op.value.length});`, 85 | ...op.value.map((v, i) => `expect(readElems${index}[${i}]).toEqual(${formatValue(v, dataType)});`), 86 | ] 87 | } 88 | } 89 | 90 | // Generate a complete TypeScript test 91 | function generateTestCase(testCase: TestCase): string { 92 | const indent = (lines: string[]): string => { 93 | return ` ${lines.join('\n ')}\n` 94 | } 95 | 96 | let code = `test('${testCase.name.replace(/'/g, "\\'")}', () => {` 97 | 98 | code += indent([ 99 | '// Create a new buffer for testing', 100 | 'const buffer = new ArrayBuffer(1024);', 101 | 'const writer = new Writer(buffer);', 102 | 'const reader = new Reader(buffer);', 103 | 'writer.reset();', 104 | ]) 105 | 106 | code += '\n' 107 | 108 | // Generate operations 109 | for (let i = 0; i < testCase.operations.length; i++) { 110 | code += indent(generateWriteOperation(testCase.operations[i], i)) 111 | } 112 | 113 | code += '\n' 114 | 115 | // Verify buffer contents 116 | code += indent([ 117 | '// Verify buffer contents', 118 | `const expected = new Uint8Array([${testCase.expectation.join(', ')}]);`, 119 | `const actual = new Uint8Array(buffer, 0, ${testCase.expectation.length});`, 120 | 'expect(Array.from(actual)).toEqual(Array.from(expected));', 121 | ]) 122 | 123 | code += '\n' 124 | 125 | // Reset reader 126 | code += indent(['// Read back values', 'reader.reset();']) 127 | 128 | code += '\n' 129 | 130 | // Read back operations 131 | for (let i = 0; i < testCase.operations.length; i++) { 132 | code += indent(generateReadOperation(testCase.operations[i], i)) 133 | } 134 | 135 | code += `});` 136 | 137 | return code 138 | } 139 | 140 | // Generate all TypeScript tests 141 | function generateTestFile(testCases: TestCase[]): string { 142 | const parts: string[] = [ 143 | `import { describe, test, expect } from 'vitest' 144 | import { Reader, Writer } from './conduit' 145 | 146 | describe('Channel Protocol Tests', () => {`, 147 | ] 148 | 149 | for (const testCase of testCases) { 150 | parts.push(indent(generateTestCase(testCase), 2)) 151 | } 152 | 153 | parts.push('});') 154 | 155 | return parts.join('\n\n') 156 | } 157 | 158 | // Helper for indentation 159 | function indent(code: string, spaces: number): string { 160 | const padding = ' '.repeat(spaces) 161 | return code 162 | .split('\n') 163 | .map(line => padding + line) 164 | .join('\n') 165 | } 166 | 167 | export const typescriptGenerator: TestGenerator = { 168 | outputFile: 'implementations/host-typescript/src/conduit.test.ts', 169 | generateTestFile, 170 | } 171 | -------------------------------------------------------------------------------- /examples/wasm-rust/shared/lib.rs: -------------------------------------------------------------------------------- 1 | #[cfg(target_arch = "wasm32")] 2 | use std::arch::wasm32::*; 3 | 4 | pub fn xor_array_i32(values: &[i32]) -> i32 { 5 | let mut total = 0i32; 6 | 7 | unsafe { 8 | let len = values.len(); 9 | 10 | const LANES: usize = 4; // i32x4 has 4 lanes 11 | const BATCH_SIZE: usize = LANES * 4; // process 4 SIMD vectors per loop 12 | 13 | // Four independent accumulators for ILP 14 | let mut acc: [v128; 4] = [ 15 | i32x4_splat(0), 16 | i32x4_splat(0), 17 | i32x4_splat(0), 18 | i32x4_splat(0), 19 | ]; 20 | let mut i = 0; 21 | 22 | // Process in batches of 4 vectors 23 | while i + BATCH_SIZE <= len { 24 | let ptr = values.as_ptr().add(i) as *const v128; 25 | let v0 = v128_load(ptr); 26 | let v1 = v128_load(ptr.add(1)); 27 | let v2 = v128_load(ptr.add(2)); 28 | let v3 = v128_load(ptr.add(3)); 29 | 30 | acc[0] = v128_xor(acc[0], v0); 31 | acc[1] = v128_xor(acc[1], v1); 32 | acc[2] = v128_xor(acc[2], v2); 33 | acc[3] = v128_xor(acc[3], v3); 34 | i += BATCH_SIZE; 35 | } 36 | 37 | // Handle any remaining full SIMD chunks 38 | while i + LANES <= len { 39 | let ptr = values.as_ptr().add(i) as *const v128; 40 | let v = v128_load(ptr); 41 | acc[0] = v128_xor(acc[0], v); 42 | i += LANES; 43 | } 44 | 45 | // Reduce the four SIMD accumulators into a scalar total via XOR of each lane 46 | 47 | for &vec in &acc { 48 | total ^= i32x4_extract_lane::<0>(vec); 49 | total ^= i32x4_extract_lane::<1>(vec); 50 | total ^= i32x4_extract_lane::<2>(vec); 51 | total ^= i32x4_extract_lane::<3>(vec); 52 | } 53 | 54 | // Handle any remaining scalar tail elements 55 | while i < len { 56 | total ^= values[i]; 57 | i += 1; 58 | } 59 | } 60 | 61 | total 62 | } 63 | 64 | pub fn sum_array_f64(values: &[f64]) -> f64 { 65 | let mut total = 0.0; 66 | 67 | unsafe { 68 | let len = values.len(); 69 | 70 | const LANES: usize = 2; // f64x2 has 2 lanes 71 | const BATCH_SIZE: usize = LANES * 4; // Process 4 SIMD vectors per iteration 72 | 73 | // Multiple accumulators for instruction-level parallelism 74 | let mut acc = [f64x2_splat(0.0); 4]; 75 | let mut i = 0; 76 | 77 | // Process batches of 4 SIMD vectors 78 | while i + BATCH_SIZE <= len { 79 | // Load directly into SIMD registers from memory 80 | let ptr = values.as_ptr().add(i); 81 | let v0 = v128_load(ptr as *const v128); 82 | let v1 = v128_load(ptr.add(LANES) as *const v128); 83 | let v2 = v128_load(ptr.add(LANES * 2) as *const v128); 84 | let v3 = v128_load(ptr.add(LANES * 3) as *const v128); 85 | 86 | // Add to independent accumulators (enables pipelining) 87 | acc[0] = f64x2_add(acc[0], v0); 88 | acc[1] = f64x2_add(acc[1], v1); 89 | acc[2] = f64x2_add(acc[2], v2); 90 | acc[3] = f64x2_add(acc[3], v3); 91 | i += BATCH_SIZE; 92 | } 93 | 94 | // Handle remaining SIMD-sized chunks 95 | while i + LANES <= len { 96 | let ptr = values.as_ptr().add(i); 97 | let v = v128_load(ptr as *const v128); 98 | acc[0] = f64x2_add(acc[0], v); 99 | i += LANES; 100 | } 101 | 102 | // Sum all accumulators 103 | 104 | for j in 0..4 { 105 | total += f64x2_extract_lane::<0>(acc[j]) + f64x2_extract_lane::<1>(acc[j]); 106 | } 107 | 108 | // Handle remaining scalar elements 109 | while i < len { 110 | total += *values.get_unchecked(i); 111 | i += 1; 112 | } 113 | } 114 | 115 | total 116 | } 117 | 118 | fn multiply_4x4_f32_single(a: &[f32], b: &[f32], result: &mut [f32]) { 119 | result[0] = a[0]*b[0] + a[1]*b[4] + a[2]*b[8] + a[3]*b[12]; 120 | result[1] = a[0]*b[1] + a[1]*b[5] + a[2]*b[9] + a[3]*b[13]; 121 | result[2] = a[0]*b[2] + a[1]*b[6] + a[2]*b[10] + a[3]*b[14]; 122 | result[3] = a[0]*b[3] + a[1]*b[7] + a[2]*b[11] + a[3]*b[15]; 123 | 124 | result[4] = a[4]*b[0] + a[5]*b[4] + a[6]*b[8] + a[7]*b[12]; 125 | result[5] = a[4]*b[1] + a[5]*b[5] + a[6]*b[9] + a[7]*b[13]; 126 | result[6] = a[4]*b[2] + a[5]*b[6] + a[6]*b[10] + a[7]*b[14]; 127 | result[7] = a[4]*b[3] + a[5]*b[7] + a[6]*b[11] + a[7]*b[15]; 128 | 129 | result[8] = a[8]*b[0] + a[9]*b[4] + a[10]*b[8] + a[11]*b[12]; 130 | result[9] = a[8]*b[1] + a[9]*b[5] + a[10]*b[9] + a[11]*b[13]; 131 | result[10] = a[8]*b[2] + a[9]*b[6] + a[10]*b[10] + a[11]*b[14]; 132 | result[11] = a[8]*b[3] + a[9]*b[7] + a[10]*b[11] + a[11]*b[15]; 133 | 134 | result[12] = a[12]*b[0] + a[13]*b[4] + a[14]*b[8] + a[15]*b[12]; 135 | result[13] = a[12]*b[1] + a[13]*b[5] + a[14]*b[9] + a[15]*b[13]; 136 | result[14] = a[12]*b[2] + a[13]*b[6] + a[14]*b[10] + a[15]*b[14]; 137 | result[15] = a[12]*b[3] + a[13]*b[7] + a[14]*b[11] + a[15]*b[15]; 138 | } 139 | 140 | pub fn multiply_4x4_f32( 141 | a_matrices: &[f32], 142 | b_matrices: &[f32], 143 | result_matrices: &mut [f32] 144 | ) { 145 | let num_matrices = a_matrices.len() / 16; 146 | 147 | // Process each pair of matrices 148 | for i in 0..num_matrices { 149 | let start_idx = i * 16; 150 | let end_idx = start_idx + 16; 151 | 152 | let a_matrix = &a_matrices[start_idx..end_idx]; 153 | let b_matrix = &b_matrices[start_idx..end_idx]; 154 | let r_matrix = &mut result_matrices[start_idx..end_idx]; 155 | 156 | multiply_4x4_f32_single(a_matrix, b_matrix, r_matrix); 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /test-gen/languages/rust.ts: -------------------------------------------------------------------------------- 1 | import { TestCase, Operation, TestGenerator, DataType } from '../types' 2 | 3 | type RustDataType = 'u8' | 'u32' | 'i32' | 'f32' | 'f64' 4 | 5 | const dataTypeMap: Record = { 6 | Uint8: 'u8', 7 | Uint32: 'u32', 8 | Int32: 'i32', 9 | Float32: 'f32', 10 | Float64: 'f64', 11 | } 12 | 13 | // Format a value for Rust based on its type 14 | function formatValue(value: number, dataType: RustDataType): string { 15 | if (dataType === 'f32' || dataType === 'f64') { 16 | // Special case: handle negative zero 17 | if (Object.is(value, -0)) { 18 | return '-0.0' 19 | } 20 | 21 | // Special case: float literals in Rust need decimal point or suffix 22 | if (Number.isInteger(value) && !String(value).includes('e')) { 23 | return `${value}.0` 24 | } 25 | } 26 | 27 | // Special case: large u32 values as hex 28 | if (dataType === 'u32' && value > 0x1000000) { 29 | return `0x${value.toString(16)}` 30 | } 31 | 32 | // Special case: i32 minimum value 33 | if (dataType === 'i32' && value === -2147483648) { 34 | return 'i32::MIN' 35 | } 36 | 37 | // Default case: just convert to string 38 | return `${value}` 39 | } 40 | 41 | // Generate a single operation for Rust 42 | function generateWriteOperation(op: Operation, index: number): string[] { 43 | const dataType = dataTypeMap[op.dataType] 44 | 45 | switch (op.type) { 46 | case 'write': 47 | return [`writer.write_${dataType}(${formatValue(op.value, dataType)});`] 48 | 49 | case 'init': 50 | return [`let ptr${index} = writer.init_${dataType}();`, `unsafe { *ptr${index} = ${formatValue(op.value, dataType)}; }`] 51 | 52 | case 'copyArray': 53 | return [`writer.copy_array_${dataType}(&[${op.value.map(v => formatValue(v, dataType)).join(', ')}]);`] 54 | 55 | case 'copyElements': 56 | return [`writer.copy_elements_${dataType}(&[${op.value.map(v => formatValue(v, dataType)).join(', ')}]);`] 57 | 58 | case 'initArray': 59 | if (op.value.length === 0) { 60 | return [`let _arr${index} = writer.init_array_${dataType}(0);`] 61 | } 62 | return [ 63 | `let arr${index} = writer.init_array_${dataType}(${op.value.length});`, 64 | ...op.value.map((v, i) => `arr${index}[${i}] = ${formatValue(v, dataType)};`), 65 | ] 66 | 67 | case 'initElements': 68 | if (op.value.length === 0) { 69 | return [`let _arr${index} = writer.init_elements_${dataType}(0);`] 70 | } 71 | return [ 72 | `let arr${index} = writer.init_elements_${dataType}(${op.value.length});`, 73 | ...op.value.map((v, i) => `arr${index}[${i}] = ${formatValue(v, dataType)};`), 74 | ] 75 | } 76 | } 77 | 78 | function generateReadOperation(op: Operation, index: number): string[] { 79 | const dataType = dataTypeMap[op.dataType] 80 | 81 | switch (op.type) { 82 | case 'write': 83 | case 'init': 84 | return [`assert_eq!(${formatValue(op.value, dataType)}, reader.read_${dataType}());`] 85 | 86 | case 'copyArray': 87 | case 'initArray': 88 | if (op.value.length === 0) { 89 | return [`let _read_arr${index} = reader.read_array_${dataType}();`, `assert_eq!(0, _read_arr${index}.len());`] 90 | } 91 | return [ 92 | `let read_arr${index} = reader.read_array_${dataType}();`, 93 | `assert_eq!(${op.value.length}, read_arr${index}.len());`, 94 | ...op.value.map((v, i) => `assert_eq!(${formatValue(v, dataType)}, read_arr${index}[${i}]);`), 95 | ] 96 | 97 | case 'copyElements': 98 | case 'initElements': 99 | if (op.value.length === 0) { 100 | return [`let _read_elems${index} = reader.read_elements_${dataType}(0);`, `assert_eq!(0, _read_elems${index}.len());`] 101 | } 102 | return [ 103 | `let read_elems${index} = reader.read_elements_${dataType}(${op.value.length});`, 104 | ...op.value.map((v, i) => `assert_eq!(${formatValue(v, dataType)}, read_elems${index}[${i}]);`), 105 | ] 106 | } 107 | } 108 | 109 | // Generate a complete Rust test 110 | function generateTestCase(testCase: TestCase): string { 111 | const indent = (lines: string[]): string => { 112 | return ` ${lines.join('\n ')}\n` 113 | } 114 | 115 | let code = ` #[test]\n fn ${testCase.name.toLowerCase().replace(/[^a-z0-9]/g, '_')}() {\n` 116 | 117 | code += indent(['let mut storage = [0u64; 32];']) 118 | 119 | code += indent(['{', ' let mut writer = Writer::from(&mut storage);']) 120 | 121 | // Generate operations 122 | for (let i = 0; i < testCase.operations.length; i++) { 123 | const operations = generateWriteOperation(testCase.operations[i], i) 124 | for (const op of operations) { 125 | code += ` ${op}\n` 126 | } 127 | } 128 | 129 | code += indent(['}']) 130 | 131 | code += '\n' 132 | 133 | // Verify buffer contents 134 | code += indent([ 135 | `let expected = [${testCase.expectation.join(', ')}u8];`, 136 | `let actual = unsafe { std::slice::from_raw_parts(storage.as_ptr() as *const u8, ${testCase.expectation.length}) };`, 137 | 'assert_eq!(&expected[..], actual);', 138 | ]) 139 | 140 | code += '\n' 141 | 142 | // Create reader and read back 143 | code += indent(['let mut reader = Reader::from(&mut storage);', 'reader.reset();']) 144 | 145 | code += '\n' 146 | 147 | for (let i = 0; i < testCase.operations.length; i++) { 148 | code += indent(generateReadOperation(testCase.operations[i], i)) 149 | } 150 | 151 | code += ` }` 152 | 153 | return code 154 | } 155 | 156 | // Generate all Rust tests 157 | function generateTestFile(testCases: TestCase[]): string { 158 | const parts: string[] = [ 159 | `use super::{Reader, Writer}; 160 | 161 | #[cfg(test)] 162 | mod tests { 163 | use super::*;`, 164 | ] 165 | 166 | for (const testCase of testCases) { 167 | parts.push(generateTestCase(testCase)) 168 | } 169 | 170 | parts.push('}') 171 | 172 | return parts.join('\n\n') 173 | } 174 | 175 | export const rustGenerator: TestGenerator = { 176 | outputFile: 'implementations/wasm-rust/conduit/test.rs', 177 | generateTestFile, 178 | } 179 | -------------------------------------------------------------------------------- /docs/protocol-conduit.md: -------------------------------------------------------------------------------- 1 | # `zaw::conduit` Fixed-Buffer Channel Protocol 2 | 3 | ## 1. Overview 4 | 5 | This document specifies a lightweight, zero-allocation binary channel protocol for inter‑language communication over a fixed‑size buffer. 6 | 7 | This protocol defines a shared memory layout and read/write semantics enabling zero-allocation data exchange between producers (writers) and consumers (readers). It is suitable for tight coupling between languages such as JavaScript, Zig, or Rust within a single address space. 8 | 9 | ## 2. Conformance 10 | 11 | Implementations that conform to this specification MUST: 12 | 13 | 1. Provide a contiguous, 8-byte aligned buffer as the backing store for the channel. 14 | 15 | 2. Expose operations to write and read primitive and array types with correct alignment. 16 | 17 | 3. Support resetting the read/write cursor. 18 | 19 | 4. Enforce bounds checks in debug builds. 20 | 21 | 5. Use separate memory regions for input and output channels. 22 | 23 | Implementations MAY: 24 | 25 | 1. Expose convenience methods to convert Uint8 values to and from strings 26 | 2. Support additional primitives. 27 | 3. Use language idioms to refer to primitives e.g. `u8` or `Uint8` 28 | 4. Specify element types in either comptime parameters or function names aligned with language idioms e.g. 29 | - OK 30 | - `writer.writeArray(u8, value)` 31 | - `writer.writeUint8Array(value)` 32 | - Not OK 33 | - `writer.writeArray('u8', value)` (do not use strings) 34 | 35 | ## 3. Terminology 36 | 37 | - **Buffer**: A contiguous block of memory with 8-byte alignment. 38 | - **Writer**: An entity that serializes values into the buffer. 39 | - **Reader**: An entity that deserializes values from the buffer. 40 | - **Offset**: The current byte index within the buffer. 41 | - **Alignment**: Byte boundary alignment required by certain types (e.g., 8‑byte for `Float64`). 42 | 43 | ## 4. Data Types 44 | 45 | The protocol supports the following primitive types: 46 | 47 | - `Uint8`: 8-bit unsigned integer, little endian, no alignment. 48 | - `Uint32`: 32‑bit unsigned integer, little‑endian, aligned to 4-byte boundary. 49 | - `Int32`: 32‑bit signed integer, little‑endian, aligned to 4-byte boundary. 50 | - `Float32`: 32‑bit IEEE‑754 floating point, little‑endian, aligned to 4-byte boundary. 51 | - `Float64`: 64‑bit IEEE‑754 floating point, little‑endian, aligned to 8‑byte boundary. 52 | 53 | It also supports arrays of these primitives: 54 | 55 | - `Uint8[]` 56 | - `Uint32[]` 57 | - `Int32[]` 58 | - `Float32[]` 59 | - `Float64[]` 60 | 61 | ## 5. Encoding Rules 62 | 63 | All multi‑byte values use little‑endian byte order. 64 | 65 | ### 5.1 Primitive Values 66 | 67 | 1. **Uint8**: Stored at the current `offset`; `offset` advances by 1. 68 | 2. **Uint32 / Int32 / Float32**: `offset` aligned up to 4 bytes, then value stored; `offset` advances by 4. 69 | 3. **Float64**: `offset` aligned up to 8 bytes, then value stored; `offset` advances by 8. 70 | 71 | ### 5.2 Arrays 72 | 73 | An array encoding consists of: 74 | 75 | 1. A length prefix: a `Uint32` indicating element count, aligned to 4 bytes. 76 | 77 | 2. Raw contiguous element values, with the first element aligned up to 1, 4 or 8 byte boundary as required by the element type. 78 | 79 | 3. Advance `offset` past the element region. 80 | 81 | ### 5.3 Elements 82 | 83 | "Elements" refers a fixed-length array with no length prefix. An elements encoding consists of: 84 | 85 | 1. Raw contiguous element values, with the first element aligned up to 1, 4 or 8 byte boundary as required by the element type. 86 | 87 | 2. Advance `offset` past the element region. 88 | 89 | ## 6. API Operations 90 | 91 | ### 6.1 Common 92 | 93 | Where `type` is a **primitive type**: 94 | 95 | | Operation | Semantics | 96 | | ------------------------ | ---------------------------------------------------------------------------------- | 97 | | `sizeOf(type)`\* | Returns `1` for `Uint8`, `4` for `Uint32` / `Int32` / `Float32`, `8` for `Float64` | 98 | | `alignTo(type)`\* | Aligns `offset` up to a multiple of `sizeOf(type)`. | 99 | | `advance(type, count)`\* | Advances the offset by `count * sizeOf(type)`. | 100 | | `reset()` | Set `offset = 0`. | 101 | 102 | _\*Internal methods - not strictily required as a part of the API, included here to illustrate behaviour and simplify implementations_ 103 | 104 | ### 6.2 Writer 105 | 106 | | Operation | Semantics | 107 | | ------------------------- | --------------------------------------------------------------------------------------------------------- | 108 | | `write(type, value)` | `alignTo(type)`;
encode value at `offset`;
`advance(type, 1)`. | 109 | | `init(type)` | `alignTo(type)`;
return mutable pointer or setter for `offset`;
`advance(type, 1)`. | 110 | | `initElements(type, len)` | `alignTo(type)`;
return mutable slice of `len` elements starting at `offset`;
`advance(type, len)`. | 111 | | `initArray(type, len)` | `write(Uint32, len)`;
return `initElements(type, len)`. | 112 | | `copyElements(type, arr)` | `alignTo(type)`;
encode raw elements into storage starting at `offset`;
`advance(type, len)`. | 113 | | `copyArray(type, arr)` | `write(Uint32, len)`;
`copyElements(type, arr)`. | 114 | 115 | ### 6.3 Reader 116 | 117 | | Operation | Semantics | 118 | | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | 119 | | `read(type)` | `alignTo(type)`;
return value at `offset`;
`advance(type, 1)`; | 120 | | `readArray(type)` | `len = read(Uint32)`;
`alignTo(type)`;
return immutable slice of `len` elements starting at `offset`;
`advance(type, len)`. | 121 | | `readElements(type, len)` | `alignTo(type)`;
return immutable slice of `len` elements starting at `offset`;
`advance(type, len)`. | 122 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `zaw` 2 | 3 | [![Zig](https://img.shields.io/github/actions/workflow/status/stylearcade/zaw/ci-build-zig.yml?label=Zig%200.15.2&logo=zig)](https://github.com/stylearcade/zaw/actions/workflows/ci-build-zig.yml) 4 | [![Rust](https://img.shields.io/github/actions/workflow/status/stylearcade/zaw/ci-build-rust.yml?label=Rust&logo=rust)](https://github.com/stylearcade/zaw/actions/workflows/ci-build-rust.yml) 5 | 6 | ## Zero-Allocation WASM @ Style Arcade 7 | 8 | The purpose of `zaw` is to make it easier to achieve the original promise of WebAssembly: 9 | 10 | **High-performance, low-overhead acceleration for targeted code - without rewriting your entire application.** 11 | 12 | ## The upshot 13 | 14 | With `zaw`, you'll be able to offload individual algorithms, rather than entire modules, and keep your WebAssembly code lean and simple - truly unlocking the original vision of the WebAssembly founding team. 15 | 16 | ### Performance 17 | 18 | **Up to 10x faster than pure JavaScript and 2.5x faster than wasm-bindgen for XOR Int32Array Bench** 19 | 20 | | Element Count | Winner | vs `zaw` | vs `js` | vs `wasm-bindgen` | 21 | | ------------- | ------ | ----------- | ----------- | ----------------- | 22 | | 10 | `js` | 2.0x faster | - | 4.0x faster | 23 | | 100 | `zaw` | - | 1.2x faster | 2.0x faster | 24 | | 1,000 | `zaw` | - | 5.5x faster | 2.6x faster | 25 | | 10,000 | `zaw` | - | 9.9x faster | 2.6x faster | 26 | | 100,000 | `zaw` | - | 9.7x faster | 2.5x faster | 27 | 28 | **Why XOR Int32Array _isn't_ a ridiculous benchmark** 29 | 30 | It seems counterintuitive, but this is the best possible test for a WebAssembly protocol: 31 | 32 | - It leverages SIMD and instruction pipelining not available in Javascript 33 | - It uses the smallest native WASM type 34 | - It aligns to real-world buffer and matrix use cases that require data transfer 35 | - It doesn't hide slow interop in the way fibonacci, digits of pi, prime factorisation or other toy examples do 36 | - It shows an improvement over javascript even with a small element count (100) 37 | 38 | And by targeting the _cheapest_ algorithm possible, we can see the performance of the interop itself. 39 | 40 | ## Getting Started 41 | 42 | - See our [getting started guide](docs/getting-started.md) for installation & code samples 43 | - Fork [zaw-starter-zig](https://github.com/stylearcade/zaw-starter-zig) or [zaw-starter-rust](https://github.com/stylearcade/zaw-starter-rust) 44 | 45 | ## Repository Overview 46 | 47 | In this repository you will find: 48 | 49 | - **Protocols** 50 | - [`zaw::conduit`](docs/protocol-conduit.md) - A protocol for zero-allocation communication across a fixed buffer 51 | - [`zaw::interop`](docs/protocol-interop.md) - Batteries-included protocol and host API specification for interop 52 | - Logging 53 | - Error handling, useful panic messages, optional stack traces 54 | - Handling memory.grow() and detached buffers 55 | - **Implementations** 56 | - Hosts 57 | - [Typescript](implementations/host-typescript/) 58 | - Go _(coming later)_ 59 | - WASM 60 | - [Zig](implementations/wasm-zig/) 61 | - [Rust](implementations/wasm-rust/) 62 | - [**Benchmarks**](docs/benchmarks.md) 63 | - XOR Int32Array 64 | - Sum Float64Array 65 | - 4x4 Float32 Matrix multiplication 66 | 67 | ## Motivation 68 | 69 | ### The original vision of WebAssembly 70 | 71 | Before we talk about what motivates us at Style Arcade, it's worth touching on the motivations of the original authors of the [spec](https://dl.acm.org/doi/10.1145/3140587.3062363): 72 | 73 | > _Engineers from the four major browser vendors have risen to the challenge and collaboratively designed a portable low-level bytecode called WebAssembly. It offers compact representation, efficient validation and compilation, and safe **low to no-overhead execution**_. 74 | 75 | This goal of low to no-overhead, especially at the boundary, is evident in their design decisions: 76 | 77 | - The deterministic, linear memory model makes shared memory & fixed-buffer communication a breeze 78 | - Memory isolation is not only great for memory safety, it also enables single-process execution and eliminates context switching 79 | - The restricted type system of imports/exports eliminates abstraction, dispatch and boxing/unboxing 80 | 81 | And finally, most importantly, WebAssembly is _appropriately low level_. The things they left _out_ of the spec are the things that enable real performance. 82 | 83 | **So if the founders baked low-overhead into the original specification...what's the problem?** 84 | 85 | ### WebAssembly acceleration today 86 | 87 | For teams that are building things today, the discourse around WebAssembly interop is dominated projects like [`wasm-bindgen`](https://github.com/rustwasm/wasm-bindgen), which are great for high-level interactions and developer accessibility, but terrible for low-level performance. 88 | 89 | When faced with overhead issues, the most often prescribed solution is to move more of your application into WASM and reduce the number of times you cross the boundary. 90 | 91 | Here's how that plays out: 92 | 93 | > _We should re-write that algorithm in Rust!_ 94 | > 95 | > _Wow my prototype is 3x faster, let's definitely do this!_ 96 | > 97 | > _I don't understand why the app isn't faster...is it even using the WASM module?_ 98 | > 99 | > google.com/search?q=why+wasm+slow 〈ᇂ\_ᇂ |||〉 100 | > 101 | > _We need to re-write the entire module in Rust..._ 102 | > 103 | > _We may as well re-write our whole application in Rust right? ¯\\_(ツ)_/¯_ 104 | 105 | This is a long way from the vision of _"low to no-overhead execution"_, or _"the next asm.js to accelerate your code"_. 106 | 107 | ### What about Component Model? 108 | 109 | The [Component Model](https://component-model.bytecodealliance.org/) significantly expands on the scope of `wasm-bindgen` - developer ergonomics, easier interoperability and also enabling WebAssembly modules to import each other to create the "modular docker" of the future alongside [WASI](https://wasi.dev/): 110 | 111 | > _If WASM+WASI existed in 2008, we wouldn't have needed to created Docker. That's how important it is. Webassembly on the server is the future of computing. A standardized system interface was the missing link. Let's hope WASI is up to the task!_ - [Solomon Hykes, March 2019](https://x.com/solomonstre/status/1111004913222324225) 112 | 113 | This is a great initiative, but it's not available today and is still in [draft status](https://eunomia.dev/blog/2025/02/16/wasi-and-the-webassembly-component-model-current-status/), and it has the same core performance issue as `wasm-bindgen` - **per-call dynamic allocation**. 114 | 115 | There's a [proposal to support fixed-size arrays](https://github.com/WebAssembly/component-model/issues/385), and another to support [generalised fixed memory communication via resources](https://github.com/WebAssembly/component-model/issues/398), but neither of these are on the critical path. 116 | 117 | If either of these make it into standard then the `zaw` project will happily update the benchmarks to prove we're getting the maximum performance available, and there's a lot more we want to contribute than just low-overhead interop. 118 | 119 | But the bottom line is, **if you need something today, then `zaw` is ready to go.** 120 | 121 | ### Why `zaw`? Why Style Arcade? 122 | 123 | Retail data, although not the most buzzworthy problem space, has an extraordinarily high ratio of compute to data, especially in apparel due to sizing. You can't compute much in advance, and many retail KPIs require incredibly granular, turing complete, bottom-up metrics. 124 | 125 | This project is being driven by two key performance goals at Style Arcade: 126 | 127 | - For data structures, algorithms and AI inference, we want accelerated _execution_ 128 | - For everything else, we want accelerated _development_ 129 | 130 | For us, _"re-writing the entire module in Rust"_ meant moving a lot of fast-moving and complex business logic into the slowest moving container possible. This just wasn't viable. We wanted to continue using a higher-level language (Typescript) for the majority of our codebase so we can focus on delivering value and not incur the huge overhead of systems engineering at every level of our software. 131 | 132 | We were also still very much stuck on the "dream" of WebAssembly where we could micro-offload _dozens_ of algorithms - and didn't want to mess around with hand-crafted buffer communication each time. 133 | 134 | We were certain that hybrid-level WebAssembly was the answer, but that we'd need something different to `wasm-bindgen`. 135 | 136 | So we went back to the drawing board, and focused solely on the communication overhead - and following an initial proof-of-concept in Zig, we developed `zaw::conduit` to allow bidirectional sharing of many data types, and then `zaw::interop` to make creating new modules very easy and overcome the typical headaches of WebAssembly development. 137 | 138 | Check out our [benchmarks](docs/benchmarks.md) to find out where we landed, and see our [basic-beans examples](docs/getting-started.md) to see how easy it is to get started. 139 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /docs/benchmarks.md: -------------------------------------------------------------------------------- 1 | # Benchmarks 2 | 3 | ```console 4 | Running on AMD Ryzen 9 3900X 12-Core Processor 5 | 6 | 7 | ✓ examples/host-typescript/__tests__/example.bench.ts > Typescript example host > XOR Int32Array @ 10 elements 11869ms 8 | name hz min max mean p75 p99 p995 p999 rme samples 9 | · js 11,511,227.77 0.0001 0.6338 0.0001 0.0001 0.0003 0.0005 0.0008 ±0.77% 5755614 fastest 10 | · zig 5,644,240.19 0.0001 0.6407 0.0002 0.0002 0.0003 0.0004 0.0009 ±0.62% 2822121 11 | · rust 5,630,117.80 0.0001 0.8158 0.0002 0.0002 0.0003 0.0004 0.0009 ±0.70% 2817354 12 | · rust-bindgen 2,902,861.09 0.0003 0.6807 0.0003 0.0003 0.0006 0.0008 0.0018 ±0.56% 1451431 slowest 13 | 14 | ✓ examples/host-typescript/__tests__/example.bench.ts > Typescript example host > XOR Int32Array @ 100 elements 8957ms 15 | name hz min max mean p75 p99 p995 p999 rme samples 16 | · js 4,218,820.87 0.0002 0.6489 0.0002 0.0002 0.0005 0.0006 0.0010 ±0.52% 2109411 17 | · zig 5,207,776.47 0.0001 0.8167 0.0002 0.0002 0.0004 0.0005 0.0011 ±0.67% 2603890 fastest 18 | · rust 5,160,011.72 0.0001 0.4687 0.0002 0.0002 0.0004 0.0005 0.0011 ±0.54% 2580006 19 | · rust-bindgen 2,659,941.96 0.0003 0.2934 0.0004 0.0003 0.0008 0.0010 0.0024 ±0.48% 1329971 slowest 20 | 21 | ✓ examples/host-typescript/__tests__/example.bench.ts > Typescript example host > XOR Int32Array @ 1000 elements 5786ms 22 | name hz min max mean p75 p99 p995 p999 rme samples 23 | · js 648,340.71 0.0014 0.2311 0.0015 0.0015 0.0022 0.0024 0.0164 ±0.30% 324171 slowest 24 | · zig 3,466,966.86 0.0002 0.5958 0.0003 0.0003 0.0005 0.0006 0.0013 ±0.58% 1733484 25 | · rust 3,566,155.32 0.0002 0.2859 0.0003 0.0003 0.0004 0.0005 0.0011 ±0.42% 1783078 fastest 26 | · rust-bindgen 1,380,373.36 0.0006 0.9653 0.0007 0.0006 0.0012 0.0015 0.0136 ±0.91% 690187 27 | 28 | ✓ examples/host-typescript/__tests__/example.bench.ts > Typescript example host > XOR Int32Array @ 10000 elements 3122ms 29 | name hz min max mean p75 p99 p995 p999 rme samples 30 | · js 67,419.27 0.0134 1.6051 0.0148 0.0141 0.0280 0.0374 0.0893 ±0.74% 33710 slowest 31 | · zig 665,731.52 0.0014 0.2076 0.0015 0.0014 0.0020 0.0022 0.0159 ±0.32% 332866 fastest 32 | · rust 640,345.20 0.0014 0.5524 0.0016 0.0015 0.0022 0.0024 0.0177 ±0.54% 320173 33 | · rust-bindgen 254,724.54 0.0036 0.3587 0.0039 0.0038 0.0056 0.0082 0.0319 ±0.41% 127363 34 | 35 | ✓ examples/host-typescript/__tests__/example.bench.ts > Typescript example host > XOR Int32Array @ 100000 elements 2498ms 36 | name hz min max mean p75 p99 p995 p999 rme samples 37 | · js 6,705.40 0.1331 0.7031 0.1491 0.1488 0.2769 0.3389 0.5923 ±0.77% 3353 slowest 38 | · zig 61,367.17 0.0135 0.6313 0.0163 0.0146 0.0494 0.0676 0.1527 ±0.79% 30684 39 | · rust 64,797.94 0.0134 0.4324 0.0154 0.0144 0.0335 0.0513 0.1400 ±0.59% 32399 fastest 40 | · rust-bindgen 26,069.86 0.0335 0.8571 0.0384 0.0359 0.0855 0.1155 0.2488 ±0.78% 13035 41 | 42 | ✓ examples/host-typescript/__tests__/example.bench.ts > Typescript example host > Sum Float64Array @ 10 elements 11624ms 43 | name hz min max mean p75 p99 p995 p999 rme samples 44 | · js 10,969,756.33 0.0001 1.0560 0.0001 0.0001 0.0003 0.0004 0.0008 ±0.66% 5484879 fastest 45 | · zig 5,624,696.36 0.0001 0.5347 0.0002 0.0002 0.0003 0.0004 0.0010 ±0.57% 2812366 46 | · rust 5,587,951.88 0.0001 1.0990 0.0002 0.0002 0.0003 0.0004 0.0010 ±0.89% 2793976 47 | · rust-bindgen 2,708,713.47 0.0003 0.3386 0.0004 0.0003 0.0008 0.0010 0.0023 ±0.52% 1354357 slowest 48 | 49 | ✓ examples/host-typescript/__tests__/example.bench.ts > Typescript example host > Sum Float64Array @ 100 elements 8709ms 50 | name hz min max mean p75 p99 p995 p999 rme samples 51 | · js 4,290,923.33 0.0002 1.4783 0.0002 0.0002 0.0005 0.0007 0.0011 ±0.92% 2145462 52 | · zig 4,853,764.35 0.0002 0.6633 0.0002 0.0002 0.0003 0.0004 0.0010 ±0.63% 2426883 53 | · rust 4,912,025.13 0.0002 0.6371 0.0002 0.0002 0.0003 0.0004 0.0010 ±0.69% 2456013 fastest 54 | · rust-bindgen 2,140,517.29 0.0004 0.4858 0.0005 0.0004 0.0010 0.0012 0.0035 ±0.78% 1070259 slowest 55 | 56 | ✓ examples/host-typescript/__tests__/example.bench.ts > Typescript example host > Sum Float64Array @ 1000 elements 4832ms 57 | name hz min max mean p75 p99 p995 p999 rme samples 58 | · js 600,961.60 0.0014 0.7919 0.0017 0.0016 0.0024 0.0026 0.0330 ±1.05% 300481 slowest 59 | · zig 2,271,335.06 0.0003 0.5593 0.0004 0.0004 0.0007 0.0009 0.0023 ±0.83% 1135668 60 | · rust 2,489,891.25 0.0003 0.3546 0.0004 0.0004 0.0006 0.0007 0.0016 ±0.51% 1244946 fastest 61 | · rust-bindgen 822,514.71 0.0011 0.4270 0.0012 0.0011 0.0017 0.0020 0.0156 ±0.50% 411258 62 | 63 | ✓ examples/host-typescript/__tests__/example.bench.ts > Typescript example host > Sum Float64Array @ 10000 elements 2813ms 64 | name hz min max mean p75 p99 p995 p999 rme samples 65 | · js 66,817.94 0.0135 0.5179 0.0150 0.0139 0.0307 0.0498 0.1075 ±0.54% 33409 slowest 66 | · zig 354,863.48 0.0025 0.3819 0.0028 0.0027 0.0040 0.0056 0.0258 ±0.43% 177432 fastest 67 | · rust 353,646.94 0.0025 0.4109 0.0028 0.0026 0.0040 0.0049 0.0276 ±0.44% 176824 68 | · rust-bindgen 105,437.53 0.0086 0.5203 0.0095 0.0090 0.0227 0.0358 0.1063 ±0.64% 52719 69 | 70 | ✓ examples/host-typescript/__tests__/example.bench.ts > Typescript example host > Sum Float64Array @ 100000 elements 2454ms 71 | name hz min max mean p75 p99 p995 p999 rme samples 72 | · js 6,889.66 0.1340 0.4850 0.1451 0.1460 0.2238 0.2438 0.3347 ±0.46% 3445 slowest 73 | · zig 32,345.75 0.0269 0.4331 0.0309 0.0290 0.0664 0.0919 0.1957 ±0.57% 16173 74 | · rust 33,827.99 0.0262 0.4335 0.0296 0.0280 0.0558 0.0755 0.1608 ±0.49% 16914 fastest 75 | · rust-bindgen 10,524.54 0.0869 1.0149 0.0950 0.0916 0.1911 0.2616 0.4156 ±0.80% 5263 76 | 77 | ✓ examples/host-typescript/__tests__/example.bench.ts > Typescript example host > 4x4 Float32 Matrix Multiplication, batch size 1 4354ms 78 | name hz min max mean p75 p99 p995 p999 rme samples 79 | · js 2,824,193.66 0.0002 7.3881 0.0004 0.0003 0.0012 0.0016 0.0032 ±3.35% 1412097 80 | ↓ zig [skipped] 81 | · rust 2,893,899.06 0.0003 0.3013 0.0003 0.0003 0.0008 0.0009 0.0019 ±0.44% 1446950 fastest 82 | · rust-bindgen 958,847.97 0.0007 1.0247 0.0010 0.0009 0.0027 0.0033 0.0172 ±1.51% 479425 slowest 83 | 84 | ✓ examples/host-typescript/__tests__/example.bench.ts > Typescript example host > 4x4 Float32 Matrix Multiplication, batch size 10 3051ms 85 | name hz min max mean p75 p99 p995 p999 rme samples 86 | · js 333,725.76 0.0020 3.4339 0.0030 0.0027 0.0078 0.0127 0.0889 ±2.21% 166863 87 | ↓ zig [skipped] 88 | · rust 2,331,921.94 0.0004 1.4557 0.0004 0.0004 0.0009 0.0010 0.0022 ±0.87% 1165962 fastest 89 | · rust-bindgen 309,452.59 0.0012 0.9393 0.0032 0.0034 0.0114 0.0241 0.0952 ±1.30% 154727 slowest 90 | 91 | ✓ examples/host-typescript/__tests__/example.bench.ts > Typescript example host > 4x4 Float32 Matrix Multiplication, batch size 100 2241ms 92 | name hz min max mean p75 p99 p995 p999 rme samples 93 | · js 27,794.90 0.0197 1.8288 0.0360 0.0327 0.2148 0.4487 0.9464 ±2.78% 13898 slowest 94 | ↓ zig [skipped] 95 | · rust 804,815.70 0.0010 0.8275 0.0012 0.0012 0.0019 0.0022 0.0180 ±0.63% 402408 fastest 96 | · rust-bindgen 113,270.78 0.0031 9.9288 0.0088 0.0085 0.0344 0.0513 0.1961 ±7.79% 57604 97 | 98 | ✓ examples/host-typescript/__tests__/example.bench.ts > Typescript example host > 4x4 Float32 Matrix Multiplication, batch size 1000 1874ms 99 | name hz min max mean p75 p99 p995 p999 rme samples 100 | · js 2,925.12 0.2045 1.8075 0.3419 0.3491 1.1553 1.2820 1.5434 ±2.88% 1463 slowest 101 | ↓ zig [skipped] 102 | · rust 103,041.13 0.0084 1.1079 0.0097 0.0088 0.0234 0.0342 0.1121 ±0.80% 51521 fastest 103 | · rust-bindgen 14,988.37 0.0228 7.5492 0.0667 0.0713 0.2129 0.3765 5.2584 ±8.32% 7497 104 | 105 | BENCH Summary 106 | 107 | js - examples/host-typescript/__tests__/example.bench.ts > Typescript example host > XOR Int32Array @ 10 elements 108 | 2.04x faster than zig 109 | 2.04x faster than rust 110 | 3.97x faster than rust-bindgen 111 | 112 | zig - examples/host-typescript/__tests__/example.bench.ts > Typescript example host > XOR Int32Array @ 100 elements 113 | 1.01x faster than rust 114 | 1.23x faster than js 115 | 1.96x faster than rust-bindgen 116 | 117 | rust - examples/host-typescript/__tests__/example.bench.ts > Typescript example host > XOR Int32Array @ 1000 elements 118 | 1.03x faster than zig 119 | 2.58x faster than rust-bindgen 120 | 5.50x faster than js 121 | 122 | zig - examples/host-typescript/__tests__/example.bench.ts > Typescript example host > XOR Int32Array @ 10000 elements 123 | 1.04x faster than rust 124 | 2.61x faster than rust-bindgen 125 | 9.87x faster than js 126 | 127 | rust - examples/host-typescript/__tests__/example.bench.ts > Typescript example host > XOR Int32Array @ 100000 elements 128 | 1.06x faster than zig 129 | 2.49x faster than rust-bindgen 130 | 9.66x faster than js 131 | 132 | js - examples/host-typescript/__tests__/example.bench.ts > Typescript example host > Sum Float64Array @ 10 elements 133 | 1.95x faster than zig 134 | 1.96x faster than rust 135 | 4.05x faster than rust-bindgen 136 | 137 | rust - examples/host-typescript/__tests__/example.bench.ts > Typescript example host > Sum Float64Array @ 100 elements 138 | 1.01x faster than zig 139 | 1.14x faster than js 140 | 2.29x faster than rust-bindgen 141 | 142 | rust - examples/host-typescript/__tests__/example.bench.ts > Typescript example host > Sum Float64Array @ 1000 elements 143 | 1.10x faster than zig 144 | 3.03x faster than rust-bindgen 145 | 4.14x faster than js 146 | 147 | zig - examples/host-typescript/__tests__/example.bench.ts > Typescript example host > Sum Float64Array @ 10000 elements 148 | 1.00x faster than rust 149 | 3.37x faster than rust-bindgen 150 | 5.31x faster than js 151 | 152 | rust - examples/host-typescript/__tests__/example.bench.ts > Typescript example host > Sum Float64Array @ 100000 elements 153 | 1.05x faster than zig 154 | 3.21x faster than rust-bindgen 155 | 4.91x faster than js 156 | 157 | rust - examples/host-typescript/__tests__/example.bench.ts > Typescript example host > 4x4 Float32 Matrix Multiplication, batch size 1 158 | 1.02x faster than js 159 | 3.02x faster than rust-bindgen 160 | 161 | rust - examples/host-typescript/__tests__/example.bench.ts > Typescript example host > 4x4 Float32 Matrix Multiplication, batch size 10 162 | 6.99x faster than js 163 | 7.54x faster than rust-bindgen 164 | 165 | rust - examples/host-typescript/__tests__/example.bench.ts > Typescript example host > 4x4 Float32 Matrix Multiplication, batch size 100 166 | 7.11x faster than rust-bindgen 167 | 28.96x faster than js 168 | 169 | rust - examples/host-typescript/__tests__/example.bench.ts > Typescript example host > 4x4 Float32 Matrix Multiplication, batch size 1000 170 | 6.87x faster than rust-bindgen 171 | 35.23x faster than js 172 | ``` 173 | -------------------------------------------------------------------------------- /implementations/host-typescript/src/conduit.ts: -------------------------------------------------------------------------------- 1 | import assert from 'assert' 2 | 3 | function alignUp(x: number, bytes: 4 | 8): number { 4 | const mask = bytes - 1 5 | 6 | return (x + mask) & ~mask 7 | } 8 | 9 | class Channel { 10 | private offset = 0 11 | storageUint8: Uint8Array 12 | storageUint32: Uint32Array 13 | storageInt32: Int32Array 14 | storageFloat32: Float32Array 15 | storageFloat64: Float64Array 16 | 17 | constructor(buffer: ArrayBuffer, offset = 0, sizeInBytes = buffer.byteLength) { 18 | this.storageUint8 = new Uint8Array(buffer, offset, sizeInBytes / Uint8Array.BYTES_PER_ELEMENT) 19 | this.storageUint32 = new Uint32Array(buffer, offset, sizeInBytes / Uint32Array.BYTES_PER_ELEMENT) 20 | this.storageInt32 = new Int32Array(buffer, offset, sizeInBytes / Int32Array.BYTES_PER_ELEMENT) 21 | this.storageFloat32 = new Float32Array(buffer, offset, sizeInBytes / Float32Array.BYTES_PER_ELEMENT) 22 | this.storageFloat64 = new Float64Array(buffer, offset, sizeInBytes / Float64Array.BYTES_PER_ELEMENT) 23 | } 24 | 25 | reset(): void { 26 | this.offset = 0 27 | } 28 | 29 | offset8(): number { 30 | return this.offset 31 | } 32 | 33 | offset32(): number { 34 | // align to 4 bytes 35 | this.offset = alignUp(this.offset, 4) 36 | 37 | // divide by 4 38 | return this.offset >>> 2 39 | } 40 | 41 | offset64(): number { 42 | // align to 8 bytes 43 | this.offset = alignUp(this.offset, 8) 44 | 45 | // divide by 8 46 | return this.offset >>> 3 47 | } 48 | 49 | advance8(count: number): void { 50 | this.offset += count 51 | 52 | if (this.offset > this.storageUint8.length) { 53 | throw Error('Reached end of channel') 54 | } 55 | } 56 | 57 | advance32(count: number): void { 58 | this.advance8(count * 4) 59 | } 60 | 61 | advance64(count: number): void { 62 | this.advance8(count * 8) 63 | } 64 | } 65 | 66 | export class Writer extends Channel { 67 | writeUint8(value: number): void { 68 | this.storageUint8[this.offset8()] = value 69 | this.advance8(1) 70 | } 71 | 72 | writeUint32(value: number): void { 73 | this.storageUint32[this.offset32()] = value 74 | this.advance32(1) 75 | } 76 | 77 | writeInt32(value: number): void { 78 | this.storageInt32[this.offset32()] = value 79 | this.advance32(1) 80 | } 81 | 82 | writeFloat32(value: number): void { 83 | this.storageFloat32[this.offset32()] = value 84 | this.advance32(1) 85 | } 86 | 87 | writeFloat64(value: number): void { 88 | this.storageFloat64[this.offset64()] = value 89 | this.advance64(1) 90 | } 91 | 92 | initUint8(): (value: number) => void { 93 | const offset = this.offset8() 94 | 95 | this.advance8(1) 96 | 97 | return value => { 98 | this.storageUint8[offset] = value 99 | } 100 | } 101 | 102 | initUint32(): (value: number) => void { 103 | const offset = this.offset32() 104 | 105 | this.advance32(1) 106 | 107 | return value => { 108 | this.storageUint32[offset] = value 109 | } 110 | } 111 | 112 | initInt32(): (value: number) => void { 113 | const offset = this.offset32() 114 | 115 | this.advance32(1) 116 | 117 | return value => { 118 | this.storageInt32[offset] = value 119 | } 120 | } 121 | 122 | initFloat32(): (value: number) => void { 123 | const offset = this.offset32() 124 | 125 | this.advance32(1) 126 | 127 | return value => { 128 | this.storageFloat32[offset] = value 129 | } 130 | } 131 | 132 | initFloat64(): (value: number) => void { 133 | const offset = this.offset64() 134 | 135 | this.advance64(1) 136 | 137 | return value => { 138 | this.storageFloat64[offset] = value 139 | } 140 | } 141 | 142 | initUint8Elements(length: number): Uint8Array { 143 | const start = this.offset8() 144 | 145 | this.advance8(length) 146 | 147 | return this.storageUint8.subarray(start, start + length) 148 | } 149 | 150 | initUint8Array(length: number): Uint8Array { 151 | this.writeUint32(length) 152 | return this.initUint8Elements(length) 153 | } 154 | 155 | initUint32Elements(length: number): Uint32Array { 156 | const start = this.offset32() 157 | 158 | this.advance32(length) 159 | 160 | return this.storageUint32.subarray(start, start + length) 161 | } 162 | 163 | initUint32Array(length: number): Uint32Array { 164 | this.writeUint32(length) 165 | return this.initUint32Elements(length) 166 | } 167 | 168 | initInt32Elements(length: number): Int32Array { 169 | const start = this.offset32() 170 | 171 | this.advance32(length) 172 | 173 | return this.storageInt32.subarray(start, start + length) 174 | } 175 | 176 | initInt32Array(length: number): Int32Array { 177 | this.writeUint32(length) 178 | return this.initInt32Elements(length) 179 | } 180 | 181 | initFloat32Elements(length: number): Float32Array { 182 | const start = this.offset32() 183 | 184 | this.advance32(length) 185 | 186 | return this.storageFloat32.subarray(start, start + length) 187 | } 188 | 189 | initFloat32Array(length: number): Float32Array { 190 | this.writeUint32(length) 191 | return this.initFloat32Elements(length) 192 | } 193 | 194 | initFloat64Elements(length: number): Float64Array { 195 | const start = this.offset64() 196 | 197 | this.advance64(length) 198 | 199 | return this.storageFloat64.subarray(start, start + length) 200 | } 201 | 202 | initFloat64Array(length: number): Float64Array { 203 | this.writeUint32(length) 204 | return this.initFloat64Elements(length) 205 | } 206 | 207 | copyUint8Elements(arr: Uint8Array | number[]): void { 208 | this.storageUint8.set(arr, this.offset8()) 209 | this.advance8(arr.length) 210 | } 211 | 212 | copyUint32Elements(arr: Uint32Array | number[]): void { 213 | this.storageUint32.set(arr, this.offset32()) 214 | this.advance32(arr.length) 215 | } 216 | 217 | copyInt32Elements(arr: Int32Array | number[]): void { 218 | this.storageInt32.set(arr, this.offset32()) 219 | this.advance32(arr.length) 220 | } 221 | 222 | copyFloat32Elements(arr: Float32Array | number[]): void { 223 | this.storageFloat32.set(arr, this.offset32()) 224 | this.advance32(arr.length) 225 | } 226 | 227 | copyFloat64Elements(arr: Float64Array | number[]): void { 228 | this.storageFloat64.set(arr, this.offset64()) 229 | this.advance64(arr.length) 230 | } 231 | 232 | copyUint8Array(arr: Uint8Array | number[]): void { 233 | this.writeUint32(arr.length) 234 | this.copyUint8Elements(arr) 235 | } 236 | 237 | copyUint32Array(arr: Uint32Array | number[]): void { 238 | this.writeUint32(arr.length) 239 | this.copyUint32Elements(arr) 240 | } 241 | 242 | copyInt32Array(arr: Int32Array | number[]): void { 243 | this.writeUint32(arr.length) 244 | this.copyInt32Elements(arr) 245 | } 246 | 247 | copyFloat32Array(arr: Float32Array | number[]): void { 248 | this.writeUint32(arr.length) 249 | this.copyFloat32Elements(arr) 250 | } 251 | 252 | copyFloat64Array(arr: Float64Array | number[]): void { 253 | this.writeUint32(arr.length) 254 | this.copyFloat64Elements(arr) 255 | } 256 | 257 | writeAsciiString(value: string): void { 258 | const data = this.initUint8Array(value.length) 259 | 260 | for (let i = value.length; i-- > 0; ) { 261 | data[i] = value.charCodeAt(i) 262 | } 263 | } 264 | 265 | writeUtf8String(value: string): void { 266 | const encoder = new TextEncoder() 267 | const data = encoder.encode(value) 268 | 269 | this.copyUint8Array(data) 270 | } 271 | } 272 | 273 | export class Reader extends Channel { 274 | readUint8(): number { 275 | const result = this.storageUint8[this.offset8()] 276 | 277 | this.advance8(1) 278 | 279 | return result 280 | } 281 | 282 | readUint32(): number { 283 | const result = this.storageUint32[this.offset32()] 284 | 285 | this.advance32(1) 286 | 287 | return result 288 | } 289 | 290 | readInt32(): number { 291 | const result = this.storageInt32[this.offset32()] 292 | 293 | this.advance32(1) 294 | 295 | return result 296 | } 297 | 298 | readFloat32(): number { 299 | const result = this.storageFloat32[this.offset32()] 300 | 301 | this.advance32(1) 302 | 303 | return result 304 | } 305 | 306 | readFloat64(): number { 307 | const result = this.storageFloat64[this.offset64()] 308 | 309 | this.advance64(1) 310 | 311 | return result 312 | } 313 | 314 | readUint8Elements(length: number): Uint8Array { 315 | const start = this.offset8() 316 | const view = this.storageUint8.subarray(start, start + length) 317 | 318 | this.advance8(length) 319 | 320 | return view 321 | } 322 | 323 | readUint32Elements(length: number): Uint32Array { 324 | const start = this.offset32() 325 | const view = this.storageUint32.subarray(start, start + length) 326 | 327 | this.advance32(length) 328 | 329 | return view 330 | } 331 | 332 | readInt32Elements(length: number): Int32Array { 333 | const start = this.offset32() 334 | const view = this.storageInt32.subarray(start, start + length) 335 | 336 | this.advance32(length) 337 | 338 | return view 339 | } 340 | 341 | readFloat32Elements(length: number): Float32Array { 342 | const start = this.offset32() 343 | const view = this.storageFloat32.subarray(start, start + length) 344 | 345 | this.advance32(length) 346 | 347 | return view 348 | } 349 | 350 | readFloat64Elements(length: number): Float64Array { 351 | const start = this.offset64() 352 | const view = this.storageFloat64.subarray(start, start + length) 353 | 354 | this.advance64(length) 355 | 356 | return view 357 | } 358 | 359 | readUint8Array(): Uint8Array { 360 | const length = this.readUint32() 361 | 362 | return this.readUint8Elements(length) 363 | } 364 | 365 | readUint32Array(): Uint32Array { 366 | const length = this.readUint32() 367 | 368 | return this.readUint32Elements(length) 369 | } 370 | 371 | readInt32Array(): Int32Array { 372 | const length = this.readUint32() 373 | 374 | return this.readInt32Elements(length) 375 | } 376 | 377 | readFloat32Array(): Float32Array { 378 | const length = this.readUint32() 379 | 380 | return this.readFloat32Elements(length) 381 | } 382 | 383 | readFloat64Array(): Float64Array { 384 | const length = this.readUint32() 385 | 386 | return this.readFloat64Elements(length) 387 | } 388 | 389 | readUint8Arrays(count: number): Uint8Array[] { 390 | const result = new Array(count) 391 | 392 | for (let i = 0; i < count; i++) { 393 | result[i] = this.readUint8Array() 394 | } 395 | 396 | return result 397 | } 398 | 399 | readUint32Arrays(count: number): Uint32Array[] { 400 | const result = new Array(count) 401 | 402 | for (let i = 0; i < count; i++) { 403 | result[i] = this.readUint32Array() 404 | } 405 | 406 | return result 407 | } 408 | 409 | readInt32Arrays(count: number): Int32Array[] { 410 | const result = new Array(count) 411 | 412 | for (let i = 0; i < count; i++) { 413 | result[i] = this.readInt32Array() 414 | } 415 | 416 | return result 417 | } 418 | 419 | readFloat32Arrays(count: number): Float32Array[] { 420 | const result = new Array(count) 421 | 422 | for (let i = 0; i < count; i++) { 423 | result[i] = this.readFloat32Array() 424 | } 425 | 426 | return result 427 | } 428 | 429 | readFloat64Arrays(count: number): Float64Array[] { 430 | const result = new Array(count) 431 | 432 | for (let i = 0; i < count; i++) { 433 | result[i] = this.readFloat64Array() 434 | } 435 | 436 | return result 437 | } 438 | 439 | readAsciiString(): string { 440 | const data = this.readUint8Array() 441 | 442 | return String.fromCharCode(...data) 443 | } 444 | 445 | readAsciiStrings(count: number): string[] { 446 | const results = new Array(count) 447 | 448 | for (let i = 0; i < count; i++) { 449 | results[i] = this.readAsciiString() 450 | } 451 | 452 | return results 453 | } 454 | 455 | readUtf8String(): string { 456 | const data = this.readUint8Array() 457 | const decoder = new TextDecoder() 458 | 459 | return decoder.decode(data) 460 | } 461 | 462 | readUtf8Strings(count: number): string[] { 463 | const results = new Array(count) 464 | 465 | for (let i = 0; i < count; i++) { 466 | results[i] = this.readUtf8String() 467 | } 468 | 469 | return results 470 | } 471 | } 472 | -------------------------------------------------------------------------------- /implementations/wasm-zig/src/conduit/conduit.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const builtin = @import("builtin"); 3 | 4 | fn getStorage(comptime T: type, storage: []u64) []T { 5 | const ptr: [*]T = @alignCast(@ptrCast(storage.ptr)); 6 | 7 | return ptr[0 .. storage.len * @sizeOf(u64) / @sizeOf(T)]; 8 | } 9 | 10 | fn alignUp(offset: u32, comptime bytes: u8) u32 { 11 | const mask = bytes - 1; 12 | 13 | return (offset + mask) & ~@as(u32, mask); 14 | } 15 | 16 | /// Internal channel for managing typed storage arrays and offset tracking. 17 | /// 18 | /// The Channel maintains separate typed views of the same underlying memory buffer, 19 | /// allowing efficient access to different primitive types while ensuring proper alignment. 20 | const Channel = struct { 21 | const Self = @This(); 22 | 23 | _offset: u32 = 0, 24 | _storageUint8: []u8, 25 | _storageUint32: []u32, 26 | _storageInt32: []i32, 27 | _storageFloat32: []f32, 28 | _storageFloat64: []f64, 29 | 30 | /// Creates a new Channel from 8-byte aligned storage. 31 | /// 32 | /// Reinterprets the u64 storage as different primitive type arrays. 33 | /// 34 | /// Args: 35 | /// storage8byteAligned: A slice of u64 values providing the backing memory 36 | /// 37 | /// Returns: 38 | /// A new Channel instance with typed storage views 39 | pub fn from(storage8byteAligned: []u64) Self { 40 | return Self{ 41 | ._storageUint8 = getStorage(u8, storage8byteAligned), 42 | ._storageUint32 = getStorage(u32, storage8byteAligned), 43 | ._storageInt32 = getStorage(i32, storage8byteAligned), 44 | ._storageFloat32 = getStorage(f32, storage8byteAligned), 45 | ._storageFloat64 = getStorage(f64, storage8byteAligned), 46 | }; 47 | } 48 | 49 | /// Checks if the current offset is within bounds. 50 | /// 51 | /// Panics if the offset has exceeded the buffer capacity. 52 | fn checkOffset(self: *Self) void { 53 | if (self._offset >= self._storageUint8.len) { 54 | @branchHint(.cold); 55 | @panic("Channel out of bounds"); 56 | } 57 | } 58 | 59 | /// Resets the channel offset to the beginning of the buffer. 60 | /// 61 | /// This allows reusing the same buffer for multiple operations. 62 | pub fn reset(self: *Self) void { 63 | self._offset = 0; 64 | } 65 | 66 | /// Returns the typed storage array for the given type. 67 | /// 68 | /// Args: 69 | /// T: The primitive type (u8, u32, i32, f32, or f64) 70 | /// 71 | /// Returns: 72 | /// A slice of the appropriate type viewing the underlying storage 73 | pub inline fn storage(self: *const Self, comptime T: type) []T { 74 | switch (T) { 75 | u8 => return self._storageUint8, 76 | u32 => return self._storageUint32, 77 | i32 => return self._storageInt32, 78 | f32 => return self._storageFloat32, 79 | f64 => return self._storageFloat64, 80 | else => @compileError("Invalid type, expected u8, i32, u32, f32 or f64"), 81 | } 82 | } 83 | 84 | /// Aligns the current offset to the requirements of type T. 85 | /// 86 | /// Args: 87 | /// T: The type to align for 88 | pub inline fn alignTo(self: *Self, comptime T: type) void { 89 | self._offset = alignUp(self._offset, @sizeOf(T)); 90 | self.checkOffset(); 91 | } 92 | 93 | /// Gets the properly aligned offset for type T as an array index. 94 | /// 95 | /// Args: 96 | /// T: The type to get the offset for 97 | /// 98 | /// Returns: 99 | /// The array index for the current position in the typed storage 100 | pub inline fn getOffset(self: *Self, comptime T: type) u32 { 101 | switch (T) { 102 | u8 => { 103 | return self._offset; 104 | }, 105 | u32, i32, f32 => { 106 | self.alignTo(T); 107 | 108 | return self._offset >> 2; 109 | }, 110 | f64 => { 111 | self.alignTo(T); 112 | 113 | return self._offset >> 3; 114 | }, 115 | else => @compileError("Invalid type, expected u8, i32, u32, f32 or f64"), 116 | } 117 | } 118 | 119 | /// Advances the channel offset by the specified number of elements of type T. 120 | /// 121 | /// Args: 122 | /// T: The type of elements being advanced over 123 | /// count: The number of elements to advance by 124 | pub inline fn advance(self: *Self, comptime T: type, count: u32) void { 125 | switch (T) { 126 | u8 => self._offset += count, 127 | u32, i32, f32 => self._offset += count * 4, 128 | f64 => self._offset += count * 8, 129 | else => @compileError("Invalid type, expected u8, i32, u32, f32 or f64"), 130 | } 131 | 132 | self.checkOffset(); 133 | } 134 | }; 135 | 136 | /// A zero-allocation writer for the communication channel. 137 | /// 138 | /// The Writer provides type-safe methods to write primitive values and arrays to a shared 139 | /// memory buffer. It maintains proper alignment for different data types and tracks 140 | /// the current offset position. 141 | /// 142 | /// Example: 143 | /// ```zig 144 | /// var storage = [_]u64{0} ** 1024; 145 | /// var writer = Writer.from(&storage); 146 | /// 147 | /// writer.write(u32, 42); 148 | /// writer.write(f64, 3.14159); 149 | /// 150 | /// const array = [_]i32{1, 2, 3, 4}; 151 | /// writer.copyArray(i32, &array); 152 | /// ``` 153 | pub const Writer = struct { 154 | const Self = @This(); 155 | 156 | channel: Channel, 157 | 158 | /// Creates a new Writer from a slice of u64 storage. 159 | /// 160 | /// The storage must be 8-byte aligned and will be reinterpreted as different 161 | /// primitive types as needed. 162 | /// 163 | /// Args: 164 | /// storage: A slice of u64 values to use as the backing buffer 165 | /// 166 | /// Returns: 167 | /// A new Writer instance ready to write data 168 | pub fn from(storage: []u64) Self { 169 | return .{ .channel = Channel.from(storage) }; 170 | } 171 | 172 | /// Resets the writer to the beginning of the buffer. 173 | /// 174 | /// This allows reusing the same buffer for multiple write operations. 175 | pub fn reset(self: *Self) void { 176 | self.channel.reset(); 177 | } 178 | 179 | /// Writes a value of type T to the channel. 180 | /// 181 | /// For usize values, they are converted to u32 before writing. 182 | /// 183 | /// Args: 184 | /// T: The type of value to write (u8, u32, i32, f32, f64, or usize) 185 | /// value: The value to write 186 | pub fn write(self: *Self, comptime T: type, value: T) void { 187 | if (T == usize) { 188 | self.write(u32, @intCast(value)); 189 | } else { 190 | self.channel.storage(T)[self.channel.getOffset(T)] = value; 191 | self.channel.advance(T, 1); 192 | } 193 | } 194 | 195 | /// Initializes space for a single value of type T in the channel. 196 | /// 197 | /// Returns a mutable pointer to the initialized space. 198 | /// 199 | /// Args: 200 | /// T: The type of value to initialize space for 201 | /// 202 | /// Returns: 203 | /// A mutable pointer to the initialized value 204 | pub fn init(self: *Self, comptime T: type) *T { 205 | const offset = self.channel.getOffset(T); 206 | const ptr = &self.channel.storage(T)[offset]; 207 | self.channel.advance(T, 1); 208 | return ptr; 209 | } 210 | 211 | /// Initializes space for array elements of type T without writing a length prefix. 212 | /// 213 | /// Args: 214 | /// T: The type of elements to initialize space for 215 | /// length: The number of elements to initialize 216 | /// 217 | /// Returns: 218 | /// A mutable slice of the initialized elements 219 | pub fn initElements(self: *Self, comptime T: type, length: u32) []T { 220 | const start = self.channel.getOffset(T); 221 | 222 | self.channel.advance(T, length); 223 | 224 | return self.channel.storage(T)[start .. start + length]; 225 | } 226 | 227 | /// Initializes space for an array of type T with a length prefix. 228 | /// 229 | /// Writes the array length as u32 followed by initializing space for the elements. 230 | /// 231 | /// Args: 232 | /// T: The type of elements to initialize space for 233 | /// length: The number of elements to initialize 234 | /// 235 | /// Returns: 236 | /// A mutable slice of the initialized elements 237 | pub fn initArray(self: *Self, comptime T: type, length: u32) []T { 238 | self.write(u32, length); 239 | 240 | return self.initElements(T, length); 241 | } 242 | 243 | /// Copies array elements of type T to the channel without a length prefix. 244 | /// 245 | /// Args: 246 | /// T: The type of elements to copy 247 | /// arr: The slice of elements to copy 248 | pub fn copyElements(self: *Self, comptime T: type, arr: []T) void { 249 | const start = self.channel.getOffset(T); 250 | 251 | @memcpy(self.channel.storage(T)[start .. start + arr.len], arr); 252 | 253 | self.channel.advance(T, @intCast(arr.len)); 254 | } 255 | 256 | /// Copies an array of type T to the channel with a length prefix. 257 | /// 258 | /// Writes the array length as u32 followed by all array elements. 259 | /// 260 | /// Args: 261 | /// T: The type of elements to copy 262 | /// arr: The slice of elements to copy 263 | pub fn copyArray(self: *Self, comptime T: type, arr: []T) void { 264 | self.write(u32, @intCast(arr.len)); 265 | self.copyElements(T, arr); 266 | } 267 | 268 | /// Checks if the current writer state is valid. 269 | /// 270 | /// Returns: 271 | /// An error if the channel would overflow 272 | pub fn check(self: *Self) !void { 273 | const storage = self.channel.storage(u8); 274 | const offset = self.channel.getOffset(u8); 275 | 276 | if (offset > storage.len) { 277 | return error.ChannelPointerOverflow; 278 | } 279 | } 280 | }; 281 | 282 | /// A zero-allocation reader for the communication channel. 283 | /// 284 | /// The Reader provides type-safe methods to read primitive values and arrays from a shared 285 | /// memory buffer. It maintains proper alignment for different data types and tracks 286 | /// the current offset position. 287 | /// 288 | /// Example: 289 | /// ```zig 290 | /// var storage = [_]u64{0} ** 1024; 291 | /// var reader = Reader.from(&storage); 292 | /// 293 | /// const value = reader.read(u32); 294 | /// const pi = reader.read(f64); 295 | /// 296 | /// const array = reader.readArray(i32); 297 | /// ``` 298 | pub const Reader = struct { 299 | const Self = @This(); 300 | 301 | channel: Channel, 302 | 303 | /// Creates a new Reader from a slice of u64 storage. 304 | /// 305 | /// The storage must be 8-byte aligned and will be reinterpreted as different 306 | /// primitive types as needed. 307 | /// 308 | /// Args: 309 | /// storage: A slice of u64 values to use as the backing buffer 310 | /// 311 | /// Returns: 312 | /// A new Reader instance ready to read data 313 | pub fn from(storage: []u64) Self { 314 | return .{ .channel = Channel.from(storage) }; 315 | } 316 | 317 | /// Resets the reader to the beginning of the buffer. 318 | /// 319 | /// This allows reusing the same buffer for multiple read operations. 320 | pub fn reset(self: *Self) void { 321 | self.channel.reset(); 322 | } 323 | 324 | /// Reads a value of type T from the channel. 325 | /// 326 | /// Args: 327 | /// T: The type of value to read (u8, u32, i32, f32, or f64) 328 | /// 329 | /// Returns: 330 | /// The value read from the channel 331 | pub fn read(self: *Self, comptime T: type) T { 332 | const result = self.channel.storage(T)[self.channel.getOffset(T)]; 333 | 334 | self.channel.advance(T, 1); 335 | 336 | return result; 337 | } 338 | 339 | /// Reads array elements of type T from the channel without a length prefix. 340 | /// 341 | /// Args: 342 | /// T: The type of elements to read 343 | /// length: The number of elements to read 344 | /// 345 | /// Returns: 346 | /// A slice of the elements read from the channel 347 | pub fn readElements(self: *Self, comptime T: type, length: u32) []T { 348 | const start = self.channel.getOffset(T); 349 | 350 | self.channel.advance(T, length); 351 | 352 | return self.channel.storage(T)[start .. start + length]; 353 | } 354 | 355 | /// Reads an array of type T from the channel with a length prefix. 356 | /// 357 | /// First reads the array length as u32, then reads that many elements. 358 | /// 359 | /// Args: 360 | /// T: The type of elements to read 361 | /// 362 | /// Returns: 363 | /// A slice of the elements read from the channel 364 | pub fn readArray(self: *Self, comptime T: type) []T { 365 | const length = self.read(u32); 366 | 367 | return self.readElements(T, length); 368 | } 369 | 370 | /// Reads multiple arrays of type T from the channel. 371 | /// 372 | /// Each array is read with its length prefix. 373 | /// 374 | /// Args: 375 | /// T: The type of elements in each array 376 | /// dest: A slice of slices to store the read arrays 377 | pub fn readArrays(self: *Self, comptime T: type, dest: [][]T) void { 378 | var i: u32 = 0; 379 | 380 | while (i < dest.len) : (i += 1) { 381 | dest[i] = self.readArray(T); 382 | } 383 | } 384 | }; 385 | -------------------------------------------------------------------------------- /implementations/wasm-rust/conduit/mod.rs: -------------------------------------------------------------------------------- 1 | use std::mem; 2 | use std::cell::Cell; 3 | 4 | fn align_up(offset: u32, bytes: u8) -> u32 { 5 | let mask = (bytes - 1) as u32; 6 | (offset + mask) & !mask 7 | } 8 | 9 | fn get_storage_mut(storage: &mut [u64]) -> &mut [T] { 10 | let ptr = storage.as_mut_ptr() as *mut T; 11 | let len = storage.len() * mem::size_of::() / mem::size_of::(); 12 | unsafe { std::slice::from_raw_parts_mut(ptr, len) } 13 | } 14 | 15 | /// Macro to generate write methods for Writer. 16 | /// 17 | /// Generates methods that write primitive values to the channel. 18 | /// Each generated method: 19 | /// - Takes a value of the specified type 20 | /// - Calculates the proper offset with alignment 21 | /// - Writes the value to the appropriate storage array 22 | /// - Advances the channel offset 23 | macro_rules! impl_writer_methods { 24 | ($($type:ty, $field:ident, $method_suffix:ident);*) => { 25 | $( 26 | #[doc = concat!("Writes a `", stringify!($type), "` value to the channel.")] 27 | #[doc = ""] 28 | #[doc = "# Arguments"] 29 | #[doc = ""] 30 | #[doc = concat!("* `value` - The `", stringify!($type), "` value to write")] 31 | #[doc = ""] 32 | #[doc = "# Panics"] 33 | #[doc = ""] 34 | #[doc = "Panics if the channel buffer would overflow."] 35 | pub fn $method_suffix(&mut self, value: $type) { 36 | let offset = self.channel.offset_for::<$type>(); 37 | self.channel.$field[offset as usize] = value; 38 | self.channel.advance::<$type>(1); 39 | } 40 | )* 41 | }; 42 | } 43 | 44 | /// Macro to generate array operations and init methods for Writer. 45 | /// 46 | /// Generates methods for: 47 | /// - Copying arrays with length prefix 48 | /// - Copying array elements without length prefix 49 | /// - Initializing single values 50 | /// - Initializing arrays with length prefix 51 | /// - Initializing array elements without length prefix 52 | macro_rules! impl_writer_array_methods { 53 | ($($type:ty, $field:ident, $copy_array:ident, $copy_elements:ident, $init:ident, $init_array:ident, $init_elements:ident);*) => { 54 | $( 55 | #[doc = concat!("Copies a `", stringify!($type), "` array to the channel with length prefix.")] 56 | #[doc = ""] 57 | #[doc = "Writes the array length as u32 followed by all array elements."] 58 | #[doc = ""] 59 | #[doc = "# Arguments"] 60 | #[doc = ""] 61 | #[doc = concat!("* `arr` - The `", stringify!($type), "` slice to copy")] 62 | #[doc = ""] 63 | #[doc = "# Panics"] 64 | #[doc = ""] 65 | #[doc = "Panics if the channel buffer would overflow."] 66 | pub fn $copy_array(&mut self, arr: &[$type]) { 67 | self.write_u32(arr.len() as u32); 68 | self.$copy_elements(arr); 69 | } 70 | 71 | #[doc = concat!("Copies `", stringify!($type), "` array elements to the channel without length prefix.")] 72 | #[doc = ""] 73 | #[doc = "# Arguments"] 74 | #[doc = ""] 75 | #[doc = concat!("* `arr` - The `", stringify!($type), "` slice to copy")] 76 | #[doc = ""] 77 | #[doc = "# Panics"] 78 | #[doc = ""] 79 | #[doc = "Panics if the channel buffer would overflow."] 80 | pub fn $copy_elements(&mut self, arr: &[$type]) { 81 | let start = self.channel.offset_for::<$type>() as usize; 82 | let end = start + arr.len(); 83 | self.channel.$field[start..end].copy_from_slice(arr); 84 | self.channel.advance::<$type>(arr.len() as u32); 85 | } 86 | 87 | #[doc = concat!("Initializes space for a single `", stringify!($type), "` value in the channel.")] 88 | #[doc = ""] 89 | #[doc = "Returns a mutable pointer to the initialized space."] 90 | #[doc = ""] 91 | #[doc = "# Returns"] 92 | #[doc = ""] 93 | #[doc = concat!("A mutable pointer to the initialized `", stringify!($type), "` value.")] 94 | #[doc = ""] 95 | #[doc = "# Safety"] 96 | #[doc = ""] 97 | #[doc = "The returned pointer is valid until the channel is reset."] 98 | #[doc = ""] 99 | #[doc = "# Panics"] 100 | #[doc = ""] 101 | #[doc = "Panics if the channel buffer would overflow."] 102 | pub fn $init(&mut self) -> *mut $type { 103 | let offset = self.channel.offset_for::<$type>(); 104 | self.channel.advance::<$type>(1); 105 | unsafe { self.channel.$field.as_mut_ptr().add(offset as usize) } 106 | } 107 | 108 | #[doc = concat!("Initializes space for a `", stringify!($type), "` array with length prefix.")] 109 | #[doc = ""] 110 | #[doc = "Writes the array length as u32 followed by initializing space for the elements."] 111 | #[doc = ""] 112 | #[doc = "# Arguments"] 113 | #[doc = ""] 114 | #[doc = "* `length` - The number of elements to initialize"] 115 | #[doc = ""] 116 | #[doc = "# Returns"] 117 | #[doc = ""] 118 | #[doc = concat!("A mutable slice of `", stringify!($type), "` values.")] 119 | #[doc = ""] 120 | #[doc = "# Panics"] 121 | #[doc = ""] 122 | #[doc = "Panics if the channel buffer would overflow."] 123 | pub fn $init_array(&mut self, length: u32) -> &mut [$type] { 124 | self.write_u32(length); 125 | self.$init_elements(length) 126 | } 127 | 128 | #[doc = concat!("Initializes space for `", stringify!($type), "` array elements without length prefix.")] 129 | #[doc = ""] 130 | #[doc = "# Arguments"] 131 | #[doc = ""] 132 | #[doc = "* `length` - The number of elements to initialize"] 133 | #[doc = ""] 134 | #[doc = "# Returns"] 135 | #[doc = ""] 136 | #[doc = concat!("A mutable slice of `", stringify!($type), "` values.")] 137 | #[doc = ""] 138 | #[doc = "# Panics"] 139 | #[doc = ""] 140 | #[doc = "Panics if the channel buffer would overflow."] 141 | pub fn $init_elements(&mut self, length: u32) -> &mut [$type] { 142 | let start = self.channel.offset_for::<$type>() as usize; 143 | self.channel.advance::<$type>(length); 144 | &mut self.channel.$field[start..start + length as usize] 145 | } 146 | )* 147 | }; 148 | } 149 | 150 | /// Macro to generate read methods for Reader. 151 | /// 152 | /// Generates methods for: 153 | /// - Reading single primitive values 154 | /// - Reading arrays with length prefix 155 | /// - Reading array elements without length prefix 156 | macro_rules! impl_reader_methods { 157 | ($($type:ty, $field:ident, $read_method:ident, $read_array:ident, $read_elements:ident);*) => { 158 | $( 159 | #[doc = concat!("Reads a `", stringify!($type), "` value from the channel.")] 160 | #[doc = ""] 161 | #[doc = "# Returns"] 162 | #[doc = ""] 163 | #[doc = concat!("The `", stringify!($type), "` value read from the channel.")] 164 | #[doc = ""] 165 | #[doc = "# Panics"] 166 | #[doc = ""] 167 | #[doc = "Panics if the channel buffer would overflow."] 168 | pub fn $read_method(&self) -> $type { 169 | let offset = self.channel.offset_for::<$type>(); 170 | let result = self.channel.$field[offset as usize]; 171 | self.channel.advance::<$type>(1); 172 | result 173 | } 174 | 175 | #[doc = concat!("Reads a `", stringify!($type), "` array from the channel with length prefix.")] 176 | #[doc = ""] 177 | #[doc = "First reads the array length as u32, then reads that many elements."] 178 | #[doc = ""] 179 | #[doc = "# Returns"] 180 | #[doc = ""] 181 | #[doc = concat!("A slice of `", stringify!($type), "` values.")] 182 | #[doc = ""] 183 | #[doc = "# Panics"] 184 | #[doc = ""] 185 | #[doc = "Panics if the channel buffer would overflow."] 186 | pub fn $read_array(&self) -> &[$type] { 187 | let length = self.read_u32(); 188 | self.$read_elements(length) 189 | } 190 | 191 | #[doc = concat!("Reads `", stringify!($type), "` array elements from the channel without length prefix.")] 192 | #[doc = ""] 193 | #[doc = "# Arguments"] 194 | #[doc = ""] 195 | #[doc = "* `length` - The number of elements to read"] 196 | #[doc = ""] 197 | #[doc = "# Returns"] 198 | #[doc = ""] 199 | #[doc = concat!("A slice of `", stringify!($type), "` values.")] 200 | #[doc = ""] 201 | #[doc = "# Panics"] 202 | #[doc = ""] 203 | #[doc = "Panics if the channel buffer would overflow."] 204 | pub fn $read_elements(&self, length: u32) -> &[$type] { 205 | let start = self.channel.offset_for::<$type>() as usize; 206 | self.channel.advance::<$type>(length); 207 | &self.channel.$field[start..start + length as usize] 208 | } 209 | )* 210 | }; 211 | } 212 | 213 | struct Channel<'a> { 214 | offset: Cell, 215 | storage_u8: &'a mut [u8], 216 | storage_u32: &'a mut [u32], 217 | storage_i32: &'a mut [i32], 218 | storage_f32: &'a mut [f32], 219 | storage_f64: &'a mut [f64], 220 | } 221 | 222 | impl<'a> Channel<'a> { 223 | fn from(storage: &'a mut [u64]) -> Self { 224 | // This is unsafe but necessary for the zero-allocation pattern 225 | unsafe { 226 | let storage_ptr = storage.as_mut_ptr(); 227 | let storage_len = storage.len(); 228 | 229 | Self { 230 | offset: Cell::new(0), 231 | storage_u8: get_storage_mut(std::slice::from_raw_parts_mut(storage_ptr, storage_len)), 232 | storage_u32: get_storage_mut(std::slice::from_raw_parts_mut(storage_ptr, storage_len)), 233 | storage_i32: get_storage_mut(std::slice::from_raw_parts_mut(storage_ptr, storage_len)), 234 | storage_f32: get_storage_mut(std::slice::from_raw_parts_mut(storage_ptr, storage_len)), 235 | storage_f64: get_storage_mut(std::slice::from_raw_parts_mut(storage_ptr, storage_len)), 236 | } 237 | } 238 | } 239 | 240 | fn reset(&mut self) { 241 | self.offset.set(0); 242 | } 243 | 244 | fn check_offset(&self) { 245 | if self.offset.get() > self.storage_u8.len() as u32 { 246 | panic!("Channel buffer overflow"); 247 | } 248 | } 249 | 250 | fn align_to(&self) { 251 | let current = self.offset.get(); 252 | self.offset.set(align_up(current, mem::size_of::() as u8)); 253 | self.check_offset(); 254 | } 255 | 256 | fn offset_for(&self) -> u32 { 257 | self.align_to::(); 258 | let offset = self.offset.get(); 259 | match mem::size_of::() { 260 | 1 => offset, 261 | 4 => offset >> 2, 262 | 8 => offset >> 3, 263 | _ => panic!("Invalid type size"), 264 | } 265 | } 266 | 267 | fn advance(&self, count: u32) { 268 | let current = self.offset.get(); 269 | self.offset.set(current + count * mem::size_of::() as u32); 270 | self.check_offset(); 271 | } 272 | } 273 | 274 | /// A zero-allocation writer for the communication channel. 275 | /// 276 | /// The `Writer` provides methods to write primitive values and arrays to a shared 277 | /// memory buffer. It maintains proper alignment for different data types and tracks 278 | /// the current offset position. 279 | /// 280 | /// # Examples 281 | /// 282 | /// ```rust 283 | /// # use zaw::conduit::Writer; 284 | /// let mut storage = vec![0u64; 1024]; 285 | /// let mut writer = Writer::from(&mut storage); 286 | /// 287 | /// writer.write_u32(42); 288 | /// writer.write_f64(3.14159); 289 | /// 290 | /// let array = vec![1, 2, 3, 4]; 291 | /// writer.copy_array_i32(&array); 292 | /// ``` 293 | pub struct Writer<'a> { 294 | channel: Channel<'a>, 295 | } 296 | 297 | impl<'a> Writer<'a> { 298 | /// Creates a new `Writer` from a mutable slice of u64 storage. 299 | /// 300 | /// The storage must be 8-byte aligned and will be reinterpreted as different 301 | /// primitive types as needed. 302 | /// 303 | /// # Arguments 304 | /// 305 | /// * `storage` - A mutable slice of u64 values to use as the backing buffer 306 | /// 307 | /// # Returns 308 | /// 309 | /// A new `Writer` instance ready to write data. 310 | pub fn from(storage: &'a mut [u64]) -> Self { 311 | Self { 312 | channel: Channel::from(storage), 313 | } 314 | } 315 | 316 | /// Resets the writer to the beginning of the buffer. 317 | /// 318 | /// This allows reusing the same buffer for multiple write operations. 319 | pub fn reset(&mut self) { 320 | self.channel.reset(); 321 | } 322 | 323 | /// Writes a `usize` value as a `u32` to the channel. 324 | /// 325 | /// # Arguments 326 | /// 327 | /// * `value` - The usize value to write (will be cast to u32) 328 | /// 329 | /// # Panics 330 | /// 331 | /// Panics if the channel buffer would overflow. 332 | pub fn write_usize(&mut self, value: usize) { 333 | self.write_u32(value as u32); 334 | } 335 | 336 | // Generate basic write methods using macro 337 | impl_writer_methods! { 338 | u8, storage_u8, write_u8; 339 | u32, storage_u32, write_u32; 340 | i32, storage_i32, write_i32; 341 | f32, storage_f32, write_f32; 342 | f64, storage_f64, write_f64 343 | } 344 | 345 | // Generate array and init methods using macro 346 | impl_writer_array_methods! { 347 | u8, storage_u8, copy_array_u8, copy_elements_u8, init_u8, init_array_u8, init_elements_u8; 348 | u32, storage_u32, copy_array_u32, copy_elements_u32, init_u32, init_array_u32, init_elements_u32; 349 | i32, storage_i32, copy_array_i32, copy_elements_i32, init_i32, init_array_i32, init_elements_i32; 350 | f32, storage_f32, copy_array_f32, copy_elements_f32, init_f32, init_array_f32, init_elements_f32; 351 | f64, storage_f64, copy_array_f64, copy_elements_f64, init_f64, init_array_f64, init_elements_f64 352 | } 353 | } 354 | 355 | /// A zero-allocation reader for the communication channel. 356 | /// 357 | /// The `Reader` provides methods to read primitive values and arrays from a shared 358 | /// memory buffer. It maintains proper alignment for different data types and tracks 359 | /// the current offset position. 360 | /// 361 | /// # Examples 362 | /// 363 | /// ```rust 364 | /// # use zaw::conduit::Reader; 365 | /// let mut storage = vec![0u64; 1024]; 366 | /// let mut reader = Reader::from(&mut storage); 367 | /// 368 | /// let value = reader.read_u32(); 369 | /// let pi = reader.read_f64(); 370 | /// 371 | /// let array = reader.read_array_i32(); 372 | /// ``` 373 | pub struct Reader<'a> { 374 | channel: Channel<'a>, 375 | } 376 | 377 | impl<'a> Reader<'a> { 378 | /// Creates a new `Reader` from a mutable slice of u64 storage. 379 | /// 380 | /// The storage must be 8-byte aligned and will be reinterpreted as different 381 | /// primitive types as needed. 382 | /// 383 | /// # Arguments 384 | /// 385 | /// * `storage` - A mutable slice of u64 values to use as the backing buffer 386 | /// 387 | /// # Returns 388 | /// 389 | /// A new `Reader` instance ready to read data. 390 | pub fn from(storage: &'a mut [u64]) -> Self { 391 | Self { 392 | channel: Channel::from(storage), 393 | } 394 | } 395 | 396 | /// Resets the reader to the beginning of the buffer. 397 | /// 398 | /// This allows reusing the same buffer for multiple read operations. 399 | pub fn reset(&mut self) { 400 | self.channel.reset(); 401 | } 402 | 403 | // Generate all read methods using macro 404 | impl_reader_methods! { 405 | u8, storage_u8, read_u8, read_array_u8, read_elements_u8; 406 | u32, storage_u32, read_u32, read_array_u32, read_elements_u32; 407 | i32, storage_i32, read_i32, read_array_i32, read_elements_i32; 408 | f32, storage_f32, read_f32, read_array_f32, read_elements_f32; 409 | f64, storage_f64, read_f64, read_array_f64, read_elements_f64 410 | } 411 | } 412 | 413 | #[cfg(test)] 414 | mod test; 415 | --------------------------------------------------------------------------------