Skip to content

cmd/cgo: add #cgo noescape/nocallback annotations #60399

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 14 commits into from
24 changes: 24 additions & 0 deletions src/cmd/cgo/doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,30 @@ passing uninitialized C memory to Go code if the Go code is going to
store pointer values in it. Zero out the memory in C before passing it
to Go.

# Optimizing calls of C code

When passing a Go pointer to a C function the compiler normally ensures
that the Go object lives on the heap. If the C function does not keep
a copy of the Go pointer, and never passes the Go pointer back to Go code,
then this is unnecessary. The #cgo noescape directive may be used to tell
the compiler that no Go pointers escape via the named C function.
If the noescape directive is used and the C function does not handle the
pointer safely, the program may crash or see memory corruption.

For example:

// #cgo noescape cFunctionName

When a Go function calls a C function, it prepares for the C function to
call back to a Go function. the #cgo nocallback directive may be used to
tell the compiler that these preparations are not necessary.
If the nocallback directive is used and the C function does call back into
Go code, the program will panic.

For example:

// #cgo nocallback cFunctionName

# Special cases

A few special C types which would normally be represented by a pointer
Expand Down
22 changes: 18 additions & 4 deletions src/cmd/cgo/gcc.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,18 +73,32 @@ func cname(s string) string {
return s
}

// DiscardCgoDirectives processes the import C preamble, and discards
// all #cgo CFLAGS and LDFLAGS directives, so they don't make their
// way into _cgo_export.h.
func (f *File) DiscardCgoDirectives() {
// ProcessCgoDirectives processes the import C preamble:
// 1. discards all #cgo CFLAGS, LDFLAGS, nocallback and noescape directives,
// so they don't make their way into _cgo_export.h.
// 2. parse the nocallback and noescape directives.
func (f *File) ProcessCgoDirectives() {
linesIn := strings.Split(f.Preamble, "\n")
linesOut := make([]string, 0, len(linesIn))
f.NoCallbacks = make(map[string]bool)
f.NoEscapes = make(map[string]bool)
for _, line := range linesIn {
l := strings.TrimSpace(line)
if len(l) < 5 || l[:4] != "#cgo" || !unicode.IsSpace(rune(l[4])) {
linesOut = append(linesOut, line)
} else {
linesOut = append(linesOut, "")

// #cgo (nocallback|noescape) <function name>
if fields := strings.Fields(l); len(fields) == 3 {
directive := fields[1]
funcName := fields[2]
if directive == "nocallback" {
f.NoCallbacks[funcName] = true
} else if directive == "noescape" {
f.NoEscapes[funcName] = true
}
}
}
}
f.Preamble = strings.Join(linesOut, "\n")
Expand Down
28 changes: 20 additions & 8 deletions src/cmd/cgo/internal/testerrors/errors_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,16 +39,23 @@ func check(t *testing.T, file string) {
continue
}

_, frag, ok := bytes.Cut(line, []byte("ERROR HERE: "))
if !ok {
continue
if _, frag, ok := bytes.Cut(line, []byte("ERROR HERE: ")); ok {
re, err := regexp.Compile(fmt.Sprintf(":%d:.*%s", i+1, frag))
if err != nil {
t.Errorf("Invalid regexp after `ERROR HERE: `: %#q", frag)
continue
}
errors = append(errors, re)
}
re, err := regexp.Compile(fmt.Sprintf(":%d:.*%s", i+1, frag))
if err != nil {
t.Errorf("Invalid regexp after `ERROR HERE: `: %#q", frag)
continue

if _, frag, ok := bytes.Cut(line, []byte("ERROR MESSAGE: ")); ok {
re, err := regexp.Compile(string(frag))
if err != nil {
t.Errorf("Invalid regexp after `ERROR MESSAGE: `: %#q", frag)
continue
}
errors = append(errors, re)
}
errors = append(errors, re)
}
if len(errors) == 0 {
t.Fatalf("cannot find ERROR HERE")
Expand Down Expand Up @@ -165,3 +172,8 @@ func TestMallocCrashesOnNil(t *testing.T) {
t.Fatalf("succeeded unexpectedly")
}
}

func TestNotMatchedCFunction(t *testing.T) {
file := "notmatchedcfunction.go"
check(t, file)
}
14 changes: 14 additions & 0 deletions src/cmd/cgo/internal/testerrors/testdata/notmatchedcfunction.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Copyright 2023 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package main

/*
// ERROR MESSAGE: #cgo noescape noMatchedCFunction: no matched C function
#cgo noescape noMatchedCFunction
*/
import "C"

func main() {
}
63 changes: 48 additions & 15 deletions src/cmd/cgo/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ type Package struct {
Preamble string // collected preamble for _cgo_export.h
typedefs map[string]bool // type names that appear in the types of the objects we're interested in
typedefList []typedefInfo
noCallbacks map[string]bool // C function names with #cgo nocallback directive
noEscapes map[string]bool // C function names with #cgo noescape directive
}

// A typedefInfo is an element on Package.typedefList: a typedef name
Expand All @@ -59,16 +61,18 @@ type typedefInfo struct {

// A File collects information about a single Go input file.
type File struct {
AST *ast.File // parsed AST
Comments []*ast.CommentGroup // comments from file
Package string // Package name
Preamble string // C preamble (doc comment on import "C")
Ref []*Ref // all references to C.xxx in AST
Calls []*Call // all calls to C.xxx in AST
ExpFunc []*ExpFunc // exported functions for this file
Name map[string]*Name // map from Go name to Name
NamePos map[*Name]token.Pos // map from Name to position of the first reference
Edit *edit.Buffer
AST *ast.File // parsed AST
Comments []*ast.CommentGroup // comments from file
Package string // Package name
Preamble string // C preamble (doc comment on import "C")
Ref []*Ref // all references to C.xxx in AST
Calls []*Call // all calls to C.xxx in AST
ExpFunc []*ExpFunc // exported functions for this file
Name map[string]*Name // map from Go name to Name
NamePos map[*Name]token.Pos // map from Name to position of the first reference
NoCallbacks map[string]bool // C function names that with #cgo nocallback directive
NoEscapes map[string]bool // C function names that with #cgo noescape directive
Edit *edit.Buffer
}

func (f *File) offset(p token.Pos) int {
Expand Down Expand Up @@ -374,7 +378,7 @@ func main() {
f := new(File)
f.Edit = edit.NewBuffer(b)
f.ParseGo(input, b)
f.DiscardCgoDirectives()
f.ProcessCgoDirectives()
fs[i] = f
}

Expand Down Expand Up @@ -413,6 +417,25 @@ func main() {
p.writeOutput(f, input)
}
}
cFunctions := make(map[string]bool)
for _, key := range nameKeys(p.Name) {
n := p.Name[key]
if n.FuncType != nil {
cFunctions[n.C] = true
}
}

for funcName := range p.noEscapes {
if _, found := cFunctions[funcName]; !found {
error_(token.NoPos, "#cgo noescape %s: no matched C function", funcName)
}
}

for funcName := range p.noCallbacks {
if _, found := cFunctions[funcName]; !found {
error_(token.NoPos, "#cgo nocallback %s: no matched C function", funcName)
}
}

if !*godefs {
p.writeDefs()
Expand Down Expand Up @@ -450,10 +473,12 @@ func newPackage(args []string) *Package {
os.Setenv("LC_ALL", "C")

p := &Package{
PtrSize: ptrSize,
IntSize: intSize,
CgoFlags: make(map[string][]string),
Written: make(map[string]bool),
PtrSize: ptrSize,
IntSize: intSize,
CgoFlags: make(map[string][]string),
Written: make(map[string]bool),
noCallbacks: make(map[string]bool),
noEscapes: make(map[string]bool),
}
p.addToFlag("CFLAGS", args)
return p
Expand Down Expand Up @@ -487,6 +512,14 @@ func (p *Package) Record(f *File) {
}
}

// merge nocallback & noescape
for k, v := range f.NoCallbacks {
p.noCallbacks[k] = v
}
for k, v := range f.NoEscapes {
p.noEscapes[k] = v
}

if f.ExpFunc != nil {
p.ExpFunc = append(p.ExpFunc, f.ExpFunc...)
p.Preamble += "\n" + f.Preamble
Expand Down
28 changes: 23 additions & 5 deletions src/cmd/cgo/out.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,8 @@ func (p *Package) writeDefs() {
fmt.Fprintf(fgo2, "//go:linkname _Cgo_use runtime.cgoUse\n")
fmt.Fprintf(fgo2, "func _Cgo_use(interface{})\n")
}
fmt.Fprintf(fgo2, "//go:linkname _Cgo_no_callback runtime.cgoNoCallback\n")
fmt.Fprintf(fgo2, "func _Cgo_no_callback(bool)\n")

typedefNames := make([]string, 0, len(typedef))
for name := range typedef {
Expand Down Expand Up @@ -612,6 +614,12 @@ func (p *Package) writeDefsFunc(fgo2 io.Writer, n *Name, callsMalloc *bool) {
arg = "uintptr(unsafe.Pointer(&r1))"
}

noCallback := p.noCallbacks[n.C]
if noCallback {
// disable cgocallback, will check it in runtime.
fmt.Fprintf(fgo2, "\t_Cgo_no_callback(true)\n")
}

prefix := ""
if n.AddError {
prefix = "errno := "
Expand All @@ -620,13 +628,21 @@ func (p *Package) writeDefsFunc(fgo2 io.Writer, n *Name, callsMalloc *bool) {
if n.AddError {
fmt.Fprintf(fgo2, "\tif errno != 0 { r2 = syscall.Errno(errno) }\n")
}
fmt.Fprintf(fgo2, "\tif _Cgo_always_false {\n")
if d.Type.Params != nil {
for i := range d.Type.Params.List {
fmt.Fprintf(fgo2, "\t\t_Cgo_use(p%d)\n", i)
if noCallback {
fmt.Fprintf(fgo2, "\t_Cgo_no_callback(false)\n")
}

// skip _Cgo_use when noescape exist,
// so that the compiler won't force to escape them to heap.
if !p.noEscapes[n.C] {
fmt.Fprintf(fgo2, "\tif _Cgo_always_false {\n")
if d.Type.Params != nil {
for i := range d.Type.Params.List {
fmt.Fprintf(fgo2, "\t\t_Cgo_use(p%d)\n", i)
}
}
fmt.Fprintf(fgo2, "\t}\n")
}
fmt.Fprintf(fgo2, "\t}\n")
fmt.Fprintf(fgo2, "\treturn\n")
fmt.Fprintf(fgo2, "}\n")
}
Expand Down Expand Up @@ -1612,9 +1628,11 @@ const goProlog = `
func _cgo_runtime_cgocall(unsafe.Pointer, uintptr) int32

//go:linkname _cgoCheckPointer runtime.cgoCheckPointer
//go:noescape
func _cgoCheckPointer(interface{}, interface{})

//go:linkname _cgoCheckResult runtime.cgoCheckResult
//go:noescape
func _cgoCheckResult(interface{})
`

Expand Down
5 changes: 5 additions & 0 deletions src/cmd/go/internal/modindex/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -622,6 +622,11 @@ func (ctxt *Context) saveCgo(filename string, di *build.Package, text string) er
continue
}

// #cgo (nocallback|noescape) <function name>
if fields := strings.Fields(line); len(fields) == 3 && (fields[1] == "nocallback" || fields[1] == "noescape") {
continue
}

// Split at colon.
line, argstr, ok := strings.Cut(strings.TrimSpace(line[4:]), ":")
if !ok {
Expand Down
5 changes: 5 additions & 0 deletions src/go/build/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -1687,6 +1687,11 @@ func (ctxt *Context) saveCgo(filename string, di *Package, cg *ast.CommentGroup)
continue
}

// #cgo (nocallback|noescape) <function name>
if fields := strings.Fields(line); len(fields) == 3 && (fields[1] == "nocallback" || fields[1] == "noescape") {
continue
}

// Split at colon.
line, argstr, ok := strings.Cut(strings.TrimSpace(line[4:]), ":")
if !ok {
Expand Down
8 changes: 8 additions & 0 deletions src/runtime/cgo.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,11 @@ func cgoUse(any) { throw("cgoUse should not be called") }
var cgoAlwaysFalse bool

var cgo_yield = &_cgo_yield

func cgoNoCallback(v bool) {
g := getg()
if g.nocgocallback && v {
panic("runtime: unexpected setting cgoNoCallback")
}
g.nocgocallback = v
}
4 changes: 4 additions & 0 deletions src/runtime/cgocall.go
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,10 @@ func cgocallbackg(fn, frame unsafe.Pointer, ctxt uintptr) {

osPreemptExtExit(gp.m)

if gp.nocgocallback {
panic("runtime: function marked with #cgo nocallback called back into Go")
}

cgocallbackg1(fn, frame, ctxt) // will call unlockOSThread

// At this point unlockOSThread has been called.
Expand Down
16 changes: 16 additions & 0 deletions src/runtime/crash_cgo_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -753,6 +753,22 @@ func TestNeedmDeadlock(t *testing.T) {
}
}

func TestCgoNoCallback(t *testing.T) {
got := runTestProg(t, "testprogcgo", "CgoNoCallback")
want := "function marked with #cgo nocallback called back into Go"
if !strings.Contains(got, want) {
t.Fatalf("did not see %q in output:\n%s", want, got)
}
}

func TestCgoNoEscape(t *testing.T) {
got := runTestProg(t, "testprogcgo", "CgoNoEscape")
want := "OK\n"
if got != want {
t.Fatalf("want %s, got %s\n", want, got)
}
}

func TestCgoTracebackGoroutineProfile(t *testing.T) {
output := runTestProg(t, "testprogcgo", "GoroutineProfile")
want := "OK\n"
Expand Down
1 change: 1 addition & 0 deletions src/runtime/runtime2.go
Original file line number Diff line number Diff line change
Expand Up @@ -481,6 +481,7 @@ type g struct {
parkingOnChan atomic.Bool

raceignore int8 // ignore race detection events
nocgocallback bool // whether disable callback from C
tracking bool // whether we're tracking this G for sched latency statistics
trackingSeq uint8 // used to decide whether to track this G
trackingStamp int64 // timestamp of when the G last started being tracked
Expand Down
9 changes: 9 additions & 0 deletions src/runtime/testdata/testprogcgo/cgonocallback.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// Copyright 2023 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

#include "_cgo_export.h"

void runCShouldNotCallback() {
CallbackToGo();
}
Loading