Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 42 additions & 15 deletions .github/actions/setup-binaryen/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ name: "Setup Binaryen"
description: "Install a pinned Binaryen release"
inputs:
version:
description: "Binaryen release version"
description: "LLGo Binaryen release tag; update checksums.txt together"
required: false
default: "132"
default: "llgo-v132.3"

runs:
using: "composite"
Expand All @@ -27,18 +27,30 @@ runs:
;;
esac

archive="binaryen-version_${version}-${platform}.tar.gz"
base_url="https://github.com/WebAssembly/binaryen/releases/download/version_${version}"
archive="binaryen-${version}-${platform}.tar.gz"
base_url="https://github.com/xgo-dev/binaryen/releases/download/${version}"
expected="$(awk -v platform="$platform" '$1 == platform { print $2 }' "$GITHUB_ACTION_PATH/checksums.txt")"
if [[ ! "$expected" =~ ^[0-9a-fA-F]{64}$ ]]; then
echo "Missing or invalid pinned Binaryen checksum for $platform" >&2
exit 1
fi
cd "$RUNNER_TEMP"
curl --retry 3 --retry-all-errors -fsSLO "${base_url}/${archive}"
curl --retry 3 --retry-all-errors -fsSLO "${base_url}/${archive}.sha256"
if command -v sha256sum >/dev/null; then
sha256sum --check "${archive}.sha256"
printf '%s %s\n' "$expected" "$archive" | sha256sum --check -
else
shasum -a 256 --check "${archive}.sha256"
printf '%s %s\n' "$expected" "$archive" | shasum -a 256 --check -
fi
tar -xzf "$archive" -C "$RUNNER_TEMP"
echo "$RUNNER_TEMP/binaryen-version_${version}/bin" >> "$GITHUB_PATH"
binaryen_root="$RUNNER_TEMP/binaryen-${version}"
test -x "$binaryen_root/bin/wasm-opt"
test -x "$binaryen_root/bin/wasm-emscripten-finalize"
echo "Binaryen root: $binaryen_root"
"$binaryen_root/bin/wasm-opt" --version
"$binaryen_root/bin/wasm-emscripten-finalize" --version
echo "EM_BINARYEN_ROOT=$binaryen_root" >> "$GITHUB_ENV"
echo "WASMOPT=$binaryen_root/bin/wasm-opt" >> "$GITHUB_ENV"
echo "$binaryen_root/bin" >> "$GITHUB_PATH"

- name: Install Binaryen on Windows
if: runner.os == 'Windows'
Expand All @@ -51,19 +63,34 @@ runs:
"ARM64" { "arm64-windows" }
default { throw "Unsupported Binaryen Windows host: $env:PROCESSOR_ARCHITECTURE" }
}
$archive = "binaryen-version_$version-$platform.tar.gz"
$baseUrl = "https://github.com/WebAssembly/binaryen/releases/download/version_$version"
$archive = "binaryen-$version-$platform.tar.gz"
$baseUrl = "https://github.com/xgo-dev/binaryen/releases/download/$version"
$archivePath = Join-Path $env:RUNNER_TEMP $archive
$checksumPath = "$archivePath.sha256"
$checksumRows = @(Get-Content -LiteralPath (Join-Path $env:GITHUB_ACTION_PATH "checksums.txt") |
Where-Object { ($_ -split '\s+')[0] -eq $platform })
if ($checksumRows.Count -ne 1) { throw "Missing pinned Binaryen checksum for $platform" }
$expected = ($checksumRows[0] -split '\s+')[1]
if ($expected -notmatch '^[0-9a-fA-F]{64}$') { throw "Invalid pinned Binaryen checksum for $platform" }
& curl.exe --fail --location --retry 3 --retry-all-errors --output $archivePath "$baseUrl/$archive"
if ($LASTEXITCODE -ne 0) { throw "Downloading Binaryen failed with exit code $LASTEXITCODE" }
& curl.exe --fail --location --retry 3 --retry-all-errors --output $checksumPath "$baseUrl/$archive.sha256"
if ($LASTEXITCODE -ne 0) { throw "Downloading the Binaryen checksum failed with exit code $LASTEXITCODE" }
$expected = ((Get-Content -LiteralPath $checksumPath -Raw).Trim() -split '\s+')[0]
$actual = (Get-FileHash -Algorithm SHA256 -LiteralPath $archivePath).Hash
if (-not $actual.Equals($expected, [StringComparison]::OrdinalIgnoreCase)) {
throw "Binaryen archive SHA-256 is $actual, want $expected"
}
& tar.exe -xzf $archivePath -C $env:RUNNER_TEMP
if ($LASTEXITCODE -ne 0) { throw "Extracting Binaryen failed with exit code $LASTEXITCODE" }
Add-Content -Encoding utf8 $env:GITHUB_PATH (Join-Path $env:RUNNER_TEMP "binaryen-version_$version\bin")
$binaryenRoot = Join-Path $env:RUNNER_TEMP "binaryen-$version"
$binaryenBin = Join-Path $binaryenRoot "bin"
$wasmOpt = Join-Path $binaryenBin "wasm-opt.exe"
$finalize = Join-Path $binaryenBin "wasm-emscripten-finalize.exe"
if (-not (Test-Path $wasmOpt) -or -not (Test-Path $finalize)) {
throw "Binaryen archive is missing wasm-opt or wasm-emscripten-finalize in $binaryenBin"
}
Write-Host "Binaryen root: $binaryenRoot"
& $wasmOpt --version
if ($LASTEXITCODE -ne 0) { throw "wasm-opt --version failed with exit code $LASTEXITCODE" }
& $finalize --version
if ($LASTEXITCODE -ne 0) { throw "wasm-emscripten-finalize --version failed with exit code $LASTEXITCODE" }
Add-Content -Encoding utf8 $env:GITHUB_ENV "EM_BINARYEN_ROOT=$binaryenRoot"
Add-Content -Encoding utf8 $env:GITHUB_ENV "WASMOPT=$wasmOpt"
Add-Content -Encoding utf8 $env:GITHUB_PATH $binaryenBin
9 changes: 9 additions & 0 deletions .github/actions/setup-binaryen/checksums.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# SHA-256 of xgo-dev/binaryen llgo-v132.3 release archives.
# Format: platform SHA-256. Verified against release assets and .sha256 files.
aarch64-linux d7fe6f6558bd668f5bea3c64fd700885854a4847c023cf7a3ce3286158fd077d
arm64-macos 06427b4fdd36a97d28ac5d7d1826a1aaa8af5393a3e4d50cf4f8017462f38b73
arm64-windows 58553d04ab8e67e4b741fb946e444ea11622dda53893054513559f21cbb0c7b8
node bbb11c13202433ddf98ddd84b96d1a74b9ab9a48544a37702f8a53659d7b5691
x86_64-linux c82c9cbf5f53d17f01ca66b40ddd1f2e87dc34591ca562ef4c411824997e5bf8
x86_64-macos 0cef8ddc4c59d7819b92c79be01c7655fd5f244494af474a7a31ed29a05a170c
x86_64-windows b20b4c06fb1ae4da6461f5387958979733c741e6e4a437a31a2821ae6e40bb53
2 changes: 1 addition & 1 deletion .github/workflows/benchmark.yml
Original file line number Diff line number Diff line change
Expand Up @@ -337,7 +337,7 @@ jobs:
- name: Set up Emscripten
uses: emscripten-core/setup-emsdk@v15
with:
version: "4.0.21"
version: "6.0.8"

- name: Set up Binaryen
uses: ./.github/actions/setup-binaryen
Expand Down
6 changes: 2 additions & 4 deletions .github/workflows/llgo.yml
Original file line number Diff line number Diff line change
Expand Up @@ -566,12 +566,10 @@ jobs:
- name: Set up Emscripten
uses: emscripten-core/setup-emsdk@v15
with:
version: "4.0.21"
version: "6.0.8"

- name: Set up Node for Emscripten Memory64
# Emscripten 4.0.21's Memory64 glue rejects Node versions older than
# 23. setup-emsdk currently bundles Node 22, so select an active LTS
# runtime explicitly for execution without changing emcc itself.
# Keep memory64 JavaScript execution on the pinned Node LTS runtime.
uses: actions/setup-node@v7
with:
node-version: "24"
Expand Down
12 changes: 8 additions & 4 deletions doc/wasm-proposal.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# LLGo WebAssembly Proposal

Status: draft. Tracking issue: [xgo-dev/llgo#2152](https://github.com/xgo-dev/llgo/issues/2152).
Status: active. Profile and source-compatibility work is tracked in [xgo-dev/llgo#2152](https://github.com/xgo-dev/llgo/issues/2152); the supported execution and toolchain decisions are tracked in [#2632](https://github.com/xgo-dev/llgo/issues/2632).

The supported build uses the LLGo-patched Binaryen release. Browser execution keeps Emscripten Fiber/Asyncify and advances to a bounded Web Worker pool. W32 advances to WASI threads on WAMR; single-thread WASI is retired only after threaded GC passes. These execution milestones proceed alongside W1-W3 rather than waiting for W3 to finish.

R1 through R3, including R2.1, are merged and remain the delivered foundation of this proposal. W1-W3 build on that foundation to complete the supported WebAssembly profiles before advanced engine features are added.

Expand Down Expand Up @@ -109,13 +111,15 @@ Run and classify the full applicable `test/**` suite on all four paths, the comp

## Deferred work

W64, WASI Preview 2 and WIT components, threads and Atomics, WasmGC, Exception Handling, JSPI and Stack Switching, multiple workers, and parallel goroutines are later capability work. Unsupported environments continue to use the single-worker scheduler and LLGo linear-memory GC.
W64, WASI Preview 2 and WIT components, a WasmGC heap, and JSPI/stackless execution remain later research. EH encoding is selected through the comparison in [#2632](https://github.com/xgo-dev/llgo/issues/2632); existing validated EH behavior remains supported during that work. Browser multi-worker execution and W32 WASI threads are required milestones of #2632, with the single-worker browser mode retained.

---

# LLGo WebAssembly 提案

状态:草案。跟踪 issue:[xgo-dev/llgo#2152](https://github.com/xgo-dev/llgo/issues/2152)。
状态:推进中。Profile 与源码兼容性由 [xgo-dev/llgo#2152](https://github.com/xgo-dev/llgo/issues/2152) 跟踪;正式执行架构与工具链决定由 [#2632](https://github.com/xgo-dev/llgo/issues/2632) 跟踪。

正式构建使用 LLGo 打补丁的 Binaryen 版本。浏览器保留 Emscripten Fiber/Asyncify,并推进到有上限的 Web Worker 池。W32 以 WAMR 上的 WASI threads 为目标;只有在线程 GC 验收通过后才移除单线程 WASI。这些运行时工作与 W1-W3 并行推进,不等待 W3 结束。

R1 至 R3(包括 R2.1)已经合并,作为本提案已交付的基础继续保留。W1-W3 在此基础上完成当前支持的 WebAssembly profile,再推进高级引擎特性。

Expand Down Expand Up @@ -224,4 +228,4 @@ LLGo Core Wasm C ABI 与 WIT Canonical ABI 无关。未来 WASI Preview 2 将在

## 延期范围

W64、WASI Preview 2/WIT component、threads/Atomics、WasmGC、Exception Handling、JSPI/Stack Switching、多 worker 和并行 goroutine 都是后续 capability 工作。不支持这些能力的环境继续使用单 worker 调度和 LLGo 线性内存 GC。
W64、WASI Preview 2/WIT component、WasmGC 堆以及 JSPI/stackless 执行仍属于后续研究。EH 编码按照 [#2632](https://github.com/xgo-dev/llgo/issues/2632) 的对比结果确定;期间保留已验证的 EH 行为。浏览器多 worker 和 W32 WASI threads 是 #2632 的必需里程碑,浏览器仍保留单 worker 模式。
7 changes: 7 additions & 0 deletions internal/build/build_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,13 @@ func TestMain(m *testing.M) {
os.Exit(0)
}
if mode := os.Getenv("LLGO_TEST_WASM_OPT_HELPER"); mode != "" {
if len(os.Args) == 2 && os.Args[1] == "--version" {
if mode == "fail-version" {
os.Exit(7)
}
fmt.Println("wasm-opt version 132 (test helper)")
os.Exit(0)
}
if argsFile := os.Getenv("ARGS_FILE"); argsFile != "" {
file, err := os.OpenFile(argsFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o666)
if err == nil {
Expand Down
39 changes: 31 additions & 8 deletions internal/build/wasm_postlink.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import (
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"

"github.com/xgo-dev/llgo/internal/crosscompile"
"github.com/xgo-dev/llgo/internal/optlevel"
Expand Down Expand Up @@ -101,13 +103,9 @@ func createClosedTemp(dir, pattern string) (string, error) {
}

func postLinkWasm(ctx *context, input, output string, verbose bool) error {
wasmOpt := os.Getenv("WASMOPT")
if wasmOpt == "" {
wasmOpt = "wasm-opt"
}
resolved, err := exec.LookPath(wasmOpt)
resolved, err := resolveWasmOpt()
if err != nil {
return fmt.Errorf("WebAssembly Asyncify requires wasm-opt; install Binaryen or set WASMOPT: %w", err)
return fmt.Errorf("WebAssembly Asyncify requires wasm-opt; install Binaryen or set WASMOPT or EM_BINARYEN_ROOT: %w", err)
}

preAsyncifyArgs := wasmPreAsyncifyArgs(
Expand All @@ -118,7 +116,7 @@ func postLinkWasm(ctx *context, input, output string, verbose bool) error {
)
if preAsyncifyArgs != nil {
if err := runWasmOpt(resolved, preAsyncifyArgs, verbose, ctx); err != nil {
return fmt.Errorf("wasm-opt pre-Asyncify optimization failed: %w", err)
return fmt.Errorf("wasm-opt pre-Asyncify optimization failed using %s: %w", wasmOptIdentity(resolved), err)
}
}

Expand All @@ -139,14 +137,39 @@ func postLinkWasm(ctx *context, input, output string, verbose bool) error {
ctx.buildConf.OptLevel,
)
if err := runWasmOpt(resolved, args, verbose, ctx); err != nil {
return fmt.Errorf("wasm-opt Asyncify failed: %w", err)
return fmt.Errorf("wasm-opt Asyncify failed using %s: %w", wasmOptIdentity(resolved), err)
}
if err := os.Rename(tmpName, output); err != nil {
return err
}
return nil
}

func resolveWasmOpt() (string, error) {
wasmOpt := os.Getenv("WASMOPT")
if wasmOpt == "" {
if root := os.Getenv("EM_BINARYEN_ROOT"); root != "" {
name := "wasm-opt"
if runtime.GOOS == "windows" {
name += ".exe"
}
wasmOpt = filepath.Join(root, "bin", name)
} else {
wasmOpt = "wasm-opt"
}
}
return exec.LookPath(wasmOpt)
}

func wasmOptIdentity(path string) string {
output, err := exec.Command(path, "--version").CombinedOutput()
version := strings.TrimSpace(string(output))
if err != nil || version == "" {
return path + " (version unavailable)"
}
return path + " (" + version + ")"
}

func runWasmOpt(resolved string, args []string, verbose bool, ctx *context) error {
if ctx.shouldPrintCommands(verbose) {
fmt.Fprintln(os.Stderr, resolved, args)
Expand Down
44 changes: 44 additions & 0 deletions internal/build/wasm_postlink_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,9 @@ func TestPostLinkWasmReportsToolFailure(t *testing.T) {
if err == nil || !strings.Contains(err.Error(), "wasm-opt Asyncify failed") {
t.Fatalf("postLinkWasm() error = %v", err)
}
if !strings.Contains(err.Error(), tool) || !strings.Contains(err.Error(), "wasm-opt version 132 (test helper)") {
t.Fatalf("postLinkWasm() error omits selected Binaryen tool: %v", err)
}
if data, err := os.ReadFile(output); err != nil || string(data) != "old" {
t.Fatalf("failed post-link changed final output: %q, %v", data, err)
}
Expand All @@ -263,6 +266,9 @@ func TestPostLinkWasmReportsPreAsyncifyFailure(t *testing.T) {
if err == nil || !strings.Contains(err.Error(), "wasm-opt pre-Asyncify optimization failed") {
t.Fatalf("postLinkWasm() error = %v", err)
}
if !strings.Contains(err.Error(), tool) || !strings.Contains(err.Error(), "wasm-opt version 132 (test helper)") {
t.Fatalf("postLinkWasm() error omits selected Binaryen tool: %v", err)
}
if data, err := os.ReadFile(output); err != nil || string(data) != "old" {
t.Fatalf("failed pre-optimization changed final output: %q, %v", data, err)
}
Expand Down Expand Up @@ -301,6 +307,44 @@ func TestPostLinkWasmReportsMissingTool(t *testing.T) {
}
}

func TestResolveWasmOptUsesEmscriptenBinaryenRoot(t *testing.T) {
root := t.TempDir()
bin := filepath.Join(root, "bin")
if err := os.Mkdir(bin, 0o755); err != nil {
t.Fatal(err)
}
want := writeWasmOptTestTool(t, bin)
t.Setenv("EM_BINARYEN_ROOT", root)
t.Setenv("WASMOPT", "")
got, err := resolveWasmOpt()
if err != nil || got != want {
t.Fatalf("resolveWasmOpt() = %q, %v; want %q", got, err, want)
}

override := writeWasmOptTestTool(t, t.TempDir())
t.Setenv("WASMOPT", override)
got, err = resolveWasmOpt()
if err != nil || got != override {
t.Fatalf("resolveWasmOpt() with WASMOPT = %q, %v; want %q", got, err, override)
}

t.Setenv("EM_BINARYEN_ROOT", "")
t.Setenv("WASMOPT", "")
t.Setenv("PATH", bin)
got, err = resolveWasmOpt()
if err != nil || got != want {
t.Fatalf("resolveWasmOpt() from PATH = %q, %v; want %q", got, err, want)
}
}

func TestWasmOptIdentityWhenVersionUnavailable(t *testing.T) {
tool := writeWasmOptTestTool(t, t.TempDir())
t.Setenv("LLGO_TEST_WASM_OPT_HELPER", "fail-version")
if got, want := wasmOptIdentity(tool), tool+" (version unavailable)"; got != want {
t.Fatalf("wasmOptIdentity() = %q; want %q", got, want)
}
}

func TestPostLinkWasmReportsInvalidOutputDirectory(t *testing.T) {
dir := t.TempDir()
tool := writeWasmOptTestTool(t, dir)
Expand Down
Loading