all: add bare-bones Cgo support

Этот коммит содержится в:
Ayke van Laethem 2018-11-29 13:31:16 +01:00
родитель b99bbc880a
коммит ecf6ffa62e
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: E97FF5335DFDFDED
11 изменённых файлов: 408 добавлений и 9 удалений

Просмотреть файл

@ -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:

Просмотреть файл

@ -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

Просмотреть файл

@ -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 ""
}

234
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".<something> call expressions to literal
// "C.<something>" 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
}

12
loader/libclang-cfuncs.go Обычный файл
Просмотреть файл

@ -0,0 +1,12 @@
package loader
/*
#include <clang-c/Index.h> // 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"

105
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 <clang-c/Index.h> // if this fails, install libclang-7-dev
#include <stdlib.h>
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
}

Просмотреть файл

@ -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
}

Просмотреть файл

@ -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",

9
testdata/cgo/main.c предоставленный Обычный файл
Просмотреть файл

@ -0,0 +1,9 @@
#include <stdint.h>
int32_t fortytwo() {
return 42;
}
int32_t mul(int32_t a, int32_t b) {
return a * b;
}

13
testdata/cgo/main.go предоставленный Обычный файл
Просмотреть файл

@ -0,0 +1,13 @@
package main
/*
#include <stdint.h>
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))
}

2
testdata/cgo/out.txt предоставленный Обычный файл
Просмотреть файл

@ -0,0 +1,2 @@
fortytwo: 42
mul: 15