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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 15 additions & 8 deletions compiler/compiler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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()
Expand Down Expand Up @@ -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
Expand Down
46 changes: 46 additions & 0 deletions compiler/compiler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
33 changes: 31 additions & 2 deletions compiler/symbol.go
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
86 changes: 75 additions & 11 deletions compiler/syscall.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ package compiler
// compiler builtins.

import (
"go/types"
"strconv"
"strings"

Expand Down Expand Up @@ -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" {
Expand All @@ -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, "")
}
51 changes: 51 additions & 0 deletions compiler/testdata/cgo-import-dynamic.go
Original file line number Diff line number Diff line change
@@ -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
}
34 changes: 34 additions & 0 deletions src/runtime/os_darwin.c
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

#include <fcntl.h>

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
Expand All @@ -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.

Expand Down
Loading