414 lines
11 KiB
Go
414 lines
11 KiB
Go
/*
|
|
* Copyright (c) 2024 The GoPlus Authors (goplus.org). All rights reserved.
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
package build
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"go/ast"
|
|
"go/token"
|
|
"go/types"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"github.com/goplus/llgo/internal/buildtags"
|
|
llssa "github.com/goplus/llgo/ssa"
|
|
"github.com/goplus/llgo/xtool/safesplit"
|
|
)
|
|
|
|
type cgoDecl struct {
|
|
tag string
|
|
cflags []string
|
|
ldflags []string
|
|
}
|
|
|
|
type cgoPreamble struct {
|
|
goFile string
|
|
src string
|
|
}
|
|
|
|
const (
|
|
cgoHeader = `
|
|
#include <stdlib.h>
|
|
static void* _Cmalloc(size_t size) {
|
|
return malloc(size);
|
|
}
|
|
`
|
|
)
|
|
|
|
func buildCgo(ctx *context, pkg *aPackage, files []*ast.File, externs []string, verbose bool) (llfiles, cgoLdflags []string, err error) {
|
|
cfiles, preambles, cdecls, err := parseCgo_(pkg, files)
|
|
if err != nil {
|
|
return
|
|
}
|
|
tagUsed := make(map[string]bool)
|
|
for _, cdecl := range cdecls {
|
|
if cdecl.tag != "" {
|
|
tagUsed[cdecl.tag] = false
|
|
}
|
|
}
|
|
buildtags.CheckTags(ctx.conf.BuildFlags, tagUsed)
|
|
cflags := []string{}
|
|
ldflags := []string{}
|
|
for _, cdecl := range cdecls {
|
|
if cdecl.tag == "" || tagUsed[cdecl.tag] {
|
|
if len(cdecl.cflags) > 0 {
|
|
cflags = append(cflags, cdecl.cflags...)
|
|
}
|
|
if len(cdecl.ldflags) > 0 {
|
|
ldflags = append(ldflags, cdecl.ldflags...)
|
|
}
|
|
}
|
|
}
|
|
incDirs := make(map[string]none)
|
|
for _, preamble := range preambles {
|
|
dir, _ := filepath.Split(preamble.goFile)
|
|
if _, ok := incDirs[dir]; !ok {
|
|
incDirs[dir] = none{}
|
|
cflags = append(cflags, "-I"+dir)
|
|
}
|
|
}
|
|
for _, cfile := range cfiles {
|
|
clFile(ctx, cflags, cfile, pkg.ExportFile, func(linkFile string) {
|
|
llfiles = append(llfiles, linkFile)
|
|
}, verbose)
|
|
}
|
|
re := regexp.MustCompile(`^(_cgo_[^_]+_(Cfunc|Cmacro)_)(.*)$`)
|
|
cgoSymbols := make(map[string]string)
|
|
mallocFix := false
|
|
for _, symbolName := range externs {
|
|
lastPart := symbolName
|
|
lastDot := strings.LastIndex(symbolName, ".")
|
|
if lastDot != -1 {
|
|
lastPart = symbolName[lastDot+1:]
|
|
}
|
|
if strings.HasPrefix(lastPart, "__cgo_") {
|
|
// func ptr var: main.__cgo_func_name
|
|
cgoSymbols[symbolName] = lastPart
|
|
} else if m := re.FindStringSubmatch(symbolName); len(m) > 0 {
|
|
prefix := m[1] // _cgo_hash_(Cfunc|Cmacro)_
|
|
name := m[3] // remaining part
|
|
cgoSymbols[symbolName] = name
|
|
// fix missing _cgo_9113e32b6599_Cfunc__Cmalloc
|
|
if !mallocFix && m[2] == "Cfunc" {
|
|
mallocName := prefix + "_Cmalloc"
|
|
cgoSymbols[mallocName] = "_Cmalloc"
|
|
mallocFix = true
|
|
}
|
|
}
|
|
}
|
|
for _, preamble := range preambles {
|
|
tmpFile, err := os.CreateTemp("", "-cgo-*.c")
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("failed to create temp file: %v", err)
|
|
}
|
|
tmpName := tmpFile.Name()
|
|
defer os.Remove(tmpName)
|
|
code := cgoHeader + "\n\n" + preamble.src
|
|
externDecls, err := genExternDeclsByClang(pkg, code, cflags, cgoSymbols)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("failed to generate extern decls: %v", err)
|
|
}
|
|
if err = os.WriteFile(tmpName, []byte(code+"\n\n"+externDecls), 0644); err != nil {
|
|
return nil, nil, fmt.Errorf("failed to write temp file: %v", err)
|
|
}
|
|
clFile(ctx, cflags, tmpName, pkg.ExportFile, func(linkFile string) {
|
|
llfiles = append(llfiles, linkFile)
|
|
}, verbose)
|
|
}
|
|
for _, ldflag := range ldflags {
|
|
cgoLdflags = append(cgoLdflags, safesplit.SplitPkgConfigFlags(ldflag)...)
|
|
}
|
|
return
|
|
}
|
|
|
|
// clangASTNode represents a node in clang's AST
|
|
type clangASTNode struct {
|
|
Kind string `json:"kind"`
|
|
Name string `json:"name,omitempty"`
|
|
Inner []clangASTNode `json:"inner,omitempty"`
|
|
}
|
|
|
|
func genExternDeclsByClang(pkg *aPackage, src string, cflags []string, cgoSymbols map[string]string) (string, error) {
|
|
tmpSrc, err := os.CreateTemp("", "cgo-src-*.c")
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to create temp file: %v", err)
|
|
}
|
|
defer os.Remove(tmpSrc.Name())
|
|
if err := os.WriteFile(tmpSrc.Name(), []byte(src), 0644); err != nil {
|
|
return "", fmt.Errorf("failed to write temp file: %v", err)
|
|
}
|
|
symbolNames := make(map[string]bool)
|
|
if err := getFuncNames(tmpSrc.Name(), cflags, symbolNames); err != nil {
|
|
return "", fmt.Errorf("failed to get func names: %v", err)
|
|
}
|
|
macroNames := make(map[string]bool)
|
|
if err := getMacroNames(tmpSrc.Name(), cflags, macroNames); err != nil {
|
|
return "", fmt.Errorf("failed to get macro names: %v", err)
|
|
}
|
|
|
|
b := strings.Builder{}
|
|
var toRemove []string
|
|
for cgoName, symbolName := range cgoSymbols {
|
|
if strings.HasPrefix(symbolName, "__cgo_") {
|
|
gofuncName := strings.Replace(cgoName, ".__cgo_", ".", 1)
|
|
gofn := pkg.LPkg.FuncOf(gofuncName)
|
|
cgoVar := pkg.LPkg.VarOf(cgoName)
|
|
if gofn != nil {
|
|
cgoVar.ReplaceAllUsesWith(gofn.Expr)
|
|
} else {
|
|
cfuncName := symbolName[len("__cgo_"):]
|
|
cfn := pkg.LPkg.NewFunc(cfuncName, types.NewSignatureType(nil, nil, nil, nil, nil, false), llssa.InC)
|
|
cgoVar.ReplaceAllUsesWith(cfn.Expr)
|
|
}
|
|
toRemove = append(toRemove, cgoName)
|
|
} else {
|
|
usePtr := ""
|
|
if symbolNames[symbolName] {
|
|
usePtr = "*"
|
|
} else if !macroNames[symbolName] {
|
|
continue
|
|
}
|
|
/* template:
|
|
typeof(fputs)* _cgo_1574167f3838_Cfunc_fputs;
|
|
|
|
__attribute__((constructor))
|
|
static void _init__cgo_1574167f3838_Cfunc_fputs() {
|
|
_cgo_1574167f3838_Cfunc_fputs = fputs;
|
|
}*/
|
|
b.WriteString(fmt.Sprintf(`
|
|
typeof(%s)%s %s;
|
|
|
|
__attribute__((constructor))
|
|
static void _init_%s() {
|
|
%s = %s;
|
|
}
|
|
`,
|
|
symbolName, usePtr, cgoName,
|
|
cgoName,
|
|
cgoName, symbolName))
|
|
toRemove = append(toRemove, cgoName)
|
|
}
|
|
}
|
|
for _, funcName := range toRemove {
|
|
delete(cgoSymbols, funcName)
|
|
}
|
|
return b.String(), nil
|
|
}
|
|
|
|
func getMacroNames(file string, cflags []string, macroNames map[string]bool) error {
|
|
args := append([]string{"-dM", "-E"}, cflags...)
|
|
args = append(args, file)
|
|
cmd := exec.Command("clang", args...)
|
|
output, err := cmd.Output()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, line := range strings.Split(string(output), "\n") {
|
|
if strings.HasPrefix(line, "#define ") {
|
|
define := strings.TrimPrefix(line, "#define ")
|
|
parts := strings.SplitN(define, " ", 2)
|
|
if len(parts) > 1 {
|
|
macroNames[parts[0]] = true
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func getFuncNames(file string, cflags []string, symbolNames map[string]bool) error {
|
|
args := append([]string{"-Xclang", "-ast-dump=json", "-fsyntax-only"}, cflags...)
|
|
args = append(args, file)
|
|
cmd := exec.Command("clang", args...)
|
|
cmd.Stderr = os.Stderr
|
|
output, err := cmd.Output()
|
|
if err != nil {
|
|
dump := "dump failed"
|
|
if tmpFile, err := os.CreateTemp("", "llgo-clang-ast-dump*.log"); err == nil {
|
|
dump = "dump saved to " + tmpFile.Name()
|
|
tmpFile.Write(output)
|
|
tmpFile.Close()
|
|
}
|
|
return fmt.Errorf("failed to run clang: %v, %s", err, dump)
|
|
}
|
|
var astRoot clangASTNode
|
|
if err := json.Unmarshal(output, &astRoot); err != nil {
|
|
dump := "dump failed"
|
|
if tmpFile, err := os.CreateTemp("", "llgo-clang-ast-dump*.log"); err == nil {
|
|
dump = "dump saved to " + tmpFile.Name()
|
|
tmpFile.Write(output)
|
|
tmpFile.Close()
|
|
}
|
|
return fmt.Errorf("failed to unmarshal AST: %v, %s", err, dump)
|
|
}
|
|
|
|
extractFuncNames(&astRoot, symbolNames)
|
|
return nil
|
|
}
|
|
|
|
func extractFuncNames(node *clangASTNode, funcNames map[string]bool) {
|
|
for _, inner := range node.Inner {
|
|
if inner.Kind == "FunctionDecl" && inner.Name != "" {
|
|
funcNames[inner.Name] = true
|
|
}
|
|
}
|
|
}
|
|
|
|
func parseCgo_(pkg *aPackage, files []*ast.File) (cfiles []string, preambles []cgoPreamble, cdecls []cgoDecl, err error) {
|
|
dirs := make(map[string]none)
|
|
for _, file := range files {
|
|
pos := pkg.Fset.Position(file.Name.NamePos)
|
|
dir, _ := filepath.Split(pos.Filename)
|
|
dirs[dir] = none{}
|
|
}
|
|
for dir := range dirs {
|
|
matches, err := filepath.Glob(filepath.Join(dir, "*.c"))
|
|
if err != nil {
|
|
continue
|
|
}
|
|
for _, match := range matches {
|
|
if strings.HasSuffix(match, "_test.c") {
|
|
continue
|
|
}
|
|
if fi, err := os.Stat(match); err == nil && !fi.IsDir() {
|
|
cfiles = append(cfiles, match)
|
|
}
|
|
}
|
|
}
|
|
|
|
for _, file := range files {
|
|
for _, decl := range file.Decls {
|
|
switch decl := decl.(type) {
|
|
case *ast.GenDecl:
|
|
if decl.Tok == token.IMPORT {
|
|
if doc := decl.Doc; doc != nil && len(decl.Specs) == 1 {
|
|
spec := decl.Specs[0].(*ast.ImportSpec)
|
|
if spec.Path.Value == "\"unsafe\"" {
|
|
pos := pkg.Fset.Position(doc.Pos())
|
|
preamble, flags, err := parseCgoPreamble(pos, doc.Text())
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
preambles = append(preambles, preamble)
|
|
cdecls = append(cdecls, flags...)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
func parseCgoPreamble(pos token.Position, text string) (preamble cgoPreamble, decls []cgoDecl, err error) {
|
|
b := strings.Builder{}
|
|
fline := pos.Line
|
|
fname := pos.Filename
|
|
b.WriteString(fmt.Sprintf("#line %d %q\n", fline, fname))
|
|
|
|
for _, line := range strings.Split(text, "\n") {
|
|
fline++
|
|
line = strings.TrimSpace(line)
|
|
if strings.HasPrefix(line, "#cgo ") {
|
|
var cgoDecls []cgoDecl
|
|
cgoDecls, err = parseCgoDecl(line)
|
|
if err != nil {
|
|
return
|
|
}
|
|
decls = append(decls, cgoDecls...)
|
|
b.WriteString(fmt.Sprintf("#line %d %q\n", fline, fname))
|
|
} else {
|
|
b.WriteString(line)
|
|
b.WriteString("\n")
|
|
}
|
|
}
|
|
preamble = cgoPreamble{
|
|
goFile: pos.Filename,
|
|
src: b.String(),
|
|
}
|
|
return
|
|
}
|
|
|
|
// Parse cgo directive like:
|
|
// #cgo pkg-config: python3
|
|
// #cgo windows CFLAGS: -IC:/Python312/include
|
|
// #cgo windows LDFLAGS: -LC:/Python312/libs -lpython312
|
|
// #cgo CFLAGS: -I/usr/include/python3.12
|
|
// #cgo LDFLAGS: -L/usr/lib/python3.12/config-3.12-x86_64-linux-gnu -lpython3.12
|
|
func parseCgoDecl(line string) (cgoDecls []cgoDecl, err error) {
|
|
idx := strings.Index(line, ":")
|
|
if idx == -1 {
|
|
err = fmt.Errorf("invalid cgo format: %v", line)
|
|
return
|
|
}
|
|
|
|
decl := strings.TrimSpace(line[:idx])
|
|
arg := strings.TrimSpace(line[idx+1:])
|
|
|
|
// Split on first space to remove #cgo
|
|
parts := strings.SplitN(decl, " ", 2)
|
|
if len(parts) < 2 {
|
|
err = fmt.Errorf("invalid cgo directive: %v", line)
|
|
return
|
|
}
|
|
|
|
// Process remaining part
|
|
remaining := strings.TrimSpace(parts[1])
|
|
var tag, flag string
|
|
|
|
// Split on last space to get flag
|
|
if lastSpace := strings.LastIndex(remaining, " "); lastSpace != -1 {
|
|
tag = strings.TrimSpace(remaining[:lastSpace])
|
|
flag = strings.TrimSpace(remaining[lastSpace+1:])
|
|
} else {
|
|
flag = remaining
|
|
}
|
|
|
|
switch flag {
|
|
case "pkg-config":
|
|
ldflags, e := exec.Command("pkg-config", "--libs", arg).Output()
|
|
if e != nil {
|
|
err = fmt.Errorf("pkg-config: %v", e)
|
|
return
|
|
}
|
|
cflags, e := exec.Command("pkg-config", "--cflags", arg).Output()
|
|
if e != nil {
|
|
err = fmt.Errorf("pkg-config: %v", e)
|
|
return
|
|
}
|
|
cgoDecls = append(cgoDecls, cgoDecl{
|
|
tag: tag,
|
|
cflags: safesplit.SplitPkgConfigFlags(string(cflags)),
|
|
ldflags: safesplit.SplitPkgConfigFlags(string(ldflags)),
|
|
})
|
|
case "CFLAGS":
|
|
cgoDecls = append(cgoDecls, cgoDecl{
|
|
tag: tag,
|
|
cflags: safesplit.SplitPkgConfigFlags(arg),
|
|
})
|
|
case "LDFLAGS":
|
|
cgoDecls = append(cgoDecls, cgoDecl{
|
|
tag: tag,
|
|
ldflags: safesplit.SplitPkgConfigFlags(arg),
|
|
})
|
|
}
|
|
return
|
|
}
|