// Copyright 2017 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package cshared_test import ( "bytes" "debug/elf" "debug/pe" "encoding/binary" "flag" "fmt" "log" "os" "os/exec" "path/filepath" "runtime" "strings" "sync" "testing" "unicode" ) // C compiler with args (from $(go env CC) $(go env GOGCCFLAGS)). var cc []string // ".exe" on Windows. var exeSuffix string var GOOS, GOARCH, GOROOT string var installdir, androiddir string var libSuffix, libgoname string func TestMain(m *testing.M) { os.Exit(testMain(m)) } func testMain(m *testing.M) int { log.SetFlags(log.Lshortfile) flag.Parse() if testing.Short() && os.Getenv("GO_BUILDER_NAME") == "" { fmt.Printf("SKIP - short mode and $GO_BUILDER_NAME not set\n") os.Exit(0) } GOOS = goEnv("GOOS") GOARCH = goEnv("GOARCH") GOROOT = goEnv("GOROOT") if _, err := os.Stat(GOROOT); os.IsNotExist(err) { log.Fatalf("Unable able to find GOROOT at '%s'", GOROOT) } androiddir = fmt.Sprintf("/data/local/tmp/testcshared-%d", os.Getpid()) if runtime.GOOS != GOOS && GOOS == "android" { args := append(adbCmd(), "exec-out", "mkdir", "-p", androiddir) cmd := exec.Command(args[0], args[1:]...) out, err := cmd.CombinedOutput() if err != nil { log.Fatalf("setupAndroid failed: %v\n%s\n", err, out) } defer cleanupAndroid() } cc = []string{goEnv("CC")} out := goEnv("GOGCCFLAGS") quote := '\000' start := 0 lastSpace := true backslash := false s := string(out) for i, c := range s { if quote == '\000' && unicode.IsSpace(c) { if !lastSpace { cc = append(cc, s[start:i]) lastSpace = true } } else { if lastSpace { start = i lastSpace = false } if quote == '\000' && !backslash && (c == '"' || c == '\'') { quote = c backslash = false } else if !backslash && quote == c { quote = '\000' } else if (quote == '\000' || quote == '"') && !backslash && c == '\\' { backslash = true } else { backslash = false } } } if !lastSpace { cc = append(cc, s[start:]) } switch GOOS { case "darwin", "ios": // For Darwin/ARM. // TODO(crawshaw): can we do better? cc = append(cc, []string{"-framework", "CoreFoundation", "-framework", "Foundation"}...) case "android": cc = append(cc, "-pie") } libgodir := GOOS + "_" + GOARCH switch GOOS { case "darwin", "ios": if GOARCH == "arm64" { libgodir += "_shared" } case "dragonfly", "freebsd", "linux", "netbsd", "openbsd", "solaris", "illumos": libgodir += "_shared" } cc = append(cc, "-I", filepath.Join("pkg", libgodir)) // Force reallocation (and avoid aliasing bugs) for parallel tests that append to cc. cc = cc[:len(cc):len(cc)] if GOOS == "windows" { exeSuffix = ".exe" } // Copy testdata into GOPATH/src/testcshared, along with a go.mod file // declaring the same path. GOPATH, err := os.MkdirTemp("", "cshared_test") if err != nil { log.Panic(err) } defer os.RemoveAll(GOPATH) os.Setenv("GOPATH", GOPATH) modRoot := filepath.Join(GOPATH, "src", "testcshared") if err := overlayDir(modRoot, "testdata"); err != nil { log.Panic(err) } if err := os.Chdir(modRoot); err != nil { log.Panic(err) } os.Setenv("PWD", modRoot) if err := os.WriteFile("go.mod", []byte("module testcshared\n"), 0666); err != nil { log.Panic(err) } // Directory where cgo headers and outputs will be installed. // The installation directory format varies depending on the platform. output, err := exec.Command("go", "list", "-buildmode=c-shared", "-installsuffix", "testcshared", "-f", "{{.Target}}", "./libgo").CombinedOutput() if err != nil { log.Panicf("go list failed: %v\n%s", err, output) } target := string(bytes.TrimSpace(output)) libgoname = filepath.Base(target) installdir = filepath.Dir(target) libSuffix = strings.TrimPrefix(filepath.Ext(target), ".") return m.Run() } func goEnv(key string) string { out, err := exec.Command("go", "env", key).Output() if err != nil { log.Printf("go env %s failed:\n%s", key, err) log.Panicf("%s", err.(*exec.ExitError).Stderr) } return strings.TrimSpace(string(out)) } func cmdToRun(name string) string { return "./" + name + exeSuffix } func adbCmd() []string { cmd := []string{"adb"} if flags := os.Getenv("GOANDROID_ADB_FLAGS"); flags != "" { cmd = append(cmd, strings.Split(flags, " ")...) } return cmd } func adbPush(t *testing.T, filename string) { if runtime.GOOS == GOOS || GOOS != "android" { return } args := append(adbCmd(), "push", filename, fmt.Sprintf("%s/%s", androiddir, filename)) cmd := exec.Command(args[0], args[1:]...) if out, err := cmd.CombinedOutput(); err != nil { t.Fatalf("adb command failed: %v\n%s\n", err, out) } } func adbRun(t *testing.T, env []string, adbargs ...string) string { if GOOS != "android" { t.Fatalf("trying to run adb command when operating system is not android.") } args := append(adbCmd(), "exec-out") // Propagate LD_LIBRARY_PATH to the adb shell invocation. for _, e := range env { if strings.Contains(e, "LD_LIBRARY_PATH=") { adbargs = append([]string{e}, adbargs...) break } } shellcmd := fmt.Sprintf("cd %s; %s", androiddir, strings.Join(adbargs, " ")) args = append(args, shellcmd) cmd := exec.Command(args[0], args[1:]...) out, err := cmd.CombinedOutput() if err != nil { t.Fatalf("adb command failed: %v\n%s\n", err, out) } return strings.Replace(string(out), "\r", "", -1) } func run(t *testing.T, extraEnv []string, args ...string) string { t.Helper() cmd := exec.Command(args[0], args[1:]...) if len(extraEnv) > 0 { cmd.Env = append(os.Environ(), extraEnv...) } if GOOS != "windows" { // TestUnexportedSymbols relies on file descriptor 30 // being closed when the program starts, so enforce // that in all cases. (The first three descriptors are // stdin/stdout/stderr, so we just need to make sure // that cmd.ExtraFiles[27] exists and is nil.) cmd.ExtraFiles = make([]*os.File, 28) } out, err := cmd.CombinedOutput() if err != nil { t.Fatalf("command failed: %v\n%v\n%s\n", args, err, out) } else { t.Logf("run: %v", args) } return string(out) } func runExe(t *testing.T, extraEnv []string, args ...string) string { t.Helper() if runtime.GOOS != GOOS && GOOS == "android" { return adbRun(t, append(os.Environ(), extraEnv...), args...) } return run(t, extraEnv, args...) } func runCC(t *testing.T, args ...string) string { t.Helper() // This function is run in parallel, so append to a copy of cc // rather than cc itself. return run(t, nil, append(append([]string(nil), cc...), args...)...) } func createHeaders() error { // The 'cgo' command generates a number of additional artifacts, // but we're only interested in the header. // Shunt the rest of the outputs to a temporary directory. objDir, err := os.MkdirTemp("", "testcshared_obj") if err != nil { return err } defer os.RemoveAll(objDir) // Generate a C header file for p, which is a non-main dependency // of main package libgo. // // TODO(golang.org/issue/35715): This should be simpler. args := []string{"go", "tool", "cgo", "-objdir", objDir, "-exportheader", "p.h", filepath.Join(".", "p", "p.go")} cmd := exec.Command(args[0], args[1:]...) out, err := cmd.CombinedOutput() if err != nil { return fmt.Errorf("command failed: %v\n%v\n%s\n", args, err, out) } // Generate a C header file for libgo itself. args = []string{"go", "install", "-buildmode=c-shared", "-installsuffix", "testcshared", "./libgo"} cmd = exec.Command(args[0], args[1:]...) out, err = cmd.CombinedOutput() if err != nil { return fmt.Errorf("command failed: %v\n%v\n%s\n", args, err, out) } args = []string{"go", "build", "-buildmode=c-shared", "-installsuffix", "testcshared", "-o", libgoname, filepath.Join(".", "libgo", "libgo.go")} if GOOS == "windows" && strings.HasSuffix(args[6], ".a") { args[6] = strings.TrimSuffix(args[6], ".a") + ".dll" } cmd = exec.Command(args[0], args[1:]...) out, err = cmd.CombinedOutput() if err != nil { return fmt.Errorf("command failed: %v\n%v\n%s\n", args, err, out) } if GOOS == "windows" { // We can't simply pass -Wl,--out-implib, because this relies on having imports from multiple packages, // which results in the linkers output implib getting overwritten at each step. So instead build the // import library the traditional way, using a def file. err = os.WriteFile("libgo.def", []byte("LIBRARY libgo.dll\nEXPORTS\n\tDidInitRun\n\tDidMainRun\n\tDivu\n\tFromPkg\n\t_cgo_dummy_export\n"), 0644) if err != nil { return fmt.Errorf("unable to write def file: %v", err) } out, err = exec.Command(cc[0], append(cc[1:], "-print-prog-name=dlltool")...).CombinedOutput() if err != nil { return fmt.Errorf("unable to find dlltool path: %v\n%s\n", err, out) } dlltoolpath := strings.TrimSpace(string(out)) if filepath.Ext(dlltoolpath) == "" { // Some compilers report slash-separated paths without extensions // instead of ordinary Windows paths. // Try to find the canonical name for the path. if lp, err := exec.LookPath(dlltoolpath); err == nil { dlltoolpath = lp } } args := []string{dlltoolpath, "-D", args[6], "-l", libgoname, "-d", "libgo.def"} if filepath.Ext(dlltoolpath) == "" { // This is an unfortunate workaround for // https://github.com/mstorsjo/llvm-mingw/issues/205 in which // we basically reimplement the contents of the dlltool.sh // wrapper: https://git.io/JZFlU. // TODO(thanm): remove this workaround once we can upgrade // the compilers on the windows-arm64 builder. dlltoolContents, err := os.ReadFile(args[0]) if err != nil { return fmt.Errorf("unable to read dlltool: %v\n", err) } if bytes.HasPrefix(dlltoolContents, []byte("#!/bin/sh")) && bytes.Contains(dlltoolContents, []byte("llvm-dlltool")) { base, name := filepath.Split(args[0]) args[0] = filepath.Join(base, "llvm-dlltool") var machine string switch prefix, _, _ := strings.Cut(name, "-"); prefix { case "i686": machine = "i386" case "x86_64": machine = "i386:x86-64" case "armv7": machine = "arm" case "aarch64": machine = "arm64" } if len(machine) > 0 { args = append(args, "-m", machine) } } } out, err = exec.Command(args[0], args[1:]...).CombinedOutput() if err != nil { return fmt.Errorf("unable to run dlltool to create import library: %v\n%s\n", err, out) } } if runtime.GOOS != GOOS && GOOS == "android" { args = append(adbCmd(), "push", libgoname, fmt.Sprintf("%s/%s", androiddir, libgoname)) cmd = exec.Command(args[0], args[1:]...) out, err = cmd.CombinedOutput() if err != nil { return fmt.Errorf("adb command failed: %v\n%s\n", err, out) } } return nil } var ( headersOnce sync.Once headersErr error ) func createHeadersOnce(t *testing.T) { headersOnce.Do(func() { headersErr = createHeaders() }) if headersErr != nil { t.Fatal(headersErr) } } func cleanupAndroid() { if GOOS != "android" { return } args := append(adbCmd(), "exec-out", "rm", "-rf", androiddir) cmd := exec.Command(args[0], args[1:]...) out, err := cmd.CombinedOutput() if err != nil { log.Panicf("cleanupAndroid failed: %v\n%s\n", err, out) } } // test0: exported symbols in shared lib are accessible. func TestExportedSymbols(t *testing.T) { t.Parallel() cmd := "testp0" bin := cmdToRun(cmd) createHeadersOnce(t) runCC(t, "-I", installdir, "-o", cmd, "main0.c", libgoname) adbPush(t, cmd) defer os.Remove(bin) out := runExe(t, []string{"LD_LIBRARY_PATH=."}, bin) if strings.TrimSpace(out) != "PASS" { t.Error(out) } } func checkNumberOfExportedFunctionsWindows(t *testing.T, exportAllSymbols bool) { const prog = ` package main import "C" //export GoFunc func GoFunc() { println(42) } //export GoFunc2 func GoFunc2() { println(24) } func main() { } ` tmpdir := t.TempDir() srcfile := filepath.Join(tmpdir, "test.go") objfile := filepath.Join(tmpdir, "test.dll") if err := os.WriteFile(srcfile, []byte(prog), 0666); err != nil { t.Fatal(err) } argv := []string{"build", "-buildmode=c-shared"} if exportAllSymbols { argv = append(argv, "-ldflags", "-extldflags=-Wl,--export-all-symbols") } argv = append(argv, "-o", objfile, srcfile) out, err := exec.Command("go", argv...).CombinedOutput() if err != nil { t.Fatalf("build failure: %s\n%s\n", err, string(out)) } f, err := pe.Open(objfile) if err != nil { t.Fatalf("pe.Open failed: %v", err) } defer f.Close() section := f.Section(".edata") if section == nil { t.Skip(".edata section is not present") } // TODO: deduplicate this struct from cmd/link/internal/ld/pe.go type IMAGE_EXPORT_DIRECTORY struct { _ [2]uint32 _ [2]uint16 _ [2]uint32 NumberOfFunctions uint32 NumberOfNames uint32 _ [3]uint32 } var e IMAGE_EXPORT_DIRECTORY if err := binary.Read(section.Open(), binary.LittleEndian, &e); err != nil { t.Fatalf("binary.Read failed: %v", err) } // Only the two exported functions and _cgo_dummy_export should be exported expectedNumber := uint32(3) if exportAllSymbols { if e.NumberOfFunctions <= expectedNumber { t.Fatalf("missing exported functions: %v", e.NumberOfFunctions) } if e.NumberOfNames <= expectedNumber { t.Fatalf("missing exported names: %v", e.NumberOfNames) } } else { if e.NumberOfFunctions != expectedNumber { t.Fatalf("got %d exported functions; want %d", e.NumberOfFunctions, expectedNumber) } if e.NumberOfNames != expectedNumber { t.Fatalf("got %d exported names; want %d", e.NumberOfNames, expectedNumber) } } } func TestNumberOfExportedFunctions(t *testing.T) { if GOOS != "windows" { t.Skip("skipping windows only test") } t.Parallel() t.Run("OnlyExported", func(t *testing.T) { checkNumberOfExportedFunctionsWindows(t, false) }) t.Run("All", func(t *testing.T) { checkNumberOfExportedFunctionsWindows(t, true) }) } // test1: shared library can be dynamically loaded and exported symbols are accessible. func TestExportedSymbolsWithDynamicLoad(t *testing.T) { t.Parallel() if GOOS == "windows" { t.Logf("Skipping on %s", GOOS) return } cmd := "testp1" bin := cmdToRun(cmd) createHeadersOnce(t) if GOOS != "freebsd" { runCC(t, "-o", cmd, "main1.c", "-ldl") } else { runCC(t, "-o", cmd, "main1.c") } adbPush(t, cmd) defer os.Remove(bin) out := runExe(t, nil, bin, "./"+libgoname) if strings.TrimSpace(out) != "PASS" { t.Error(out) } } // test2: tests libgo2 which does not export any functions. func TestUnexportedSymbols(t *testing.T) { t.Parallel() if GOOS == "windows" { t.Logf("Skipping on %s", GOOS) return } cmd := "testp2" bin := cmdToRun(cmd) libname := "libgo2." + libSuffix run(t, nil, "go", "build", "-buildmode=c-shared", "-installsuffix", "testcshared", "-o", libname, "./libgo2", ) adbPush(t, libname) linkFlags := "-Wl,--no-as-needed" if GOOS == "darwin" || GOOS == "ios" { linkFlags = "" } runCC(t, "-o", cmd, "main2.c", linkFlags, libname) adbPush(t, cmd) defer os.Remove(libname) defer os.Remove(bin) out := runExe(t, []string{"LD_LIBRARY_PATH=."}, bin) if strings.TrimSpace(out) != "PASS" { t.Error(out) } } // test3: tests main.main is exported on android. func TestMainExportedOnAndroid(t *testing.T) { t.Parallel() switch GOOS { case "android": break default: t.Logf("Skipping on %s", GOOS) return } cmd := "testp3" bin := cmdToRun(cmd) createHeadersOnce(t) runCC(t, "-o", cmd, "main3.c", "-ldl") adbPush(t, cmd) defer os.Remove(bin) out := runExe(t, nil, bin, "./"+libgoname) if strings.TrimSpace(out) != "PASS" { t.Error(out) } } func testSignalHandlers(t *testing.T, pkgname, cfile, cmd string) { libname := pkgname + "." + libSuffix run(t, nil, "go", "build", "-buildmode=c-shared", "-installsuffix", "testcshared", "-o", libname, pkgname, ) adbPush(t, libname) if GOOS != "freebsd" { runCC(t, "-pthread", "-o", cmd, cfile, "-ldl") } else { runCC(t, "-pthread", "-o", cmd, cfile) } adbPush(t, cmd) bin := cmdToRun(cmd) defer os.Remove(libname) defer os.Remove(bin) defer os.Remove(pkgname + ".h") out := runExe(t, nil, bin, "./"+libname) if strings.TrimSpace(out) != "PASS" { t.Error(run(t, nil, bin, libname, "verbose")) } } // test4: test signal handlers func TestSignalHandlers(t *testing.T) { t.Parallel() if GOOS == "windows" { t.Logf("Skipping on %s", GOOS) return } testSignalHandlers(t, "./libgo4", "main4.c", "testp4") } // test5: test signal handlers with os/signal.Notify func TestSignalHandlersWithNotify(t *testing.T) { t.Parallel() if GOOS == "windows" { t.Logf("Skipping on %s", GOOS) return } testSignalHandlers(t, "./libgo5", "main5.c", "testp5") } func TestPIE(t *testing.T) { t.Parallel() switch GOOS { case "linux", "android": break default: t.Logf("Skipping on %s", GOOS) return } createHeadersOnce(t) f, err := elf.Open(libgoname) if err != nil { t.Fatalf("elf.Open failed: %v", err) } defer f.Close() ds := f.SectionByType(elf.SHT_DYNAMIC) if ds == nil { t.Fatalf("no SHT_DYNAMIC section") } d, err := ds.Data() if err != nil { t.Fatalf("can't read SHT_DYNAMIC contents: %v", err) } for len(d) > 0 { var tag elf.DynTag switch f.Class { case elf.ELFCLASS32: tag = elf.DynTag(f.ByteOrder.Uint32(d[:4])) d = d[8:] case elf.ELFCLASS64: tag = elf.DynTag(f.ByteOrder.Uint64(d[:8])) d = d[16:] } if tag == elf.DT_TEXTREL { t.Fatalf("%s has DT_TEXTREL flag", libgoname) } } } // Test that installing a second time recreates the header file. func TestCachedInstall(t *testing.T) { tmpdir, err := os.MkdirTemp("", "cshared") if err != nil { t.Fatal(err) } defer os.RemoveAll(tmpdir) copyFile(t, filepath.Join(tmpdir, "src", "testcshared", "go.mod"), "go.mod") copyFile(t, filepath.Join(tmpdir, "src", "testcshared", "libgo", "libgo.go"), filepath.Join("libgo", "libgo.go")) copyFile(t, filepath.Join(tmpdir, "src", "testcshared", "p", "p.go"), filepath.Join("p", "p.go")) env := append(os.Environ(), "GOPATH="+tmpdir, "GOBIN="+filepath.Join(tmpdir, "bin")) buildcmd := []string{"go", "install", "-x", "-buildmode=c-shared", "-installsuffix", "testcshared", "./libgo"} cmd := exec.Command(buildcmd[0], buildcmd[1:]...) cmd.Dir = filepath.Join(tmpdir, "src", "testcshared") cmd.Env = env t.Log(buildcmd) out, err := cmd.CombinedOutput() t.Logf("%s", out) if err != nil { t.Fatal(err) } var libgoh, ph string walker := func(path string, info os.FileInfo, err error) error { if err != nil { t.Fatal(err) } var ps *string switch filepath.Base(path) { case "libgo.h": ps = &libgoh case "p.h": ps = &ph } if ps != nil { if *ps != "" { t.Fatalf("%s found again", *ps) } *ps = path } return nil } if err := filepath.Walk(tmpdir, walker); err != nil { t.Fatal(err) } if libgoh == "" { t.Fatal("libgo.h not installed") } if err := os.Remove(libgoh); err != nil { t.Fatal(err) } cmd = exec.Command(buildcmd[0], buildcmd[1:]...) cmd.Dir = filepath.Join(tmpdir, "src", "testcshared") cmd.Env = env t.Log(buildcmd) out, err = cmd.CombinedOutput() t.Logf("%s", out) if err != nil { t.Fatal(err) } if _, err := os.Stat(libgoh); err != nil { t.Errorf("libgo.h not installed in second run: %v", err) } } // copyFile copies src to dst. func copyFile(t *testing.T, dst, src string) { t.Helper() data, err := os.ReadFile(src) if err != nil { t.Fatal(err) } if err := os.MkdirAll(filepath.Dir(dst), 0777); err != nil { t.Fatal(err) } if err := os.WriteFile(dst, data, 0666); err != nil { t.Fatal(err) } } func TestGo2C2Go(t *testing.T) { switch GOOS { case "darwin", "ios", "windows": // Non-ELF shared libraries don't support the multiple // copies of the runtime package implied by this test. t.Skipf("linking c-shared into Go programs not supported on %s; issue 29061, 49457", GOOS) case "android": t.Skip("test fails on android; issue 29087") } t.Parallel() tmpdir, err := os.MkdirTemp("", "cshared-TestGo2C2Go") if err != nil { t.Fatal(err) } defer os.RemoveAll(tmpdir) lib := filepath.Join(tmpdir, "libtestgo2c2go."+libSuffix) var env []string if GOOS == "windows" && strings.HasSuffix(lib, ".a") { env = append(env, "CGO_LDFLAGS=-Wl,--out-implib,"+lib, "CGO_LDFLAGS_ALLOW=.*") lib = strings.TrimSuffix(lib, ".a") + ".dll" } run(t, env, "go", "build", "-buildmode=c-shared", "-o", lib, "./go2c2go/go") cgoCflags := os.Getenv("CGO_CFLAGS") if cgoCflags != "" { cgoCflags += " " } cgoCflags += "-I" + tmpdir cgoLdflags := os.Getenv("CGO_LDFLAGS") if cgoLdflags != "" { cgoLdflags += " " } cgoLdflags += "-L" + tmpdir + " -ltestgo2c2go" goenv := []string{"CGO_CFLAGS=" + cgoCflags, "CGO_LDFLAGS=" + cgoLdflags} ldLibPath := os.Getenv("LD_LIBRARY_PATH") if ldLibPath != "" { ldLibPath += ":" } ldLibPath += tmpdir runenv := []string{"LD_LIBRARY_PATH=" + ldLibPath} bin := filepath.Join(tmpdir, "m1") + exeSuffix run(t, goenv, "go", "build", "-o", bin, "./go2c2go/m1") runExe(t, runenv, bin) bin = filepath.Join(tmpdir, "m2") + exeSuffix run(t, goenv, "go", "build", "-o", bin, "./go2c2go/m2") runExe(t, runenv, bin) }