npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

wago-bind-as

v0.1.0

Published

Zero-copy shared-memory bindings between Go and AssemblyScript on Wago

Readme

The bytes in WebAssembly linear memory are the object. Go and AssemblyScript access the same fields directly—there is no recursive serialization, reflection, field-name lookup, or duplicate guest object graph on the hot path.

This repository is under active development. Exact32 version 1 is implemented; the schema and generator surface may still change before 1.0.

Why wago-bind-as

Conventional bindings rebuild a host object as a guest object before useful work begins. That cost scales with everything in the object—even fields the guest never reads.

wago-bind-as uses a compact little-endian ABI called Exact32:

Go producer ──writes──▶ one region in Wasm memory ◀──reads/mutates── AssemblyScript
  • Scalars are fixed-offset loads and stores.
  • Utf8 and Bytes are 8-byte (offset, length) spans.
  • Vec<T> is a 12-byte (offset, length, capacity) descriptor.
  • References are 32-bit offsets relative to the region, never host pointers.
  • Generated Go views use no reflection.
  • Generated AssemblyScript accessors allocate nothing.
  • Validation is lazy: reading two scalar fields never scans an untouched body.
  • Arena generations reject stale Go views after reset or guest execution.
  • ReserveBytes lets an I/O producer write directly into its final shared payload; BeginImage/SealImage create sanitized relocatable images when transport is needed.

Installation

Install the AssemblyScript package and Go runtime:

npm install wago-bind-as
go get github.com/JairusSW/wago-bind-as

Quick Start

Write ordinary AssemblyScript. Exported functions define the binding surface; reachable record classes define the shared types:

class Vec3 {
  x: f32;
  y: f32;
  z: f32;

  constructor(x: f32 = 0, y: f32 = 0, z: f32 = 0) {
    this.x = x;
    this.y = y;
    this.z = z;
  }
}

export function getVelocity(lastTick: Vec3, current: Vec3): Vec3 {
  return new Vec3(
    current.x - lastTick.x,
    current.y - lastTick.y,
    current.z - lastTick.z,
  );
}

Enable the transform and compile once:

{
  "options": {
    "transform": ["wago-bind-as"]
  }
}
asc assembly/index.ts --transform wago-bind-as -o module.wasm

During that single compilation the transform infers the type graph and function signatures, updates bindings/wago_bindings.bind.go, injects allocator and ABI metadata, then lets AssemblyScript compile the already-lowered program. There is no user-authored schema or second AS compilation.

The generated Go package exposes native structs and prebound methods:

module, err := bindings.Bind(instance)
if err != nil {
    return err
}

velocity, err := module.GetVelocity(
    bindings.Vec3{X: 1, Y: 2, Z: 3},
    bindings.Vec3{X: 4, Y: 8, Z: 5},
)
// velocity == bindings.Vec3{X: 3, Y: 6, Z: 2}

Bind checks the ABI fingerprint exported by the Wasm module, resolves functions once, and allocates reusable argument and result storage. Calls are serialized and copy only the fixed-size native values being adapted. The generated wire-view interface remains available for large, already-shared data.

The inferred function facade currently supports records made from fixed-width scalar fields. Void functions accept up to four record parameters; record-returning functions accept three because the fourth allocation-free call argument points to reusable result storage. A result returned directly as new Result(...) is treated as owned and reclaimed by the generated wrapper before it returns; returning one of the input parameters is borrowed. The transform rejects ambiguous ownership instead of guessing.

This initial facade deliberately enforces a narrow safe subset. A lowered record may only be allocated as the direct final result of a binding; helper and temporary allocations of that class are rejected because lowered classes are unmanaged. Result returns inside branches or loops are rejected until ownership can be tracked per control-flow edge. Boundary types must also have one unambiguous local name: qualified, renamed, unresolved, and duplicate same-name declarations fail compilation. Utf8 and Bytes remain available in the schema-driven wire-view API below, but are rejected in automatically lowered classes until descriptor property access can be lowered to Exact32's four-byte alignment.

Go-authored models

Go can be the source of truth when AssemblyScript should import an existing Go model. Point the declaration at a generated .bind.ts file:

//wago:bind assembly/models.bind.ts
type Vec3 struct {
    X float32
    Y float32
    Z float32
}

AssemblyScript imports it as normal source:

import { Vec3 } from "./models.bind";

When asc starts, the transform checks Go declarations before parsing entry files. A missing or stale .bind.ts is regenerated atomically, so the same compiler invocation sees the current strongly typed class. Unchanged files are not rewritten.

Go-authored bind classes currently support exported fixed-width scalar fields. Use a wago:"name" struct tag when the AssemblyScript property name should differ from the Go field name.

Generated source records its provenance:

// Code generated by wago-bind-as. DO NOT EDIT.
// Generator: 0.1.0
// Source fingerprint: 51c3...

The resolved manifest remains an internal build artifact. A separate ABI fingerprint covers layouts and function signatures; generated Go refuses to bind a mismatched Wasm module.

Ownership and safety

A region is the ownership and reclamation unit for the wire-view path. Related records and payloads are bump-allocated together and reset together, with no per-object free, reference count, or cross-language tracing collector. For the native function facade, a generated AssemblyScript wrapper copies a transient result into caller-provided storage and reclaims an owned value before the same Wasm call returns.

Important rules:

  • Do not retain Record, SpanView, UTF8View, Vector, slices, or borrowed strings outside their documented scope.
  • Reacquire views after guest execution. Descriptors may have changed.
  • Generated child, reference, span, and vector operations preserve region identity. Cross-arena assignment and allocation fail with ErrRegionMismatch; stale parents fail with ErrStale.
  • BorrowedString is explicitly unsafe to retain or use across mutation. CopyString owns its result.
  • Mutable access is serialized. Do not invoke or close the same Wago instance through another handle concurrently.
  • Host imports should use WithGuestRegion; it borrows memory through Wago's callback-scoped GuestStorage lifecycle gate.
  • Reset zeroes initialized bytes by default. WithoutResetZeroing is only for a proven single-trust-domain hot loop.
  • Bounds use widened arithmetic, and vector capacity—not only length—is validated before writable access.

Wire types

| Schema type | Exact32 representation | Size | | -------------------------------- | --------------------------------------------- | ------------: | | bool, i8…u64, f32, f64 | Little-endian scalar | 1–8 bytes | | utf8, bytes | { offset: u32, length: u32 } | 8 bytes | | ref<T> | Region-relative offset | 4 bytes | | vec<T> | { offset: u32, length: u32, capacity: u32 } | 12 bytes | | named record | Inline fixed-layout record | resolved | | array<T, N> | Inline contiguous fixed-size elements | sizeof(T)*N | | timestamp_ns, duration_ns | Signed nanoseconds | 8 bytes | | uuid, digest256 | Inline fixed bytes | 16 / 32 bytes |

Records are naturally aligned up to 8 bytes. The compiler sorts type definitions before fingerprinting, retains field order, rejects inline cycles, and permits cycles only through reference-like descriptors.

Performance

The generated Vec3 facade benchmark includes two native struct writes, one Wago call, the result read, and reclamation of the owned AssemblyScript result inside that call:

| Host | Typed GetVelocity call | Allocations | | ---------------------------- | -----------------------: | ----------: | | Apple M4 Max, Darwin/arm64 | 76.9–80.1 ns/op | 0 | | Ryzen 7 7800X3D, Linux/amd64 | 69.0–70.7 ns/op | 0 |

Ten 750 ms samples were run with Go 1.26.5 on arm64 and Go 1.22.2 on amd64 against Wago v0.1.0-beta.8. The transform replaces AssemblyScript's imported env.abort with a local unreachable() trap, allowing Wago to select its import-free prepared-entry path. This preserves trapping but omits formatted abort messages and source locations.

The checked Wago binding benchmark calls a guest that validates a 56-byte root and UTF-8 span, compares the five-byte prefix admin, and changes one f32 field.

| Host | Empty body | 32 KiB untouched body | Allocations | | ---------------------------- | --------------: | --------------------: | ----------: | | Apple M4 Max, Darwin/arm64 | 74.3–75.3 ns/op | 74.9–76.0 ns/op | 0 | | Ryzen 7 7800X3D, Linux/amd64 | 63.8–66.2 ns/op | 63.9–65.5 ns/op | 0 |

These are call/access measurements over an already-built wire object. The important result is that the untouched 32 KiB body does not change call latency. The benchmark reports latency only: it intentionally does not derive throughput from bytes the guest never reads.

Construction from the example native Go input is measured separately. It includes UTF-8 and body copies into final Wasm memory:

| Host | Empty body | 32 KiB body copy | Allocations | | ---------------------------- | ---------: | ---------------: | ----------: | | Apple M4 Max, Darwin/arm64 | 315–325 ns | 0.87–1.03 µs | 0 | | Ryzen 7 7800X3D, Linux/amd64 | 588–597 ns | 1.35–1.37 µs | 0 |

Run the benchmark locally:

npm test # builds the generated Vec3 integration fixture
go test ./build/testdata/velocity -run '^$' -bench BenchmarkGeneratedVelocityCall -benchmem -benchtime=750ms -count=5

cd bench
go test -run '^$' -bench 'Benchmark(WagoBindAS|WagoPreparedFloor|BuildNativeInput)$' -benchmem -benchtime=750ms -count=5

The lower-level wire-view request example is in examples/request; the schema-free typed facade is exercised end-to-end by the transform integration test.

Development

npm install
npm test
GOCACHE=/tmp/wago-bind-as-go-cache go test -race ./...
GOCACHE=/tmp/wago-bind-as-go-cache go vet ./...

See CONTRIBUTING.md before submitting a change. Security-sensitive issues should follow SECURITY.md.

License

MIT

Contact