Skip to content

Commit 28835fd

Browse files
committed
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.
1 parent 9ca6f72 commit 28835fd

21 files changed

+121
-60
lines changed

builder/bdwgc.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ var BoehmGC = Library{
1414
name: "bdwgc",
1515
cflags: func(target, headerPath string) []string {
1616
libdir := filepath.Join(goenv.Get("TINYGOROOT"), "lib/bdwgc")
17-
return []string{
17+
flags := []string{
1818
// use a modern environment
1919
"-DUSE_MMAP", // mmap is available
2020
"-DUSE_MUNMAP", // return memory to the OS using munmap
@@ -30,6 +30,7 @@ var BoehmGC = Library{
3030
// Use a minimal environment.
3131
"-DNO_MSGBOX_ON_ERROR", // don't call MessageBoxA on Windows
3232
"-DDONT_USE_ATEXIT",
33+
"-DNO_GETENV",
3334

3435
// Special flag to work around the lack of __data_start in ld.lld.
3536
// TODO: try to fix this in LLVM/lld directly so we don't have to
@@ -49,12 +50,13 @@ var BoehmGC = Library{
4950

5051
"-I" + libdir + "/include",
5152
}
53+
return flags
5254
},
5355
needsLibc: true,
5456
sourceDir: func() string {
5557
return filepath.Join(goenv.Get("TINYGOROOT"), "lib/bdwgc")
5658
},
57-
librarySources: func(target string) ([]string, error) {
59+
librarySources: func(target string, _ bool) ([]string, error) {
5860
sources := []string{
5961
"allchblk.c",
6062
"alloc.c",

builder/builtins.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,7 @@ var libCompilerRT = Library{
226226
// Development build.
227227
return filepath.Join(goenv.Get("TINYGOROOT"), "lib/compiler-rt-builtins")
228228
},
229-
librarySources: func(target string) ([]string, error) {
229+
librarySources: func(target string, _ bool) ([]string, error) {
230230
builtins := append([]string{}, genericBuiltins...) // copy genericBuiltins
231231
switch compileopts.CanonicalArchName(target) {
232232
case "arm":

builder/library.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ type Library struct {
3838
sourceDir func() string
3939

4040
// The source files, relative to sourceDir.
41-
librarySources func(target string) ([]string, error)
41+
librarySources func(target string, libcNeedsMalloc bool) ([]string, error)
4242

4343
// The source code for the crt1.o file, relative to sourceDir.
4444
crt1Source string
@@ -226,7 +226,7 @@ func (l *Library) load(config *compileopts.Config, tmpdir string) (job *compileJ
226226

227227
// Create jobs to compile all sources. These jobs are depended upon by the
228228
// archive job above, so must be run first.
229-
paths, err := l.librarySources(target)
229+
paths, err := l.librarySources(target, config.LibcNeedsMalloc())
230230
if err != nil {
231231
return nil, nil, err
232232
}

builder/mingw-w64.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ var libMinGW = Library{
4848
}
4949
return flags
5050
},
51-
librarySources: func(target string) ([]string, error) {
51+
librarySources: func(target string, _ bool) ([]string, error) {
5252
// These files are needed so that printf and the like are supported.
5353
var sources []string
5454
if strings.Split(target, "-")[0] == "i386" {

builder/musl.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ var libMusl = Library{
121121
return cflags
122122
},
123123
sourceDir: func() string { return filepath.Join(goenv.Get("TINYGOROOT"), "lib/musl/src") },
124-
librarySources: func(target string) ([]string, error) {
124+
librarySources: func(target string, _ bool) ([]string, error) {
125125
arch := compileopts.MuslArchitecture(target)
126126
globs := []string{
127127
"conf/*.c",

builder/picolibc.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ var libPicolibc = Library{
4343
}
4444
},
4545
sourceDir: func() string { return filepath.Join(goenv.Get("TINYGOROOT"), "lib/picolibc/newlib") },
46-
librarySources: func(target string) ([]string, error) {
46+
librarySources: func(target string, _ bool) ([]string, error) {
4747
sources := append([]string(nil), picolibcSources...)
4848
if !strings.HasPrefix(target, "avr") {
4949
// Small chips without long jumps can't compile many files (printf,

builder/wasilibc.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ var libWasiLibc = Library{
120120
return nil
121121
},
122122
sourceDir: func() string { return filepath.Join(goenv.Get("TINYGOROOT"), "lib/wasi-libc") },
123-
librarySources: func(target string) ([]string, error) {
123+
librarySources: func(target string, libcNeedsMalloc bool) ([]string, error) {
124124
type filePattern struct {
125125
glob string
126126
exclude []string
@@ -169,6 +169,11 @@ var libWasiLibc = Library{
169169
{glob: "libc-bottom-half/sources/*.c"},
170170
}
171171

172+
// We're using the Boehm GC, so we need a heap implementation in the libc.
173+
if libcNeedsMalloc {
174+
globs = append(globs, filePattern{glob: "dlmalloc/src/dlmalloc.c"})
175+
}
176+
172177
// See: LIBC_TOP_HALF_MUSL_SOURCES in the Makefile
173178
sources := []string{
174179
"libc-top-half/musl/src/misc/a64l.c",

builder/wasmbuiltins.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ var libWasmBuiltins = Library{
4141
}
4242
},
4343
sourceDir: func() string { return filepath.Join(goenv.Get("TINYGOROOT"), "lib/wasi-libc") },
44-
librarySources: func(target string) ([]string, error) {
44+
librarySources: func(target string, _ bool) ([]string, error) {
4545
return []string{
4646
// memory builtins needed for llvm.memcpy.*, llvm.memmove.*, and
4747
// llvm.memset.* LLVM intrinsics.

compileopts/config.go

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ import (
2222
// builder.Library struct but that's hard to do since we want to know the
2323
// library path in advance in several places).
2424
var libVersions = map[string]int{
25-
"musl": 3,
25+
"musl": 3,
26+
"bdwgc": 2,
2627
}
2728

2829
// Config keeps all configuration affecting the build in a single struct.
@@ -133,7 +134,7 @@ func (c *Config) GC() string {
133134
// that can be traced by the garbage collector.
134135
func (c *Config) NeedsStackObjects() bool {
135136
switch c.GC() {
136-
case "conservative", "custom", "precise":
137+
case "conservative", "custom", "precise", "boehm":
137138
for _, tag := range c.BuildTags() {
138139
if tag == "tinygo.wasm" {
139140
return true
@@ -258,6 +259,15 @@ func MuslArchitecture(triple string) string {
258259
return CanonicalArchName(triple)
259260
}
260261

262+
// Returns true if the libc needs to include malloc, for the libcs where this
263+
// matters.
264+
func (c *Config) LibcNeedsMalloc() bool {
265+
if c.GC() == "boehm" && c.Target.Libc == "wasi-libc" {
266+
return true
267+
}
268+
return false
269+
}
270+
261271
// LibraryPath returns the path to the library build directory. The path will be
262272
// a library path in the cache directory (which might not yet be built).
263273
func (c *Config) LibraryPath(name string) string {
@@ -281,9 +291,14 @@ func (c *Config) LibraryPath(name string) string {
281291
archname += "-v" + strconv.Itoa(v)
282292
}
283293

294+
options := ""
295+
if c.LibcNeedsMalloc() {
296+
options += "+malloc"
297+
}
298+
284299
// No precompiled library found. Determine the path name that will be used
285300
// in the build cache.
286-
return filepath.Join(goenv.Get("GOCACHE"), name+"-"+archname)
301+
return filepath.Join(goenv.Get("GOCACHE"), name+options+"-"+archname)
287302
}
288303

289304
// DefaultBinaryExtension returns the default extension for binaries, such as

compileopts/target.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -470,6 +470,12 @@ func defaultTarget(options *Options) (*TargetSpec, error) {
470470
return nil, fmt.Errorf("unknown GOOS=%s", options.GOOS)
471471
}
472472

473+
if spec.GC == "boehm" {
474+
// Add this file only when needed. This fixes a build failure on
475+
// Windows.
476+
spec.ExtraFiles = append(spec.ExtraFiles, "src/runtime/gc_boehm.c")
477+
}
478+
473479
// Target triples (which actually have four components, but are called
474480
// triples for historical reasons) have the form:
475481
// arch-vendor-os-environment

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ module github.com/tinygo-org/tinygo
33
go 1.22.0
44

55
require (
6-
github.com/aykevl/go-wasm v0.0.2-0.20240825160117-b76c3f9f0982
6+
github.com/aykevl/go-wasm v0.0.2-0.20250317121156-42b86c494139
77
github.com/blakesmith/ar v0.0.0-20150311145944-8bd4349a67f2
88
github.com/chromedp/cdproto v0.0.0-20220113222801-0725d94bb6ee
99
github.com/chromedp/chromedp v0.7.6

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
github.com/aykevl/go-wasm v0.0.2-0.20240825160117-b76c3f9f0982 h1:cD7QfvrJdYmBw2tFP/VyKPT8ZESlcrwSwo7SvH9Y4dc=
2-
github.com/aykevl/go-wasm v0.0.2-0.20240825160117-b76c3f9f0982/go.mod h1:7sXyiaA0WtSogCu67R2252fQpVmJMh9JWJ9ddtGkpWw=
1+
github.com/aykevl/go-wasm v0.0.2-0.20250317121156-42b86c494139 h1:2O/WuAt8J5id3khcAtVB90czG80m+v0sfkLE07GrCVg=
2+
github.com/aykevl/go-wasm v0.0.2-0.20250317121156-42b86c494139/go.mod h1:7sXyiaA0WtSogCu67R2252fQpVmJMh9JWJ9ddtGkpWw=
33
github.com/blakesmith/ar v0.0.0-20150311145944-8bd4349a67f2 h1:oMCHnXa6CCCafdPDbMh/lWRhRByN0VFLvv+g+ayx1SI=
44
github.com/blakesmith/ar v0.0.0-20150311145944-8bd4349a67f2/go.mod h1:PkYb9DJNAwrSvRx5DYA+gUcOIgTGVMNkfSCbZM8cWpI=
55
github.com/chromedp/cdproto v0.0.0-20211126220118-81fa0469ad77/go.mod h1:At5TxYYdxkbQL0TSefRjhLE3Q0lgvqKKMSFUglJ7i1U=

lib/bdwgc

Submodule bdwgc updated 214 files

main_test.go

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,11 +178,27 @@ func TestBuild(t *testing.T) {
178178

179179
t.Run("WebAssembly", func(t *testing.T) {
180180
t.Parallel()
181+
181182
runPlatTests(optionsFromTarget("wasm", sema), tests, t)
183+
// Test with -gc=boehm.
184+
t.Run("gc.go-boehm", func(t *testing.T) {
185+
t.Parallel()
186+
optionsBoehm := optionsFromTarget("wasm", sema)
187+
optionsBoehm.GC = "boehm"
188+
runTest("gc.go", optionsBoehm, t, nil, nil)
189+
})
182190
})
183-
t.Run("WASI", func(t *testing.T) {
191+
t.Run("WASIp1", func(t *testing.T) {
184192
t.Parallel()
185193
runPlatTests(optionsFromTarget("wasip1", sema), tests, t)
194+
195+
// Test with -gc=boehm.
196+
t.Run("gc.go-boehm", func(t *testing.T) {
197+
t.Parallel()
198+
optionsBoehm := optionsFromTarget("wasip1", sema)
199+
optionsBoehm.GC = "boehm"
200+
runTest("gc.go", optionsBoehm, t, nil, nil)
201+
})
186202
})
187203
t.Run("WASIp2", func(t *testing.T) {
188204
t.Parallel()

src/runtime/arch_tinygowasm_malloc.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
//go:build tinygo.wasm && !(custommalloc || wasm_unknown)
1+
//go:build tinygo.wasm && !(custommalloc || wasm_unknown || gc.boehm)
22

33
package runtime
44

src/runtime/gc_boehm.c

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
//go:build none
2+
3+
// This file is included in the build on systems that support the Boehm GC,
4+
// despite the //go:build line above.
5+
6+
#include <stdint.h>
7+
8+
typedef void (* GC_push_other_roots_proc)(void);
9+
void GC_set_push_other_roots(GC_push_other_roots_proc);
10+
11+
typedef void(* GC_warn_proc)(const char *msg, uintptr_t arg);
12+
void GC_set_warn_proc(GC_warn_proc p);
13+
14+
void tinygo_runtime_bdwgc_callback(void);
15+
16+
static void callback(void) {
17+
tinygo_runtime_bdwgc_callback();
18+
}
19+
20+
static void warn_proc(const char *msg, uintptr_t arg) {
21+
}
22+
23+
void tinygo_runtime_bdwgc_init(void) {
24+
GC_set_push_other_roots(callback);
25+
#if defined(__wasm__)
26+
// There are a lot of warnings on WebAssembly in the form:
27+
//
28+
// GC Warning: Repeated allocation of very large block (appr. size 68 KiB):
29+
// May lead to memory leak and poor performance
30+
//
31+
// The usual advice is to use something like GC_malloc_ignore_off_page but
32+
// unfortunately for most allocations that's not allowed: Go allocations can
33+
// legitimately hold pointers further than one page in the allocation. So
34+
// instead we just disable the warning.
35+
GC_set_warn_proc(warn_proc);
36+
#endif
37+
}

src/runtime/gc_boehm.go

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ package runtime
2020

2121
import (
2222
"internal/gclayout"
23-
"internal/reflectlite"
2423
"internal/task"
2524
"unsafe"
2625
)
@@ -39,22 +38,30 @@ var needsResumeWorld bool
3938
func initHeap() {
4039
libgc_init()
4140

42-
libgc_set_push_other_roots(gcCallbackPtr)
41+
// Call GC_set_push_other_roots(gcCallback) in C because of function
42+
// signature differences that do matter in WebAssembly.
43+
gcInit()
4344
}
4445

45-
var gcCallbackPtr = reflectlite.ValueOf(gcCallback).UnsafePointer()
46+
//export tinygo_runtime_bdwgc_init
47+
func gcInit()
4648

49+
//export tinygo_runtime_bdwgc_callback
4750
func gcCallback() {
4851
// Mark globals and all stacks, and stop the world if we're using threading.
4952
gcMarkReachable()
5053

51-
if needsResumeWorld {
52-
// Should never happen, check for it anyway.
53-
runtimePanic("gc: world already stopped")
54-
}
54+
// If we use a scheduler with parallelism (the threads scheduler for
55+
// example), we need to call gcResumeWorld() after scanning has finished.
56+
if hasParallelism {
57+
if needsResumeWorld {
58+
// Should never happen, check for it anyway.
59+
runtimePanic("gc: world already stopped")
60+
}
5561

56-
// Note that we need to resume the world after finishing the GC call.
57-
needsResumeWorld = true
62+
// Note that we need to resume the world after finishing the GC call.
63+
needsResumeWorld = true
64+
}
5865
}
5966

6067
func markRoots(start, end uintptr) {

src/runtime/gc_stack_portable.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
//go:build (gc.conservative || gc.custom || gc.precise) && tinygo.wasm
1+
//go:build (gc.conservative || gc.custom || gc.precise || gc.boehm) && tinygo.wasm
22

33
package runtime
44

targets/wasip1.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@
2323
"--no-demangle"
2424
],
2525
"extra-files": [
26-
"src/runtime/asm_tinygowasm.S"
26+
"src/runtime/asm_tinygowasm.S",
27+
"src/runtime/gc_boehm.c"
2728
],
2829
"emulator": "wasmtime run --dir={tmpDir}::/tmp {}"
2930
}

targets/wasm.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@
2424
"--no-demangle"
2525
],
2626
"extra-files": [
27-
"src/runtime/asm_tinygowasm.S"
27+
"src/runtime/asm_tinygowasm.S",
28+
"src/runtime/gc_boehm.c"
2829
],
2930
"emulator": "node {root}/targets/wasm_exec.js {}"
3031
}

tests/runtime_wasi/malloc_test.go

Lines changed: 0 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -124,32 +124,3 @@ func TestMallocFree(t *testing.T) {
124124
})
125125
}
126126
}
127-
128-
func TestMallocEmpty(t *testing.T) {
129-
ptr := libc_malloc(0)
130-
if ptr != nil {
131-
t.Errorf("expected nil pointer, got %p", ptr)
132-
}
133-
}
134-
135-
func TestCallocEmpty(t *testing.T) {
136-
ptr := libc_calloc(0, 1)
137-
if ptr != nil {
138-
t.Errorf("expected nil pointer, got %p", ptr)
139-
}
140-
ptr = libc_calloc(1, 0)
141-
if ptr != nil {
142-
t.Errorf("expected nil pointer, got %p", ptr)
143-
}
144-
}
145-
146-
func TestReallocEmpty(t *testing.T) {
147-
ptr := libc_malloc(1)
148-
if ptr == nil {
149-
t.Error("expected pointer but was nil")
150-
}
151-
ptr = libc_realloc(ptr, 0)
152-
if ptr != nil {
153-
t.Errorf("expected nil pointer, got %p", ptr)
154-
}
155-
}

0 commit comments

Comments
 (0)