wasm: add Boehm GC support

This adds support for `-gc=boehm` on `-target=wasip1` and `-target=wasm`
(in a browser or NodeJS). Notably it does *not* add Boehm GC support for
`-target=wasip2`, since that target doesn't have a real libc.
This commit is contained in:
Ayke van Laethem
2025-03-17 13:37:44 +01:00
committed by Ron Evans
parent 9c3b706533
commit c08b2dfe06
21 changed files with 121 additions and 60 deletions
+4 -2
View File
@@ -14,7 +14,7 @@ var BoehmGC = Library{
name: "bdwgc",
cflags: func(target, headerPath string) []string {
libdir := filepath.Join(goenv.Get("TINYGOROOT"), "lib/bdwgc")
return []string{
flags := []string{
// use a modern environment
"-DUSE_MMAP", // mmap is available
"-DUSE_MUNMAP", // return memory to the OS using munmap
@@ -30,6 +30,7 @@ var BoehmGC = Library{
// Use a minimal environment.
"-DNO_MSGBOX_ON_ERROR", // don't call MessageBoxA on Windows
"-DDONT_USE_ATEXIT",
"-DNO_GETENV",
// Special flag to work around the lack of __data_start in ld.lld.
// TODO: try to fix this in LLVM/lld directly so we don't have to
@@ -49,12 +50,13 @@ var BoehmGC = Library{
"-I" + libdir + "/include",
}
return flags
},
needsLibc: true,
sourceDir: func() string {
return filepath.Join(goenv.Get("TINYGOROOT"), "lib/bdwgc")
},
librarySources: func(target string) ([]string, error) {
librarySources: func(target string, _ bool) ([]string, error) {
sources := []string{
"allchblk.c",
"alloc.c",
+1 -1
View File
@@ -226,7 +226,7 @@ var libCompilerRT = Library{
// Development build.
return filepath.Join(goenv.Get("TINYGOROOT"), "lib/compiler-rt-builtins")
},
librarySources: func(target string) ([]string, error) {
librarySources: func(target string, _ bool) ([]string, error) {
builtins := append([]string{}, genericBuiltins...) // copy genericBuiltins
switch compileopts.CanonicalArchName(target) {
case "arm":
+2 -2
View File
@@ -38,7 +38,7 @@ type Library struct {
sourceDir func() string
// The source files, relative to sourceDir.
librarySources func(target string) ([]string, error)
librarySources func(target string, libcNeedsMalloc bool) ([]string, error)
// The source code for the crt1.o file, relative to sourceDir.
crt1Source string
@@ -226,7 +226,7 @@ func (l *Library) load(config *compileopts.Config, tmpdir string) (job *compileJ
// Create jobs to compile all sources. These jobs are depended upon by the
// archive job above, so must be run first.
paths, err := l.librarySources(target)
paths, err := l.librarySources(target, config.LibcNeedsMalloc())
if err != nil {
return nil, nil, err
}
+1 -1
View File
@@ -48,7 +48,7 @@ var libMinGW = Library{
}
return flags
},
librarySources: func(target string) ([]string, error) {
librarySources: func(target string, _ bool) ([]string, error) {
// These files are needed so that printf and the like are supported.
var sources []string
if strings.Split(target, "-")[0] == "i386" {
+1 -1
View File
@@ -121,7 +121,7 @@ var libMusl = Library{
return cflags
},
sourceDir: func() string { return filepath.Join(goenv.Get("TINYGOROOT"), "lib/musl/src") },
librarySources: func(target string) ([]string, error) {
librarySources: func(target string, _ bool) ([]string, error) {
arch := compileopts.MuslArchitecture(target)
globs := []string{
"conf/*.c",
+1 -1
View File
@@ -43,7 +43,7 @@ var libPicolibc = Library{
}
},
sourceDir: func() string { return filepath.Join(goenv.Get("TINYGOROOT"), "lib/picolibc/newlib") },
librarySources: func(target string) ([]string, error) {
librarySources: func(target string, _ bool) ([]string, error) {
sources := append([]string(nil), picolibcSources...)
if !strings.HasPrefix(target, "avr") {
// Small chips without long jumps can't compile many files (printf,
+6 -1
View File
@@ -120,7 +120,7 @@ var libWasiLibc = Library{
return nil
},
sourceDir: func() string { return filepath.Join(goenv.Get("TINYGOROOT"), "lib/wasi-libc") },
librarySources: func(target string) ([]string, error) {
librarySources: func(target string, libcNeedsMalloc bool) ([]string, error) {
type filePattern struct {
glob string
exclude []string
@@ -169,6 +169,11 @@ var libWasiLibc = Library{
{glob: "libc-bottom-half/sources/*.c"},
}
// We're using the Boehm GC, so we need a heap implementation in the libc.
if libcNeedsMalloc {
globs = append(globs, filePattern{glob: "dlmalloc/src/dlmalloc.c"})
}
// See: LIBC_TOP_HALF_MUSL_SOURCES in the Makefile
sources := []string{
"libc-top-half/musl/src/misc/a64l.c",
+1 -1
View File
@@ -41,7 +41,7 @@ var libWasmBuiltins = Library{
}
},
sourceDir: func() string { return filepath.Join(goenv.Get("TINYGOROOT"), "lib/wasi-libc") },
librarySources: func(target string) ([]string, error) {
librarySources: func(target string, _ bool) ([]string, error) {
return []string{
// memory builtins needed for llvm.memcpy.*, llvm.memmove.*, and
// llvm.memset.* LLVM intrinsics.
+18 -3
View File
@@ -22,7 +22,8 @@ import (
// builder.Library struct but that's hard to do since we want to know the
// library path in advance in several places).
var libVersions = map[string]int{
"musl": 3,
"musl": 3,
"bdwgc": 2,
}
// Config keeps all configuration affecting the build in a single struct.
@@ -138,7 +139,7 @@ func (c *Config) GC() string {
// that can be traced by the garbage collector.
func (c *Config) NeedsStackObjects() bool {
switch c.GC() {
case "conservative", "custom", "precise":
case "conservative", "custom", "precise", "boehm":
for _, tag := range c.BuildTags() {
if tag == "tinygo.wasm" {
return true
@@ -263,6 +264,15 @@ func MuslArchitecture(triple string) string {
return CanonicalArchName(triple)
}
// Returns true if the libc needs to include malloc, for the libcs where this
// matters.
func (c *Config) LibcNeedsMalloc() bool {
if c.GC() == "boehm" && c.Target.Libc == "wasi-libc" {
return true
}
return false
}
// LibraryPath returns the path to the library build directory. The path will be
// a library path in the cache directory (which might not yet be built).
func (c *Config) LibraryPath(name string) string {
@@ -286,9 +296,14 @@ func (c *Config) LibraryPath(name string) string {
archname += "-v" + strconv.Itoa(v)
}
options := ""
if c.LibcNeedsMalloc() {
options += "+malloc"
}
// No precompiled library found. Determine the path name that will be used
// in the build cache.
return filepath.Join(goenv.Get("GOCACHE"), name+"-"+archname)
return filepath.Join(goenv.Get("GOCACHE"), name+options+"-"+archname)
}
// DefaultBinaryExtension returns the default extension for binaries, such as
+6
View File
@@ -470,6 +470,12 @@ func defaultTarget(options *Options) (*TargetSpec, error) {
return nil, fmt.Errorf("unknown GOOS=%s", options.GOOS)
}
if spec.GC == "boehm" {
// Add this file only when needed. This fixes a build failure on
// Windows.
spec.ExtraFiles = append(spec.ExtraFiles, "src/runtime/gc_boehm.c")
}
// Target triples (which actually have four components, but are called
// triples for historical reasons) have the form:
// arch-vendor-os-environment
+1 -1
View File
@@ -3,7 +3,7 @@ module github.com/tinygo-org/tinygo
go 1.22.0
require (
github.com/aykevl/go-wasm v0.0.2-0.20240825160117-b76c3f9f0982
github.com/aykevl/go-wasm v0.0.2-0.20250317121156-42b86c494139
github.com/blakesmith/ar v0.0.0-20150311145944-8bd4349a67f2
github.com/chromedp/cdproto v0.0.0-20220113222801-0725d94bb6ee
github.com/chromedp/chromedp v0.7.6
+2 -2
View File
@@ -1,5 +1,5 @@
github.com/aykevl/go-wasm v0.0.2-0.20240825160117-b76c3f9f0982 h1:cD7QfvrJdYmBw2tFP/VyKPT8ZESlcrwSwo7SvH9Y4dc=
github.com/aykevl/go-wasm v0.0.2-0.20240825160117-b76c3f9f0982/go.mod h1:7sXyiaA0WtSogCu67R2252fQpVmJMh9JWJ9ddtGkpWw=
github.com/aykevl/go-wasm v0.0.2-0.20250317121156-42b86c494139 h1:2O/WuAt8J5id3khcAtVB90czG80m+v0sfkLE07GrCVg=
github.com/aykevl/go-wasm v0.0.2-0.20250317121156-42b86c494139/go.mod h1:7sXyiaA0WtSogCu67R2252fQpVmJMh9JWJ9ddtGkpWw=
github.com/blakesmith/ar v0.0.0-20150311145944-8bd4349a67f2 h1:oMCHnXa6CCCafdPDbMh/lWRhRByN0VFLvv+g+ayx1SI=
github.com/blakesmith/ar v0.0.0-20150311145944-8bd4349a67f2/go.mod h1:PkYb9DJNAwrSvRx5DYA+gUcOIgTGVMNkfSCbZM8cWpI=
github.com/chromedp/cdproto v0.0.0-20211126220118-81fa0469ad77/go.mod h1:At5TxYYdxkbQL0TSefRjhLE3Q0lgvqKKMSFUglJ7i1U=
+17 -1
View File
@@ -178,11 +178,27 @@ func TestBuild(t *testing.T) {
t.Run("WebAssembly", func(t *testing.T) {
t.Parallel()
runPlatTests(optionsFromTarget("wasm", sema), tests, t)
// Test with -gc=boehm.
t.Run("gc.go-boehm", func(t *testing.T) {
t.Parallel()
optionsBoehm := optionsFromTarget("wasm", sema)
optionsBoehm.GC = "boehm"
runTest("gc.go", optionsBoehm, t, nil, nil)
})
})
t.Run("WASI", func(t *testing.T) {
t.Run("WASIp1", func(t *testing.T) {
t.Parallel()
runPlatTests(optionsFromTarget("wasip1", sema), tests, t)
// Test with -gc=boehm.
t.Run("gc.go-boehm", func(t *testing.T) {
t.Parallel()
optionsBoehm := optionsFromTarget("wasip1", sema)
optionsBoehm.GC = "boehm"
runTest("gc.go", optionsBoehm, t, nil, nil)
})
})
t.Run("WASIp2", func(t *testing.T) {
t.Parallel()
+1 -1
View File
@@ -1,4 +1,4 @@
//go:build tinygo.wasm && !(custommalloc || wasm_unknown)
//go:build tinygo.wasm && !(custommalloc || wasm_unknown || gc.boehm)
package runtime
+37
View File
@@ -0,0 +1,37 @@
//go:build none
// This file is included in the build on systems that support the Boehm GC,
// despite the //go:build line above.
#include <stdint.h>
typedef void (* GC_push_other_roots_proc)(void);
void GC_set_push_other_roots(GC_push_other_roots_proc);
typedef void(* GC_warn_proc)(const char *msg, uintptr_t arg);
void GC_set_warn_proc(GC_warn_proc p);
void tinygo_runtime_bdwgc_callback(void);
static void callback(void) {
tinygo_runtime_bdwgc_callback();
}
static void warn_proc(const char *msg, uintptr_t arg) {
}
void tinygo_runtime_bdwgc_init(void) {
GC_set_push_other_roots(callback);
#if defined(__wasm__)
// There are a lot of warnings on WebAssembly in the form:
//
// GC Warning: Repeated allocation of very large block (appr. size 68 KiB):
// May lead to memory leak and poor performance
//
// The usual advice is to use something like GC_malloc_ignore_off_page but
// unfortunately for most allocations that's not allowed: Go allocations can
// legitimately hold pointers further than one page in the allocation. So
// instead we just disable the warning.
GC_set_warn_proc(warn_proc);
#endif
}
+16 -9
View File
@@ -20,7 +20,6 @@ package runtime
import (
"internal/gclayout"
"internal/reflectlite"
"internal/task"
"unsafe"
)
@@ -39,22 +38,30 @@ var needsResumeWorld bool
func initHeap() {
libgc_init()
libgc_set_push_other_roots(gcCallbackPtr)
// Call GC_set_push_other_roots(gcCallback) in C because of function
// signature differences that do matter in WebAssembly.
gcInit()
}
var gcCallbackPtr = reflectlite.ValueOf(gcCallback).UnsafePointer()
//export tinygo_runtime_bdwgc_init
func gcInit()
//export tinygo_runtime_bdwgc_callback
func gcCallback() {
// Mark globals and all stacks, and stop the world if we're using threading.
gcMarkReachable()
if needsResumeWorld {
// Should never happen, check for it anyway.
runtimePanic("gc: world already stopped")
}
// If we use a scheduler with parallelism (the threads scheduler for
// example), we need to call gcResumeWorld() after scanning has finished.
if hasParallelism {
if needsResumeWorld {
// Should never happen, check for it anyway.
runtimePanic("gc: world already stopped")
}
// Note that we need to resume the world after finishing the GC call.
needsResumeWorld = true
// Note that we need to resume the world after finishing the GC call.
needsResumeWorld = true
}
}
func markRoots(start, end uintptr) {
+1 -1
View File
@@ -1,4 +1,4 @@
//go:build (gc.conservative || gc.custom || gc.precise) && tinygo.wasm
//go:build (gc.conservative || gc.custom || gc.precise || gc.boehm) && tinygo.wasm
package runtime
+2 -1
View File
@@ -23,7 +23,8 @@
"--no-demangle"
],
"extra-files": [
"src/runtime/asm_tinygowasm.S"
"src/runtime/asm_tinygowasm.S",
"src/runtime/gc_boehm.c"
],
"emulator": "wasmtime run --dir={tmpDir}::/tmp {}"
}
+2 -1
View File
@@ -24,7 +24,8 @@
"--no-demangle"
],
"extra-files": [
"src/runtime/asm_tinygowasm.S"
"src/runtime/asm_tinygowasm.S",
"src/runtime/gc_boehm.c"
],
"emulator": "node {root}/targets/wasm_exec.js {}"
}
-29
View File
@@ -124,32 +124,3 @@ func TestMallocFree(t *testing.T) {
})
}
}
func TestMallocEmpty(t *testing.T) {
ptr := libc_malloc(0)
if ptr != nil {
t.Errorf("expected nil pointer, got %p", ptr)
}
}
func TestCallocEmpty(t *testing.T) {
ptr := libc_calloc(0, 1)
if ptr != nil {
t.Errorf("expected nil pointer, got %p", ptr)
}
ptr = libc_calloc(1, 0)
if ptr != nil {
t.Errorf("expected nil pointer, got %p", ptr)
}
}
func TestReallocEmpty(t *testing.T) {
ptr := libc_malloc(1)
if ptr == nil {
t.Error("expected pointer but was nil")
}
ptr = libc_realloc(ptr, 0)
if ptr != nil {
t.Errorf("expected nil pointer, got %p", ptr)
}
}