From bbe3d24fb9556331df272a6310c3963eb2c6b3f2 Mon Sep 17 00:00:00 2001 From: Ayke van Laethem Date: Sat, 2 Sep 2023 13:52:21 +0200 Subject: [PATCH] cgo: add support for `#cgo noescape` lines Here is the proposal: https://github.com/golang/go/issues/56378 They are documented here: https://pkg.go.dev/cmd/cgo@master#hdr-Optimizing_calls_of_C_code This would have been very useful to fix https://github.com/tinygo-org/bluetooth/issues/176 in a nice way. That bug is now fixed in a different way using a wrapper function, but once this new noescape pragma gets included in TinyGo we could remove the workaround and use `#cgo noescape` instead. --- cgo/cgo.go | 53 +++++++++++++++++++++++++++++++++++++ cgo/libclang.go | 10 ++++++- cgo/testdata/errors.go | 5 ++++ cgo/testdata/errors.out.go | 9 ++++--- cgo/testdata/symbols.go | 5 ++++ cgo/testdata/symbols.out.go | 5 ++++ 6 files changed, 83 insertions(+), 4 deletions(-) diff --git a/cgo/cgo.go b/cgo/cgo.go index 67979230cc..abd5d5ed7c 100644 --- a/cgo/cgo.go +++ b/cgo/cgo.go @@ -18,6 +18,7 @@ import ( "go/scanner" "go/token" "path/filepath" + "sort" "strconv" "strings" @@ -36,6 +37,7 @@ type cgoPackage struct { fset *token.FileSet tokenFiles map[string]*token.File definedGlobally map[string]ast.Node + noescapingFuncs map[string]*noescapingFunc // #cgo noescape lines anonDecls map[interface{}]string cflags []string // CFlags from #cgo lines ldflags []string // LDFlags from #cgo lines @@ -74,6 +76,13 @@ type bitfieldInfo struct { endBit int64 // may be 0 meaning "until the end of the field" } +// Information about a #cgo noescape line in the source code. +type noescapingFunc struct { + name string + pos token.Pos + used bool // true if used somewhere in the source (for proper error reporting) +} + // cgoAliases list type aliases between Go and C, for types that are equivalent // in both languages. See addTypeAliases. var cgoAliases = map[string]string{ @@ -171,6 +180,7 @@ func Process(files []*ast.File, dir, importPath string, fset *token.FileSet, cfl fset: fset, tokenFiles: map[string]*token.File{}, definedGlobally: map[string]ast.Node{}, + noescapingFuncs: map[string]*noescapingFunc{}, anonDecls: map[interface{}]string{}, visitedFiles: map[string][]byte{}, } @@ -330,6 +340,22 @@ func Process(files []*ast.File, dir, importPath string, fset *token.FileSet, cfl }) } + // Show an error when a #cgo noescape line isn't used in practice. + // This matches upstream Go. I think the goal is to avoid issues with + // misspelled function names, which seems very useful. + var unusedNoescapeLines []*noescapingFunc + for _, value := range p.noescapingFuncs { + if !value.used { + unusedNoescapeLines = append(unusedNoescapeLines, value) + } + } + sort.SliceStable(unusedNoescapeLines, func(i, j int) bool { + return unusedNoescapeLines[i].pos < unusedNoescapeLines[j].pos + }) + for _, value := range unusedNoescapeLines { + p.addError(value.pos, fmt.Sprintf("function %#v not found in #cgo noescape line", value.name)) + } + // Print the newly generated in-memory AST, for debugging. //ast.Print(fset, p.generated) @@ -395,6 +421,33 @@ func (p *cgoPackage) parseCGoPreprocessorLines(text string, pos token.Pos) strin } text = text[:lineStart] + string(spaces) + text[lineEnd:] + allFields := strings.Fields(line[4:]) + switch allFields[0] { + case "noescape": + // The code indicates that pointer parameters will not be captured + // by the called C function. + if len(allFields) < 2 { + p.addErrorAfter(pos, text[:lineStart], "missing function name in #cgo noescape line") + continue + } + if len(allFields) > 2 { + p.addErrorAfter(pos, text[:lineStart], "multiple function names in #cgo noescape line") + continue + } + name := allFields[1] + p.noescapingFuncs[name] = &noescapingFunc{ + name: name, + pos: pos, + used: false, + } + continue + case "nocallback": + // We don't do anything special when calling a C function, so there + // appears to be no optimization that we can do here. + // Accept, but ignore the parameter for compatibility. + continue + } + // Get the text before the colon in the #cgo directive. colon := strings.IndexByte(line, ':') if colon < 0 { diff --git a/cgo/libclang.go b/cgo/libclang.go index 27ac1b576f..04d0a70aeb 100644 --- a/cgo/libclang.go +++ b/cgo/libclang.go @@ -253,10 +253,18 @@ func (f *cgoFile) createASTNode(name string, c clangCursor) (ast.Node, any) { }, }, } + var doc []string if C.clang_isFunctionTypeVariadic(cursorType) != 0 { + doc = append(doc, "//go:variadic") + } + if _, ok := f.noescapingFuncs[name]; ok { + doc = append(doc, "//go:noescape") + f.noescapingFuncs[name].used = true + } + if len(doc) != 0 { decl.Doc.List = append(decl.Doc.List, &ast.Comment{ Slash: pos - 1, - Text: "//go:variadic", + Text: strings.Join(doc, "\n"), }) } for i := 0; i < numArgs; i++ { diff --git a/cgo/testdata/errors.go b/cgo/testdata/errors.go index 7ca5b79600..b16c8c6572 100644 --- a/cgo/testdata/errors.go +++ b/cgo/testdata/errors.go @@ -10,6 +10,11 @@ typedef struct { typedef someType noType; // undefined type +// Some invalid noescape lines +#cgo noescape +#cgo noescape foo bar +#cgo noescape unusedFunction + #define SOME_CONST_1 5) // invalid const syntax #define SOME_CONST_2 6) // const not used (so no error) #define SOME_CONST_3 1234 // const too large for byte diff --git a/cgo/testdata/errors.out.go b/cgo/testdata/errors.out.go index 716fd771bb..108a397fbc 100644 --- a/cgo/testdata/errors.out.go +++ b/cgo/testdata/errors.out.go @@ -1,9 +1,12 @@ // CGo errors: +// testdata/errors.go:14:1: missing function name in #cgo noescape line +// testdata/errors.go:15:1: multiple function names in #cgo noescape line // testdata/errors.go:4:2: warning: some warning // testdata/errors.go:11:9: error: unknown type name 'someType' -// testdata/errors.go:22:5: warning: another warning -// testdata/errors.go:13:23: unexpected token ), expected end of expression -// testdata/errors.go:19:26: unexpected token ), expected end of expression +// testdata/errors.go:27:5: warning: another warning +// testdata/errors.go:18:23: unexpected token ), expected end of expression +// testdata/errors.go:24:26: unexpected token ), expected end of expression +// testdata/errors.go:3:1: function "unusedFunction" not found in #cgo noescape line // Type checking errors after CGo processing: // testdata/errors.go:102: cannot use 2 << 10 (untyped int constant 2048) as C.char value in variable declaration (overflows) diff --git a/cgo/testdata/symbols.go b/cgo/testdata/symbols.go index fb585c2f87..c8029a1481 100644 --- a/cgo/testdata/symbols.go +++ b/cgo/testdata/symbols.go @@ -9,6 +9,10 @@ static void staticfunc(int x); // Global variable signatures. extern int someValue; + +void notEscapingFunction(int *a); + +#cgo noescape notEscapingFunction */ import "C" @@ -18,6 +22,7 @@ func accessFunctions() { C.variadic0() C.variadic2(3, 5) C.staticfunc(3) + C.notEscapingFunction(nil) } func accessGlobals() { diff --git a/cgo/testdata/symbols.out.go b/cgo/testdata/symbols.out.go index f2826ae2e9..f99299edaa 100644 --- a/cgo/testdata/symbols.out.go +++ b/cgo/testdata/symbols.out.go @@ -60,5 +60,10 @@ func C.staticfunc!symbols.go(x C.int) var C.staticfunc!symbols.go$funcaddr unsafe.Pointer +//export notEscapingFunction +//go:noescape +func C.notEscapingFunction(a *C.int) + +var C.notEscapingFunction$funcaddr unsafe.Pointer //go:extern someValue var C.someValue C.int