diff --git a/compiler/compiler.go b/compiler/compiler.go index 51197bac34..d4d1de4be8 100644 --- a/compiler/compiler.go +++ b/compiler/compiler.go @@ -88,6 +88,7 @@ type compilerContext struct { diagnostics []error functionInfos map[*ssa.Function]functionInfo astComments map[string]*ast.CommentGroup + cgoImportDynamic map[string]string // //go:cgo_import_dynamic local name -> remote symbol embedGlobals map[string][]*loader.EmbedFile pkg *types.Package loaderPkg *loader.Package // current package being compiled (for AST access) @@ -100,14 +101,15 @@ type compilerContext struct { // importantly with a newly created LLVM context and module. func newCompilerContext(moduleName string, machine llvm.TargetMachine, config *Config, dumpSSA bool) *compilerContext { c := &compilerContext{ - Config: config, - DumpSSA: dumpSSA, - difiles: make(map[string]llvm.Metadata), - ditypes: make(map[types.Type]llvm.Metadata), - machine: machine, - targetData: machine.CreateTargetData(), - functionInfos: map[*ssa.Function]functionInfo{}, - astComments: map[string]*ast.CommentGroup{}, + Config: config, + DumpSSA: dumpSSA, + difiles: make(map[string]llvm.Metadata), + ditypes: make(map[types.Type]llvm.Metadata), + machine: machine, + targetData: machine.CreateTargetData(), + functionInfos: map[*ssa.Function]functionInfo{}, + astComments: map[string]*ast.CommentGroup{}, + cgoImportDynamic: map[string]string{}, } c.ctx = llvm.NewContext() @@ -3670,6 +3672,11 @@ func (b *builder) createConvert(typeFrom, typeTo types.Type, value llvm.Value, p // which can all be directly lowered to IR. However, there is also the channel // receive operator which is handled in the runtime directly. func (b *builder) createUnOp(unop *ssa.UnOp) (llvm.Value, error) { + if unop.Op == token.MUL { + if value := b.createDarwinCgoImportDynamicLoad(unop); !value.IsNil() { + return value, nil + } + } x := b.getValue(unop.X, getPos(unop)) switch unop.Op { case token.NOT: // !x diff --git a/compiler/compiler_test.go b/compiler/compiler_test.go index d9188790cd..52f4af104a 100644 --- a/compiler/compiler_test.go +++ b/compiler/compiler_test.go @@ -174,6 +174,52 @@ func TestOptimizedLargeAggregateABI(t *testing.T) { } } +func TestDarwinCgoImportDynamic(t *testing.T) { + options := &compileopts.Options{GOOS: "darwin", GOARCH: "arm64"} + mod, errs := testCompilePackage(t, options, "cgo-import-dynamic.go") + if len(errs) != 0 { + for _, err := range errs { + t.Error(err) + } + return + } + defer mod.Dispose() + + ir := mod.String() + if !strings.Contains(ir, `declare void @"remote$INODE64"()`) { + t.Error("missing external declaration for cgo_import_dynamic remote symbol") + } + if !strings.Contains(ir, `ptrtoint (ptr @"remote$INODE64" to i64)`) { + t.Error("trampoline address load was not replaced with the remote symbol address") + } + if strings.Contains(ir, "load i64, ptr @main.libc_test_trampoline_addr") { + t.Error("trampoline address global was loaded instead of using the remote symbol address") + } + if !strings.Contains(ir, "ptrtoint (ptr @syscall_libc_ioctl to i64)") { + t.Error("variadic ioctl import was not routed through its fixed-signature wrapper") + } + for _, remote := range []string{"open", "openat", "fcntl"} { + if !strings.Contains(ir, "ptrtoint (ptr @syscall_libc_"+remote+" to i64)") { + t.Errorf("variadic %s import was not routed through its fixed-signature wrapper", remote) + } + if strings.Contains(ir, "declare void @"+remote+"()") { + t.Errorf("variadic %s import was declared directly instead of using its wrapper", remote) + } + } + if !strings.Contains(ir, "ptrtoint (ptr @remote_nolib to i64)") { + t.Error("cgo_import_dynamic without a library operand was not honored") + } + if !strings.Contains(ir, "ptrtoint (ptr @libc_self to i64)") { + t.Error("cgo_import_dynamic without a remote symbol did not default to the local symbol") + } + if !strings.Contains(ir, "load i32, ptr @main.libc_badtype_trampoline_addr") { + t.Error("load of a non-uintptr trampoline global was replaced instead of being left alone") + } + if strings.Contains(ir, "@bad_remote") { + t.Error("a declaration was created for the remote symbol of a non-uintptr trampoline global") + } +} + // normalizeIR canonicalizes LLVM-version-specific IR spellings for comparison // and when regenerating golden files. func normalizeIR(s string) string { diff --git a/compiler/symbol.go b/compiler/symbol.go index 944f74240e..b58e75821b 100644 --- a/compiler/symbol.go +++ b/compiler/symbol.go @@ -763,10 +763,39 @@ func (c *compilerContext) fileForFunc(f *ssa.Function) *ast.File { return nil } -// loadASTComments loads comments on globals from the AST, for use later in the -// program. In particular, they are required for //go:extern pragmas on globals. +// loadASTComments loads comments from the AST that cannot be read on demand +// while compiling a function, for use later in the program. This covers doc +// comments on globals (required for //go:extern pragmas) and free-standing +// file-level //go:cgo_import_dynamic directives, which are not attached to any +// declaration and apply to the whole package. func (c *compilerContext) loadASTComments(pkg *loader.Package) { for _, file := range pkg.Files { + // cgo_import_dynamic directives are file-level pragmas. Darwin's + // generated syscall wrappers use the local symbol to name an assembly + // trampoline and the remote symbol to name the actual dylib function. + // Like the gc compiler, accept all three operand forms: + // + // //go:cgo_import_dynamic local [remote ["library"]] + // + // The remote symbol defaults to the local symbol when omitted. The + // library operand is not needed here (the linker resolves the symbol + // against the libraries it already links) and is ignored, so a library + // path containing spaces does not break parsing. A repeated local + // symbol keeps the last remote symbol, matching gc's behavior of + // simply recording each directive. + for _, group := range file.Comments { + for _, comment := range group.List { + parts := strings.Fields(comment.Text) + if len(parts) >= 2 && parts[0] == "//go:cgo_import_dynamic" { + local, remote := parts[1], parts[1] + if len(parts) >= 3 { + remote = parts[2] + } + c.cgoImportDynamic[local] = remote + } + } + } + for _, decl := range file.Decls { switch decl := decl.(type) { case *ast.GenDecl: diff --git a/compiler/syscall.go b/compiler/syscall.go index 5172e78380..f5a16e1c77 100644 --- a/compiler/syscall.go +++ b/compiler/syscall.go @@ -4,6 +4,7 @@ package compiler // compiler builtins. import ( + "go/types" "strconv" "strings" @@ -523,11 +524,10 @@ func (b *builder) createDarwinFuncPCABI0Call(instr *ssa.CallCommon) llvm.Value { // Extract the libc function name. name := strings.TrimPrefix(strings.TrimSuffix(calledFn.Name(), "_trampoline"), "libc_") - if name == "open" { - // Special case: open() is a variadic function and can't be called like - // a regular function. Therefore, we need to use a wrapper implemented - // in C. - name = "syscall_libc_open" + if wrapper, ok := darwinVariadicImports[name]; ok { + // Variadic functions can't be called like a regular function, so use a + // wrapper implemented in C. See the comment on darwinVariadicImports. + name = wrapper } if b.GOARCH == "amd64" { if name == "fdopendir" || name == "readdir_r" { @@ -538,16 +538,80 @@ func (b *builder) createDarwinFuncPCABI0Call(instr *ssa.CallCommon) llvm.Value { } } - // Obtain the C function. - // Use a simple function (no parameters or return value) because all we need - // is the address of the function. + return b.createDarwinImportedFunctionAddr(name) +} + +// darwinVariadicImports maps the variadic libc functions imported by Darwin +// syscall wrappers to fixed-signature C wrappers defined in +// src/runtime/os_darwin.c. The syscall engine calls an imported address +// through a fixed-signature function pointer (tinygo_syscallX and friends in +// src/runtime/os_darwin.c), which passes every argument in a register. A +// variadic callee, however, takes its variadic arguments from the stack on +// darwin/arm64, so calling one of these functions directly makes it read +// garbage arguments (a direct ioctl call observably failed with EFAULT, and +// a direct fcntl(F_SETFL) wrote garbage flags). This applies to both +// trampoline flavors: the standard library's function-based pattern +// (createDarwinFuncPCABI0Call above) and the address-global pattern used by +// golang.org/x/sys (createDarwinCgoImportDynamicLoad below). +// +// The set comes from cross-referencing the symbols that darwin's generated +// syscall wrappers import (the //go:cgo_import_dynamic directives in +// zsyscall_darwin_*.go, both in golang.org/x/sys/unix and in the standard +// library) against their Darwin SDK declarations: of those imports, exactly +// open(2), openat(2), fcntl(2), and ioctl(2) are declared variadic (see +// sys/fcntl.h and sys/ioctl.h, or lib/macos-minimal-sdk's copies). +var darwinVariadicImports = map[string]string{ + "fcntl": "syscall_libc_fcntl", + "ioctl": "syscall_libc_ioctl", + "open": "syscall_libc_open", + "openat": "syscall_libc_openat", +} + +// Lower a load from a Darwin libc trampoline address global. Packages such as +// golang.org/x/sys/unix declare globals named libc_*_trampoline_addr and use +// assembly to initialize them to trampolines for symbols imported with +// //go:cgo_import_dynamic. TinyGo cannot compile that assembly, so use the +// imported dylib symbol directly, just like createDarwinFuncPCABI0Call does for +// the standard library's function-based trampoline pattern. +func (b *builder) createDarwinCgoImportDynamicLoad(unop *ssa.UnOp) llvm.Value { + if b.GOOS != "darwin" { + return llvm.Value{} + } + + global, ok := unop.X.(*ssa.Global) + if !ok { + return llvm.Value{} + } + const suffix = "_trampoline_addr" + if !strings.HasPrefix(global.Name(), "libc_") || !strings.HasSuffix(global.Name(), suffix) { + return llvm.Value{} + } + // The replacement value is a ptrtoint to uintptr, so only replace loads of + // uintptr-typed globals; the trampoline address pattern always uses plain + // uintptr variables. Anything else keeps its normal load. + if basic, ok := global.Type().(*types.Pointer).Elem().Underlying().(*types.Basic); !ok || basic.Kind() != types.Uintptr { + return llvm.Value{} + } + + local := strings.TrimSuffix(global.Name(), suffix) + remote, ok := b.cgoImportDynamic[local] + if !ok { + return llvm.Value{} + } + if wrapper, ok := darwinVariadicImports[remote]; ok { + remote = wrapper + } + + return b.createDarwinImportedFunctionAddr(remote) +} + +func (b *builder) createDarwinImportedFunctionAddr(name string) llvm.Value { + // The signature does not matter: the declaration is only used for its + // address, which is passed to a syscall implementation as a uintptr. llvmFn := b.mod.NamedFunction(name) if llvmFn.IsNil() { llvmFnType := llvm.FunctionType(b.ctx.VoidType(), nil, false) llvmFn = llvm.AddFunction(b.mod, name, llvmFnType) } - - // Cast the function pointer to a uintptr (because that's what - // abi.FuncPCABI0 returns). return b.CreatePtrToInt(llvmFn, b.uintptrType, "") } diff --git a/compiler/testdata/cgo-import-dynamic.go b/compiler/testdata/cgo-import-dynamic.go new file mode 100644 index 0000000000..d5127ad184 --- /dev/null +++ b/compiler/testdata/cgo-import-dynamic.go @@ -0,0 +1,51 @@ +package main + +var libc_test_trampoline_addr uintptr +var libc_ioctl_trampoline_addr uintptr +var libc_open_trampoline_addr uintptr +var libc_openat_trampoline_addr uintptr +var libc_fcntl_trampoline_addr uintptr +var libc_nolib_trampoline_addr uintptr +var libc_self_trampoline_addr uintptr +var libc_badtype_trampoline_addr uint32 + +//go:cgo_import_dynamic libc_test remote$INODE64 "/usr/lib/libSystem.B.dylib" +//go:cgo_import_dynamic libc_ioctl ioctl "/usr/lib/libSystem.B.dylib" +//go:cgo_import_dynamic libc_open open "/usr/lib/libSystem.B.dylib" +//go:cgo_import_dynamic libc_openat openat "/usr/lib/libSystem.B.dylib" +//go:cgo_import_dynamic libc_fcntl fcntl "/usr/lib/libSystem.B.dylib" +//go:cgo_import_dynamic libc_nolib remote_nolib +//go:cgo_import_dynamic libc_self +//go:cgo_import_dynamic libc_badtype bad_remote "/usr/lib/libSystem.B.dylib" + +func loadImportedFunctionAddress() uintptr { + return libc_test_trampoline_addr +} + +func loadImportedIoctlAddress() uintptr { + return libc_ioctl_trampoline_addr +} + +func loadImportedOpenAddress() uintptr { + return libc_open_trampoline_addr +} + +func loadImportedOpenatAddress() uintptr { + return libc_openat_trampoline_addr +} + +func loadImportedFcntlAddress() uintptr { + return libc_fcntl_trampoline_addr +} + +func loadImportedNoLibraryAddress() uintptr { + return libc_nolib_trampoline_addr +} + +func loadImportedSelfAddress() uintptr { + return libc_self_trampoline_addr +} + +func loadImportedBadTypeAddress() uint32 { + return libc_badtype_trampoline_addr +} diff --git a/src/runtime/os_darwin.c b/src/runtime/os_darwin.c index 5d7cd7c71d..8f6034af37 100644 --- a/src/runtime/os_darwin.c +++ b/src/runtime/os_darwin.c @@ -4,6 +4,8 @@ #include +extern int ioctl(int fd, unsigned long request, ...); + // Wrapper function because 'open' is a variadic function and variadic functions // use a different (incompatible) calling convention on darwin/arm64. // This function is referenced from the compiler, when it sees a @@ -12,6 +14,38 @@ int syscall_libc_open(const char *pathname, int flags, mode_t mode) { return open(pathname, flags, mode); } +// Wrapper for ioctl, which is variadic just like open and therefore also uses +// an incompatible calling convention on darwin/arm64. Use uintptr_t arguments +// to match the fixed-signature call made by tinygo_syscall below. +int syscall_libc_ioctl(uintptr_t fd, uintptr_t request, uintptr_t arg) { + return ioctl((int)fd, request, (void *)arg); +} + +// Wrappers for the remaining variadic libc functions that darwin syscall +// wrappers import with //go:cgo_import_dynamic (see darwinVariadicImports in +// compiler/syscall.go): of the symbols imported by the generated +// zsyscall_darwin_*.go files in golang.org/x/sys/unix and the standard +// library, exactly open, openat, fcntl, and ioctl are declared variadic in +// the Darwin SDK headers (sys/fcntl.h and sys/ioctl.h). The tinygo_syscall* +// functions below call through fixed-signature function pointers that pass +// every argument in a register, while a variadic callee takes its variadic +// arguments from the stack on darwin/arm64, so each of these needs a +// fixed-signature wrapper. The uintptr_t parameters match the uintptr +// arguments the Go syscall engine passes. + +// fcntl's third argument is an int for some commands and a pointer for +// others; passing the raw pointer-sized value covers both. +int syscall_libc_fcntl(uintptr_t fd, uintptr_t cmd, uintptr_t arg) { + return fcntl((int)fd, (int)cmd, (void *)arg); +} + +// openat is invoked by x/sys through syscall6 with six arguments, the last +// two of which are zero padding; the two extra register arguments are +// harmless to a four-parameter callee. +int syscall_libc_openat(uintptr_t dirfd, uintptr_t pathname, uintptr_t flags, uintptr_t mode) { + return openat((int)dirfd, (const char *)pathname, (int)flags, (mode_t)mode); +} + // The following functions are called by the runtime because Go can't call // function pointers directly.