Skip to content

Commit

Permalink
cgo: support errno value as second return parameter
Browse files Browse the repository at this point in the history
Making this work on all targets was interesting but there's now a test
in place to make sure this works on all targets that have the CGo test
enabled (which is almost all targets).
  • Loading branch information
aykevl committed Nov 20, 2024
1 parent 548fba8 commit 6593cf2
Show file tree
Hide file tree
Showing 23 changed files with 263 additions and 16 deletions.
1 change: 1 addition & 0 deletions builder/picolibc.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ var libPicolibc = Library{
"-D__OBSOLETE_MATH_FLOAT=1", // use old math code that doesn't expect a FPU
"-D__OBSOLETE_MATH_DOUBLE=0",
"-D_WANT_IO_C99_FORMATS",
"-D__PICOLIBC_ERRNO_FUNCTION=__errno_location",
"-nostdlibinc",
"-isystem", newlibDir + "/libc/include",
"-I" + newlibDir + "/libc/tinystdio",
Expand Down
121 changes: 117 additions & 4 deletions cgo/cgo.go
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,8 @@ typedef unsigned long long _Cgo_ulonglong;
// first.
// These functions will be modified to get a "C." prefix, so the source below
// doesn't reflect the final AST.
const generatedGoFilePrefix = `
const generatedGoFilePrefixBase = `
import "syscall"
import "unsafe"
var _ unsafe.Pointer
Expand Down Expand Up @@ -169,6 +170,74 @@ func __CBytes([]byte) unsafe.Pointer
func CBytes(b []byte) unsafe.Pointer {
return C.__CBytes(b)
}
//go:linkname C.__get_errno_num runtime.cgo_errno
func __get_errno_num() uintptr
`

const generatedGoFilePrefixOther = generatedGoFilePrefixBase + `
func __get_errno() error {
return syscall.Errno(C.__get_errno_num())
}
`

// Windows uses fake errno values in the syscall package.
// See for example: https://github.com/golang/go/issues/23468
// TinyGo uses mingw-w64 though, which does have defined errno values. Since the
// syscall package is the standard library one we can't change it, but we can
// map the errno values to match the values in the syscall package.
// Source of the errno values: lib/mingw-w64/mingw-w64-headers/crt/errno.h
const generatedGoFilePrefixWindows = generatedGoFilePrefixBase + `
var __errno_mapping = [...]syscall.Errno{
1: syscall.EPERM,
2: syscall.ENOENT,
3: syscall.ESRCH,
4: syscall.EINTR,
5: syscall.EIO,
6: syscall.ENXIO,
7: syscall.E2BIG,
8: syscall.ENOEXEC,
9: syscall.EBADF,
10: syscall.ECHILD,
11: syscall.EAGAIN,
12: syscall.ENOMEM,
13: syscall.EACCES,
14: syscall.EFAULT,
16: syscall.EBUSY,
17: syscall.EEXIST,
18: syscall.EXDEV,
19: syscall.ENODEV,
20: syscall.ENOTDIR,
21: syscall.EISDIR,
22: syscall.EINVAL,
23: syscall.ENFILE,
24: syscall.EMFILE,
25: syscall.ENOTTY,
27: syscall.EFBIG,
28: syscall.ENOSPC,
29: syscall.ESPIPE,
30: syscall.EROFS,
31: syscall.EMLINK,
32: syscall.EPIPE,
33: syscall.EDOM,
34: syscall.ERANGE,
36: syscall.EDEADLK,
38: syscall.ENAMETOOLONG,
39: syscall.ENOLCK,
40: syscall.ENOSYS,
41: syscall.ENOTEMPTY,
42: syscall.EILSEQ,
}
func __get_errno() error {
num := C.__get_errno_num()
if num < uintptr(len(__errno_mapping)) {
if mapped := __errno_mapping[num]; mapped != 0 {
return mapped
}
}
return syscall.Errno(num)
}
`

// Process extracts `import "C"` statements from the AST, parses the comment
Expand All @@ -178,7 +247,7 @@ func CBytes(b []byte) unsafe.Pointer {
// functions), the CFLAGS and LDFLAGS found in #cgo lines, and a map of file
// hashes of the accessed C header files. If there is one or more error, it
// returns these in the []error slice but still modifies the AST.
func Process(files []*ast.File, dir, importPath string, fset *token.FileSet, cflags []string) ([]*ast.File, []string, []string, []string, map[string][]byte, []error) {
func Process(files []*ast.File, dir, importPath string, fset *token.FileSet, cflags []string, goos string) ([]*ast.File, []string, []string, []string, map[string][]byte, []error) {
p := &cgoPackage{
packageName: files[0].Name.Name,
currentDir: dir,
Expand Down Expand Up @@ -210,7 +279,12 @@ func Process(files []*ast.File, dir, importPath string, fset *token.FileSet, cfl
// Construct a new in-memory AST for CGo declarations of this package.
// The first part is written as Go code that is then parsed, but more code
// is added later to the AST to declare functions, globals, etc.
goCode := "package " + files[0].Name.Name + "\n\n" + generatedGoFilePrefix
goCode := "package " + files[0].Name.Name + "\n\n"
if goos == "windows" {
goCode += generatedGoFilePrefixWindows
} else {
goCode += generatedGoFilePrefixOther
}
p.generated, err = parser.ParseFile(fset, dir+"/!cgo.go", goCode, parser.ParseComments)
if err != nil {
// This is always a bug in the cgo package.
Expand All @@ -225,7 +299,7 @@ func Process(files []*ast.File, dir, importPath string, fset *token.FileSet, cfl
switch decl := decl.(type) {
case *ast.FuncDecl:
switch decl.Name.Name {
case "CString", "GoString", "GoStringN", "__GoStringN", "GoBytes", "__GoBytes", "CBytes", "__CBytes":
case "CString", "GoString", "GoStringN", "__GoStringN", "GoBytes", "__GoBytes", "CBytes", "__CBytes", "__get_errno_num", "__get_errno", "__errno_mapping":
// Adjust the name to have a "C." prefix so it is correctly
// resolved.
decl.Name.Name = "C." + decl.Name.Name
Expand Down Expand Up @@ -1279,6 +1353,45 @@ extern __typeof(%s) %s __attribute__((alias(%#v)));
// separate namespace (no _Cgo_ hacks like in gc).
func (f *cgoFile) walker(cursor *astutil.Cursor, names map[string]clangCursor) bool {
switch node := cursor.Node().(type) {
case *ast.AssignStmt:
// An assign statement could be something like this:
//
// val, errno := C.some_func()
//
// Check whether it looks like that, and if so, read the errno value and
// return it as the second return value. The call will be transformed
// into something like this:
//
// val, errno := C.some_func(), C.__get_errno()
if len(node.Lhs) != 2 || len(node.Rhs) != 1 {
return true
}
rhs, ok := node.Rhs[0].(*ast.CallExpr)
if !ok {
return true
}
fun, ok := rhs.Fun.(*ast.SelectorExpr)
if !ok {
return true
}
x, ok := fun.X.(*ast.Ident)
if !ok {
return true
}
if found, ok := names[fun.Sel.Name]; ok && x.Name == "C" {
// Replace "C"."some_func" into "C.somefunc".
rhs.Fun = &ast.Ident{
NamePos: x.NamePos,
Name: f.getASTDeclName(fun.Sel.Name, found, true),
}
// Add the errno value as the second value in the statement.
node.Rhs = append(node.Rhs, &ast.CallExpr{
Fun: &ast.Ident{
NamePos: node.Lhs[1].End(),
Name: "C.__get_errno",
},
})
}
case *ast.CallExpr:
fun, ok := node.Fun.(*ast.SelectorExpr)
if !ok {
Expand Down
27 changes: 23 additions & 4 deletions cgo/cgo_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,15 +56,15 @@ func TestCGo(t *testing.T) {
}

// Process the AST with CGo.
cgoFiles, _, _, _, _, cgoErrors := Process([]*ast.File{f}, "testdata", "main", fset, cflags)
cgoFiles, _, _, _, _, cgoErrors := Process([]*ast.File{f}, "testdata", "main", fset, cflags, "linux")

// Check the AST for type errors.
var typecheckErrors []error
config := types.Config{
Error: func(err error) {
typecheckErrors = append(typecheckErrors, err)
},
Importer: simpleImporter{},
Importer: newSimpleImporter(),
Sizes: types.SizesFor("gccgo", "arm"),
}
_, err = config.Check("", fset, append([]*ast.File{f}, cgoFiles...), nil)
Expand Down Expand Up @@ -202,14 +202,33 @@ func Test_cgoPackage_isEquivalentAST(t *testing.T) {
}

// simpleImporter implements the types.Importer interface, but only allows
// importing the unsafe package.
// importing the syscall and unsafe packages.
type simpleImporter struct {
syscallPkg *types.Package
}

func newSimpleImporter() *simpleImporter {
i := &simpleImporter{}

// Implement a dummy syscall package with the Errno type.
i.syscallPkg = types.NewPackage("syscall", "syscall")
obj := types.NewTypeName(token.NoPos, i.syscallPkg, "Errno", nil)
named := types.NewNamed(obj, nil, nil)
i.syscallPkg.Scope().Insert(obj)
named.SetUnderlying(types.Typ[types.Uintptr])
sig := types.NewSignatureType(nil, nil, nil, types.NewTuple(), types.NewTuple(types.NewParam(token.NoPos, i.syscallPkg, "", types.Typ[types.String])), false)
named.AddMethod(types.NewFunc(token.NoPos, i.syscallPkg, "Error", sig))
i.syscallPkg.MarkComplete()

return i
}

// Import implements the Importer interface. For testing usage only: it only
// supports importing the unsafe package.
func (i simpleImporter) Import(path string) (*types.Package, error) {
func (i *simpleImporter) Import(path string) (*types.Package, error) {
switch path {
case "syscall":
return i.syscallPkg, nil
case "unsafe":
return types.Unsafe, nil
default:
Expand Down
8 changes: 8 additions & 0 deletions cgo/testdata/basic.out.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package main

import "syscall"
import "unsafe"

var _ unsafe.Pointer
Expand Down Expand Up @@ -31,6 +32,13 @@ func C.CBytes(b []byte) unsafe.Pointer {
return C.__CBytes(b)
}

//go:linkname C.__get_errno_num runtime.cgo_errno
func C.__get_errno_num() uintptr

func C.__get_errno() error {
return syscall.Errno(C.__get_errno_num())
}

type (
C.char uint8
C.schar int8
Expand Down
8 changes: 8 additions & 0 deletions cgo/testdata/const.out.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package main

import "syscall"
import "unsafe"

var _ unsafe.Pointer
Expand Down Expand Up @@ -31,6 +32,13 @@ func C.CBytes(b []byte) unsafe.Pointer {
return C.__CBytes(b)
}

//go:linkname C.__get_errno_num runtime.cgo_errno
func C.__get_errno_num() uintptr

func C.__get_errno() error {
return syscall.Errno(C.__get_errno_num())
}

type (
C.char uint8
C.schar int8
Expand Down
8 changes: 8 additions & 0 deletions cgo/testdata/errors.out.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@

package main

import "syscall"
import "unsafe"

var _ unsafe.Pointer
Expand Down Expand Up @@ -55,6 +56,13 @@ func C.CBytes(b []byte) unsafe.Pointer {
return C.__CBytes(b)
}

//go:linkname C.__get_errno_num runtime.cgo_errno
func C.__get_errno_num() uintptr

func C.__get_errno() error {
return syscall.Errno(C.__get_errno_num())
}

type (
C.char uint8
C.schar int8
Expand Down
8 changes: 8 additions & 0 deletions cgo/testdata/flags.out.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

package main

import "syscall"
import "unsafe"

var _ unsafe.Pointer
Expand Down Expand Up @@ -36,6 +37,13 @@ func C.CBytes(b []byte) unsafe.Pointer {
return C.__CBytes(b)
}

//go:linkname C.__get_errno_num runtime.cgo_errno
func C.__get_errno_num() uintptr

func C.__get_errno() error {
return syscall.Errno(C.__get_errno_num())
}

type (
C.char uint8
C.schar int8
Expand Down
8 changes: 8 additions & 0 deletions cgo/testdata/symbols.out.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package main

import "syscall"
import "unsafe"

var _ unsafe.Pointer
Expand Down Expand Up @@ -31,6 +32,13 @@ func C.CBytes(b []byte) unsafe.Pointer {
return C.__CBytes(b)
}

//go:linkname C.__get_errno_num runtime.cgo_errno
func C.__get_errno_num() uintptr

func C.__get_errno() error {
return syscall.Errno(C.__get_errno_num())
}

type (
C.char uint8
C.schar int8
Expand Down
8 changes: 8 additions & 0 deletions cgo/testdata/types.out.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package main

import "syscall"
import "unsafe"

var _ unsafe.Pointer
Expand Down Expand Up @@ -31,6 +32,13 @@ func C.CBytes(b []byte) unsafe.Pointer {
return C.__CBytes(b)
}

//go:linkname C.__get_errno_num runtime.cgo_errno
func C.__get_errno_num() uintptr

func C.__get_errno() error {
return syscall.Errno(C.__get_errno_num())
}

type (
C.char uint8
C.schar int8
Expand Down
2 changes: 2 additions & 0 deletions compileopts/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,7 @@ func (c *Config) CFlags(libclang bool) []string {
"-isystem", filepath.Join(path, "include"),
"-isystem", filepath.Join(picolibcDir, "include"),
"-isystem", filepath.Join(picolibcDir, "tinystdio"),
"-D__PICOLIBC_ERRNO_FUNCTION=__errno_location",
)
case "musl":
root := goenv.Get("TINYGOROOT")
Expand All @@ -340,6 +341,7 @@ func (c *Config) CFlags(libclang bool) []string {
"-nostdlibinc",
"-isystem", filepath.Join(path, "include"),
"-isystem", filepath.Join(root, "lib", "musl", "arch", arch),
"-isystem", filepath.Join(root, "lib", "musl", "arch", "generic"),
"-isystem", filepath.Join(root, "lib", "musl", "include"),
)
case "wasi-libc":
Expand Down
2 changes: 1 addition & 1 deletion loader/loader.go
Original file line number Diff line number Diff line change
Expand Up @@ -485,7 +485,7 @@ func (p *Package) parseFiles() ([]*ast.File, error) {
var initialCFlags []string
initialCFlags = append(initialCFlags, p.program.config.CFlags(true)...)
initialCFlags = append(initialCFlags, "-I"+p.Dir)
generated, headerCode, cflags, ldflags, accessedFiles, errs := cgo.Process(files, p.program.workingDir, p.ImportPath, p.program.fset, initialCFlags)
generated, headerCode, cflags, ldflags, accessedFiles, errs := cgo.Process(files, p.program.workingDir, p.ImportPath, p.program.fset, initialCFlags, p.program.config.GOOS())
p.CFlags = append(initialCFlags, cflags...)
p.CGoHeaders = headerCode
for path, hash := range accessedFiles {
Expand Down
13 changes: 13 additions & 0 deletions src/runtime/baremetal.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,3 +86,16 @@ func AdjustTimeOffset(offset int64) {
// TODO: do this atomically?
timeOffset += offset
}

// Picolibc is not configured to define its own errno value, instead it calls
// __errno_location.
// TODO: a global works well enough for now (same as errno on Linux with
// -scheduler=tasks), but this should ideally be a thread-local variable stored
// in task.Task.
// Especially when we add multicore support for microcontrollers.
var errno int32

//export __errno_location
func libc_errno_location() *int32 {
return &errno
}
Loading

0 comments on commit 6593cf2

Please sign in to comment.