Skip to content
Open
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
6 changes: 6 additions & 0 deletions builder/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,12 @@ func Build(pkgName, outpath, tmpdir string, config *compileopts.Config) (BuildRe
defer unlock()
libcDependencies = append(libcDependencies, dummyCompileJob(filepath.Join(filepath.Dir(libcJob.result), "crt1.o")))
libcDependencies = append(libcDependencies, libcJob)
case "glibc":
libDir, err := compileopts.GlibcLibDir(config.GOARCH())
if err != nil {
return BuildResult{}, err
}
libcDependencies = append(libcDependencies, glibcStartupJobs(libDir)...)
case "picolibc":
libcJob, unlock, err := libPicolibc.load(config, tmpdir)
if err != nil {
Expand Down
19 changes: 19 additions & 0 deletions builder/glibc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package builder

import (
"path/filepath"
)

// The system glibc, linked dynamically.

// glibcStartupJobs returns the startup objects to link, in the order they must
// appear. crt1.o defines _start, which calls __libc_start_main with the main
// that TinyGo's runtime exports; crti.o and crtn.o bracket the .init and .fini
// sections, so crtn.o has to come last.
func glibcStartupJobs(libDir string) []*compileJob {
var jobs []*compileJob
for _, name := range []string{"crt1.o", "crti.o", "crtn.o"} {
jobs = append(jobs, dummyCompileJob(filepath.Join(libDir, name)))
}
return jobs
}
6 changes: 6 additions & 0 deletions compileopts/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -483,6 +483,12 @@ func (c *Config) LibcCFlags() []string {
"-isystem", filepath.Join(picolibcDir, "stdio"),
"-D__PICOLIBC_ERRNO_FUNCTION=__errno_location",
}
case "glibc":
// Use the system headers that belong to the installed glibc. The
// gnu triple makes clang look for a GCC install, and cgo treats the
// warning it prints with more than one GCC version as an error.
// Older clang does not know this warning, so silence that too.
return []string{"-Wno-unknown-warning-option", "-Wno-gcc-install-dir-libstdcxx"}
case "musl":
root := goenv.Get("TINYGOROOT")
path := c.LibraryPath("musl")
Expand Down
11 changes: 11 additions & 0 deletions compileopts/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (

var (
validBuildModeOptions = []string{"default", "c-shared", "wasi-legacy"}
validLibcOptions = []string{"musl", "glibc"}
validGCOptions = []string{"none", "leaking", "conservative", "custom", "precise", "boehm"}
validSchedulerOptions = []string{"none", "tasks", "asyncify", "threads", "cores"}
validSerialOptions = []string{"none", "uart", "usb", "rtt"}
Expand All @@ -34,6 +35,7 @@ type Options struct {
TrimPath bool // -trimpath flag
Opt string
GC string
Libc string // -libc flag
PanicStrategy string
PanicUnwind string
Scheduler string
Expand Down Expand Up @@ -89,6 +91,15 @@ func (o *Options) Verify() error {
}
}

if o.Libc != "" {
valid := slices.Contains(validLibcOptions, o.Libc)
if !valid {
return fmt.Errorf(`invalid libc option '%s': valid values are %s`,
o.Libc,
strings.Join(validLibcOptions, ", "))
}
}

if o.Scheduler != "" {
valid := slices.Contains(validSchedulerOptions, o.Scheduler)
if !valid {
Expand Down
92 changes: 88 additions & 4 deletions compileopts/target.go
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,23 @@ func (spec *TargetSpec) resolveInherits() error {

// Load a target specification.
func LoadTarget(options *Options) (*TargetSpec, error) {
spec, err := loadTarget(options)
if err != nil {
return nil, err
}
// -libc only selects between musl and glibc on the default linux
// target. Reject it where it would otherwise be ignored.
if options.Libc != "" && spec.Libc != options.Libc {
target := options.Target
if target == "" {
target = options.GOOS + "/" + options.GOARCH
}
return nil, fmt.Errorf("-libc=%s is not supported for %s", options.Libc, target)
}
return spec, nil
}

func loadTarget(options *Options) (*TargetSpec, error) {
if options.Target == "" && options.GOARCH == "wasm" {
// Set a specific target if we're building from a known GOOS/GOARCH
// combination that is defined in a target JSON file.
Expand Down Expand Up @@ -282,6 +299,50 @@ func GetTargetSpecs() (map[string]*TargetSpec, error) {
return maps, nil
}

// glibcMultiarchDirs are the Debian and Ubuntu multiarch directories that
// hold libc.so and the crt objects for each GOARCH.
var glibcMultiarchDirs = map[string]string{
"amd64": "/usr/lib/x86_64-linux-gnu",
"arm64": "/usr/lib/aarch64-linux-gnu",
}

// glibcLibDirs returns the directories to search for the glibc of goarch.
// Most other distributions use a flat lib64 or lib. Those hold the host
// libraries, so they are only searched for a native build. lib64 comes first
// because on Fedora lib holds the i686 libraries.
func glibcLibDirs(goarch, hostGOARCH string) []string {
dirs := []string{glibcMultiarchDirs[goarch]}
if goarch == hostGOARCH {
dirs = append(dirs, "/usr/lib64", "/usr/lib")
}
return dirs
}

// GlibcLibDir returns the directory holding the glibc startup objects and
// libc.so for goarch, for -libc=glibc. crt1.o is what it looks for, because
// that is the object that must be linked; libc.so and the other crt files sit
// beside it.
func GlibcLibDir(goarch string) (string, error) {
dirs := glibcLibDirs(goarch, runtime.GOARCH)
for _, dir := range dirs {
if _, err := os.Stat(filepath.Join(dir, "crt1.o")); err == nil {
return dir, nil
}
}
return "", fmt.Errorf("could not find glibc crt1.o for %s in any of %v", goarch, dirs)
}

// glibcDynamicLinkers is the path to ld.so per GOARCH, used by -libc=glibc.
// These are fixed by each architecture's ABI rather than by the distribution,
// which is why they can be a table rather than something to go looking for.
// Only 64-bit architectures are listed: on 32-bit glibc off_t is 32 bits
// unless _FILE_OFFSET_BITS=64 is set, but the runtime passes a 64-bit offset
// to mmap.
var glibcDynamicLinkers = map[string]string{
"amd64": "/lib64/ld-linux-x86-64.so.2",
"arm64": "/lib/ld-linux-aarch64.so.1",
}

// Load a target from environment variables (which default to
// runtime.GOOS/runtime.GOARCH).
func defaultTarget(options *Options) (*TargetSpec, error) {
Expand Down Expand Up @@ -436,6 +497,22 @@ func defaultTarget(options *Options) (*TargetSpec, error) {
spec.Linker = "ld.lld"
spec.RTLib = "compiler-rt"
spec.Libc = "musl"
if options.Libc == "glibc" {
linker, ok := glibcDynamicLinkers[options.GOARCH]
if !ok {
return nil, fmt.Errorf("-libc=glibc is not supported on GOARCH=%s: supported values are amd64, arm64", options.GOARCH)
}
spec.Libc = "glibc"
libDir, err := GlibcLibDir(options.GOARCH)
if err != nil {
return nil, err
}
// -L because ld.lld does not search the system library
// directories by itself, the way a compiler driver would.
// Without --dynamic-linker lld emits no PT_INTERP, and the
// shared libraries are never resolved.
spec.LDFlags = append(spec.LDFlags, "-L"+libDir, "-lc", "--dynamic-linker="+linker)
}
spec.LDFlags = append(spec.LDFlags, "--gc-sections")
if options.GOARCH == "arm64" {
// Disable outline atomics. For details, see:
Expand Down Expand Up @@ -509,12 +586,19 @@ func defaultTarget(options *Options) (*TargetSpec, error) {
if options.GOOS == "windows" {
spec.Triple += "-gnu"
} else if options.GOOS == "linux" {
// We use musl on Linux (not glibc) so we should use -musleabi* instead
// of -gnueabi*.
// We use musl on Linux by default (not glibc) so we should use
// -musleabi* instead of -gnueabi*. Under -libc=glibc it is the
// other way around: the triple has to say gnu, both so that clang
// lays out ABI-visible types the way glibc does and so that the
// cached compiler-rt and libc builds are keyed separately from the
// musl ones.
// The *hf suffix selects between soft/hard floating point ABI.
if spec.SoftFloat {
switch {
case spec.Libc == "glibc":
spec.Triple += "-gnu"
case spec.SoftFloat:
spec.Triple += "-musleabi"
} else {
default:
spec.Triple += "-musleabihf"
}
}
Expand Down
42 changes: 42 additions & 0 deletions compileopts/target_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -258,3 +258,45 @@ func TestConfigPanicUnwind(t *testing.T) {
})
}
}

func TestLoadTargetLibc(t *testing.T) {
tests := []struct {
name string
options Options
}{
{"glibc with a target", Options{Target: "arduino", Libc: "glibc"}},
{"musl with a target", Options{Target: "wasip1", Libc: "musl"}},
{"glibc on windows", Options{GOOS: "windows", GOARCH: "amd64", Libc: "glibc"}},
{"glibc on 386", Options{GOOS: "linux", GOARCH: "386", Libc: "glibc"}},
{"glibc on arm", Options{GOOS: "linux", GOARCH: "arm", GOARM: "6", Libc: "glibc"}},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
if _, err := LoadTarget(&tc.options); err == nil {
t.Errorf("LoadTarget should reject -libc=%s", tc.options.Libc)
}
})
}

if _, err := LoadTarget(&Options{GOOS: "linux", GOARCH: "amd64", Libc: "musl"}); err != nil {
t.Errorf("LoadTarget should accept -libc=musl on linux/amd64: %v", err)
}
}

func TestGlibcLibDirs(t *testing.T) {
tests := []struct {
goarch, hostGOARCH string
want []string
}{
{"amd64", "amd64", []string{"/usr/lib/x86_64-linux-gnu", "/usr/lib64", "/usr/lib"}},
{"arm64", "arm64", []string{"/usr/lib/aarch64-linux-gnu", "/usr/lib64", "/usr/lib"}},
{"arm64", "amd64", []string{"/usr/lib/aarch64-linux-gnu"}},
{"amd64", "arm64", []string{"/usr/lib/x86_64-linux-gnu"}},
}
for _, tc := range tests {
got := glibcLibDirs(tc.goarch, tc.hostGOARCH)
if !reflect.DeepEqual(got, tc.want) {
t.Errorf("glibcLibDirs(%q, %q) = %v, want %v", tc.goarch, tc.hostGOARCH, got, tc.want)
}
}
}
2 changes: 2 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -1779,6 +1779,7 @@ func main() {

opt := flag.String("opt", "z", "optimization level: 0, 1, 2, s, z")
gc := flag.String("gc", "", "garbage collector to use (none, leaking, conservative, custom, precise, boehm)")
libc := flag.String("libc", "", "libc to use on linux (musl, glibc)")
panicStrategy := flag.String("panic", "print", "panic strategy (print, trap)")
panicUnwind := flag.String("panic-unwind", "", "panic unwind strategy (auto, explicit, none)")
scheduler := flag.String("scheduler", "", "which scheduler to use (none, tasks, cores, threads, asyncify)")
Expand Down Expand Up @@ -1961,6 +1962,7 @@ func main() {
Monitor: *monitor,
BaudRate: *baudrate,
Timeout: *timeout,
Libc: *libc,
WITPackage: witPackage,
WITWorld: witWorld,
GoCompatibility: *gocompatibility,
Expand Down
12 changes: 12 additions & 0 deletions main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -778,6 +778,18 @@ func TestBuild(t *testing.T) {
emuCheck(t, options)
runTest("cgo/", options, t, nil, nil)
})
t.Run("glibc", func(t *testing.T) {
// Link against the system glibc, dynamically, instead of the
// musl that TinyGo builds. This needs the glibc development
// files (crt1.o, libc.so and the headers).
t.Parallel()
if _, err := compileopts.GlibcLibDir(runtime.GOARCH); err != nil {
t.Skip(err)
}
options := optionsFromTarget("", sema)
options.Libc = "glibc"
runTest("glibc/", options, t, nil, nil)
})
} else if runtime.GOOS == "windows" {
if runtime.GOARCH != "386" {
t.Run("Windows386", func(t *testing.T) {
Expand Down
57 changes: 57 additions & 0 deletions testdata/glibc/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package main

// This program is built with -libc=glibc. It calls into the system glibc, which
// is linked dynamically, so it only runs if the binary names the dynamic linker
// and if glibc owns the thread pointer.

/*
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <gnu/libc-version.h>

// rangeErrno returns the errno that strtol sets for a value that does not fit
// in a long. errno is a thread-local variable in glibc, so this reads glibc's
// TLS through the thread pointer.
static int rangeErrno(void) {
errno = 0;
strtol("99999999999999999999999999", NULL, 10);
return errno;
}
*/
import "C"

import (
"runtime"
"strings"
"unsafe"
)

func main() {
// gnu_get_libc_version exists only in glibc, so this does not link with
// musl.
version := C.GoString(C.gnu_get_libc_version())
println("glibc version 2.x:", strings.HasPrefix(version, "2."))

// malloc and free use the tcache, which glibc keeps in TLS.
cs := C.CString("hello from glibc")
dup := C.strdup(cs)
println("strdup:", C.GoString(dup))
C.free(unsafe.Pointer(dup))
C.free(unsafe.Pointer(cs))

println("errno is ERANGE:", C.rangeErrno() == C.ERANGE)

// Do the same on a goroutine, which has its own thread under the threads
// scheduler, and thus its own glibc TLS block.
done := make(chan bool)
go func() {
p := C.malloc(64)
C.free(p)
done <- C.rangeErrno() == C.ERANGE
}()
println("errno is ERANGE on goroutine:", <-done)

runtime.GC()
println("done")
}
5 changes: 5 additions & 0 deletions testdata/glibc/out.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
glibc version 2.x: true
strdup: hello from glibc
errno is ERANGE: true
errno is ERANGE on goroutine: true
done
Loading