diff --git a/.github/workflows/llgo.yml b/.github/workflows/llgo.yml index b04652658d..ca68918982 100644 --- a/.github/workflows/llgo.yml +++ b/.github/workflows/llgo.yml @@ -259,12 +259,19 @@ jobs: printf ' %s\n' "${selected[@]}" # Run per-package and print elapsed time to keep logs readable. + std_pkgs=() for pkg in "${selected[@]}"; do echo "==> llgo test -timeout=20m -bench=^BenchmarkGo126 -benchtime=1x ${pkg}" SECONDS=0 llgo test -timeout=20m -bench='^BenchmarkGo126' -benchtime=1x "${pkg}" + if [[ "${{ matrix.os }}" == ubuntu-latest && "${{ matrix.go }}" == 1.26.5 && "${pkg}" == */test/std/* ]]; then + std_pkgs+=("${pkg}") + fi echo "==> done ${pkg} (${SECONDS}s)" done + if [[ "${#std_pkgs[@]}" -ne 0 ]]; then + dev/test_std_buildmodes.sh "${std_pkgs[@]}" + fi hello: name: hello (${{ matrix.lane }}, ${{ matrix.os }}, LLVM ${{ matrix.llvm }}, Go ${{ matrix.go }}) diff --git a/_demo/go/export/export.go b/_demo/go/export/export.go index 2dd5392704..2f2bee1a41 100644 --- a/_demo/go/export/export.go +++ b/_demo/go/export/export.go @@ -1,6 +1,7 @@ package main import ( + "fmt" "runtime" "unsafe" @@ -134,6 +135,11 @@ func RunGoroutine(value int) int { return <-ch } +//export FormatValue +func FormatValue(value string, number int) string { + return fmt.Sprintf("%s:%d", value, number) +} + // Functions with small struct parameters and return values //export CreateSmallStruct diff --git a/_demo/go/export/libexport.h.want b/_demo/go/export/libexport.h.want index 67deee32be..919f305869 100644 --- a/_demo/go/export/libexport.h.want +++ b/_demo/go/export/libexport.h.want @@ -121,6 +121,9 @@ mul(float a, float b); void github_com_goplus_llgo__demo_go_export_c_init(void) GO_SYMBOL_RENAME("github.com/goplus/llgo/_demo/go/export/c.init") +intptr_t +AllThreadsSyscallStatus(void); + main_ComplexData CreateComplexData(void); @@ -163,6 +166,9 @@ CreateStringMap(void); C_XType CreateXType(int32_t id, GoString name, double value, _Bool flag); +GoString +FormatValue(GoString value, intptr_t number); + main_FuncInfoResult GetFuncInfo(void); diff --git a/_demo/go/export/runtime_hooks_linux.go b/_demo/go/export/runtime_hooks_linux.go new file mode 100644 index 0000000000..5ecf8aa427 --- /dev/null +++ b/_demo/go/export/runtime_hooks_linux.go @@ -0,0 +1,28 @@ +//go:build linux + +package main + +import "syscall" + +func gettimeofdayStatus() int { + var tv syscall.Timeval + if err := syscall.Gettimeofday(&tv); err != nil { + if errno, ok := err.(syscall.Errno); ok { + return int(errno) + } + return int(syscall.EINVAL) + } + if tv.Sec <= 0 { + return int(syscall.EINVAL) + } + return 0 +} + +//export AllThreadsSyscallStatus +func AllThreadsSyscallStatus() int { + if status := gettimeofdayStatus(); status != 0 { + return status + } + _, _, err := syscall.AllThreadsSyscall(syscall.SYS_GETPID, 0, 0, 0) + return int(err) +} diff --git a/_demo/go/export/runtime_hooks_other.go b/_demo/go/export/runtime_hooks_other.go new file mode 100644 index 0000000000..2d5176f5ea --- /dev/null +++ b/_demo/go/export/runtime_hooks_other.go @@ -0,0 +1,8 @@ +//go:build !linux + +package main + +//export AllThreadsSyscallStatus +func AllThreadsSyscallStatus() int { + return 0 +} diff --git a/_demo/go/export/use/Makefile b/_demo/go/export/use/Makefile index a7c75c2722..ee7dcee847 100644 --- a/_demo/go/export/use/Makefile +++ b/_demo/go/export/use/Makefile @@ -24,6 +24,7 @@ else SHARED_EXT = so PLATFORM_LIBS = $(shell pkg-config --libs libunwind 2>/dev/null || echo -lunwind) endif +STATIC_STDLIB_LIBS = $(shell pkg-config --libs libffi 2>/dev/null || echo -lffi) -lresolv # Library and flags based on link type ifeq ($(LINK_TYPE),shared) @@ -35,7 +36,7 @@ ifeq ($(LINK_TYPE),shared) else BUILDMODE = c-archive LIBRARY = ../libexport.a - LDFLAGS = $(LIBRARY) $(RUNTIME_LIBS) $(PLATFORM_LIBS) + LDFLAGS = $(LIBRARY) $(RUNTIME_LIBS) $(PLATFORM_LIBS) $(STATIC_STDLIB_LIBS) BUILD_MSG = "Building Go static library..." LINK_MSG = "Linking with static library..." endif diff --git a/_demo/go/export/use/main.c b/_demo/go/export/use/main.c index 4e1a911b45..4c6534a7c8 100644 --- a/_demo/go/export/use/main.c +++ b/_demo/go/export/use/main.c @@ -2,6 +2,7 @@ #include #include #include +#include #include #include "../libexport.h" @@ -59,6 +60,16 @@ int main() { HelloWorld(); printf("\n"); + // Verify that a C library can initialize and call standard-library paths + // that depend on the runtime hooks supplied by LLGo. + GoString formatted = FormatValue((GoString){"answer", 6}, 42); + assert(go_string_equals(formatted, "answer:42")); +#ifdef __linux__ + assert(AllThreadsSyscallStatus() == ENOTSUP); +#else + assert(AllThreadsSyscallStatus() == 0); +#endif + // Test small struct main_SmallStruct small = CreateSmallStruct(5, 1); // 1 for true assert(small.ID == 5); diff --git a/cmd/internal/test/test.go b/cmd/internal/test/test.go index d0b972042f..1911718dde 100644 --- a/cmd/internal/test/test.go +++ b/cmd/internal/test/test.go @@ -27,6 +27,7 @@ func init() { flags.AddCommonFlags(&Cmd.Flag) flags.AddCompilerVerboseFlag(&Cmd.Flag) flags.AddBuildFlags(&Cmd.Flag) + flags.AddBuildModeFlags(&Cmd.Flag) flags.AddTestFlags(&Cmd.Flag) flags.AddTestBinaryFlags(&Cmd.Flag) flags.AddEmulatorFlags(&Cmd.Flag) @@ -43,7 +44,7 @@ func runCmd(cmd *base.Command, args []string) { } conf := build.NewDefaultConf(build.ModeTest) - if err := flags.UpdateConfig(conf); err != nil { + if err := flags.UpdateBuildConfig(conf); err != nil { fmt.Fprintln(os.Stderr, err) mockable.Exit(1) } diff --git a/cmd/internal/test/test_test.go b/cmd/internal/test/test_test.go index ae34f7acad..726893ebed 100644 --- a/cmd/internal/test/test_test.go +++ b/cmd/internal/test/test_test.go @@ -10,7 +10,7 @@ import ( ) func TestBuildFlagsWiring(t *testing.T) { - if goBuildFlags.Flag != &Cmd.Flag || Cmd.Flag.Lookup("ldflags") == nil { + if goBuildFlags.Flag != &Cmd.Flag || Cmd.Flag.Lookup("ldflags") == nil || Cmd.Flag.Lookup("buildmode") == nil { t.Fatal("test build flags are not bound to the test command") } } diff --git a/dev/test_std_buildmodes.sh b/dev/test_std_buildmodes.sh new file mode 100755 index 0000000000..d477bf9c53 --- /dev/null +++ b/dev/test_std_buildmodes.sh @@ -0,0 +1,135 @@ +#!/bin/bash + +set -euo pipefail + +if [[ $# -eq 0 ]]; then + echo "usage: $0 ./test/std/package [...]" >&2 + exit 2 +fi + +root_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +test_pkgs=("$@") +import_paths=() +stems=() +groups=() +max_group=0 +for test_pkg in "${test_pkgs[@]}"; do + read -r import_path package_dir < <(go list -tags=llgo -f '{{.ImportPath}} {{.Dir}}' "${test_pkg}") + case "${import_path}" in + github.com/goplus/llgo/test/std/*) ;; + *) + echo "not a test/std package: ${test_pkg}" >&2 + exit 2 + ;; + esac + stem="$(basename "${package_dir}").test" + group=0 + for i in "${!stems[@]}"; do + if [[ "${stems[$i]}" == "${stem}" ]]; then + group=$((group + 1)) + fi + done + import_paths+=("${import_path}") + stems+=("${stem}") + groups+=("${group}") + if (( group > max_group )); then + max_group="${group}" + fi +done + +# Keep the unique package in its own compiler invocation. Its generic use of +# weak.Pointer can otherwise assign shared instantiations to libunique.test and +# leave another output with unresolved weak.Pointer methods. +for i in "${!import_paths[@]}"; do + if [[ "${import_paths[$i]}" == "github.com/goplus/llgo/test/std/unique" ]]; then + max_group=$((max_group + 1)) + groups[$i]="${max_group}" + fi +done + +llgo_cmd="${LLGO:-llgo}" +if [[ "${llgo_cmd}" == */* && "${llgo_cmd}" != /* ]]; then + llgo_cmd="$(cd "$(dirname "${llgo_cmd}")" && pwd)/$(basename "${llgo_cmd}")" +fi +runner_source="${root_dir}/dev/test_std_buildmodes/runner.c" + +runtime_libs=(-lpthread -lm -lresolv) +if [[ "$(uname -s)" == Darwin ]]; then + runtime_libs+=(-framework CoreFoundation -framework Security) +fi +for dependency in bdw-gc libuv libffi; do + if pkg-config --exists "${dependency}"; then + while IFS= read -r flag; do + [[ -n "${flag}" ]] && runtime_libs+=("${flag}") + done < <(pkg-config --libs "${dependency}" | xargs -n1) + fi +done +if [[ "$(uname -s)" != Darwin ]] && pkg-config --exists libunwind; then + while IFS= read -r flag; do + [[ -n "${flag}" ]] && runtime_libs+=("${flag}") + done < <(pkg-config --libs libunwind | xargs -n1) +fi + +work_dir="$(mktemp -d "${root_dir}/.std-buildmodes.XXXXXX")" +trap 'rm -rf "${work_dir}"' EXIT + +for mode in c-shared c-archive; do + for ((group = 0; group <= max_group; group++)); do + group_imports=() + for i in "${!import_paths[@]}"; do + if [[ "${groups[$i]}" -eq "${group}" ]]; then + group_imports+=("${import_paths[$i]}") + fi + done + if [[ "${#group_imports[@]}" -eq 0 ]]; then + continue + fi + echo "==> ${mode}: compile ${#group_imports[@]} test package(s)" + ( + cd "${work_dir}" + "${llgo_cmd}" test -c -buildmode="${mode}" "${group_imports[@]}" + ) + + for i in "${!import_paths[@]}"; do + if [[ "${groups[$i]}" -ne "${group}" ]]; then + continue + fi + import_path="${import_paths[$i]}" + stem="${stems[$i]}" + test_main_pkg="${import_path}.test" + runner_base="${work_dir}/runner-${i}" + echo "==> ${test_pkgs[$i]}: run ${mode}" + clang -x c -c "${runner_source}" \ + "-DGO_TEST_PACKAGE=\"${test_main_pkg}\"" \ + -o "${runner_base}.o" + + if [[ "${mode}" == c-shared ]]; then + if [[ "$(uname -s)" == Darwin ]]; then + library="${work_dir}/lib${stem}.dylib" + else + library="${work_dir}/lib${stem}.so" + fi + clang++ "${runner_base}.o" -o "${runner_base}" \ + -L"${work_dir}" "-l${stem}" "${runtime_libs[@]}" + LD_LIBRARY_PATH="${work_dir}${LD_LIBRARY_PATH:+:${LD_LIBRARY_PATH}}" \ + DYLD_LIBRARY_PATH="${work_dir}${DYLD_LIBRARY_PATH:+:${DYLD_LIBRARY_PATH}}" \ + "${runner_base}" + else + library="${work_dir}/lib${stem}.a" + clang++ "${runner_base}.o" -o "${runner_base}" \ + "${library}" "${runtime_libs[@]}" + "${runner_base}" + fi + + if [[ ! -s "${library}" ]]; then + echo "missing ${mode} library: ${library}" >&2 + exit 1 + fi + for artifact in "${runner_base}" "${runner_base}.o" "${library}" "${work_dir}/lib${stem}.h"; do + if [[ -e "${artifact}" ]]; then + unlink "${artifact}" + fi + done + done + done +done diff --git a/dev/test_std_buildmodes/runner.c b/dev/test_std_buildmodes/runner.c new file mode 100644 index 0000000000..2ebed8d8af --- /dev/null +++ b/dev/test_std_buildmodes/runner.c @@ -0,0 +1,22 @@ +#ifndef GO_TEST_PACKAGE +#error GO_TEST_PACKAGE must name the generated Go test main package +#endif + +#ifdef __APPLE__ +#define GO_SYMBOL(name) __asm__("_" name) +#else +#define GO_SYMBOL(name) __asm__(name) +#endif + +extern void llgo_test_init(void) GO_SYMBOL(GO_TEST_PACKAGE ".init"); +extern void llgo_test_run(void) GO_SYMBOL(GO_TEST_PACKAGE ".main"); +extern int __llgo_argc; +extern char **__llgo_argv; + +int main(int argc, char **argv) { + __llgo_argc = argc; + __llgo_argv = argv; + llgo_test_init(); + llgo_test_run(); + return 0; +} diff --git a/doc/_readme/scripts/check_std_cover.sh b/doc/_readme/scripts/check_std_cover.sh index bbf3d7ff65..15448b9712 100755 --- a/doc/_readme/scripts/check_std_cover.sh +++ b/doc/_readme/scripts/check_std_cover.sh @@ -18,15 +18,42 @@ if [ "${#packages[@]}" -eq 0 ]; then fi args=() +covered_packages=() for pkg in "${packages[@]}"; do rel_path="${pkg#${module_path}/}" if [[ "${rel_path}" != test/std/* ]]; then continue fi stdlib_pkg="${rel_path#test/std/}" + covered_packages+=("${stdlib_pkg}") + if [[ "${stdlib_pkg}" == "runtime" ]]; then + continue + fi args+=("-pkg" "${stdlib_pkg}") done +expected_file="$(mktemp)" +covered_file="$(mktemp)" +trap 'rm -f "${expected_file}" "${covered_file}"' EXIT + +go list std \ + | awk '!/(^|\/)internal(\/|$)/ && !/(^|\/)vendor(\/|$)/' \ + | sort -u > "${expected_file}" +printf '%s\n' "${covered_packages[@]}" | sort -u > "${covered_file}" + +missing_packages="$(comm -23 "${expected_file}" "${covered_file}")" +if [[ -n "${missing_packages}" ]]; then + echo "Public standard-library packages missing test/std coverage:" >&2 + while IFS= read -r pkg; do + echo " - ${pkg}" >&2 + done <<< "${missing_packages}" + exit 1 +fi + +expected_count="$(wc -l < "${expected_file}" | tr -d ' ')" +covered_count="$(wc -l < "${covered_file}" | tr -d ' ')" +echo "Public standard-library package coverage: ${covered_count}/${expected_count}" + printf '+ go run ./chore/check_std_symbols' for arg in "${args[@]}"; do printf ' %q' "${arg}" diff --git a/internal/build/build.go b/internal/build/build.go index 2de1f96536..f6ec5423b9 100644 --- a/internal/build/build.go +++ b/internal/build/build.go @@ -1349,9 +1349,9 @@ func linkObjFiles(ctx *context, app string, objFiles, linkArgs []string, verbose return cmd.Link(buildArgs...) } -// cSharedExportArgs keeps //export functions as shared-library link roots. The -// functions live in package archives and otherwise remain unreferenced, so the -// linker can omit both their object files and dynamic symbols. +// cSharedExportArgs keeps //export functions and synthetic test entry points as +// shared-library link roots. They live in package archives and otherwise remain +// unreferenced, so the linker can omit both their object files and symbols. func cSharedExportArgs(ctx *context, pkgs []*aPackage) []string { if ctx == nil || ctx.buildConf == nil || ctx.buildConf.BuildMode != BuildModeCShared { return nil @@ -1366,6 +1366,10 @@ func cSharedExportArgs(ctx *context, pkgs []*aPackage) []string { exports[name] = none{} } } + if ctx.mode == ModeTest && pkg.Package != nil && pkg.Name == "main" && strings.HasSuffix(pkg.PkgPath, ".test") { + exports[pkg.PkgPath+".init"] = none{} + exports[pkg.PkgPath+".main"] = none{} + } } names := make([]string, 0, len(exports)) for name := range exports { diff --git a/internal/build/build_test.go b/internal/build/build_test.go index c26241cc20..d360831f97 100644 --- a/internal/build/build_test.go +++ b/internal/build/build_test.go @@ -630,6 +630,26 @@ func TestCSharedExportArgs(t *testing.T) { } } +func TestCSharedExportArgsKeepsTestMain(t *testing.T) { + prog := llssa.NewProgram(nil) + defer prog.Dispose() + lpkg := prog.NewPackage("example.com/p.test", "example.com/p.test") + pkgs := []*aPackage{{ + Package: &packages.Package{ + Name: "main", + PkgPath: "example.com/p.test", + }, + LPkg: lpkg, + }} + ctx := &context{ + mode: ModeTest, + buildConf: &Config{BuildMode: BuildModeCShared, Goos: "linux"}, + } + if got, want := strings.Join(cSharedExportArgs(ctx, pkgs), " "), "-Wl,--undefined=example.com/p.test.init -Wl,--undefined=example.com/p.test.main"; got != want { + t.Fatalf("test main cSharedExportArgs = %q, want %q", got, want) + } +} + func TestApplyBuildModeCompileFlags(t *testing.T) { tests := []struct { name string diff --git a/internal/build/main_module.go b/internal/build/main_module.go index 5dbf0ad81f..1b46299007 100644 --- a/internal/build/main_module.go +++ b/internal/build/main_module.go @@ -76,10 +76,27 @@ func genMainModule(ctx *context, rtPkgPath string, pkg *packages.Package, cfg *g }, LPkg: mainPkg, } + var rtInit llssa.Function + if cfg.rtInit { + rtInit = declareNoArgFunc(mainPkg, rtPkgPath+".init") + } + var abiInit llssa.Function + if cfg.abiInit != 0 { + abiInit = mainPkg.InitAbiTypesFor("init$abitypes", func(sym *llssa.AbiSymbol) bool { + if _, ok := cfg.abiSymbols[sym.Name]; !ok { + return false + } + return filterAbiSymbol(cfg.abiInit, sym) + }) + } if ctx.buildConf.BuildMode != BuildModeExe { - if cfg.rtInit { - defineLibraryRuntimeInit(mainPkg, declareNoArgFunc(mainPkg, rtPkgPath+".init")) + if rtInit != nil || abiInit != nil { + initArraySection := "" + if ctx.buildConf.BuildMode == BuildModeCShared && ctx.buildConf.Goos == "linux" { + initArraySection = ".init_array" + } + defineLibraryRuntimeInit(mainPkg, rtInit, abiInit, initArraySection) } return mainAPkg } @@ -95,21 +112,6 @@ func genMainModule(ctx *context, rtPkgPath string, pkg *packages.Package, cfg *g pyFinalize = declareNoArgFunc(mainPkg, "Py_Finalize") } - var rtInit llssa.Function - if cfg.rtInit { - rtInit = declareNoArgFunc(mainPkg, rtPkgPath+".init") - } - - var abiInit llssa.Function - if cfg.abiInit != 0 { - abiInit = mainPkg.InitAbiTypesFor("init$abitypes", func(sym *llssa.AbiSymbol) bool { - if _, ok := cfg.abiSymbols[sym.Name]; !ok { - return false - } - return filterAbiSymbol(cfg.abiInit, sym) - }) - } - mainInit := declareNoArgFunc(mainPkg, pkg.PkgPath+".init") mainMain := declareNoArgFunc(mainPkg, pkg.PkgPath+".main") @@ -130,20 +132,34 @@ func genMainModule(ctx *context, rtPkgPath string, pkg *packages.Package, cfg *g return mainAPkg } -// defineLibraryRuntimeInit arranges for the LLGo runtime to be initialized -// before a C program calls an exported Go function. llvm.global_ctors is -// lowered to the platform's native constructor mechanism for both shared -// libraries and archive members. -func defineLibraryRuntimeInit(pkg llssa.Package, rtInit llssa.Function) { +// defineLibraryRuntimeInit arranges for the LLGo runtime and reflection ABI +// tables to be initialized before a C program calls an exported Go function. +// Linux shared libraries use .init_array explicitly because the raw LLVM +// target machine lowers llvm.global_ctors to legacy .ctors; other library +// formats use LLVM's platform-specific constructor lowering. +func defineLibraryRuntimeInit(pkg llssa.Package, rtInit, abiInit llssa.Function, initArraySection string) { const ctorName = "__llgo_runtime_ctor" ctor := pkg.NewFunc(ctorName, llssa.NoArgsNoRet, llssa.InC) ctorValue := pkg.Module().NamedFunction(ctorName) ctorValue.SetLinkage(llvm.InternalLinkage) b := ctor.MakeBody(1) - b.Call(rtInit.Expr) + if rtInit != nil { + b.Call(rtInit.Expr) + } + if abiInit != nil { + b.Call(abiInit.Expr) + } b.Return() mod := pkg.Module() + if initArraySection != "" { + initEntry := llvm.AddGlobal(mod, ctorValue.Type(), "__llgo_runtime_ctor_init") + initEntry.SetInitializer(ctorValue) + initEntry.SetGlobalConstant(true) + initEntry.SetSection(initArraySection) + initEntry.SetVisibility(llvm.HiddenVisibility) + return + } llvmCtx := mod.Context() priority := llvm.ConstInt(llvmCtx.Int32Type(), 65535, false) entry := llvmCtx.ConstStruct([]llvm.Value{ diff --git a/internal/build/main_module_test.go b/internal/build/main_module_test.go index f803182482..b2b9bf6aa6 100644 --- a/internal/build/main_module_test.go +++ b/internal/build/main_module_test.go @@ -97,10 +97,14 @@ func TestGenMainModuleLibraryInitializesRuntime(t *testing.T) { mod := genMainModule(ctx, llssa.PkgRuntime, pkg, &genConfig{rtInit: true}) ir := mod.LPkg.String() checks := []string{ - "@llvm.global_ctors = appending global", "define internal void @__llgo_runtime_ctor()", "call void @\"github.com/goplus/llgo/runtime/internal/runtime.init\"()", } + if mode == BuildModeCShared { + checks = append(checks, `@__llgo_runtime_ctor_init = hidden constant ptr @__llgo_runtime_ctor, section ".init_array"`) + } else { + checks = append(checks, "@llvm.global_ctors = appending global") + } for _, want := range checks { if !strings.Contains(ir, want) { t.Fatalf("library module IR missing %q:\n%s", want, ir) @@ -113,6 +117,21 @@ func TestGenMainModuleLibraryInitializesRuntime(t *testing.T) { } } +func TestDefineLibraryRuntimeInitInitializesAbiTypes(t *testing.T) { + prog := llssa.NewProgram(nil) + pkg := prog.NewPackage("", "example.com/foo.main") + rtInit := declareNoArgFunc(pkg, llssa.PkgRuntime+".init") + abiInit := declareNoArgFunc(pkg, "init$abitypes") + + defineLibraryRuntimeInit(pkg, rtInit, abiInit, "") + + ir := pkg.String() + assertInOrder(t, ir, + "call void @\"github.com/goplus/llgo/runtime/internal/runtime.init\"()", + "call void @\"init$abitypes\"()", + ) +} + func assertInOrder(t *testing.T, s string, wants ...string) { t.Helper() offset := 0 diff --git a/runtime/internal/lib/runtime/atomic_pointer_llgo.go b/runtime/internal/lib/runtime/atomic_pointer_llgo.go new file mode 100644 index 0000000000..bf7156fd83 --- /dev/null +++ b/runtime/internal/lib/runtime/atomic_pointer_llgo.go @@ -0,0 +1,23 @@ +//go:build darwin || linux + +package runtime + +import ( + "sync/atomic" + "unsafe" +) + +// LLGo's conservative collector does not require Go write barriers, so these +// hooks can use the standard library's architecture-specific uintptr atomics. + +//go:linkname atomic_storePointer internal/runtime/atomic.storePointer +func atomic_storePointer(ptr *unsafe.Pointer, new unsafe.Pointer) { + atomic.StoreUintptr((*uintptr)(unsafe.Pointer(ptr)), uintptr(new)) +} + +//go:linkname atomic_casPointer internal/runtime/atomic.casPointer +func atomic_casPointer(ptr *unsafe.Pointer, old, new unsafe.Pointer) bool { + return atomic.CompareAndSwapUintptr( + (*uintptr)(unsafe.Pointer(ptr)), uintptr(old), uintptr(new), + ) +} diff --git a/runtime/internal/lib/runtime/cgo_handle_llgo.go b/runtime/internal/lib/runtime/cgo_handle_llgo.go new file mode 100644 index 0000000000..6f75db2f52 --- /dev/null +++ b/runtime/internal/lib/runtime/cgo_handle_llgo.go @@ -0,0 +1,66 @@ +// Copyright 2021 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 runtime + +import ( + psync "github.com/goplus/llgo/runtime/internal/clite/pthread/sync" + _ "unsafe" +) + +// These functions provide runtime/cgo.Handle without pulling in the gc +// runtime's C callback bridge. LLGo supplies its own C callback trampoline. + +//go:linkname cgoNewHandle runtime/cgo.NewHandle +func cgoNewHandle(v any) uintptr { + cgoHandleState.once.Do(initCgoHandleState) + cgoHandleState.mu.Lock() + cgoHandleState.next++ + h := cgoHandleState.next + if h == 0 { + cgoHandleState.mu.Unlock() + panic("runtime/cgo: ran out of handle space") + } + cgoHandleState.handles[h] = v + cgoHandleState.mu.Unlock() + return h +} + +//go:linkname cgoHandleValue runtime/cgo.Handle.Value +func cgoHandleValue(h uintptr) any { + cgoHandleState.once.Do(initCgoHandleState) + cgoHandleState.mu.Lock() + v, ok := cgoHandleState.handles[h] + cgoHandleState.mu.Unlock() + if !ok { + panic("runtime/cgo: misuse of an invalid Handle") + } + return v +} + +//go:linkname cgoHandleDelete runtime/cgo.Handle.Delete +func cgoHandleDelete(h uintptr) { + cgoHandleState.once.Do(initCgoHandleState) + cgoHandleState.mu.Lock() + _, ok := cgoHandleState.handles[h] + if ok { + delete(cgoHandleState.handles, h) + } + cgoHandleState.mu.Unlock() + if !ok { + panic("runtime/cgo: misuse of an invalid Handle") + } +} + +var cgoHandleState struct { + once psync.Once + mu psync.Mutex + next uintptr + handles map[uintptr]any +} + +func initCgoHandleState() { + cgoHandleState.mu.Init(nil) + cgoHandleState.handles = make(map[uintptr]any) +} diff --git a/runtime/internal/lib/runtime/cgroup_linux_go126_llgo.go b/runtime/internal/lib/runtime/cgroup_linux_go126_llgo.go new file mode 100644 index 0000000000..994b024a01 --- /dev/null +++ b/runtime/internal/lib/runtime/cgroup_linux_go126_llgo.go @@ -0,0 +1,10 @@ +//go:build linux && go1.26 + +package runtime + +import _ "unsafe" + +//go:linkname cgroup_throw internal/runtime/cgroup.throw +func cgroup_throw(s string) { + throw(s) +} diff --git a/runtime/internal/lib/runtime/debug_linkname_llgo.go b/runtime/internal/lib/runtime/debug_linkname_llgo.go index 544eac3898..efa363d216 100644 --- a/runtime/internal/lib/runtime/debug_linkname_llgo.go +++ b/runtime/internal/lib/runtime/debug_linkname_llgo.go @@ -2,6 +2,8 @@ package runtime import ( _ "unsafe" + + llrt "github.com/goplus/llgo/runtime/internal/runtime" ) var ( @@ -35,7 +37,9 @@ func setGCPercent(in int32) (out int32) { } //go:linkname setPanicOnFault runtime/debug.setPanicOnFault -func setPanicOnFault(_ bool) (old bool) { return false } +func setPanicOnFault(in bool) (out bool) { + return llrt.SetPanicOnFault(in) +} //go:linkname setMaxThreads runtime/debug.setMaxThreads func setMaxThreads(in int) (out int) { diff --git a/runtime/internal/lib/runtime/error.go b/runtime/internal/lib/runtime/error.go index cc4bb7e614..993f150bc4 100644 --- a/runtime/internal/lib/runtime/error.go +++ b/runtime/internal/lib/runtime/error.go @@ -1,6 +1,33 @@ package runtime -import "github.com/goplus/llgo/runtime/internal/runtime" +import ( + llruntime "github.com/goplus/llgo/runtime/internal/runtime" + _ "unsafe" +) -type TypeAssertionError = runtime.TypeAssertionError -type PanicNilError = runtime.PanicNilError +type TypeAssertionError = llruntime.TypeAssertionError +type PanicNilError = llruntime.PanicNilError + +// The public runtime package aliases these LLGo runtime error types. Provide +// the method symbols under their public names as well, so method expressions +// and C-library builds can retain them. + +//go:linkname typeAssertionErrorRuntimeError runtime.(*TypeAssertionError).RuntimeError +func typeAssertionErrorRuntimeError(e *llruntime.TypeAssertionError) { + e.RuntimeError() +} + +//go:linkname typeAssertionErrorError runtime.(*TypeAssertionError).Error +func typeAssertionErrorError(e *llruntime.TypeAssertionError) string { + return e.Error() +} + +//go:linkname panicNilErrorRuntimeError runtime.(*PanicNilError).RuntimeError +func panicNilErrorRuntimeError(e *llruntime.PanicNilError) { + e.RuntimeError() +} + +//go:linkname panicNilErrorError runtime.(*PanicNilError).Error +func panicNilErrorError(e *llruntime.PanicNilError) string { + return e.Error() +} diff --git a/runtime/internal/lib/runtime/gettimeofday_linux_amd64_llgo.go b/runtime/internal/lib/runtime/gettimeofday_linux_amd64_llgo.go new file mode 100644 index 0000000000..41c075b0a5 --- /dev/null +++ b/runtime/internal/lib/runtime/gettimeofday_linux_amd64_llgo.go @@ -0,0 +1,26 @@ +//go:build linux && amd64 && !baremetal + +package runtime + +import ( + "unsafe" + + c "github.com/goplus/llgo/runtime/internal/clite" + cliteos "github.com/goplus/llgo/runtime/internal/clite/os" + clitesyscall "github.com/goplus/llgo/runtime/internal/clite/syscall" +) + +//go:linkname c_gettimeofday C.gettimeofday +func c_gettimeofday(tv *clitesyscall.Timeval, tz unsafe.Pointer) c.Int + +// syscall.Gettimeofday and syscall.Time call an amd64 assembly helper. LLGo's +// replacement syscall package does not include that symbol, so forward it to +// libc while preserving syscall's errno result. +// +//go:linkname syscall_gettimeofday syscall.gettimeofday +func syscall_gettimeofday(tv *clitesyscall.Timeval) clitesyscall.Errno { + if c_gettimeofday(tv, nil) != 0 { + return clitesyscall.Errno(cliteos.Errno()) + } + return 0 +} diff --git a/runtime/internal/lib/runtime/os_darwin.go b/runtime/internal/lib/runtime/os_darwin.go index e0f3fa16bb..d4a8cc0f97 100644 --- a/runtime/internal/lib/runtime/os_darwin.go +++ b/runtime/internal/lib/runtime/os_darwin.go @@ -9,11 +9,27 @@ func sysctlbynameInt32(name []byte) (int32, int32) { return ret, out } +func sysctlbynameBytes(name, out []byte) int32 { + nout := uintptr(len(out)) + return sysctlbyname(&name[0], &out[0], &nout, nil, 0) +} + //go:linkname internal_cpu_getsysctlbyname internal/cpu.getsysctlbyname func internal_cpu_getsysctlbyname(name []byte) (int32, int32) { return sysctlbynameInt32(name) } +//go:linkname internal_cpu_sysctlbynameInt32 internal/cpu.sysctlbynameInt32 +func internal_cpu_sysctlbynameInt32(name []byte) (int32, int32) { + return sysctlbynameInt32(name) +} + +//go:linkname internal_cpu_sysctlbynameBytes internal/cpu.sysctlbynameBytes +func internal_cpu_sysctlbynameBytes(name, out []byte) int32 { + return sysctlbynameBytes(name, out) +} + +//go:cgo_import_dynamic libc_sysctlbyname sysctlbyname "/usr/lib/libSystem.B.dylib" var libc_sysctlbyname_trampoline_addr uintptr // adapted from runtime/sys_darwin.go in the pattern of sysctl() above, as defined in x/sys/unix diff --git a/runtime/internal/lib/runtime/plugin_llgo.go b/runtime/internal/lib/runtime/plugin_llgo.go new file mode 100644 index 0000000000..9d1c2f6a32 --- /dev/null +++ b/runtime/internal/lib/runtime/plugin_llgo.go @@ -0,0 +1,17 @@ +package runtime + +import _ "unsafe" + +type pluginInitTask struct{} + +// LLGo does not yet integrate modules loaded by the Go plugin package with its +// runtime package metadata. Return a deterministic error instead of leaving +// the standard plugin package with unresolved runtime symbols. +// +//go:linkname plugin_lastmoduleinit plugin.lastmoduleinit +func plugin_lastmoduleinit() (string, map[string]any, []*pluginInitTask, string) { + return "", nil, nil, "plugin loading is not supported by llgo" +} + +//go:linkname runtime_doInit runtime.doInit +func runtime_doInit([]*pluginInitTask) {} diff --git a/runtime/internal/lib/runtime/pprof_linkname_llgo.go b/runtime/internal/lib/runtime/pprof_linkname_llgo.go index 4488dbd97f..3dff79d9a4 100644 --- a/runtime/internal/lib/runtime/pprof_linkname_llgo.go +++ b/runtime/internal/lib/runtime/pprof_linkname_llgo.go @@ -43,16 +43,17 @@ func pprof_cyclesPerSecond() int64 { } var ( - cpuProfilePeriodRecord = []uint64{3, 0, 100} // [len, timestamp, hz] - cpuProfilePeriodTags = []unsafe.Pointer{nil} // one tag slot for the period record - cpuProfileEOF = true // minimal stub: no streaming samples + // C library entry points may reach this hook before ordinary package + // initialization, so keep the minimal profile data in static arrays. + cpuProfilePeriodRecord = [3]uint64{3, 0, 100} // [len, timestamp, hz] + cpuProfilePeriodTags [1]unsafe.Pointer // one tag slot for the period record ) //go:linkname runtime_pprof_readProfile runtime/pprof.readProfile func runtime_pprof_readProfile() (data []uint64, tags []unsafe.Pointer, eof bool) { // Provide a minimal, valid profile stream for runtime/pprof. // The stdlib expects at least the initial "period" record (3 uint64s). - return cpuProfilePeriodRecord, cpuProfilePeriodTags, cpuProfileEOF + return cpuProfilePeriodRecord[:], cpuProfilePeriodTags[:], true } //go:linkname pprof_goroutineProfileWithLabels runtime.pprof_goroutineProfileWithLabels diff --git a/runtime/internal/lib/runtime/pprof_runtime_stub_llgo.go b/runtime/internal/lib/runtime/pprof_runtime_stub_llgo.go index c74b6d1421..9a5fec50f5 100644 --- a/runtime/internal/lib/runtime/pprof_runtime_stub_llgo.go +++ b/runtime/internal/lib/runtime/pprof_runtime_stub_llgo.go @@ -9,7 +9,16 @@ import ( ) type StackRecord struct { - Stack []uintptr + Stack0 [32]uintptr +} + +func (r *StackRecord) Stack() []uintptr { + for i, pc := range r.Stack0 { + if pc == 0 { + return r.Stack0[:i] + } + } + return r.Stack0[:] } type MemProfileRecord struct { diff --git a/runtime/internal/lib/runtime/synctest_llgo.go b/runtime/internal/lib/runtime/synctest_llgo.go index 4d1a89d8d5..df08c497f4 100644 --- a/runtime/internal/lib/runtime/synctest_llgo.go +++ b/runtime/internal/lib/runtime/synctest_llgo.go @@ -6,6 +6,14 @@ import "unsafe" // Minimal synctest stubs for llgo. +//go:linkname synctest_run internal/synctest.Run +func synctest_run(f func()) { + f() +} + +//go:linkname synctest_wait internal/synctest.Wait +func synctest_wait() {} + //go:linkname synctest_isInBubble internal/synctest.IsInBubble func synctest_isInBubble() bool { return false diff --git a/runtime/internal/lib/runtime/syscall_linux_llgo.go b/runtime/internal/lib/runtime/syscall_linux_llgo.go new file mode 100644 index 0000000000..44c07816d4 --- /dev/null +++ b/runtime/internal/lib/runtime/syscall_linux_llgo.go @@ -0,0 +1,25 @@ +//go:build linux + +package runtime + +import ( + "unsafe" + + clitesyscall "github.com/goplus/llgo/runtime/internal/clite/syscall" +) + +// LLGo may run alongside threads created by C libraries, which the Go runtime +// cannot enumerate. Match Go's cgo behavior and report AllThreadsSyscall as +// unsupported rather than changing process credentials on only one thread. +// +//go:linkname syscall_runtime_doAllThreadsSyscall syscall.runtime_doAllThreadsSyscall +//go:uintptrescapes +func syscall_runtime_doAllThreadsSyscall(_, _, _, _, _, _, _ uintptr) (r1, r2, err uintptr) { + return ^uintptr(0), ^uintptr(0), uintptr(clitesyscall.ENOTSUP) +} + +//go:linkname syscall_cgocaller syscall.cgocaller +//go:uintptrescapes +func syscall_cgocaller(_ unsafe.Pointer, _ ...uintptr) uintptr { + return uintptr(clitesyscall.ENOTSUP) +} diff --git a/runtime/internal/runtime/defer_tls.go b/runtime/internal/runtime/defer_tls.go deleted file mode 100644 index 6a430e9924..0000000000 --- a/runtime/internal/runtime/defer_tls.go +++ /dev/null @@ -1,42 +0,0 @@ -//go:build !baremetal - -/* - * Copyright (c) 2025 The XGo Authors (xgo.dev). All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package runtime - -import "github.com/goplus/llgo/runtime/internal/clite/tls" - -var deferTLS = tls.Alloc[*Defer](func(head **Defer) { - if head != nil { - *head = nil - } -}) - -// SetThreadDefer associates the current thread with the given defer chain. -func SetThreadDefer(head *Defer) { - deferTLS.Set(head) -} - -// GetThreadDefer returns the current thread's defer chain head. -func GetThreadDefer() *Defer { - return deferTLS.Get() -} - -// ClearThreadDefer resets the current thread's defer chain to nil. -func ClearThreadDefer() { - deferTLS.Clear() -} diff --git a/runtime/internal/runtime/defer_tls_baremetal.go b/runtime/internal/runtime/defer_tls_baremetal.go deleted file mode 100644 index d8c4938cca..0000000000 --- a/runtime/internal/runtime/defer_tls_baremetal.go +++ /dev/null @@ -1,39 +0,0 @@ -//go:build baremetal - -/* - * Copyright (c) 2025 The XGo Authors (xgo.dev). All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package runtime - -// globalDeferHead stores the current defer chain head. -// In baremetal single-threaded environment, a global variable -// replaces pthread TLS. -var globalDeferHead *Defer - -// SetThreadDefer associates the current thread with the given defer chain. -func SetThreadDefer(head *Defer) { - globalDeferHead = head -} - -// GetThreadDefer returns the current thread's defer chain head. -func GetThreadDefer() *Defer { - return globalDeferHead -} - -// ClearThreadDefer resets the current thread's defer chain to nil. -func ClearThreadDefer() { - globalDeferHead = nil -} diff --git a/runtime/internal/runtime/g.go b/runtime/internal/runtime/g.go new file mode 100644 index 0000000000..dc249a33ba --- /dev/null +++ b/runtime/internal/runtime/g.go @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2026 The XGo Authors (xgo.dev). All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package runtime + +import "unsafe" + +// g holds the runtime state owned by one LLGo goroutine. +type g struct { + defer_ *Defer + panic_ unsafe.Pointer + goexit bool + isMain bool + paniconfault bool +} + +// SetPanicOnFault updates the fault behavior requested by the current +// goroutine and reports its previous value. +func SetPanicOnFault(in bool) (out bool) { + gp := getg() + out = gp.paniconfault + gp.paniconfault = in + return out +} + +// SetThreadDefer associates the current goroutine with the given defer chain. +func SetThreadDefer(head *Defer) { + getg().defer_ = head +} + +// GetThreadDefer returns the current goroutine's defer chain head. +func GetThreadDefer() *Defer { + return getg().defer_ +} + +// ClearThreadDefer resets the current goroutine's defer chain to nil. +func ClearThreadDefer() { + getg().defer_ = nil +} diff --git a/runtime/internal/runtime/g_global.go b/runtime/internal/runtime/g_global.go new file mode 100644 index 0000000000..255733ff01 --- /dev/null +++ b/runtime/internal/runtime/g_global.go @@ -0,0 +1,25 @@ +//go:build !llgo || baremetal + +/* + * Copyright (c) 2026 The XGo Authors (xgo.dev). All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package runtime + +var globalG g + +func getg() *g { + return &globalG +} diff --git a/runtime/internal/runtime/g_pthread.go b/runtime/internal/runtime/g_pthread.go new file mode 100644 index 0000000000..048ecc1dc6 --- /dev/null +++ b/runtime/internal/runtime/g_pthread.go @@ -0,0 +1,66 @@ +//go:build llgo && !baremetal + +/* + * Copyright (c) 2026 The XGo Authors (xgo.dev). All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package runtime + +import ( + "unsafe" + + c "github.com/goplus/llgo/runtime/internal/clite" + "github.com/goplus/llgo/runtime/internal/clite/pthread" +) + +var gKey = newGKey() + +func newGKey() pthread.Key { + var key pthread.Key + if ret := key.Create(pthread.KeyDestructor(destroyG)); ret != 0 { + c.Fprintf(c.Stderr, c.Str("runtime: pthread_key_create failed (errno=%d)\n"), ret) + panic("runtime: failed to create getg key") + } + return key +} + +func getg() *g { + if ptr := gKey.Get(); ptr != nil { + return (*g)(ptr) + } + ptr := AllocRoot(unsafe.Sizeof(g{})) + if ptr == nil { + panic("runtime: failed to allocate g") + } + c.Memset(ptr, 0, unsafe.Sizeof(g{})) + if ret := gKey.Set(ptr); ret != 0 { + FreeRoot(ptr) + c.Fprintf(c.Stderr, c.Str("runtime: pthread_setspecific failed (errno=%d)\n"), ret) + panic("runtime: failed to install g") + } + return (*g)(ptr) +} + +func destroyG(ptr c.Pointer) { + gp := (*g)(ptr) + if gp == nil { + return + } + if gp.panic_ != nil { + c.Free(gp.panic_) + } + *gp = g{} + FreeRoot(ptr) +} diff --git a/runtime/internal/runtime/z_default.go b/runtime/internal/runtime/z_default.go index d0757f9cb1..7f0b7601ba 100644 --- a/runtime/internal/runtime/z_default.go +++ b/runtime/internal/runtime/z_default.go @@ -16,7 +16,8 @@ var ( // Rethrow rethrows a panic. func Rethrow(link *Defer) { - if ptr := excepKey.Get(); ptr != nil { + gp := getg() + if ptr := gp.panic_; ptr != nil { if link == nil { TracePanic(*(*any)(ptr)) if PanicTraceback == nil || !PanicTraceback(2) { @@ -27,7 +28,7 @@ func Rethrow(link *Defer) { } else { c.Siglongjmp(link.Addr, 1) } - } else if ptr := goexitKey.Get(); ptr != nil { + } else if gp.goexit { // Goexit must run deferred functions before terminating the current // goroutine. Reuse the longjmp-based defer unwinding: // 1) If we have a defer frame, longjmp to it so it can execute defers. @@ -36,7 +37,7 @@ func Rethrow(link *Defer) { if link != nil { c.Siglongjmp(link.Addr, 1) } - if pthread.Equal(mainThread, pthread.Self()) != 0 { + if gp.isMain { fatal("no goroutines (main called runtime.Goexit) - deadlock!") c.Exit(2) } diff --git a/runtime/internal/runtime/z_rt.go b/runtime/internal/runtime/z_rt.go index 17c89f55da..d771ab94cf 100644 --- a/runtime/internal/runtime/z_rt.go +++ b/runtime/internal/runtime/z_rt.go @@ -20,7 +20,6 @@ import ( "unsafe" c "github.com/goplus/llgo/runtime/internal/clite" - "github.com/goplus/llgo/runtime/internal/clite/pthread" "github.com/goplus/llgo/runtime/internal/clite/setjmp" ) @@ -38,9 +37,10 @@ type Defer struct { // Recover recovers a panic. func Recover() (ret any) { - ptr := excepKey.Get() + gp := getg() + ptr := gp.panic_ if ptr != nil { - excepKey.Set(nil) + gp.panic_ = nil ret = *(*any)(ptr) c.Free(ptr) if PanicRecovered != nil { @@ -58,26 +58,20 @@ func Panic(v any) { SavePanicCallerFrames() ptr := c.Malloc(unsafe.Sizeof(v)) *(*any)(ptr) = v - excepKey.Set(ptr) + gp := getg() + gp.panic_ = ptr - Rethrow((*Defer)(c.GoDeferData())) + Rethrow(gp.defer_) } -var ( - excepKey pthread.Key - goexitKey pthread.Key - mainThread pthread.Thread -) - func Goexit() { - goexitKey.Set(unsafe.Pointer(&goexitKey)) - Rethrow((*Defer)(c.GoDeferData())) + gp := getg() + gp.goexit = true + Rethrow(gp.defer_) } func init() { - excepKey.Create(nil) - goexitKey.Create(nil) - mainThread = pthread.Self() + getg().isMain = true } // ----------------------------------------------------------------------------- diff --git a/test/go/runtime_g_state_test.go b/test/go/runtime_g_state_test.go new file mode 100644 index 0000000000..8d808ac5df --- /dev/null +++ b/test/go/runtime_g_state_test.go @@ -0,0 +1,80 @@ +package gotest + +import ( + "runtime" + "testing" +) + +type runtimeGStateResult struct { + id int + recovered any + order string +} + +func runtimeGStatePanic(id int, ready chan<- struct{}, start <-chan struct{}, results chan<- runtimeGStateResult) { + order := "" + defer func() { + result := runtimeGStateResult{id: id, recovered: recover(), order: order + "r"} + results <- result + }() + defer func() { + order += "d" + }() + ready <- struct{}{} + <-start + order += "p" + panic(id) +} + +func TestRuntimeGStateIsolation(t *testing.T) { + ready := make(chan struct{}, 2) + start := make(chan struct{}) + results := make(chan runtimeGStateResult, 2) + for id := 1; id <= 2; id++ { + go runtimeGStatePanic(id, ready, start, results) + } + <-ready + <-ready + close(start) + + seen := [3]bool{} + for i := 0; i < 2; i++ { + result := <-results + if result.id < 1 || result.id > 2 { + t.Fatalf("unexpected goroutine id %d", result.id) + } + if seen[result.id] { + t.Fatalf("duplicate result from goroutine %d", result.id) + } + seen[result.id] = true + if result.recovered != result.id { + t.Fatalf("goroutine %d recovered %v", result.id, result.recovered) + } + if result.order != "pdr" { + t.Fatalf("goroutine %d defer order = %q, want %q", result.id, result.order, "pdr") + } + } + + goexitDone := make(chan any, 1) + go func() { + defer func() { + goexitDone <- recover() + }() + runtime.Goexit() + goexitDone <- "Goexit returned" + }() + if recovered := <-goexitDone; recovered != nil { + t.Fatalf("recover during Goexit = %v, want nil", recovered) + } + + var recovered any + func() { + defer func() { + recovered = recover() + }() + panic("main panic") + }() + if recovered != "main panic" { + t.Fatalf("main goroutine recovered %v, want %q", recovered, "main panic") + } +} diff --git a/test/llgoext/runtime_g_test.go b/test/llgoext/runtime_g_test.go new file mode 100644 index 0000000000..52789908d7 --- /dev/null +++ b/test/llgoext/runtime_g_test.go @@ -0,0 +1,169 @@ +//go:build llgo + +/* + * Copyright (c) 2026 The XGo Authors (xgo.dev). All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package llgoext + +import ( + "testing" + "unsafe" +) + +//go:linkname runtimeGetGForTest github.com/goplus/llgo/runtime/internal/runtime.getg +func runtimeGetGForTest() unsafe.Pointer + +//go:linkname runtimeGetThreadDeferForGTest github.com/goplus/llgo/runtime/internal/runtime.GetThreadDefer +func runtimeGetThreadDeferForGTest() unsafe.Pointer + +//go:linkname runtimeSetThreadDeferForGTest github.com/goplus/llgo/runtime/internal/runtime.SetThreadDefer +func runtimeSetThreadDeferForGTest(unsafe.Pointer) + +//go:linkname runtimeClearThreadDeferForGTest github.com/goplus/llgo/runtime/internal/runtime.ClearThreadDefer +func runtimeClearThreadDeferForGTest() + +type runtimeGPointerResult struct { + first unsafe.Pointer + second unsafe.Pointer +} + +func TestRuntimeGetGIsolation(t *testing.T) { + mainG := runtimeGetGForTest() + if mainG == nil || runtimeGetGForTest() != mainG { + t.Fatalf("main getg is not stable: %p", mainG) + } + + ready := make(chan unsafe.Pointer, 2) + start := make(chan struct{}) + results := make(chan runtimeGPointerResult, 2) + for i := 0; i < 2; i++ { + go func() { + first := runtimeGetGForTest() + ready <- first + <-start + results <- runtimeGPointerResult{first: first, second: runtimeGetGForTest()} + }() + } + firstReady := <-ready + secondReady := <-ready + close(start) + + first := <-results + second := <-results + for i, result := range []runtimeGPointerResult{first, second} { + if result.first == nil || result.first != result.second { + t.Fatalf("goroutine %d getg values = (%p, %p), want one stable non-nil pointer", i, result.first, result.second) + } + if result.first == mainG { + t.Fatalf("goroutine %d shared main g %p", i, mainG) + } + } + if firstReady == secondReady { + t.Fatalf("goroutines shared g %p", firstReady) + } +} + +type runtimeDeferProbeResult struct { + before unsafe.Pointer + inside unsafe.Pointer + after unsafe.Pointer +} + +func runtimeDeferProbe(ready chan<- unsafe.Pointer, start <-chan struct{}, results chan<- runtimeDeferProbeResult) { + result := runtimeDeferProbeResult{before: runtimeGetThreadDeferForGTest()} + func() { + defer func() {}() + result.inside = runtimeGetThreadDeferForGTest() + ready <- result.inside + <-start + }() + result.after = runtimeGetThreadDeferForGTest() + results <- result +} + +func TestRuntimeDeferHeadIsolation(t *testing.T) { + mainHead := runtimeGetThreadDeferForGTest() + ready := make(chan unsafe.Pointer, 2) + start := make(chan struct{}) + results := make(chan runtimeDeferProbeResult, 2) + + go runtimeDeferProbe(ready, start, results) + go runtimeDeferProbe(ready, start, results) + first := <-ready + second := <-ready + close(start) + + if first == nil || second == nil { + t.Fatalf("active defer heads = (%p, %p), want two non-nil heads", first, second) + } + if first == second { + t.Fatalf("concurrent goroutines shared defer head %p", first) + } + for i := 0; i < 2; i++ { + result := <-results + if result.inside == nil { + t.Fatal("active defer head is nil") + } + if result.after != result.before { + t.Fatalf("defer head after return = %p, want previous %p", result.after, result.before) + } + } + if got := runtimeGetThreadDeferForGTest(); got != mainHead { + t.Fatalf("main defer head changed to %p, want %p", got, mainHead) + } +} + +type runtimeDeferAccessorResult struct { + want unsafe.Pointer + got unsafe.Pointer + cleared unsafe.Pointer +} + +func runtimeDeferAccessorProbe(token unsafe.Pointer, results chan<- runtimeDeferAccessorResult) { + previous := runtimeGetThreadDeferForGTest() + runtimeSetThreadDeferForGTest(token) + got := runtimeGetThreadDeferForGTest() + runtimeClearThreadDeferForGTest() + cleared := runtimeGetThreadDeferForGTest() + runtimeSetThreadDeferForGTest(previous) + results <- runtimeDeferAccessorResult{want: token, got: got, cleared: cleared} +} + +func TestRuntimeDeferAccessors(t *testing.T) { + results := make(chan runtimeDeferAccessorResult, 2) + first := new(byte) + second := new(byte) + go runtimeDeferAccessorProbe(unsafe.Pointer(first), results) + go runtimeDeferAccessorProbe(unsafe.Pointer(second), results) + + for i := 0; i < 2; i++ { + result := <-results + if result.got != result.want { + t.Fatalf("defer head = %p, want %p", result.got, result.want) + } + if result.cleared != nil { + t.Fatalf("cleared defer head = %p, want nil", result.cleared) + } + } +} + +var runtimeGSink unsafe.Pointer + +func BenchmarkRuntimeGetG(b *testing.B) { + for i := 0; i < b.N; i++ { + runtimeGSink = runtimeGetGForTest() + } +} diff --git a/test/std/crypto/ecdsa/go126_symbols_test.go b/test/std/crypto/ecdsa/go126_symbols_test.go index 93cca5fba9..8b3ed39463 100644 --- a/test/std/crypto/ecdsa/go126_symbols_test.go +++ b/test/std/crypto/ecdsa/go126_symbols_test.go @@ -23,6 +23,13 @@ func TestKeyBytes(t *testing.T) { if len(privateBytes) != 32 { t.Fatalf("PrivateKey.Bytes length = %d, want 32", len(privateBytes)) } + parsedPrivate, err := ecdsa.ParseRawPrivateKey(elliptic.P256(), privateBytes) + if err != nil { + t.Fatal(err) + } + if parsedPrivate.D.Cmp(privateKey.D) != 0 { + t.Fatal("parsed private key differs from generated key") + } publicBytes, err := privateKey.PublicKey.Bytes() if err != nil { @@ -32,4 +39,11 @@ func TestKeyBytes(t *testing.T) { if !bytes.Equal(publicBytes, wantPublic) { t.Fatalf("PublicKey.Bytes = %x, want %x", publicBytes, wantPublic) } + parsedPublic, err := ecdsa.ParseUncompressedPublicKey(elliptic.P256(), publicBytes) + if err != nil { + t.Fatal(err) + } + if !parsedPublic.Equal(&privateKey.PublicKey) { + t.Fatal("parsed public key differs from generated key") + } } diff --git a/test/std/crypto/hpke/hpke_test.go b/test/std/crypto/hpke/hpke_test.go new file mode 100644 index 0000000000..ad9c76fc15 --- /dev/null +++ b/test/std/crypto/hpke/hpke_test.go @@ -0,0 +1,111 @@ +//go:build go1.26 + +package hpke_test + +import ( + "bytes" + "crypto/ecdh" + "crypto/hpke" + "crypto/rand" + "testing" +) + +func TestSealOpen(t *testing.T) { + private, err := ecdh.P256().GenerateKey(rand.Reader) + if err != nil { + t.Fatal(err) + } + publicKey, err := hpke.NewDHKEMPublicKey(private.PublicKey()) + if err != nil { + t.Fatal(err) + } + privateKey, err := hpke.NewDHKEMPrivateKey(private) + if err != nil { + t.Fatal(err) + } + info := []byte("llgo hpke") + plaintext := []byte("standard library") + ciphertext, err := hpke.Seal(publicKey, hpke.HKDFSHA256(), hpke.AES128GCM(), info, plaintext) + if err != nil { + t.Fatal(err) + } + got, err := hpke.Open(privateKey, hpke.HKDFSHA256(), hpke.AES128GCM(), info, ciphertext) + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(got, plaintext) { + t.Fatalf("Open = %q, want %q", got, plaintext) + } + if _, err := hpke.Open(privateKey, hpke.HKDFSHA256(), hpke.AES128GCM(), []byte("wrong info"), ciphertext); err == nil { + t.Fatal("Open with different info unexpectedly succeeded") + } +} + +func TestSenderRecipient(t *testing.T) { + private, err := ecdh.P256().GenerateKey(rand.Reader) + if err != nil { + t.Fatal(err) + } + publicKey, err := hpke.NewDHKEMPublicKey(private.PublicKey()) + if err != nil { + t.Fatal(err) + } + privateKey, err := hpke.NewDHKEMPrivateKey(private) + if err != nil { + t.Fatal(err) + } + encapsulation, sender, err := hpke.NewSender(publicKey, hpke.HKDFSHA256(), hpke.AES128GCM(), []byte("info")) + if err != nil { + t.Fatal(err) + } + recipient, err := hpke.NewRecipient(encapsulation, privateKey, hpke.HKDFSHA256(), hpke.AES128GCM(), []byte("info")) + if err != nil { + t.Fatal(err) + } + ciphertext, err := sender.Seal([]byte("aad"), []byte("message")) + if err != nil { + t.Fatal(err) + } + plaintext, err := recipient.Open([]byte("aad"), ciphertext) + if err != nil || string(plaintext) != "message" { + t.Fatalf("Open = (%q, %v)", plaintext, err) + } + senderExport, err := sender.Export("context", 32) + if err != nil { + t.Fatal(err) + } + recipientExport, err := recipient.Export("context", 32) + if err != nil || !bytes.Equal(senderExport, recipientExport) { + t.Fatal("sender and recipient exports differ") + } +} + +func TestComponentInterfaces(t *testing.T) { + var kem hpke.KEM = hpke.DHKEM(ecdh.P256()) + if kem.ID() == 0 { + t.Fatal("DHKEM returned a zero KEM identifier") + } + private, err := kem.GenerateKey() + if err != nil { + t.Fatal(err) + } + var privateKey hpke.PrivateKey = private + privateBytes, err := privateKey.Bytes() + if err != nil || len(privateBytes) == 0 { + t.Fatalf("private key serialization = %d bytes, %v", len(privateBytes), err) + } + if privateKey.KEM().ID() != kem.ID() { + t.Fatal("private key reports a different KEM") + } + + var publicKey hpke.PublicKey = privateKey.PublicKey() + if len(publicKey.Bytes()) == 0 || publicKey.KEM().ID() != kem.ID() { + t.Fatal("public key serialization or KEM identity is invalid") + } + + var kdf hpke.KDF = hpke.HKDFSHA256() + var aead hpke.AEAD = hpke.AES128GCM() + if kdf.ID() == 0 || aead.ID() == 0 { + t.Fatalf("invalid ciphersuite identifiers: KDF=%d AEAD=%d", kdf.ID(), aead.ID()) + } +} diff --git a/test/std/crypto/mlkem/mlkemtest/mlkemtest_test.go b/test/std/crypto/mlkem/mlkemtest/mlkemtest_test.go new file mode 100644 index 0000000000..915c9e521a --- /dev/null +++ b/test/std/crypto/mlkem/mlkemtest/mlkemtest_test.go @@ -0,0 +1,60 @@ +//go:build go1.26 + +package mlkemtest_test + +import ( + "bytes" + "crypto/mlkem" + "crypto/mlkem/mlkemtest" + "testing" +) + +func TestDeterministicEncapsulation(t *testing.T) { + key, err := mlkem.NewDecapsulationKey768(make([]byte, mlkem.SeedSize)) + if err != nil { + t.Fatal(err) + } + shared, ciphertext, err := mlkemtest.Encapsulate768(key.EncapsulationKey(), make([]byte, 32)) + if err != nil { + t.Fatal(err) + } + decapsulated, err := key.Decapsulate(ciphertext) + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(shared, decapsulated) { + t.Fatal("encapsulated and decapsulated keys differ") + } + repeatedShared, repeatedCiphertext, err := mlkemtest.Encapsulate768(key.EncapsulationKey(), make([]byte, 32)) + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(repeatedShared, shared) || !bytes.Equal(repeatedCiphertext, ciphertext) { + t.Fatal("Encapsulate768 is not deterministic for fixed randomness") + } +} + +func TestDeterministicEncapsulation1024(t *testing.T) { + key, err := mlkem.NewDecapsulationKey1024(make([]byte, mlkem.SeedSize)) + if err != nil { + t.Fatal(err) + } + shared, ciphertext, err := mlkemtest.Encapsulate1024(key.EncapsulationKey(), make([]byte, 32)) + if err != nil { + t.Fatal(err) + } + decapsulated, err := key.Decapsulate(ciphertext) + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(shared, decapsulated) { + t.Fatal("encapsulated and decapsulated keys differ") + } + repeatedShared, repeatedCiphertext, err := mlkemtest.Encapsulate1024(key.EncapsulationKey(), make([]byte, 32)) + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(repeatedShared, shared) || !bytes.Equal(repeatedCiphertext, ciphertext) { + t.Fatal("Encapsulate1024 is not deterministic for fixed randomness") + } +} diff --git a/test/std/crypto/tls/tls_test.go b/test/std/crypto/tls/tls_test.go index 8a980d6340..7bdfc9d7a6 100644 --- a/test/std/crypto/tls/tls_test.go +++ b/test/std/crypto/tls/tls_test.go @@ -38,7 +38,21 @@ func generateTestCert(t *testing.T) ([]byte, []byte) { } certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER}) - keyDER, _ := x509.MarshalPKCS8PrivateKey(priv) + keyDER, err := x509.MarshalPKCS8PrivateKey(priv) + if err != nil { + t.Fatalf("Failed to marshal private key: %v", err) + } + parsedKey, err := x509.ParsePKCS8PrivateKey(keyDER) + if err != nil { + t.Fatalf("Failed to parse private key: %v", err) + } + parsedRSA, ok := parsedKey.(*rsa.PrivateKey) + if !ok { + t.Fatalf("Parsed private key has type %T, want *rsa.PrivateKey", parsedKey) + } + if !parsedRSA.Equal(priv) { + t.Fatal("Parsed private key differs from generated key") + } keyPEM := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: keyDER}) return certPEM, keyPEM diff --git a/test/std/crypto/x509/go126_symbols_test.go b/test/std/crypto/x509/go126_symbols_test.go index a5fdd30131..a74721c499 100644 --- a/test/std/crypto/x509/go126_symbols_test.go +++ b/test/std/crypto/x509/go126_symbols_test.go @@ -4,6 +4,7 @@ package x509_test import ( "crypto/x509" + "encoding/asn1" "testing" ) @@ -19,3 +20,16 @@ func TestUsageFormatting(t *testing.T) { t.Fatalf("KeyUsageDigitalSignature.String = %q", got) } } + +func TestOIDFromASN1OID(t *testing.T) { + oid, err := x509.OIDFromASN1OID(asn1.ObjectIdentifier{1, 2, 840, 113549}) + if err != nil { + t.Fatal(err) + } + if got := oid.String(); got != "1.2.840.113549" { + t.Fatalf("OID.String = %q", got) + } + if _, err := x509.OIDFromASN1OID(asn1.ObjectIdentifier{1, 40}); err == nil { + t.Fatal("OIDFromASN1OID accepted an invalid second arc") + } +} diff --git a/test/std/go/types/go126_symbols_test.go b/test/std/go/types/go126_symbols_test.go index ce74b4b703..79e775db3b 100644 --- a/test/std/go/types/go126_symbols_test.go +++ b/test/std/go/types/go126_symbols_test.go @@ -21,3 +21,21 @@ func TestVarKind(t *testing.T) { t.Fatalf("LocalVar.String = %q", got) } } + +func TestLookupSelection(t *testing.T) { + field := types.NewField(token.NoPos, nil, "Value", types.Typ[types.String], false) + typ := types.NewStruct([]*types.Var{field}, nil) + selection, ok := types.LookupSelection(typ, true, nil, "Value") + if !ok { + t.Fatal("LookupSelection did not find Value") + } + if selection.Obj() != field || selection.Indirect() { + t.Fatalf("LookupSelection = (%v, indirect %v)", selection.Obj(), selection.Indirect()) + } + if index := selection.Index(); len(index) != 1 || index[0] != 0 { + t.Fatalf("Selection.Index = %v", index) + } + if _, ok := types.LookupSelection(typ, true, nil, "Missing"); ok { + t.Fatal("LookupSelection unexpectedly found Missing") + } +} diff --git a/test/std/io/fs/go126_symbols_test.go b/test/std/io/fs/go126_symbols_test.go index 4588286265..abaaacbfa3 100644 --- a/test/std/io/fs/go126_symbols_test.go +++ b/test/std/io/fs/go126_symbols_test.go @@ -18,11 +18,18 @@ func TestReadLinkFS(t *testing.T) { if err != nil || got != "target.txt" { t.Fatalf("ReadLink = %q, %v; want %q, nil", got, err, "target.txt") } - info, err := readLinkFS.Lstat("link.txt") + info, err := fs.Lstat(filesystem, "link.txt") if err != nil { t.Fatal(err) } if info.Mode()&fs.ModeSymlink == 0 { t.Fatalf("Lstat mode = %v, want symlink", info.Mode()) } + targetInfo, err := fs.Stat(filesystem, "link.txt") + if err != nil { + t.Fatal(err) + } + if targetInfo.Mode()&fs.ModeSymlink != 0 || targetInfo.Size() != int64(len("target")) { + t.Fatalf("Stat followed link to mode %v, size %d", targetInfo.Mode(), targetInfo.Size()) + } } diff --git a/test/std/log/slog/go126_symbols_test.go b/test/std/log/slog/go126_symbols_test.go index 53030b22bc..abfdbdadbd 100644 --- a/test/std/log/slog/go126_symbols_test.go +++ b/test/std/log/slog/go126_symbols_test.go @@ -5,6 +5,7 @@ package slog_test import ( "bytes" "context" + "encoding/json" "log/slog" "runtime" "strings" @@ -33,21 +34,46 @@ func TestMultiHandlerAndRecordSource(t *testing.T) { } withGroup := multi.WithGroup("details") record := slog.NewRecord(time.Unix(1, 0), slog.LevelInfo, "grouped", 0) - record.Add("files", 2) + record.AddAttrs(slog.GroupAttrs("build", slog.Int("files", 2))) if err := withGroup.Handle(ctx, record); err != nil { t.Fatal(err) } - for name, output := range map[string]string{"text": first.String(), "json": second.String()} { - if !strings.Contains(output, "direct") || !strings.Contains(output, "compiled") || !strings.Contains(output, "compiler") || !strings.Contains(output, "details") || !strings.Contains(output, "files") { - t.Fatalf("%s handler output is incomplete: %q", name, output) + textOutput := first.String() + for _, want := range []string{"msg=direct", "msg=compiled component=compiler", "msg=grouped details.build.files=2"} { + if !strings.Contains(textOutput, want) { + t.Fatalf("text handler output %q does not contain %q", textOutput, want) } } + jsonLines := bytes.Split(bytes.TrimSpace(second.Bytes()), []byte("\n")) + if len(jsonLines) != 3 { + t.Fatalf("JSON handler wrote %d records, want 3: %q", len(jsonLines), second.String()) + } + records := make([]map[string]any, len(jsonLines)) + for i, line := range jsonLines { + if err := json.Unmarshal(line, &records[i]); err != nil { + t.Fatalf("JSON record %d is invalid: %v: %q", i, err, line) + } + } + if records[0]["msg"] != "direct" { + t.Fatalf("first JSON record = %#v", records[0]) + } + if records[1]["msg"] != "compiled" || records[1]["component"] != "compiler" { + t.Fatalf("second JSON record = %#v", records[1]) + } + details, ok := records[2]["details"].(map[string]any) + if !ok { + t.Fatalf("third JSON record has no details group: %#v", records[2]) + } + build, ok := details["build"].(map[string]any) + if !ok || build["files"] != float64(2) { + t.Fatalf("third JSON record has wrong build group: %#v", records[2]) + } pcs := make([]uintptr, 1) runtime.Callers(1, pcs) sourceRecord := slog.NewRecord(time.Time{}, slog.LevelInfo, "source", pcs[0]) source := sourceRecord.Source() - if source == nil || source.Function == "" || source.Line == 0 { + if source == nil || !strings.Contains(source.Function, "TestMultiHandlerAndRecordSource") || source.Line == 0 { t.Fatalf("Record.Source = %#v", source) } } diff --git a/test/std/plugin/plugin_test.go b/test/std/plugin/plugin_test.go new file mode 100644 index 0000000000..1785b46a6e --- /dev/null +++ b/test/std/plugin/plugin_test.go @@ -0,0 +1,37 @@ +//go:build darwin || linux + +package plugin_test + +import ( + "path/filepath" + "plugin" + "reflect" + "strings" + "testing" +) + +func TestOpenMissingPlugin(t *testing.T) { + path := filepath.Join(t.TempDir(), "missing.so") + if _, err := plugin.Open(path); err == nil { + t.Fatal("Open of a missing plugin succeeded") + } else if !strings.Contains(err.Error(), "missing") { + t.Fatalf("Open error %q does not identify the missing plugin", err) + } +} + +func TestPluginAPISurface(t *testing.T) { + pluginType := reflect.TypeOf((*plugin.Plugin)(nil)).Elem() + if pluginType.Name() != "Plugin" || pluginType.PkgPath() != "plugin" { + t.Fatalf("unexpected Plugin type: %v from %q", pluginType, pluginType.PkgPath()) + } + + lookup := (*plugin.Plugin).Lookup + if reflect.ValueOf(lookup).Pointer() == 0 { + t.Fatal("Plugin.Lookup has no callable entry point") + } + + var symbol plugin.Symbol = "llgo" + if got, ok := symbol.(string); !ok || got != "llgo" { + t.Fatalf("Symbol did not preserve its dynamic value: %#v", symbol) + } +} diff --git a/test/std/reflect/type_test.go b/test/std/reflect/type_test.go index 6d3213931b..a8f12c3bfa 100644 --- a/test/std/reflect/type_test.go +++ b/test/std/reflect/type_test.go @@ -1,6 +1,7 @@ package reflect_test import ( + "math/big" "reflect" "testing" ) @@ -44,3 +45,23 @@ func TestTypeKind(t *testing.T) { } } } + +func TestTypeForMatchesStructFieldType(t *testing.T) { + type holder struct { + Value *big.Int + } + + direct := reflect.TypeFor[*big.Int]() + repeated := reflect.TypeFor[*big.Int]() + field := reflect.TypeFor[holder]().Field(0).Type + if direct != repeated { + t.Fatalf("repeated TypeFor returned distinct types: %v and %v", direct, repeated) + } + if direct != field { + t.Fatalf("TypeFor type %v differs from struct field type %v", direct, field) + } + value := reflect.ValueOf(&holder{}).Elem().Field(0).Addr().Interface() + if _, ok := value.(**big.Int); !ok { + t.Fatalf("reflected field address has type %T, want **big.Int", value) + } +} diff --git a/test/std/runtime/cgo/cgo_test.go b/test/std/runtime/cgo/cgo_test.go new file mode 100644 index 0000000000..8b41c5259d --- /dev/null +++ b/test/std/runtime/cgo/cgo_test.go @@ -0,0 +1,35 @@ +package cgo_test + +import ( + "reflect" + "runtime/cgo" + "testing" +) + +func TestHandleLifecycle(t *testing.T) { + type payload struct{ name string } + want := &payload{name: "llgo"} + handle := cgo.NewHandle(want) + if got, ok := handle.Value().(*payload); !ok || got != want { + t.Fatalf("Value = %#v, want the original payload", got) + } + handle.Delete() + if panicValue := panicFrom(func() { handle.Value() }); panicValue == nil { + t.Fatal("Value on a deleted handle did not panic") + } +} + +func TestIncompleteTypeIdentity(t *testing.T) { + typ := reflect.TypeOf(cgo.Incomplete{}) + if typ.Name() != "Incomplete" || typ.PkgPath() != "runtime/cgo" { + t.Fatalf("unexpected incomplete C type marker: %v from %q", typ, typ.PkgPath()) + } +} + +func panicFrom(f func()) (value any) { + defer func() { + value = recover() + }() + f() + return nil +} diff --git a/test/std/runtime/coverage/coverage_test.go b/test/std/runtime/coverage/coverage_test.go new file mode 100644 index 0000000000..36986edb94 --- /dev/null +++ b/test/std/runtime/coverage/coverage_test.go @@ -0,0 +1,61 @@ +package coverage_test + +import ( + "bytes" + "os" + "runtime/coverage" + "strings" + "testing" +) + +func TestRuntimeCoverageWritersAgree(t *testing.T) { + var meta bytes.Buffer + metaErr := coverage.WriteMeta(&meta) + metaDir := t.TempDir() + metaDirErr := coverage.WriteMetaDir(metaDir) + checkWriteResults(t, "metadata", "covmeta.", meta.Len(), metaErr, metaDir, metaDirErr) + + var counters bytes.Buffer + counterErr := coverage.WriteCounters(&counters) + counterDir := t.TempDir() + counterDirErr := coverage.WriteCountersDir(counterDir) + checkWriteResults(t, "counters", "covcounters.", counters.Len(), counterErr, counterDir, counterDirErr) + + clearErr := coverage.ClearCounters() + if counterErr != nil && clearErr == nil { + t.Fatalf("ClearCounters succeeded although WriteCounters is unavailable: %v", counterErr) + } +} + +func checkWriteResults(t *testing.T, name, prefix string, directBytes int, directErr error, dir string, dirErr error) { + t.Helper() + if (directErr == nil) != (dirErr == nil) { + t.Fatalf("%s writer availability differs: direct=%v, directory=%v", name, directErr, dirErr) + } + entries, err := os.ReadDir(dir) + if err != nil { + t.Fatal(err) + } + if directErr != nil { + if directBytes != 0 || len(entries) != 0 { + t.Fatalf("unavailable %s writers produced %d bytes and %d files", name, directBytes, len(entries)) + } + return + } + if directBytes == 0 { + t.Fatalf("Write%s succeeded without writing data", name) + } + for _, entry := range entries { + if strings.HasPrefix(entry.Name(), prefix) { + info, err := entry.Info() + if err != nil { + t.Fatal(err) + } + if info.Size() == 0 { + t.Fatalf("%s file %q is empty", name, entry.Name()) + } + return + } + } + t.Fatalf("Write%sDir produced no %q file: %v", name, prefix, entries) +} diff --git a/test/std/runtime/debug/debug_test.go b/test/std/runtime/debug/debug_test.go new file mode 100644 index 0000000000..3daa48d98f --- /dev/null +++ b/test/std/runtime/debug/debug_test.go @@ -0,0 +1,154 @@ +package debug_test + +import ( + "io" + "os" + "reflect" + "runtime" + "runtime/debug" + "strings" + "testing" +) + +func TestStackReportsCaller(t *testing.T) { + stack := string(debug.Stack()) + if !strings.Contains(stack, "TestStackReportsCaller") { + t.Fatalf("Stack does not contain the caller: %q", stack) + } +} + +func TestPrintStackReportsCaller(t *testing.T) { + r, w, err := os.Pipe() + if err != nil { + t.Fatal(err) + } + oldStderr := os.Stderr + os.Stderr = w + debug.PrintStack() + os.Stderr = oldStderr + if err := w.Close(); err != nil { + t.Fatal(err) + } + stack, err := io.ReadAll(r) + if err != nil { + t.Fatal(err) + } + if err := r.Close(); err != nil { + t.Fatal(err) + } + if !strings.Contains(string(stack), "TestPrintStackReportsCaller") { + t.Fatalf("PrintStack does not contain the caller: %q", stack) + } +} + +func TestRuntimeSettings(t *testing.T) { + var stats debug.GCStats + runtime.GC() + debug.ReadGCStats(&stats) + if stats.NumGC < 0 || stats.PauseTotal < 0 { + t.Fatalf("invalid GC statistics: %#v", stats) + } + + previousGC := debug.SetGCPercent(100) + if got := debug.SetGCPercent(previousGC); got != 100 { + t.Fatalf("SetGCPercent restore returned %d, want 100", got) + } + previousLimit := debug.SetMemoryLimit(1 << 30) + if got := debug.SetMemoryLimit(previousLimit); got != 1<<30 { + t.Fatalf("SetMemoryLimit restore returned %d, want %d", got, int64(1<<30)) + } + previousStack := debug.SetMaxStack(1 << 30) + if got := debug.SetMaxStack(previousStack); got != 1<<30 { + t.Fatalf("SetMaxStack restore returned %d, want %d", got, 1<<30) + } + previousThreads := debug.SetMaxThreads(10001) + if got := debug.SetMaxThreads(previousThreads); got != 10001 { + t.Fatalf("SetMaxThreads restore returned %d, want 10001", got) + } + previousPanicOnFault := debug.SetPanicOnFault(true) + if got := debug.SetPanicOnFault(previousPanicOnFault); !got { + t.Fatal("SetPanicOnFault did not report the previous enabled state") + } + + debug.SetTraceback("single") + debug.FreeOSMemory() +} + +func TestPanicOnFaultStateIsGoroutineLocal(t *testing.T) { + previous := debug.SetPanicOnFault(true) + defer debug.SetPanicOnFault(previous) + + result := make(chan [2]bool, 1) + go func() { + first := debug.SetPanicOnFault(true) + second := debug.SetPanicOnFault(false) + result <- [2]bool{first, second} + }() + if got := <-result; got != [2]bool{false, true} { + t.Fatalf("new goroutine SetPanicOnFault states = %v, want [false true]", got) + } + if got := debug.SetPanicOnFault(previous); !got { + t.Fatal("child goroutine changed the parent SetPanicOnFault state") + } +} + +func TestCrashAndHeapDumpOutputs(t *testing.T) { + crashFile, err := os.CreateTemp(t.TempDir(), "crash-*.log") + if err != nil { + t.Fatal(err) + } + if err := debug.SetCrashOutput(crashFile, debug.CrashOptions{}); err != nil { + t.Fatal(err) + } + if err := debug.SetCrashOutput(nil, debug.CrashOptions{}); err != nil { + t.Fatal(err) + } + if err := crashFile.Close(); err != nil { + t.Fatal(err) + } + + heapFile, err := os.CreateTemp(t.TempDir(), "heap-*.dump") + if err != nil { + t.Fatal(err) + } + debug.WriteHeapDump(heapFile.Fd()) + if _, err := heapFile.WriteString("fd-remains-open"); err != nil { + t.Fatalf("heap dump closed its output descriptor: %v", err) + } + if err := heapFile.Close(); err != nil { + t.Fatal(err) + } +} + +func TestBuildInfoParsingAndReading(t *testing.T) { + const encoded = "go\t1.26\npath\texample.com/app\nmod\texample.com/app\tv1.2.3\th1:main\ndep\texample.com/dep\tv0.4.5\th1:dep\nbuild\t-compiler=gc\n" + info, err := debug.ParseBuildInfo(encoded) + if err != nil { + t.Fatal(err) + } + if info.Path != "example.com/app" || info.Main.Path != "example.com/app" || info.Main.Version != "v1.2.3" { + t.Fatalf("parsed main module = %#v, path %q", info.Main, info.Path) + } + if len(info.Deps) != 1 || info.Deps[0].Path != "example.com/dep" || info.Deps[0].Version != "v0.4.5" { + t.Fatalf("parsed dependencies = %#v", info.Deps) + } + if len(info.Settings) != 1 || info.Settings[0].Key != "-compiler" || info.Settings[0].Value != "gc" { + t.Fatalf("parsed settings = %#v", info.Settings) + } + roundTrip, err := debug.ParseBuildInfo(info.String()) + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(roundTrip, info) { + t.Fatalf("build info changed after round trip:\nfirst: %#v\nsecond: %#v", info, roundTrip) + } + + if info, ok := debug.ReadBuildInfo(); ok { + if !strings.HasPrefix(info.GoVersion, "go") { + t.Fatalf("GoVersion = %q", info.GoVersion) + } + if info.String() == "" { + t.Fatal("BuildInfo.String returned an empty string") + } + } +} diff --git a/test/std/runtime/race/race_test.go b/test/std/runtime/race/race_test.go new file mode 100644 index 0000000000..6c318875ed --- /dev/null +++ b/test/std/runtime/race/race_test.go @@ -0,0 +1,10 @@ +package race_test + +import ( + _ "runtime/race" + "testing" +) + +func TestPackageImports(t *testing.T) { + t.Log("runtime/race intentionally has no public API") +} diff --git a/test/std/runtime/runtime_operations_test.go b/test/std/runtime/runtime_operations_test.go new file mode 100644 index 0000000000..641c5a2f64 --- /dev/null +++ b/test/std/runtime/runtime_operations_test.go @@ -0,0 +1,60 @@ +package runtime_test + +import ( + "runtime" + "testing" +) + +func TestGoexitRunsDeferredCalls(t *testing.T) { + done := make(chan struct{}) + go func() { + defer close(done) + runtime.Goexit() + }() + <-done +} + +func TestRuntimeFunctionInformation(t *testing.T) { + pc, _, _, ok := runtime.Caller(0) + if !ok { + t.Fatal("Caller failed") + } + fn := runtime.FuncForPC(pc) + if fn == nil || fn.Entry() == 0 || fn.Name() == "" { + t.Fatal("FuncForPC returned incomplete information") + } + if file, line := fn.FileLine(pc); file == "" || line == 0 { + t.Fatal("FileLine returned incomplete information") + } +} + +func TestRuntimeRecordMethods(t *testing.T) { + mem := runtime.MemProfileRecord{ + AllocBytes: 1024, + FreeBytes: 256, + AllocObjects: 8, + FreeObjects: 3, + Stack0: [32]uintptr{11, 22}, + } + if got := mem.InUseBytes(); got != 768 { + t.Fatalf("InUseBytes = %d, want 768", got) + } + if got := mem.InUseObjects(); got != 5 { + t.Fatalf("InUseObjects = %d, want 5", got) + } + if stack := mem.Stack(); len(stack) != 2 || stack[0] != 11 || stack[1] != 22 { + t.Fatalf("MemProfileRecord.Stack = %v", stack) + } + + record := runtime.StackRecord{Stack0: [32]uintptr{33, 44}} + if stack := record.Stack(); len(stack) != 2 || stack[0] != 33 || stack[1] != 44 { + t.Fatalf("StackRecord.Stack = %v", stack) + } +} + +func TestRuntimeErrorMethods(t *testing.T) { + panicNil := new(runtime.PanicNilError) + if got := panicNil.Error(); got != "panic called with nil argument" { + t.Fatalf("PanicNilError.Error = %q", got) + } +} diff --git a/test/std/runtime/runtime_test.go b/test/std/runtime/runtime_test.go new file mode 100644 index 0000000000..8cdb81872c --- /dev/null +++ b/test/std/runtime/runtime_test.go @@ -0,0 +1,42 @@ +package runtime_test + +import ( + "runtime" + "strings" + "testing" +) + +func TestRuntimeInformationAndCallers(t *testing.T) { + if runtime.GOOS == "" || runtime.GOARCH == "" || runtime.Compiler == "" { + t.Fatal("runtime target information is empty") + } + if runtime.NumCPU() < 1 || runtime.GOMAXPROCS(0) < 1 { + t.Fatal("runtime CPU information is invalid") + } + if !strings.HasPrefix(runtime.Version(), "go") { + t.Fatalf("Version = %q", runtime.Version()) + } + pcs := make([]uintptr, 16) + n := runtime.Callers(0, pcs) + if n == 0 { + t.Fatal("Callers returned no frames") + } + frames := runtime.CallersFrames(pcs[:n]) + foundTest := false + for { + frame, more := frames.Next() + if strings.Contains(frame.Function, "TestRuntimeInformationAndCallers") { + if frame.File == "" || frame.Line == 0 { + t.Fatalf("test frame has incomplete source information: %#v", frame) + } + foundTest = true + break + } + if !more { + break + } + } + if !foundTest { + t.Fatal("CallersFrames did not report TestRuntimeInformationAndCallers") + } +} diff --git a/test/std/testing/cryptotest/cryptotest_test.go b/test/std/testing/cryptotest/cryptotest_test.go new file mode 100644 index 0000000000..475bcbdc0d --- /dev/null +++ b/test/std/testing/cryptotest/cryptotest_test.go @@ -0,0 +1,26 @@ +//go:build go1.26 + +package cryptotest_test + +import ( + "bytes" + "crypto/rand" + "testing" + "testing/cryptotest" +) + +func TestSetGlobalRandom(t *testing.T) { + cryptotest.SetGlobalRandom(t, 1) + first := make([]byte, 32) + if _, err := rand.Read(first); err != nil { + t.Fatal(err) + } + cryptotest.SetGlobalRandom(t, 1) + second := make([]byte, 32) + if _, err := rand.Read(second); err != nil { + t.Fatal(err) + } + if !bytes.Equal(first, second) { + t.Fatal("resetting the seed did not reproduce the random stream") + } +} diff --git a/test/std/testing/synctest/synctest_test.go b/test/std/testing/synctest/synctest_test.go new file mode 100644 index 0000000000..f5de1ee28b --- /dev/null +++ b/test/std/testing/synctest/synctest_test.go @@ -0,0 +1,30 @@ +//go:build go1.26 + +package synctest_test + +import ( + "testing" + "testing/synctest" +) + +func TestCallbackExecution(t *testing.T) { + calls := 0 + cleanupRan := false + synctest.Test(t, func(bubbleT *testing.T) { + calls++ + if bubbleT.Name() != t.Name() { + bubbleT.Fatalf("callback test name = %q, want %q", bubbleT.Name(), t.Name()) + } + bubbleT.Cleanup(func() { cleanupRan = true }) + synctest.Wait() + if calls != 1 { + bubbleT.Fatalf("Wait resumed with callback count %d", calls) + } + }) + if calls != 1 { + t.Fatalf("synctest callback ran %d times, want 1", calls) + } + if !cleanupRan { + t.Fatal("synctest callback cleanup did not run before Test returned") + } +}