Skip to content

Commit

Permalink
cgo: add support for #cgo noescape lines
Browse files Browse the repository at this point in the history
Here is the proposal:
golang/go#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
tinygo-org/bluetooth#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.
  • Loading branch information
aykevl committed Nov 19, 2024
1 parent 83ec9e8 commit a45b476
Show file tree
Hide file tree
Showing 6 changed files with 87 additions and 8 deletions.
53 changes: 53 additions & 0 deletions cgo/cgo.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"go/scanner"
"go/token"
"path/filepath"
"sort"
"strconv"
"strings"

Expand All @@ -42,6 +43,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
Expand Down Expand Up @@ -80,6 +82,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{
Expand Down Expand Up @@ -186,6 +195,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{},
}
Expand Down Expand Up @@ -344,6 +354,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 in #cgo noescape line is not used", value.name))
}

// Print the newly generated in-memory AST, for debugging.
//ast.Print(fset, p.generated)

Expand Down Expand Up @@ -409,6 +435,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 {
Expand Down
10 changes: 9 additions & 1 deletion cgo/libclang.go
Original file line number Diff line number Diff line change
Expand Up @@ -256,10 +256,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++ {
Expand Down
5 changes: 5 additions & 0 deletions cgo/testdata/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
17 changes: 10 additions & 7 deletions cgo/testdata/errors.out.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
// 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:26:5: warning: another warning
// testdata/errors.go:13:23: unexpected token ), expected end of expression
// testdata/errors.go:21:26: unexpected token ), expected end of expression
// testdata/errors.go:16:33: unexpected token ), expected end of expression
// testdata/errors.go:17:34: unexpected token ), expected end of expression
// testdata/errors.go:31:5: warning: another warning
// testdata/errors.go:18:23: unexpected token ), expected end of expression
// testdata/errors.go:26:26: unexpected token ), expected end of expression
// testdata/errors.go:21:33: unexpected token ), expected end of expression
// testdata/errors.go:22:34: unexpected token ), expected end of expression
// -: unexpected token INT, expected end of expression
// testdata/errors.go:30:35: unexpected number of parameters: expected 2, got 3
// testdata/errors.go:31:31: unexpected number of parameters: expected 2, got 1
// testdata/errors.go:35:35: unexpected number of parameters: expected 2, got 3
// testdata/errors.go:36:31: unexpected number of parameters: expected 2, got 1
// testdata/errors.go:3:1: function "unusedFunction" in #cgo noescape line is not used

// 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)
Expand Down
5 changes: 5 additions & 0 deletions cgo/testdata/symbols.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ static void staticfunc(int x);
// Global variable signatures.
extern int someValue;
void notEscapingFunction(int *a);
#cgo noescape notEscapingFunction
*/
import "C"

Expand All @@ -18,6 +22,7 @@ func accessFunctions() {
C.variadic0()
C.variadic2(3, 5)
C.staticfunc(3)
C.notEscapingFunction(nil)
}

func accessGlobals() {
Expand Down
5 changes: 5 additions & 0 deletions cgo/testdata/symbols.out.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,5 +67,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

0 comments on commit a45b476

Please sign in to comment.