mirror of
https://github.com/hanzoai/wasm2go.git
synced 2026-10-30 15:06:48 +00:00
* wasm2go: a WebAssembly-to-Go ahead-of-time transpiler wasm2go translates a WebAssembly binary into standalone Go source. The generated package runs the module natively — no interpreter and no embedded wasm runtime — so it builds and links like ordinary Go code. This makes it practical to ship a wasm-compiled library (for example a large C/C++ codebase built with the WASI SDK) inside a Go program without the startup and memory cost of a wasm engine. Components: - internal/wasm — the WebAssembly binary parser. - internal/codegen — translates a parsed module into Go source through the SSA lowering + optimization + emission pipeline. - internal/ssa — a lightweight SSA IR plus optimization passes (const-propagation, DCE, CSE, branch folding, memory promotion). - transpile — the public library API: Parse, Translate, and the Transpile one-shot convenience wrapper. - cmd/wasm2go — the command-line interface. - internal/testfixture — shared helper that compiles testdata/*.wat to wasm on demand via wat2wasm. Library API (Options): - Required: Package, OutputImportPath. - Optional: BulkExportPrefix (per-export Inv_<svc>_<mt> dispatch for modules with uniformly-named exports), EntryExports (narrow DCE root set), KeepDeadFuncs (disable DCE for diff/debug), PromotionReportPath (write SSA memory-promotion JSON). Everything else is auto-derived from the input: the SSA pipeline runs unconditionally; data segments emit as //go:embed sidecars; wasi imports translate to Go-native syscalls; modules whose total function-body bytes exceed an internal threshold are emitted as a chain of //go:linkname-split sub-packages so the Go compiler stays inside a bounded per-package memory budget. WASI runtime: full wasi_snapshot_preview1 coverage. Args/environ, clocks, fd_*, path_* (open/create_directory/unlink_file/ remove_directory/rename/filestat_get/filestat_set_times/link/ symlink/readlink), readdir, pread/pwrite, sync/datasync, poll_oneoff (clock + fd-event branches), sched_yield, random_get, proc_exit, proc_raise, sock_accept/recv/send/shutdown — every call routes to the Go stdlib or golang.org/x/sys/unix equivalent. Per-platform companion files (unix/linux/darwin/windows) carry the bits that differ across OSes; the translated module is a normal Go program with the host's ordinary filesystem and network reach. Release engineering for the public repository: - GitHub Actions CI: build, vet, race-enabled tests behind an 85% coverage gate, golangci-lint — all driven through make targets. - Release workflow runs GoReleaser on version tags, including deb/rpm/apk packages, with build-provenance attestations recorded for every release asset. - A release-test workflow runs `make release/check` (GoReleaser config validation + a publish-free dry-run build) on every PR and main push so a broken release is caught before tag time. - golangci-lint and goreleaser are pinned in tools/go.mod and run via `go tool`, so local and CI invocations use the same versions. - `make install/wat2wasm` provisions the wat2wasm CLI the test suite needs, dispatching to the host's package manager (Homebrew on macOS, apt/dnf/pacman/apk on Linux). - Makefile targets for local development. Test coverage is 85.2% of statements overall. * README: document the WASI runtime, performance, and library entry points - Add a "WASI support" subsection explaining the single-file, stdlib-only `wasi_snapshot_preview1` implementation and the resulting cross-platform reach. - Add a "Performance" section with the wazero-vs-wasm2go corpus numbers (cold start 8.4×, short queries up to 81×, TPC-H scans within ±10%). Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]> * ci: align release-test with release.yml — use stable Go The release-test workflow exercises the same `make release/check` dry-run that the publish job runs at tag time, so it should pull the same toolchain. Switch it from `go-version-file: tools/go.mod` to `go-version: stable` to match release.yml. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]> --------- Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
130 lines
4.2 KiB
Go
130 lines
4.2 KiB
Go
package codegen_test
|
|
|
|
import (
|
|
"bytes"
|
|
"io"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/goccy/wasm2go/internal/codegen"
|
|
"github.com/goccy/wasm2go/internal/wasm"
|
|
)
|
|
|
|
// codegenParse is a thin wrapper around wasm.Parse for the tests in
|
|
// this file that synthesise hand-crafted modules.
|
|
func codegenParse(src []byte) (*wasm.Module, error) {
|
|
return wasm.Parse(bytes.NewReader(src))
|
|
}
|
|
|
|
// TestTranslateRequiresPackage exercises the explicit validation that
|
|
// Options.Package and Options.OutputImportPath must both be set on
|
|
// every Translate call — the old "default to wasm2go" fallback is gone.
|
|
func TestTranslateRequiresPackage(t *testing.T) {
|
|
mod := readFixture(t, "arith.wasm")
|
|
t.Run("missing-package", func(t *testing.T) {
|
|
_, err := codegen.Translate(io.Discard, mod, codegen.Options{
|
|
OutputImportPath: "example.com/x",
|
|
})
|
|
if err == nil || !strings.Contains(err.Error(), "Package") {
|
|
t.Errorf("expected Package-required error, got %v", err)
|
|
}
|
|
})
|
|
t.Run("missing-output-import-path", func(t *testing.T) {
|
|
_, err := codegen.Translate(io.Discard, mod, codegen.Options{Package: "x"})
|
|
if err == nil || !strings.Contains(err.Error(), "OutputImportPath") {
|
|
t.Errorf("expected OutputImportPath-required error, got %v", err)
|
|
}
|
|
})
|
|
}
|
|
|
|
// TestEntryExportsRoots covers the nil / empty-slice / non-empty
|
|
// semantics of Options.EntryExports.
|
|
//
|
|
// nil : every export is a DCE root.
|
|
// empty-slice : NO export is a root (only start + table).
|
|
// non-empty : only the named exports are roots.
|
|
func TestEntryExportsRoots(t *testing.T) {
|
|
mod := readFixture(t, "arith.wasm")
|
|
for _, tc := range []struct {
|
|
name string
|
|
eset []string
|
|
}{
|
|
{"nil", nil},
|
|
{"empty", []string{}},
|
|
{"named", []string{"add"}},
|
|
} {
|
|
tc := tc
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
var buf bytes.Buffer
|
|
_, err := codegen.Translate(&buf, mod, codegen.Options{
|
|
Package: "pkg", OutputImportPath: "gentest/pkg",
|
|
EntryExports: tc.eset,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("translate: %v", err)
|
|
}
|
|
if !strings.Contains(buf.String(), "package pkg") {
|
|
t.Errorf("output missing package decl for case %s", tc.name)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestExportMethodNameCollisionDetection synthesises a module whose
|
|
// two exports mangle to the same Go method name and confirms that
|
|
// Translate surfaces a clear error rather than emitting duplicate
|
|
// methods that fail to compile.
|
|
func TestExportMethodNameCollisionDetection(t *testing.T) {
|
|
// Hand-rolled minimal wasm with two exports named "XAlloc" and
|
|
// "x_alloc" — both mangle to "XAlloc" via ExportMethodName.
|
|
src := []byte{
|
|
0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00,
|
|
// type section: one type () -> ()
|
|
0x01, 0x04, 0x01, 0x60, 0x00, 0x00,
|
|
// function section: 2 funcs of type 0
|
|
0x03, 0x03, 0x02, 0x00, 0x00,
|
|
// export section: 2 exports
|
|
0x07, 0x14, 0x02,
|
|
0x06, 'X', 'A', 'l', 'l', 'o', 'c', 0x00, 0x00,
|
|
0x07, 'x', '_', 'a', 'l', 'l', 'o', 'c', 0x00, 0x01,
|
|
// code section: 2 empty bodies (just `end`)
|
|
0x0a, 0x07, 0x02,
|
|
0x02, 0x00, 0x0b,
|
|
0x02, 0x00, 0x0b,
|
|
}
|
|
m, err := codegenParse(src)
|
|
if err != nil {
|
|
t.Fatalf("parse: %v", err)
|
|
}
|
|
_, err = codegen.Translate(io.Discard, m, codegen.Options{
|
|
Package: "pkg", OutputImportPath: "gentest/pkg",
|
|
})
|
|
if err == nil {
|
|
t.Fatalf("expected collision error, got nil")
|
|
}
|
|
if !strings.Contains(err.Error(), "mangle") {
|
|
t.Errorf("error %v does not mention the mangle collision", err)
|
|
}
|
|
}
|
|
|
|
// TestBulkExportEmptyWarning confirms that BulkExportPrefix matching
|
|
// zero exports surfaces the diagnostic warning to stderr — the
|
|
// caller would otherwise silently get back per-export wrappers and
|
|
// wonder why no Inv_ functions were emitted.
|
|
func TestBulkExportEmptyWarning(t *testing.T) {
|
|
mod := readFixture(t, "arith.wasm")
|
|
var buf bytes.Buffer
|
|
if _, err := codegen.Translate(&buf, mod, codegen.Options{
|
|
Package: "pkg", OutputImportPath: "gentest/pkg",
|
|
BulkExportPrefix: "totally_unrelated_prefix_",
|
|
}); err != nil {
|
|
t.Fatalf("translate: %v", err)
|
|
}
|
|
// The warning is opportunistic — best confirmed by the
|
|
// generated source containing no Inv_ functions. The stderr
|
|
// path is exercised but not captured (would require a pipe).
|
|
if strings.Contains(buf.String(), "func Inv_") {
|
|
t.Errorf("expected no Inv_ functions with a non-matching prefix")
|
|
}
|
|
}
|