Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 12 additions & 7 deletions .github/actions/setup-deps/ubuntu_apt.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,25 @@

def configure(apt_dir, architecture):
if architecture in ("amd64", "i386"):
archive = "https://archive.ubuntu.com/ubuntu/"
security = "https://security.ubuntu.com/ubuntu/"
azure = "https://azure.archive.ubuntu.com/ubuntu/"
archive = (azure, "https://archive.ubuntu.com/ubuntu/")
security = (azure, "https://security.ubuntu.com/ubuntu/")
else:
archive = security = "https://ports.ubuntu.com/ubuntu-ports/"
archive = security = ("https://ports.ubuntu.com/ubuntu-ports/",)

# The runner's mirror+file source selected a malformed https:/ URL and
# then stalled on another mirror for the entire 45-minute Wasm job.
# Preserve source suites, components and signing keys; replace only the
# two Ubuntu mirror lists when the runner already uses them.
for name, url in (("apt-mirrors.txt", archive),
("apt-mirrors-security.txt", security)):
# two Ubuntu mirror lists when the runner already uses them. APT tries
# another mirror on download failures; explicit priorities avoid randomly
# selecting an unavailable archive endpoint before the Azure mirror.
for name, mirrors in (("apt-mirrors.txt", archive),
("apt-mirrors-security.txt", security)):
path = apt_dir / name
if path.exists():
path.write_text(url + "\n")
path.write_text("".join(
f"{url}\tpriority:{priority}\n"
for priority, url in enumerate(mirrors, 1)))

(apt_dir / "apt.conf.d" / "80-llgo-downloads").write_text('''\
Acquire::ForceIPv4 "true";
Expand Down
18 changes: 12 additions & 6 deletions .github/actions/setup-deps/ubuntu_apt_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,16 @@
class UbuntuAPTTest(unittest.TestCase):
def test_runner_mirror_lists(self):
for arch, archive, security in (
("amd64", "https://archive.ubuntu.com/ubuntu/",
"https://security.ubuntu.com/ubuntu/"),
("arm64", "https://ports.ubuntu.com/ubuntu-ports/",
"https://ports.ubuntu.com/ubuntu-ports/"),
("amd64", "https://azure.archive.ubuntu.com/ubuntu/\tpriority:1\n"
"https://archive.ubuntu.com/ubuntu/\tpriority:2\n",
"https://azure.archive.ubuntu.com/ubuntu/\tpriority:1\n"
"https://security.ubuntu.com/ubuntu/\tpriority:2\n"),
("i386", "https://azure.archive.ubuntu.com/ubuntu/\tpriority:1\n"
"https://archive.ubuntu.com/ubuntu/\tpriority:2\n",
"https://azure.archive.ubuntu.com/ubuntu/\tpriority:1\n"
"https://security.ubuntu.com/ubuntu/\tpriority:2\n"),
("arm64", "https://ports.ubuntu.com/ubuntu-ports/\tpriority:1\n",
"https://ports.ubuntu.com/ubuntu-ports/\tpriority:1\n"),
):
with self.subTest(arch=arch), tempfile.TemporaryDirectory() as tmp:
root = pathlib.Path(tmp)
Expand All @@ -29,8 +35,8 @@ def test_runner_mirror_lists(self):
third_party.write_text("https://apt.llvm.org/noble/\n")
configure(root, arch)
configure(root, arch)
self.assertEqual((root / "apt-mirrors.txt").read_text(), archive + "\n")
self.assertEqual((root / "apt-mirrors-security.txt").read_text(), security + "\n")
self.assertEqual((root / "apt-mirrors.txt").read_text(), archive)
self.assertEqual((root / "apt-mirrors-security.txt").read_text(), security)
self.assertEqual(sources.read_text(), source_text)
self.assertEqual(third_party.read_text(), "https://apt.llvm.org/noble/\n")

Expand Down
47 changes: 47 additions & 0 deletions internal/build/native_tail_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package build

import (
stdcontext "context"
"os/exec"
"path/filepath"
"runtime"
"strings"
"testing"
"time"

"github.com/xgo-dev/llgo/internal/lto"
)

func TestNativeTailTransferIntegration(t *testing.T) {
if runtime.GOARCH != "arm64" || (runtime.GOOS != "linux" && runtime.GOOS != "darwin") {
t.Skip("native ARM64 bridge")
}
if testing.Short() {
t.Skip("builds and runs native stack-switching assembly")
}
root, err := filepath.Abs("../..")
if err != nil {
t.Fatal(err)
}
t.Setenv("LLGO_ROOT", root)
dir := filepath.Join(root, "internal/build/testdata/native_tail")
for _, tc := range []struct {
mode lto.Mode
tags string
}{{lto.Off, ""}, {lto.Full, ""}, {lto.Off, "nogc"}} {
t.Run(tc.mode.String()+"/"+tc.tags, func(t *testing.T) {
conf := NewDefaultConf(ModeBuild)
conf.LTO, conf.Tags = tc.mode, tc.tags
conf.OutFile = filepath.Join(t.TempDir(), "native-tail")
if _, err := Build(Invocation{Args: []string{"."}, Config: conf, Dir: dir}); err != nil {
t.Fatal(err)
}
ctx, cancel := stdcontext.WithTimeout(t.Context(), 30*time.Second)
defer cancel()
out, err := exec.CommandContext(ctx, conf.OutFile).CombinedOutput()
if err != nil || strings.TrimSpace(string(out)) != "entry/resume/stack/registers/gc: OK" {
t.Fatalf("native transfer: %v\n%s", err, out)
}
})
}
}
12 changes: 9 additions & 3 deletions internal/build/plan9asm.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"
"os"
"path/filepath"
"slices"
"strings"

"github.com/xgo-dev/llgo/internal/packages"
Expand All @@ -14,8 +15,13 @@ import (
extplan9asm "github.com/xgo-dev/plan9asm"
)

func plan9asmTranslateOptions(conf *Config) llplan9asm.TranslateOptions {
func plan9asmTranslateOptions(conf *Config, targetTags []string) llplan9asm.TranslateOptions {
opt := llplan9asm.TranslateOptions{GOARM: conf.GOARM, GOAMD64: conf.GOAMD64}
tags := splitSourcePatchBuildTags(conf.Tags)
opt.NativeBDWGC = conf.Goarch == "arm64" &&
(conf.Goos == "darwin" || conf.Goos == "linux") &&
!slices.Contains(tags, "nogc") && !slices.Contains(tags, "baremetal") &&
!slices.Contains(targetTags, "nogc") && !slices.Contains(targetTags, "baremetal")
if conf.Goarch == "386" && conf.GO386 == "softfloat" {
opt.X87Mode = extplan9asm.X87Software
}
Expand Down Expand Up @@ -88,7 +94,7 @@ func compileSelectedPkgSFiles(ctx *context, aPkg *aPackage, pkg *packages.Packag
return nil, fmt.Errorf("%s: native assembly %s: %w", pkg.PkgPath, sfile, err)
}
} else {
tr, err := llplan9asm.TranslateSourceModuleForPkgWithOptions(pkg, sfile, src, ctx.buildConf.Goos, ctx.buildConf.Goarch, plan9asmTranslateOptions(ctx.buildConf))
tr, err := llplan9asm.TranslateSourceModuleForPkgWithOptions(pkg, sfile, src, ctx.buildConf.Goos, ctx.buildConf.Goarch, plan9asmTranslateOptions(ctx.buildConf, ctx.crossCompile.BuildTags))
if err != nil {
// Some stdlib .s files are comment-only placeholders (e.g. internal/cpu/cpu.s).
// Skip those silently.
Expand Down Expand Up @@ -319,7 +325,7 @@ func plan9asmSigsForPkg(ctx *context, pkgPath string) (map[string]struct{}, erro
if err != nil {
return nil, fmt.Errorf("%s: read %s: %w", pkg.PkgPath, sfile, err)
}
tr, err := llplan9asm.TranslateSourceForPkgWithOptions(pkg, sfile, src, ctx.buildConf.Goos, ctx.buildConf.Goarch, plan9asmTranslateOptions(ctx.buildConf))
tr, err := llplan9asm.TranslateSourceForPkgWithOptions(pkg, sfile, src, ctx.buildConf.Goos, ctx.buildConf.Goarch, plan9asmTranslateOptions(ctx.buildConf, ctx.crossCompile.BuildTags))
if err != nil {
if strings.Contains(err.Error(), "no TEXT directive found") {
continue
Expand Down
29 changes: 28 additions & 1 deletion internal/build/plan9asm_altpkg_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ func TestPlan9AsmTranslateOptions(t *testing.T) {
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
got := plan9asmTranslateOptions(&test.conf)
got := plan9asmTranslateOptions(&test.conf, nil)
if got.GOARM != test.conf.GOARM {
t.Fatalf("GOARM = %q, want %q", got.GOARM, test.conf.GOARM)
}
Expand All @@ -45,6 +45,33 @@ func TestInternalRuntimeSysUsesPlan9AsmWithoutAltPkg(t *testing.T) {
}
}

func TestPlan9AsmNativeCollectorSelection(t *testing.T) {
for _, tc := range []struct {
name string
conf Config
want bool
}{
{"darwin", Config{Goos: "darwin", Goarch: "arm64"}, true},
{"linux", Config{Goos: "linux", Goarch: "arm64"}, true},
{"nogc", Config{Goos: "linux", Goarch: "arm64", Tags: "other,nogc"}, false},
{"baremetal", Config{Goos: "linux", Goarch: "arm64", Tags: "baremetal"}, false},
{"named native target", Config{Goos: "linux", Goarch: "arm64", Target: "custom"}, true},
{"other architecture", Config{Goos: "linux", Goarch: "amd64"}, false},
} {
t.Run(tc.name, func(t *testing.T) {
if got := plan9asmTranslateOptions(&tc.conf, nil).NativeBDWGC; got != tc.want {
t.Fatalf("NativeBDWGC = %t, want %t", got, tc.want)
}
})
}
for _, tag := range []string{"nogc", "baremetal"} {
conf := Config{Goos: "linux", Goarch: "arm64", Target: "custom"}
if plan9asmTranslateOptions(&conf, []string{tag}).NativeBDWGC {
t.Fatalf("target tag %s acquired a BDWGC dependency", tag)
}
}
}

func TestPlan9AsmDefaultsSupport386(t *testing.T) {
conf := &Config{Goarch: "386"}
for _, pkgPath := range []string{
Expand Down
98 changes: 98 additions & 0 deletions internal/build/testdata/native_tail/_wrap/probe.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
#include <stdint.h>
#include <stdatomic.h>

#define GUEST_BODY \
"sub sp, sp, #32\n" \
"mov x29, sp\n" \
"ldr x9, [x0]\n" \
"add x9, x9, #42\n" \
"str x9, [x0, #8]\n" \
"mov x19, #19\n" \
"mov x20, #20\n" \
"mov x21, #21\n" \
"mov x22, #22\n" \
"mov x23, #23\n" \
"mov x24, #24\n" \
"mov x25, #25\n" \
"mov x26, #26\n" \
"mov x27, #27\n" \
"mov x28, #28\n" \
"fmov d8, x19\n" \
"fmov d9, x19\n" \
"fmov d10, x19\n" \
"fmov d11, x19\n" \
"fmov d12, x19\n" \
"fmov d13, x19\n" \
"fmov d14, x19\n" \
"fmov d15, x19\n" \
"ldr x29, [x0, #16]\n" \
"ldr x30, [x0, #32]\n" \
"ldr x9, [x0, #24]\n" \
"mov sp, x9\n" \
"ret\n"

__attribute__((naked)) static void guest(void) {
__asm__ volatile(
"str x29, [x0, #16]\n"
"mov x9, sp\n"
"str x9, [x0, #24]\n"
"str x30, [x0, #32]\n"
"mov sp, x26\n"
GUEST_BODY);
}

__attribute__((naked)) static void resume_guest(void) {
__asm__ volatile(GUEST_BODY);
}

uintptr_t native_tail_guest_addr(void) { return (uintptr_t)&guest; }
uintptr_t native_tail_resume_addr(void) { return (uintptr_t)&resume_guest; }

__attribute__((naked)) static void gc_guest(void) {
__asm__ volatile(
"str x29, [x0, #16]\n"
"mov x9, sp\n"
"str x9, [x0, #24]\n"
"str x30, [x0, #32]\n"
"mov sp, x26\n"
"add x10, x0, #40\n"
"mov x9, #1\n"
"stlr x9, [x10]\n"
"add x10, x0, #48\n"
"1: ldar x9, [x10]\n"
"cbz x9, 1b\n"
GUEST_BODY);
}

uintptr_t native_tail_gc_guest_addr(void) { return (uintptr_t)&gc_guest; }
void native_tail_wait_entered(uintptr_t *flag) {
while (!atomic_load_explicit((_Atomic uintptr_t *)flag, memory_order_acquire)) {}
}
void native_tail_release_guest(uintptr_t *flag) {
atomic_store_explicit((_Atomic uintptr_t *)flag, 1, memory_order_release);
}

int native_tail_check(uintptr_t entry, uintptr_t code, uintptr_t context, uintptr_t stack) {
register uint64_t r19 __asm__("x19") = 1019;
register uint64_t r20 __asm__("x20") = 1020;
register uint64_t r21 __asm__("x21") = 1021;
register uint64_t r22 __asm__("x22") = 1022;
register uint64_t r23 __asm__("x23") = 1023;
register uint64_t r24 __asm__("x24") = 1024;
register uint64_t r25 __asm__("x25") = 1025;
register uint64_t r26 __asm__("x26") = 1026;
register uint64_t r27 __asm__("x27") = 1027;
register uint64_t r28 __asm__("x28") = 1028;
register double v8 __asm__("d8") = 1008.0;
register double v9 __asm__("d9") = 1009.0;
register double v10 __asm__("d10") = 1010.0;
register double v11 __asm__("d11") = 1011.0;
register double v12 __asm__("d12") = 1012.0;
register double v13 __asm__("d13") = 1013.0;
register double v14 __asm__("d14") = 1014.0;
register double v15 __asm__("d15") = 1015.0;
__asm__ volatile("" : "+r"(r19), "+r"(r20), "+r"(r21), "+r"(r22), "+r"(r23), "+r"(r24), "+r"(r25), "+r"(r26), "+r"(r27), "+r"(r28), "+w"(v8), "+w"(v9), "+w"(v10), "+w"(v11), "+w"(v12), "+w"(v13), "+w"(v14), "+w"(v15));
((void (*)(uintptr_t, uintptr_t, uintptr_t))entry)(code, context, stack);
__asm__ volatile("" : "+r"(r19), "+r"(r20), "+r"(r21), "+r"(r22), "+r"(r23), "+r"(r24), "+r"(r25), "+r"(r26), "+r"(r27), "+r"(r28), "+w"(v8), "+w"(v9), "+w"(v10), "+w"(v11), "+w"(v12), "+w"(v13), "+w"(v14), "+w"(v15));
return r19 == 1019 && r20 == 1020 && r21 == 1021 && r22 == 1022 && r23 == 1023 && r24 == 1024 && r25 == 1025 && r26 == 1026 && r27 == 1027 && r28 == 1028 && v8 == 1008.0 && v9 == 1009.0 && v10 == 1010.0 && v11 == 1011.0 && v12 == 1012.0 && v13 == 1013.0 && v14 == 1014.0 && v15 == 1015.0;
}
18 changes: 18 additions & 0 deletions internal/build/testdata/native_tail/entry_arm64.s
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#include "textflag.h"

TEXT ·Enter(SB), NOSPLIT|NOFRAME, $0-24
MOVD code+0(FP), R27
MOVD context+8(FP), R0
MOVD stack+16(FP), R26
JMP (R27)

TEXT ·Resume(SB), NOSPLIT|NOFRAME, $0-24
MOVD code+0(FP), R20
MOVD context+8(FP), R0
MOVD stack+16(FP), R19
MOVD R29, 16(R0)
MOVD RSP, R27
MOVD R27, 24(R0)
MOVD R30, 32(R0)
MOVD R19, RSP
JMP (R20)
3 changes: 3 additions & 0 deletions internal/build/testdata/native_tail/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module example.com/native-tail

go 1.27.0
76 changes: 76 additions & 0 deletions internal/build/testdata/native_tail/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package main

import (
"fmt"
"reflect"
"runtime"
"unsafe"
)

const LLGoFiles = "_wrap/probe.c"

func Enter(code, context, stack uintptr)
func Resume(code, context, stack uintptr)

//go:linkname guest C.native_tail_guest_addr
func guest() uintptr

//go:linkname resumeGuest C.native_tail_resume_addr
func resumeGuest() uintptr

//go:linkname check C.native_tail_check
func check(entry, code, context, stack uintptr) int

//go:linkname gcGuest C.native_tail_gc_guest_addr
func gcGuest() uintptr

//go:linkname waitEntered C.native_tail_wait_entered
func waitEntered(flag *uintptr)

//go:linkname releaseGuest C.native_tail_release_guest
func releaseGuest(flag *uintptr)

func main() {
var context [7]uintptr
stack := make([]byte, 65536)
top := (uintptr(unsafe.Pointer(&stack[len(stack)-1])) + 1) &^ 15
for i := uintptr(0); i < 1000; i++ {
context[0] = i
ptr := uintptr(unsafe.Pointer(&context[0]))
if check(reflect.ValueOf(Enter).Pointer(), guest(), ptr, top) != 1 || context[1] != i+42 {
panic("entry corrupted physical state")
}
// Direct calls also exercise LTO's inlining decisions around the
// physical ABI boundary, independently of the C register checker.
Enter(guest(), ptr, top)
if context[1] != i+42 {
panic("direct entry corrupted physical state")
}
// Simulate returning to Go for a host call, then resuming a guest stack.
context[0] += 100
if check(reflect.ValueOf(Resume).Pointer(), resumeGuest(), ptr, top) != 1 || context[1] != i+142 {
panic("resume corrupted physical state")
}
Resume(resumeGuest(), ptr, top)
if context[1] != i+142 {
panic("direct resume corrupted physical state")
}
}
// Force a collection while the native thread is executing on its guest
// stack. The handshake prevents GC from completing before entry.
done := make(chan struct{})
go func() {
waitEntered(&context[5])
runtime.GC()
releaseGuest(&context[6])
close(done)
}()
context[0] = 700
Enter(gcGuest(), uintptr(unsafe.Pointer(&context[0])), top)
<-done
if context[1] != 742 {
panic("collection during guest execution corrupted state")
}
runtime.KeepAlive(stack)
fmt.Println("entry/resume/stack/registers/gc: OK")
}
Loading
Loading