mirror of
https://github.com/tinygo-org/tinygo.git
synced 2026-07-26 06:38:42 +00:00
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:
committed by
Ron Evans
parent
9c3b706533
commit
c08b2dfe06
+4
-2
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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",
|
||||
|
||||
@@ -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.
|
||||
|
||||
+17
-2
@@ -23,6 +23,7 @@ import (
|
||||
// library path in advance in several places).
|
||||
var libVersions = map[string]int{
|
||||
"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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
+1
-1
Submodule lib/bdwgc updated: d1ff06cc50...1166f11f7d
+17
-1
@@ -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,4 +1,4 @@
|
||||
//go:build tinygo.wasm && !(custommalloc || wasm_unknown)
|
||||
//go:build tinygo.wasm && !(custommalloc || wasm_unknown || gc.boehm)
|
||||
|
||||
package runtime
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
+10
-3
@@ -20,7 +20,6 @@ package runtime
|
||||
|
||||
import (
|
||||
"internal/gclayout"
|
||||
"internal/reflectlite"
|
||||
"internal/task"
|
||||
"unsafe"
|
||||
)
|
||||
@@ -39,15 +38,22 @@ 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 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")
|
||||
@@ -55,6 +61,7 @@ func gcCallback() {
|
||||
|
||||
// Note that we need to resume the world after finishing the GC call.
|
||||
needsResumeWorld = true
|
||||
}
|
||||
}
|
||||
|
||||
func markRoots(start, end uintptr) {
|
||||
|
||||
@@ -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
@@ -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
@@ -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 {}"
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user