Skip to content
Closed
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
10 changes: 9 additions & 1 deletion .github/workflows/llgo.yml
Original file line number Diff line number Diff line change
Expand Up @@ -489,6 +489,14 @@ jobs:
run_wasm_scheduler "$RUNNER_TEMP/wasm-scheduler.mjs"
GOOS=wasip1 GOARCH=wasm llgo build -o "$RUNNER_TEMP/wasm-scheduler-wasip1.wasm" ./internal/build/testdata/wasm-scheduler
run_wasi_scheduler "$RUNNER_TEMP/wasm-scheduler-wasip1.wasm"
GOOS=js GOARCH=wasm llgo build -tags=llgo_wasm_gc -o "$RUNNER_TEMP/wasm-gc-go.mjs" ./internal/build/testdata/wasm-gc
node --input-type=module -e "import Module from '$RUNNER_TEMP/wasm-gc-go.mjs'; await Module();"
llgo build -target wasm -tags=llgo_wasm_gc -o "$RUNNER_TEMP/wasm-gc.mjs" ./internal/build/testdata/wasm-gc
node --input-type=module -e "import Module from '$RUNNER_TEMP/wasm-gc.mjs'; await Module();"
GOOS=wasip1 GOARCH=wasm LLGO_WASI_THREADS=0 llgo build -tags=llgo_wasm_gc -o "$RUNNER_TEMP/wasm-gc-wasip1.wasm" ./internal/build/testdata/wasm-gc
wasm-tools validate --features all "$RUNNER_TEMP/wasm-gc-wasip1.wasm"
test "$(wasmtime run -W exceptions=y "$RUNNER_TEMP/wasm-gc-wasip1.wasm" 2>&1)" = "wasm gc ok"
file "$RUNNER_TEMP/runtime-js.wasm" \
"$RUNNER_TEMP/runtime-wasip1.wasm" \
"$RUNNER_TEMP/runtime-wasip1-threads.wasm"
"$RUNNER_TEMP/runtime-wasip1-threads.wasm" \
"$RUNNER_TEMP/wasm-gc-wasip1.wasm"
34 changes: 34 additions & 0 deletions internal/build/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import (
"strings"
"sync"
"sync/atomic"
"unicode"

"golang.org/x/tools/go/ssa"

Expand Down Expand Up @@ -385,6 +386,9 @@ func Build(inv Invocation) ([]Package, error) {
if conf.Target != "" && export.GOARCH != "" {
conf.Goarch = export.GOARCH
}
if err := configureWasmGC(conf, &export); err != nil {
return nil, err
}
if conf.AppExt == "" {
conf.AppExt = defaultAppExt(conf)
}
Expand Down Expand Up @@ -778,6 +782,36 @@ func defaultBuildTags(goarch, target string) string {
return tags
}

func configureWasmGC(conf *Config, export *crosscompile.Export) error {
if conf.Goarch != "wasm" || !hasBuildTag(conf.Tags, "llgo_wasm_gc") {
return nil
}
switch conf.Goos {
case "js":
if !slices.Contains(export.LDFLAGS, "-sMALLOC=none") {
export.LDFLAGS = append(export.LDFLAGS, "-sMALLOC=none")
}
case "wasip1":
if IsWasiThreadsEnabled() {
return errors.New("llgo_wasm_gc requires single-worker WASI (set LLGO_WASI_THREADS=0)")
}
default:
return fmt.Errorf("llgo_wasm_gc does not support GOOS=%s", conf.Goos)
}
return nil
}

func hasBuildTag(tags, want string) bool {
for _, tag := range strings.FieldsFunc(tags, func(r rune) bool {
return r == ',' || unicode.IsSpace(r)
}) {
if tag == want {
return true
}
}
return false
}

func effectiveTypeSizes(sizes types.Sizes, goos, goarch, target string) types.Sizes {
// Named wasm targets use the native wasm32 data model. The raw js/wasm
// entry point keeps Go's 64-bit word model and is emitted as Memory64.
Expand Down
41 changes: 39 additions & 2 deletions internal/build/build_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -322,14 +322,50 @@ func TestEffectiveWasmTypeSizes(t *testing.T) {
}
}

func TestConfigureWasmGC(t *testing.T) {
t.Setenv("LLGO_WASI_THREADS", "0")
tests := []struct {
name string
conf Config
want bool
err bool
}{
{name: "wasm32", conf: Config{Goos: "js", Goarch: "wasm", Tags: "llgo_wasm_gc"}, want: true},
{name: "comma separated tags", conf: Config{Goos: "js", Goarch: "wasm", Tags: "other,llgo_wasm_gc"}, want: true},
{name: "default wasm", conf: Config{Goos: "js", Goarch: "wasm"}},
{name: "WASI", conf: Config{Goos: "wasip1", Goarch: "wasm", Tags: "llgo_wasm_gc"}},
{name: "unsupported host", conf: Config{Goos: "linux", Goarch: "wasm", Tags: "llgo_wasm_gc"}, err: true},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
export := crosscompile.Export{}
err := configureWasmGC(&test.conf, &export)
if (err != nil) != test.err {
t.Fatalf("configureWasmGC error = %v, want error %v", err, test.err)
}
if got := slices.Contains(export.LDFLAGS, "-sMALLOC=none"); got != test.want {
t.Fatalf("MALLOC=none present = %v, want %v", got, test.want)
}
})
}
}

func TestConfigureWasmGCRejectsWASIThreads(t *testing.T) {
t.Setenv("LLGO_WASI_THREADS", "1")
conf := Config{Goos: "wasip1", Goarch: "wasm", Tags: "llgo_wasm_gc"}
if err := configureWasmGC(&conf, &crosscompile.Export{}); err == nil {
t.Fatal("expected llgo_wasm_gc with WASI threads to fail")
}
}

func TestWasmRuntimeAvoidsNativeHostDependencies(t *testing.T) {
runtimeDir := filepath.Join(env.LLGoRuntimeDir(), "internal", "lib", "runtime")
for _, goos := range []string{"js", "wasip1"} {
t.Run(goos, func(t *testing.T) {
ctx := gobuild.Default
ctx.GOOS = goos
ctx.GOARCH = "wasm"
ctx.BuildTags = []string{"llgo", "nogc"}
ctx.BuildTags = []string{"llgo", "nogc", "llgo_wasm_gc"}
pkg, err := ctx.ImportDir(runtimeDir, 0)
if err != nil {
t.Fatal(err)
Expand All @@ -356,8 +392,9 @@ func TestWasmRuntimeAvoidsNativeHostDependencies(t *testing.T) {
}

for _, name := range []string{
"mfinal_nogc.go",
"mfinal_wasm.go",
"runtime_baremetal.go",
"runtime_gc_nonmoving.go",
"signal_baremetal_llgo.go",
"time_wasm_llgo.go",
"unwind_wasm_llgo.go",
Expand Down
23 changes: 23 additions & 0 deletions internal/build/testdata/wasm-gc/abi.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#include <stddef.h>
#include <stdint.h>

#if defined(__EMSCRIPTEN__)
#include <emscripten/heap.h>
#endif

int llgo_test_gc_aligned_alloc(void) {
#if defined(__EMSCRIPTEN__)
void *ptr = emscripten_builtin_memalign(65536, 257);
if (ptr == NULL || (uintptr_t)ptr % 65536 != 0) {
return 0;
}
unsigned char *bytes = ptr;
bytes[0] = 0x5a;
bytes[256] = 0xa5;
if (bytes[0] != 0x5a || bytes[256] != 0xa5) {
return 0;
}
emscripten_builtin_free(ptr);
#endif
return 1;
}
8 changes: 8 additions & 0 deletions internal/build/testdata/wasm-gc/abi.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package main

import _ "unsafe"

const LLGoFiles = "abi.c"

//go:linkname testAlignedAlloc C.llgo_test_gc_aligned_alloc
func testAlignedAlloc() int32
89 changes: 89 additions & 0 deletions internal/build/testdata/wasm-gc/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package main

import "runtime"

type payload struct {
value uint64
}

var (
globalRoot *payload
garbage *payload
liveChunks [][]byte
)

func main() {
if testAlignedAlloc() == 0 {
panic("aligned allocation failed")
}
testRoots()
testReclamation()
testHeapGrowth()
println("wasm gc ok")
}

func testRoots() {
globalRoot = &payload{value: 0x12345678}
runtime.GC()
if globalRoot.value != 0x12345678 {
panic("global root was not retained")
}
globalRoot = nil
}

//go:noinline
func allocateGarbage() {
for i := 0; i < 1024; i++ {
garbage = &payload{value: uint64(i)}
}
garbage = nil
}

func testReclamation() {
runtime.GC()
var before runtime.MemStats
runtime.ReadMemStats(&before)

allocateGarbage()
runtime.GC()

var after runtime.MemStats
runtime.ReadMemStats(&after)
if after.TotalAlloc <= before.TotalAlloc || after.Mallocs <= before.Mallocs {
panic("allocation statistics did not advance")
}
if after.Frees <= before.Frees {
panic("unreachable objects were not reclaimed")
}
}

func testHeapGrowth() {
var before runtime.MemStats
runtime.ReadMemStats(&before)

const chunkSize = 1 << 20
chunkCount := int(before.HeapSys/chunkSize) + 8
if chunkCount > 128 {
panic("initial heap is too large for the bounded growth test")
}
liveChunks = make([][]byte, 0, chunkCount)
for i := 0; i < chunkCount; i++ {
chunk := make([]byte, chunkSize)
chunk[0] = byte(i + 1)
chunk[len(chunk)-1] = byte(i + 2)
liveChunks = append(liveChunks, chunk)
}

var after runtime.MemStats
runtime.ReadMemStats(&after)
if after.HeapSys <= before.HeapSys {
panic("heap did not grow")
}
runtime.GC()
for i, chunk := range liveChunks {
if chunk[0] != byte(i+1) || chunk[len(chunk)-1] != byte(i+2) {
panic("live object was corrupted during heap growth")
}
}
liveChunks = nil
}
2 changes: 1 addition & 1 deletion runtime/internal/clite/pthread/pthread_gc.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//go:build !nogc && !baremetal
//go:build !nogc && !baremetal && !wasm

/*
* Copyright (c) 2024 The XGo Authors (xgo.dev). All rights reserved.
Expand Down
2 changes: 1 addition & 1 deletion runtime/internal/clite/pthread/pthread_nogc.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//go:build nogc || baremetal
//go:build nogc || baremetal || wasm

/*
* Copyright (c) 2024 The XGo Authors (xgo.dev). All rights reserved.
Expand Down
2 changes: 1 addition & 1 deletion runtime/internal/clite/tls/tls_gc.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//go:build llgo && !baremetal && !nogc
//go:build llgo && !baremetal && !wasm && !nogc

/*
* Copyright (c) 2025 The XGo Authors (xgo.dev). All rights reserved.
Expand Down
2 changes: 1 addition & 1 deletion runtime/internal/clite/tls/tls_nogc.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//go:build llgo && (nogc || baremetal)
//go:build llgo && (nogc || baremetal || wasm)

/*
* Copyright (c) 2025 The XGo Authors (xgo.dev). All rights reserved.
Expand Down
2 changes: 1 addition & 1 deletion runtime/internal/lib/runtime/mfinal.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//go:build !nogc
//go:build !nogc && !wasm

// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
Expand Down
2 changes: 1 addition & 1 deletion runtime/internal/lib/runtime/mfinal_nogc.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//go:build nogc
//go:build nogc && (!wasm || !llgo_wasm_gc)

package runtime

Expand Down
8 changes: 8 additions & 0 deletions runtime/internal/lib/runtime/mfinal_wasm.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
//go:build wasm && llgo_wasm_gc

package runtime

// SetFinalizer is not implemented by the initial WebAssembly collector.
func SetFinalizer(obj any, finalizer any) {
_, _ = obj, finalizer
}
2 changes: 1 addition & 1 deletion runtime/internal/lib/runtime/runtime_gc.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//go:build !nogc && !baremetal
//go:build !nogc && !baremetal && !wasm

package runtime

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//go:build !nogc && baremetal
//go:build (baremetal && !nogc) || (wasm && llgo_wasm_gc)

package runtime

Expand Down
2 changes: 1 addition & 1 deletion runtime/internal/lib/runtime/runtime_nogc.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//go:build nogc
//go:build nogc && (!wasm || !llgo_wasm_gc)

package runtime

Expand Down
53 changes: 53 additions & 0 deletions runtime/internal/runtime/tinygogc/_wrap/gc_wasm.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
#include <stddef.h>
#include <stdint.h>

#if defined(__EMSCRIPTEN__)
#include <emscripten/heap.h>
#include <emscripten/stack.h>
#else
extern unsigned char __stack_high;
#endif

extern unsigned char __data_end;
extern unsigned char __global_base;
extern unsigned char __heap_base;

#define LLGO_WASM_PAGE_SIZE 65536

uintptr_t llgo_gc_globals_start(void) {
return (uintptr_t)&__global_base;
}

uintptr_t llgo_gc_globals_end(void) {
return (uintptr_t)&__data_end;
}

uintptr_t llgo_gc_heap_base(void) {
return (uintptr_t)&__heap_base;
}

uintptr_t llgo_gc_stack_top(void) {
#if defined(__EMSCRIPTEN__)
return (uintptr_t)emscripten_stack_get_base();
#else
return (uintptr_t)&__stack_high;
#endif
}

uintptr_t llgo_gc_memory_size(void) {
return (uintptr_t)__builtin_wasm_memory_size(0) * LLGO_WASM_PAGE_SIZE;
}

int llgo_gc_grow_memory(uintptr_t required) {
#if defined(__EMSCRIPTEN__)
return emscripten_resize_heap(required);
#else
uintptr_t current = llgo_gc_memory_size();
if (required <= current) {
return 1;
}
uintptr_t pages = (required - current + LLGO_WASM_PAGE_SIZE - 1) /
LLGO_WASM_PAGE_SIZE;
return __builtin_wasm_memory_grow(0, pages) != (size_t)-1;
#endif
}
Loading
Loading