/* * 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" "github.com/goplus/llgo/internal/safesplit" llssa "github.com/goplus/llgo/ssa" ) type cgoDecl struct { tag string cflags []string ldflags []string } type cgoPreamble struct { goFile string src string } const ( cgoHeader = ` #include static void* _Cmalloc(size_t size) { return malloc(size); } ` ) func buildCgo(ctx *context, pkg *aPackage, files []*ast.File, externs []string, verbose bool) (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) { cgoLdflags = append(cgoLdflags, 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, 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, err } if err = os.WriteFile(tmpName, []byte(code+"\n\n"+externDecls), 0644); err != nil { return nil, err } clFile(ctx, cflags, tmpName, pkg.ExportFile, func(linkFile string) { cgoLdflags = append(cgoLdflags, 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 "", err } defer os.Remove(tmpSrc.Name()) if err := os.WriteFile(tmpSrc.Name(), []byte(src), 0644); err != nil { return "", err } symbolNames := make(map[string]bool) if err := getFuncNames(tmpSrc.Name(), cflags, symbolNames); err != nil { return "", err } macroNames := make(map[string]bool) if err := getMacroNames(tmpSrc.Name(), cflags, macroNames); err != nil { return "", 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...) output, err := cmd.Output() if err != nil { return err } var astRoot clangASTNode if err := json.Unmarshal(output, &astRoot); err != nil { return err } 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 }