builder, cgo: support function definitions in CGo headers
For example, the following did not work before but does work with this change: // int add(int a, int b) { // return a + b; // } import "C" func main() { println("add:", C.add(3, 5)) } Even better, the functions in the header are compiled together with the rest of the Go code and so they can be optimized together! Currently, inlining is not yet allowed but const-propagation across functions works. This should be improved in the future.
Этот коммит содержится в:
родитель
138add2b96
коммит
e02727679f
7 изменённых файлов: 86 добавлений и 10 удалений
|
@ -207,6 +207,50 @@ func Build(pkgName, outpath string, config *compileopts.Config, action func(Buil
|
|||
return errors.New("verification error after compiling package " + pkg.ImportPath)
|
||||
}
|
||||
|
||||
// Load bitcode of CGo headers and join the modules together.
|
||||
// This may seem vulnerable to cache problems, but this is not
|
||||
// the case: the Go code that was just compiled already tracks
|
||||
// all C files that are read and hashes them.
|
||||
// These headers could be compiled in parallel but the benefit
|
||||
// is so small that it's probably not worth parallelizing.
|
||||
// Packages are compiled independently anyway.
|
||||
for _, cgoHeader := range pkg.CGoHeaders {
|
||||
// Store the header text in a temporary file.
|
||||
f, err := ioutil.TempFile(dir, "cgosnippet-*.c")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = f.Write([]byte(cgoHeader))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
f.Close()
|
||||
|
||||
// Compile the code (if there is any) to bitcode.
|
||||
flags := append([]string{"-c", "-emit-llvm", "-o", f.Name() + ".bc", f.Name()}, pkg.CFlags...)
|
||||
if config.Options.PrintCommands != nil {
|
||||
config.Options.PrintCommands("clang", flags...)
|
||||
}
|
||||
err = runCCompiler(flags...)
|
||||
if err != nil {
|
||||
return &commandError{"failed to build CGo header", "", err}
|
||||
}
|
||||
|
||||
// Load and link the bitcode.
|
||||
// This makes it possible to optimize the functions defined
|
||||
// in the header together with the Go code. In particular,
|
||||
// this allows inlining. It also ensures there is only one
|
||||
// file per package to cache.
|
||||
headerMod, err := mod.Context().ParseBitcodeFile(f.Name() + ".bc")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load bitcode file: %w", err)
|
||||
}
|
||||
err = llvm.LinkModules(mod, headerMod)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to link module: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Erase all globals that are part of the undefinedGlobals list.
|
||||
// This list comes from the -ldflags="-X pkg.foo=val" option.
|
||||
// Instead of setting the value directly in the AST (which would
|
||||
|
|
37
cgo/cgo.go
37
cgo/cgo.go
|
@ -29,6 +29,7 @@ import (
|
|||
type cgoPackage struct {
|
||||
generated *ast.File
|
||||
generatedPos token.Pos
|
||||
cgoHeaders []string
|
||||
errors []error
|
||||
dir string
|
||||
fset *token.FileSet
|
||||
|
@ -158,10 +159,11 @@ typedef unsigned long long _Cgo_ulonglong;
|
|||
// Process extracts `import "C"` statements from the AST, parses the comment
|
||||
// with libclang, and modifies the AST to use this information. It returns a
|
||||
// newly created *ast.File that should be added to the list of to-be-parsed
|
||||
// files, 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 string, fset *token.FileSet, cflags []string) (*ast.File, []string, []string, map[string][]byte, []error) {
|
||||
// files, the CGo header snippets that should be compiled (for inline
|
||||
// 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 string, fset *token.FileSet, cflags []string) (*ast.File, []string, []string, []string, map[string][]byte, []error) {
|
||||
p := &cgoPackage{
|
||||
dir: dir,
|
||||
fset: fset,
|
||||
|
@ -184,7 +186,7 @@ func Process(files []*ast.File, dir string, fset *token.FileSet, cflags []string
|
|||
// Find the absolute path for this package.
|
||||
packagePath, err := filepath.Abs(fset.File(files[0].Pos()).Name())
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, []error{
|
||||
return nil, nil, nil, nil, nil, []error{
|
||||
scanner.Error{
|
||||
Pos: fset.Position(files[0].Pos()),
|
||||
Msg: "cgo: cannot find absolute path: " + err.Error(), // TODO: wrap this error
|
||||
|
@ -258,6 +260,7 @@ func Process(files []*ast.File, dir string, fset *token.FileSet, cflags []string
|
|||
// Find `import "C"` statements in the file.
|
||||
var statements []*ast.GenDecl
|
||||
for _, f := range files {
|
||||
var cgoHeader string
|
||||
for i := 0; i < len(f.Decls); i++ {
|
||||
decl := f.Decls[i]
|
||||
genDecl, ok := decl.(*ast.GenDecl)
|
||||
|
@ -284,11 +287,33 @@ func Process(files []*ast.File, dir string, fset *token.FileSet, cflags []string
|
|||
// Found a CGo statement.
|
||||
statements = append(statements, genDecl)
|
||||
|
||||
// Store the text of the CGo fragment so it can be compiled. This is
|
||||
// for cases like these, where functions are defined directly in the
|
||||
// header:
|
||||
// // int add(int a, int b) {
|
||||
// // return a + b;
|
||||
// // }
|
||||
// import "C"
|
||||
if genDecl.Doc != nil {
|
||||
position := fset.Position(genDecl.Doc.Pos())
|
||||
lines := []string{fmt.Sprintf("# %d %#v\n", position.Line, position.Filename)}
|
||||
for _, line := range strings.Split(getCommentText(genDecl.Doc), "\n") {
|
||||
if strings.HasPrefix(strings.TrimSpace(line), "#cgo") {
|
||||
line = ""
|
||||
}
|
||||
lines = append(lines, line)
|
||||
}
|
||||
fragment := strings.Join(lines, "\n")
|
||||
cgoHeader += fragment
|
||||
}
|
||||
|
||||
// Remove this import declaration.
|
||||
f.Decls = append(f.Decls[:i], f.Decls[i+1:]...)
|
||||
i--
|
||||
}
|
||||
|
||||
p.cgoHeaders = append(p.cgoHeaders, cgoHeader)
|
||||
|
||||
// Print the AST, for debugging.
|
||||
//ast.Print(fset, f)
|
||||
}
|
||||
|
@ -436,7 +461,7 @@ func Process(files []*ast.File, dir string, fset *token.FileSet, cflags []string
|
|||
// Print the newly generated in-memory AST, for debugging.
|
||||
//ast.Print(fset, p.generated)
|
||||
|
||||
return p.generated, p.cflags, p.ldflags, p.visitedFiles, p.errors
|
||||
return p.generated, p.cgoHeaders, p.cflags, p.ldflags, p.visitedFiles, p.errors
|
||||
}
|
||||
|
||||
// makePathsAbsolute converts some common path compiler flags (-I, -L) from
|
||||
|
|
|
@ -56,7 +56,7 @@ func TestCGo(t *testing.T) {
|
|||
}
|
||||
|
||||
// Process the AST with CGo.
|
||||
cgoAST, _, _, _, cgoErrors := Process([]*ast.File{f}, "testdata", fset, cflags)
|
||||
cgoAST, _, _, _, _, cgoErrors := Process([]*ast.File{f}, "testdata", fset, cflags)
|
||||
|
||||
// Check the AST for type errors.
|
||||
var typecheckErrors []error
|
||||
|
|
|
@ -283,14 +283,14 @@ func CompilePackage(moduleName string, pkg *loader.Package, ssaPkg *ssa.Package,
|
|||
if c.Debug {
|
||||
c.mod.AddNamedMetadataOperand("llvm.module.flags",
|
||||
c.ctx.MDNode([]llvm.Metadata{
|
||||
llvm.ConstInt(c.ctx.Int32Type(), 1, false).ConstantAsMetadata(), // Error on mismatch
|
||||
llvm.ConstInt(c.ctx.Int32Type(), 2, false).ConstantAsMetadata(), // Warning on mismatch
|
||||
c.ctx.MDString("Debug Info Version"),
|
||||
llvm.ConstInt(c.ctx.Int32Type(), 3, false).ConstantAsMetadata(), // DWARF version
|
||||
}),
|
||||
)
|
||||
c.mod.AddNamedMetadataOperand("llvm.module.flags",
|
||||
c.ctx.MDNode([]llvm.Metadata{
|
||||
llvm.ConstInt(c.ctx.Int32Type(), 1, false).ConstantAsMetadata(),
|
||||
llvm.ConstInt(c.ctx.Int32Type(), 7, false).ConstantAsMetadata(), // Max on mismatch
|
||||
c.ctx.MDString("Dwarf Version"),
|
||||
llvm.ConstInt(c.ctx.Int32Type(), 4, false).ConstantAsMetadata(),
|
||||
}),
|
||||
|
|
|
@ -72,6 +72,7 @@ type Package struct {
|
|||
Files []*ast.File
|
||||
FileHashes map[string][]byte
|
||||
CFlags []string // CFlags used during CGo preprocessing (only set if CGo is used)
|
||||
CGoHeaders []string // text above 'import "C"' lines
|
||||
Pkg *types.Package
|
||||
info types.Info
|
||||
}
|
||||
|
@ -397,8 +398,9 @@ func (p *Package) parseFiles() ([]*ast.File, error) {
|
|||
if p.program.clangHeaders != "" {
|
||||
initialCFlags = append(initialCFlags, "-Xclang", "-internal-isystem", "-Xclang", p.program.clangHeaders)
|
||||
}
|
||||
generated, cflags, ldflags, accessedFiles, errs := cgo.Process(files, p.program.workingDir, p.program.fset, initialCFlags)
|
||||
generated, headerCode, cflags, ldflags, accessedFiles, errs := cgo.Process(files, p.program.workingDir, p.program.fset, initialCFlags)
|
||||
p.CFlags = append(initialCFlags, cflags...)
|
||||
p.CGoHeaders = headerCode
|
||||
for path, hash := range accessedFiles {
|
||||
p.FileHashes[path] = hash
|
||||
}
|
||||
|
|
4
testdata/cgo/main.go
предоставленный
4
testdata/cgo/main.go
предоставленный
|
@ -10,6 +10,7 @@ int mul(int, int);
|
|||
*/
|
||||
import "C"
|
||||
|
||||
// int headerfunc(int a) { return a + 1; }
|
||||
import "C"
|
||||
|
||||
import "unsafe"
|
||||
|
@ -43,6 +44,9 @@ func main() {
|
|||
println("variadic0:", C.variadic0())
|
||||
println("variadic2:", C.variadic2(3, 5))
|
||||
|
||||
// functions in the header C snippet
|
||||
println("headerfunc:", C.headerfunc(5))
|
||||
|
||||
// equivalent types
|
||||
var goInt8 int8 = 5
|
||||
var _ C.int8_t = goInt8
|
||||
|
|
1
testdata/cgo/out.txt
предоставленный
1
testdata/cgo/out.txt
предоставленный
|
@ -15,6 +15,7 @@ callback 1: 50
|
|||
callback 2: 600
|
||||
variadic0: 1
|
||||
variadic2: 15
|
||||
headerfunc: 6
|
||||
bool: true true
|
||||
float: +3.100000e+000
|
||||
double: +3.200000e+000
|
||||
|
|
Загрузка…
Создание таблицы
Сослаться в новой задаче