10 KiB
Contributing to WasmForge
Thanks for your interest in WasmForge. This guide covers everything a new contributor needs to start hacking: repository layout, prerequisites, build and test workflows, and the conventions we follow for code, commits, and PRs.
For the deeper "how this works" reference, see docs/ARCHITECTURE.md. For the catalogue of every build-time environment variable, see docs/ENVIRONMENT.md.
Repository layout
| Path | What lives here |
|---|---|
cmd/wasmforge/ |
The user-facing CLI (wasmforge build, run, clean, etc.) |
cmd/gen-ghost-profile/ |
Tool that turns any Go binary into a gopclntab ghost profile |
cmd/gen-build-assets/ |
Generates the embedded internal/build/build_assets.tar.gz |
cmd/gen-ptrmasks/ |
Regenerates Win32 pointer-mask tables from win32json metadata |
internal/build/ |
The three-stage Go pipeline (GOROOT prep, WASM compile, host embed) and the R80 stealth recipe |
internal/hostmod/ |
90+ host functions wazero exposes to guest code (networking, OS proxies, Win32, darwin, NativeAOT) |
internal/runtime/ |
wazero runtime configuration and execution |
internal/patch/ |
Stdlib + C# source patchers (AST transforms applied before compile) |
internal/sysshim/windows/ |
golang.org/x/sys/windows shim for wasip1 targets |
internal/names/ |
Per-build identifier randomization tables |
internal/platform/ |
Platform-specific socket handling on host |
internal/devtools/ |
Maintainer-only diagnostic binaries (pointer-mask audit, host-export lister, live-import probe, mono runner) |
dotnet/ |
C bridge, P/Invoke implementations, C# helpers, and assembly stubs for NativeAOT-WASI |
guest/ |
Guest-side libraries that ship inside the guest WASM (win32, darwin, rawnet) |
examples/ |
Runnable example programs — start here if you're new |
testdata/ |
Self-verifying test programs for each host-function group |
test/ |
Integration tests and the parity harness (//go:build integration gated) |
docs/ |
User docs and the architecture reference |
docs/internals/ |
Contributor-targeted reference docs (AST patcher, host API contract, parity harness) |
wazero/ |
Pinned + lightly forked wazero runtime (per-build identifier randomization) |
Prerequisites
-
Go 1.25 or later. The host binary, all
cmd/*targets, and the patched GOROOT all use the same Go toolchain. -
Make (any reasonably modern GNU/BSD make).
-
If you're going to touch the C# / .NET path:
- .NET 10 SDK (preview / RC channel as of writing — see
Dockerfile.buildfor the exact--qualitysetting), - NativeAOT-LLVM workload (
dotnet workload install wasi-experimental), - WASI SDK 24 (
WASI_SDK_PATHenv var), - the
wasm-component-ldwrapper atscripts/wasm-component-ld-wrapper.sh.
For a hands-off setup, use the Docker container described in
docs/CSHARP.md— it bundles every C# prerequisite and pins versions. - .NET 10 SDK (preview / RC channel as of writing — see
WasmForge has been developed and tested on macOS (Intel + Apple Silicon) and Linux x64. Cross-compiled output runs on Windows x64 and macOS, and the toolchain itself runs on Linux/macOS hosts.
Building wasmforge itself
Two paths, with different trade-offs:
# Quick: compile the CLI directly. Fine for development against the local
# tree. Uses live source under internal/hostmod/, internal/runtime/, etc.
go build -o wasmforge ./cmd/wasmforge
# Canonical: regenerate the embedded build_assets.tar.gz first, then build.
# This is what `make build` does. The archive snapshots internal/hostmod,
# internal/runtime, internal/names, and the wazero fork — wasmforge embeds
# it via //go:embed so distribution-mode builds (binary shipped without
# source tree) can still compile their host stage.
make build
If you modify anything under internal/hostmod/, internal/runtime/,
internal/names/, or wazero/, you must regenerate the archive
(make generate or make build) before distribution-mode builds will
reflect your changes. Development-mode builds (with the local source tree
present) are unaffected.
Running tests
The repository ships several test surfaces; they have different prerequisites.
Unit tests (no external deps)
go vet ./...
go test ./...
Unit tests for the host module, build pipeline, patchers, and devtools all run from a fresh clone with no environment setup.
testdata/ end-to-end programs
Each program under testdata/ is a self-verifying executable that prints
PASS: / FAIL: lines. Run the ones touching the area you changed:
./wasmforge build -o /tmp/test ./testdata/echo_tcp && /tmp/test
GOOS=windows GOARCH=amd64 ./wasmforge build -o /tmp/test.exe ./testdata/win32_registry
# (then run /tmp/test.exe on a Windows host)
Integration + parity tests
The test/ tree is its own Go module and is gated by
//go:build integration tags. Running it requires a target Windows host
that your test runner can reach. See docs/internals/PARITY-HARNESS.md.
Target configuration uses WASMFORGE_PARITY_* env vars (_DOMAIN, _DC,
_DC_IP, _CA, _USER). Defaults match the GOAD (Game of Active
Directory) topology stood
up under Ludus — see
test/parity/internal/lab/lab.go for the full list (sevenkingdoms.local,
dc01.sevenkingdoms.local, 10.3.10.10, etc.). Tests SKIP cleanly when
the target is unreachable, so vetting on a machine with no lab works.
cd test
go vet ./...
WASMFORGE_PARITY_DOMAIN=corp.example.com \
WASMFORGE_PARITY_DC=dc01.corp.example.com \
go test -tags integration ./parity/...
Adding a new host function
Host functions are how guest WebAssembly code reaches host OS APIs. The pattern is the same for networking, OS proxies, Win32, darwin, and NativeAOT functions.
- Add the implementation in
internal/hostmod/. Use platform-specific files (_windows.go,_darwin.go,_stub.go) when the function maps to a syscall not portable across hosts. - Register the function in the relevant
register*Functions()body inmodule.go(orwin32.go/darwin.gofor platform-scoped registries). - Add no-op or
ENOSYSstubs for platforms that don't implement it. The build tags must keep the package compilable on every supported host. - Add the anonymized export name in
internal/names/names.go. Host exports are renamed per build to defeat static signature matching. - Add the
//go:wasmimport env <anonymized_name>declaration in the matchingguest/*/imports.go. The guest sees only the anonymized name. - Wrap the raw import in a high-level Go API in the guest package so
guest code never deals with raw
uintptror untyped errnos. - Add a
testdata/program exercising the new function end-to-end.
For the contract-test side (every registered export must appear in the
host-api-contract), see docs/internals/HOST-API-CONTRACT.md —
internal/hostmod/contract_test.go enforces it in CI.
Adding a new guest library
- Create the package under
guest/. - Add
imports.gocontaining//go:wasmimport env <anonymized_name>declarations. Build-tag every file//go:build wasip1. - Add a high-level Go API that wraps the raw imports with sensible types, errors, and lifecycle (Open/Close, etc.).
- Document the new library in
docs/ARCHITECTURE.mdunder "Guest Libraries" and link atestdata/program demonstrating it.
Compiling complex Go projects: the wasip1 stub pattern
When you compile a third-party Go project for wasip1, the compiler's
relaxWindowsBuildConstraints pass rewrites every !windows constraint to
!windows && !wasip1. Files with platform-specific implementations under
_generic.go / _other.go get excluded — and if anything references
their exported symbols, the build fails with "undefined" errors.
The fix is to provide _wasip1.go stub files:
- Read the build error and identify the undefined function or type.
- Locate the
_generic.go/_other.gofile that normally defines it. - Create a
foo_wasip1.gosibling with stub implementations. Returning empty values orErrNotSupportedis usually fine — the goal is to satisfy the linker, not to functionally implement the platform API. - The compiler's collision handler will reconcile the new
_wasip1.gofile with any existing_windows.go/_generic.gosiblings automatically.
Code style
go fmtandgo vetmust pass cleanly. CI rejects PRs that don't.- Comments should explain why — the non-obvious constraint, the surprising design choice, the workaround for a specific upstream bug. Don't narrate what the code does when the code is self-explanatory.
- Avoid backwards-compatibility shims for unreleased code. We're pre-1.0; breaking changes are fine as long as they're called out in the PR.
- Don't add
-s -wor-trimpathto the embedder. WasmForge intentionally preserves Go debug info — stripping has historically increased detection rates from ML-based classifiers that flag stripped Go binaries as suspicious by default. Seedocs/ARCHITECTURE.md"WASM Embedding and AV Evasion" for the longer reasoning.
Commits and pull requests
- Conventional Commits: prefix the subject line with
feat:,fix:,chore:,docs:,refactor:,test:. The body should focus on the why — what motivated the change, what alternative was rejected, what the reviewer should pay attention to. - One concern per PR. Mechanical renames and behavior changes go in separate PRs.
- Test plan in the PR description. Use the template at
.github/PULL_REQUEST_TEMPLATE.md. Check the boxes that apply; explain what you did for the rest. - Don't commit submodule pointer updates for
wazero/unless you explicitly intend to bump the fork. Accidental pointer bumps are the most common source of PR conflicts. - CI must pass before merge.
go vet,go test, build matrix.
Security
If you find a security issue (memory safety, sandbox escape, supply-chain risk in the build pipeline), please don't open a public issue. See SECURITY.md for the disclosure process.
Conduct
This project follows the Contributor Covenant 2.1. Treat one another with respect. Disagreement is fine; harassment isn't.
Questions
Open a GitHub Discussion for design questions, integration help, or anything that isn't a bug or a concrete feature request. Bug reports and feature requests go through the Issue templates.