From ecf6ffa62ed45791362952caffefeffea4d96ed7 Mon Sep 17 00:00:00 2001 From: Ayke van Laethem Date: Thu, 29 Nov 2018 13:31:16 +0100 Subject: [PATCH] all: add bare-bones Cgo support --- .travis.yml | 2 +- Dockerfile | 2 +- ir/ir.go | 5 + loader/cgo.go | 234 ++++++++++++++++++++++++++++++++++++++ loader/libclang-cfuncs.go | 12 ++ loader/libclang.go | 105 +++++++++++++++++ loader/loader.go | 32 ++++-- target.go | 1 + testdata/cgo/main.c | 9 ++ testdata/cgo/main.go | 13 +++ testdata/cgo/out.txt | 2 + 11 files changed, 408 insertions(+), 9 deletions(-) create mode 100644 loader/cgo.go create mode 100644 loader/libclang-cfuncs.go create mode 100644 loader/libclang.go create mode 100644 testdata/cgo/main.c create mode 100644 testdata/cgo/main.go create mode 100644 testdata/cgo/out.txt diff --git a/.travis.yml b/.travis.yml index 76e63bce..0f221d4e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,7 +7,7 @@ before_install: - echo "deb http://apt.llvm.org/trusty/ llvm-toolchain-trusty-7 main" | sudo tee -a /etc/apt/sources.list - echo "deb http://ppa.launchpad.net/ubuntu-toolchain-r/test/ubuntu trusty main" | sudo tee -a /etc/apt/sources.list - sudo apt-get update -qq - - sudo apt-get install llvm-7-dev clang-7 binutils-arm-none-eabi qemu-system-arm --allow-unauthenticated -y + - sudo apt-get install llvm-7-dev clang-7 libclang-7-dev binutils-arm-none-eabi qemu-system-arm --allow-unauthenticated -y - sudo ln -s /usr/bin/clang-7 /usr/local/bin/cc # work around missing -no-pie in old GCC version install: diff --git a/Dockerfile b/Dockerfile index 21c3e785..2dcf01f0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,7 +4,7 @@ FROM golang:latest AS tinygo-base RUN wget -O- https://apt.llvm.org/llvm-snapshot.gpg.key| apt-key add - && \ echo "deb http://apt.llvm.org/stretch/ llvm-toolchain-stretch-7 main" >> /etc/apt/sources.list && \ apt-get update && \ - apt-get install -y llvm-7-dev + apt-get install -y llvm-7-dev libclang-7-dev RUN wget -O- https://raw.githubusercontent.com/golang/dep/master/install.sh | sh diff --git a/ir/ir.go b/ir/ir.go index 91822695..242755ab 100644 --- a/ir/ir.go +++ b/ir/ir.go @@ -362,8 +362,13 @@ func (f *Function) LinkName() string { func (f *Function) CName() string { name := f.Name() if strings.HasPrefix(name, "_Cfunc_") { + // emitted by `go tool cgo` return name[len("_Cfunc_"):] } + if strings.HasPrefix(name, "C.") { + // created by ../loader/cgo.go + return name[2:] + } return "" } diff --git a/loader/cgo.go b/loader/cgo.go new file mode 100644 index 00000000..dc7d0824 --- /dev/null +++ b/loader/cgo.go @@ -0,0 +1,234 @@ +package loader + +// This file extracts the `import "C"` statement from the source and modifies +// the AST for Cgo. It does not use libclang directly (see libclang.go). + +import ( + "go/ast" + "go/token" + "strconv" +) + +// fileInfo holds all Cgo-related information of a given *ast.File. +type fileInfo struct { + *ast.File + filename string + functions []*functionInfo + types []string + importCPos token.Pos +} + +// functionInfo stores some information about a Cgo function found by libclang +// and declared in the AST. +type functionInfo struct { + name string + args []paramInfo + result string +} + +// paramInfo is a parameter of a Cgo function (see functionInfo). +type paramInfo struct { + name string + typeName string +} + +// aliasInfo encapsulates aliases between C and Go, like C.int32_t -> int32. See +// addTypeAliases. +type aliasInfo struct { + typeName string + goTypeName string +} + +// processCgo extracts the `import "C"` statement from the AST, parses the +// comment with libclang, and modifies the AST to use this information. +func (p *Package) processCgo(filename string, f *ast.File) error { + info := &fileInfo{ + File: f, + filename: filename, + } + + // Find `import "C"` statements in the file. + for i := 0; i < len(f.Decls); i++ { + decl := f.Decls[i] + genDecl, ok := decl.(*ast.GenDecl) + if !ok { + continue + } + if len(genDecl.Specs) != 1 { + continue + } + spec, ok := genDecl.Specs[0].(*ast.ImportSpec) + if !ok { + continue + } + path, err := strconv.Unquote(spec.Path.Value) + if err != nil { + panic("could not parse import path: " + err.Error()) + } + if path != "C" { + continue + } + cgoComment := genDecl.Doc.Text() + + // Stored for later use by generated functions, to use a somewhat sane + // source location. + info.importCPos = spec.Path.ValuePos + + err = info.parseFragment(cgoComment) + if err != nil { + return err + } + + // Remove this import declaration. + f.Decls = append(f.Decls[:i], f.Decls[i+1:]...) + i-- + } + + // Print the AST, for debugging. + //ast.Print(p.fset, f) + + // Declare functions found by libclang. + info.addFuncDecls() + + // Forward C types to Go types (like C.uint32_t -> uint32). + info.addTypeAliases() + + // Patch the AST to use the declared types and functions. + ast.Inspect(f, info.walker) + + return nil +} + +// addFuncDecls adds the C function declarations found by libclang in the +// comment above the `import "C"` statement. +func (info *fileInfo) addFuncDecls() { + // TODO: replace all uses of importCPos with the real locations from + // libclang. + for _, fn := range info.functions { + obj := &ast.Object{ + Kind: ast.Fun, + Name: "C." + fn.name, + } + args := make([]*ast.Field, len(fn.args)) + decl := &ast.FuncDecl{ + Name: &ast.Ident{ + NamePos: info.importCPos, + Name: "C." + fn.name, + Obj: obj, + }, + Type: &ast.FuncType{ + Func: info.importCPos, + Params: &ast.FieldList{ + Opening: info.importCPos, + List: args, + Closing: info.importCPos, + }, + Results: &ast.FieldList{ + List: []*ast.Field{ + &ast.Field{ + Type: &ast.Ident{ + NamePos: info.importCPos, + Name: "C." + fn.result, + }, + }, + }, + }, + }, + } + obj.Decl = decl + for i, arg := range fn.args { + args[i] = &ast.Field{ + Names: []*ast.Ident{ + &ast.Ident{ + NamePos: info.importCPos, + Name: arg.name, + Obj: &ast.Object{ + Kind: ast.Var, + Name: "C." + arg.name, + Decl: decl, + }, + }, + }, + Type: &ast.Ident{ + NamePos: info.importCPos, + Name: "C." + arg.typeName, + }, + } + } + info.Decls = append(info.Decls, decl) + } +} + +// addTypeAliases aliases some built-in Go types with their equivalent C types. +// It adds code like the following to the AST: +// +// type ( +// C.int8_t = int8 +// C.int16_t = int16 +// // ... +// ) +func (info *fileInfo) addTypeAliases() { + aliases := []aliasInfo{ + aliasInfo{"C.int8_t", "int8"}, + aliasInfo{"C.int16_t", "int16"}, + aliasInfo{"C.int32_t", "int32"}, + aliasInfo{"C.int64_t", "int64"}, + aliasInfo{"C.uint8_t", "uint8"}, + aliasInfo{"C.uint16_t", "uint16"}, + aliasInfo{"C.uint32_t", "uint32"}, + aliasInfo{"C.uint64_t", "uint64"}, + aliasInfo{"C.uintptr_t", "uintptr"}, + } + gen := &ast.GenDecl{ + TokPos: info.importCPos, + Tok: token.TYPE, + Lparen: info.importCPos, + Rparen: info.importCPos, + } + for _, alias := range aliases { + obj := &ast.Object{ + Kind: ast.Typ, + Name: alias.typeName, + } + typeSpec := &ast.TypeSpec{ + Name: &ast.Ident{ + NamePos: info.importCPos, + Name: alias.typeName, + Obj: obj, + }, + Assign: info.importCPos, + Type: &ast.Ident{ + NamePos: info.importCPos, + Name: alias.goTypeName, + }, + } + obj.Decl = typeSpec + gen.Specs = append(gen.Specs, typeSpec) + } + info.Decls = append(info.Decls, gen) +} + +// walker replaces all "C". call expressions to literal +// "C." expressions. This is impossible to write in Go (a dot cannot +// be used in the middle of a name) so is used as a new namespace for C call +// expressions. +func (info *fileInfo) walker(node ast.Node) bool { + switch node := node.(type) { + case *ast.CallExpr: + fun, ok := node.Fun.(*ast.SelectorExpr) + if !ok { + return true + } + x, ok := fun.X.(*ast.Ident) + if !ok { + return true + } + if x.Name == "C" { + node.Fun = &ast.Ident{ + NamePos: x.NamePos, + Name: "C." + fun.Sel.Name, + } + } + } + return true +} diff --git a/loader/libclang-cfuncs.go b/loader/libclang-cfuncs.go new file mode 100644 index 00000000..88064528 --- /dev/null +++ b/loader/libclang-cfuncs.go @@ -0,0 +1,12 @@ +package loader + +/* +#include // if this fails, install libclang-7-dev + +// The gateway function +int tinygo_clang_visitor_cgo(CXCursor c, CXCursor parent, CXClientData client_data) { + int tinygo_clang_visitor(CXCursor c, CXCursor parent, CXClientData client_data); + return tinygo_clang_visitor(c, parent, client_data); +} +*/ +import "C" diff --git a/loader/libclang.go b/loader/libclang.go new file mode 100644 index 00000000..ca1d8142 --- /dev/null +++ b/loader/libclang.go @@ -0,0 +1,105 @@ +package loader + +// This file parses a fragment of C with libclang and stores the result for AST +// modification. It does not touch the AST itself. + +import ( + "errors" + "unsafe" +) + +/* +#cgo CFLAGS: -I/usr/lib/llvm-7/include +#cgo LDFLAGS: -L/usr/lib/llvm-7/lib -lclang +#include // if this fails, install libclang-7-dev +#include + +int tinygo_clang_visitor_cgo(CXCursor c, CXCursor parent, CXClientData client_data); +*/ +import "C" + +var globalFileInfo *fileInfo + +func (info *fileInfo) parseFragment(fragment string) error { + index := C.clang_createIndex(0, 1) + defer C.clang_disposeIndex(index) + + filenameC := C.CString("cgo-fake.c") + defer C.free(unsafe.Pointer(filenameC)) + + fragmentC := C.CString(fragment) + defer C.free(unsafe.Pointer(fragmentC)) + + unsavedFile := C.struct_CXUnsavedFile{ + Filename: filenameC, + Length: C.ulong(len(fragment)), + Contents: fragmentC, + } + + var unit C.CXTranslationUnit + errCode := C.clang_parseTranslationUnit2( + index, + filenameC, + (**C.char)(unsafe.Pointer(uintptr(0))), 0, // command line args + &unsavedFile, 1, // unsaved files + C.CXTranslationUnit_None, + &unit) + if errCode != 0 { + panic("loader: failed to parse source with libclang") + } + defer C.clang_disposeTranslationUnit(unit) + + if C.clang_getNumDiagnostics(unit) != 0 { + return errors.New("cgo: libclang cannot parse fragment") + } + + if globalFileInfo != nil { + // There is a race condition here but that doesn't really matter as it + // is a sanity check anyway. + panic("libclang.go cannot be used concurrently yet") + } + globalFileInfo = info + defer func() { + globalFileInfo = nil + }() + + cursor := C.clang_getTranslationUnitCursor(unit) + C.clang_visitChildren(cursor, (*[0]byte)((unsafe.Pointer(C.tinygo_clang_visitor_cgo))), C.CXClientData(uintptr(0))) + + return nil +} + +//export tinygo_clang_visitor +func tinygo_clang_visitor(c, parent C.CXCursor, client_data C.CXClientData) C.int { + info := globalFileInfo + kind := C.clang_getCursorKind(c) + switch kind { + case C.CXCursor_FunctionDecl: + name := getString(C.clang_getCursorSpelling(c)) + cursorType := C.clang_getCursorType(c) + if C.clang_isFunctionTypeVariadic(cursorType) != 0 { + return C.CXChildVisit_Continue // not supported + } + numArgs := C.clang_Cursor_getNumArguments(c) + fn := &functionInfo{name: name} + info.functions = append(info.functions, fn) + for i := C.int(0); i < numArgs; i++ { + arg := C.clang_Cursor_getArgument(c, C.uint(i)) + argName := getString(C.clang_getCursorSpelling(arg)) + argType := C.clang_getArgType(cursorType, C.uint(i)) + argTypeName := getString(C.clang_getTypeSpelling(argType)) + fn.args = append(fn.args, paramInfo{argName, argTypeName}) + } + resultType := C.clang_getCursorResultType(c) + resultTypeName := getString(C.clang_getTypeSpelling(resultType)) + fn.result = resultTypeName + } + return C.CXChildVisit_Continue +} + +func getString(clangString C.CXString) (s string) { + rawString := C.clang_getCString(clangString) + s = C.GoString(rawString) + C.clang_disposeString(clangString) + return +} diff --git a/loader/loader.go b/loader/loader.go index 26a3fb03..c67ad39e 100644 --- a/loader/loader.go +++ b/loader/loader.go @@ -268,10 +268,6 @@ func (p *Package) Check() error { // parseFiles parses the loaded list of files and returns this list. func (p *Package) parseFiles() ([]*ast.File, error) { - if len(p.CgoFiles) != 0 { - return nil, errors.New("loader: todo cgo: " + p.CgoFiles[0]) - } - // TODO: do this concurrently. var files []*ast.File var fileErrs []error @@ -279,9 +275,27 @@ func (p *Package) parseFiles() ([]*ast.File, error) { f, err := p.parseFile(filepath.Join(p.Package.Dir, file), parser.ParseComments) if err != nil { fileErrs = append(fileErrs, err) - } else { - files = append(files, f) + continue } + if err != nil { + fileErrs = append(fileErrs, err) + continue + } + files = append(files, f) + } + for _, file := range p.CgoFiles { + path := filepath.Join(p.Package.Dir, file) + f, err := p.parseFile(path, parser.ParseComments) + if err != nil { + fileErrs = append(fileErrs, err) + continue + } + err = p.processCgo(path, f) + if err != nil { + fileErrs = append(fileErrs, err) + continue + } + files = append(files, f) } if len(fileErrs) != 0 { return nil, Errors{p, fileErrs} @@ -298,7 +312,7 @@ func (p *Package) Import(to string) (*types.Package, error) { if _, ok := p.Imports[to]; ok { return p.Imports[to].Pkg, nil } else { - panic("package not imported: " + to) + return nil, errors.New("package not imported: " + to) } } @@ -309,6 +323,10 @@ func (p *Package) Import(to string) (*types.Package, error) { func (p *Package) importRecursively() error { p.Importing = true for _, to := range p.Package.Imports { + if to == "C" { + // Do Cgo processing in a later stage. + continue + } if _, ok := p.Imports[to]; ok { continue } diff --git a/target.go b/target.go index 117dac35..21286c3a 100644 --- a/target.go +++ b/target.go @@ -155,6 +155,7 @@ func LoadTarget(target string) (*TargetSpec, error) { *spec = TargetSpec{ Triple: target, BuildTags: []string{runtime.GOOS, runtime.GOARCH}, + Compiler: commands["clang"], Linker: "cc", LDFlags: []string{"-no-pie"}, // WARNING: clang < 5.0 requires -nopie Objcopy: "objcopy", diff --git a/testdata/cgo/main.c b/testdata/cgo/main.c new file mode 100644 index 00000000..315805aa --- /dev/null +++ b/testdata/cgo/main.c @@ -0,0 +1,9 @@ +#include + +int32_t fortytwo() { + return 42; +} + +int32_t mul(int32_t a, int32_t b) { + return a * b; +} diff --git a/testdata/cgo/main.go b/testdata/cgo/main.go new file mode 100644 index 00000000..f2c5b533 --- /dev/null +++ b/testdata/cgo/main.go @@ -0,0 +1,13 @@ +package main + +/* +#include +int32_t fortytwo(void); +int32_t mul(int32_t a, int32_t b); +*/ +import "C" + +func main() { + println("fortytwo:", C.fortytwo()) + println("mul:", C.mul(int32(3), 5)) +} diff --git a/testdata/cgo/out.txt b/testdata/cgo/out.txt new file mode 100644 index 00000000..e89fc913 --- /dev/null +++ b/testdata/cgo/out.txt @@ -0,0 +1,2 @@ +fortytwo: 42 +mul: 15