From a64f4219e950a14f8314c9a2cba6f5b24be5371b Mon Sep 17 00:00:00 2001 From: Li Jie Date: Thu, 14 Nov 2024 16:13:13 +0800 Subject: [PATCH] cgo: full supports cgo preambles and auto compile c files --- _demo/cgocfiles/cgocfiles.go | 6 - _demo/cgofull/bar.go | 16 ++ _demo/cgofull/cgofull.go | 75 +++++++++ _demo/cgofull/foo.c | 6 + _demo/cgofull/foo.go | 16 ++ _demo/cgofull/foo.h | 7 + _demo/cgofull/py.go | 24 +++ _demo/cgopython/cgopython.go | 5 - cl/_testrt/strlen/out.ll | 1 + cl/_testrt/struct/out.ll | 1 + cl/_testrt/typalias/out.ll | 1 + cl/compile.go | 102 ++++++++++-- cl/import.go | 9 +- cl/instr.go | 34 +++- internal/build/build.go | 43 +++-- internal/build/cgo.go | 295 +++++++++++++++++++++++++++++++++++ 16 files changed, 588 insertions(+), 53 deletions(-) create mode 100644 _demo/cgofull/bar.go create mode 100644 _demo/cgofull/cgofull.go create mode 100644 _demo/cgofull/foo.c create mode 100644 _demo/cgofull/foo.go create mode 100644 _demo/cgofull/foo.h create mode 100644 _demo/cgofull/py.go create mode 100644 internal/build/cgo.go diff --git a/_demo/cgocfiles/cgocfiles.go b/_demo/cgocfiles/cgocfiles.go index 91d21f57..aaef07da 100644 --- a/_demo/cgocfiles/cgocfiles.go +++ b/_demo/cgocfiles/cgocfiles.go @@ -6,12 +6,6 @@ package main import "C" import "fmt" -// TODO(lijie): workaround for c files compiling -const ( - LLGoFiles = "in.c" - LLGoPackage = "link" -) - func main() { r := C.test_structs(&C.s4{a: 1}, &C.s8{a: 1, b: 2}, &C.s12{a: 1, b: 2, c: 3}, &C.s16{a: 1, b: 2, c: 3, d: 4}, &C.s20{a: 1, b: 2, c: 3, d: 4, e: 5}) fmt.Println(r) diff --git a/_demo/cgofull/bar.go b/_demo/cgofull/bar.go new file mode 100644 index 00000000..08e6830f --- /dev/null +++ b/_demo/cgofull/bar.go @@ -0,0 +1,16 @@ +package main + +/* +#cgo CFLAGS: -DBAR +#include +#include "foo.h" +static void foo(Foo* f) { + printf("foo in bar: %d\n", f->a); +} +*/ +import "C" + +func Bar(f *C.Foo) { + C.print_foo(f) + C.foo(f) +} diff --git a/_demo/cgofull/cgofull.go b/_demo/cgofull/cgofull.go new file mode 100644 index 00000000..37f07a48 --- /dev/null +++ b/_demo/cgofull/cgofull.go @@ -0,0 +1,75 @@ +package main + +/* +#include +#include "foo.h" +typedef struct { + int a; +} s4; + +typedef struct { + int a; + int b; +} s8; + +typedef struct { + int a; + int b; + int c; +} s12; + +typedef struct { + int a; + int b; + int c; + int d; +} s16; + +typedef struct { + int a; + int b; + int c; + int d; + int e; +} s20; + +static int test_structs(s4* s4, s8* s8, s12* s12, s16* s16, s20* s20) { + printf("s4.a: %d\n", s4->a); + printf("s8.a: %d, s8.b: %d\n", s8->a, s8->b); + printf("s12.a: %d, s12.b: %d, s12.c: %d\n", s12->a, s12->b, s12->c); + printf("s16.a: %d, s16.b: %d, s16.c: %d, s16.d: %d\n", s16->a, s16->b, s16->c, s16->d); + printf("s20.a: %d, s20.b: %d, s20.c: %d, s20.d: %d, s20.e: %d\n", s20->a, s20->b, s20->c, s20->d, s20->e); + + return s4->a + s8->a + s8->b + s12->a + s12->b + s12->c + s16->a + s16->b + s16->c + s16->d + s20->a + s20->b + s20->c + s20->d + s20->e; +} + +static void test_macros() { +#ifdef FOO + printf("FOO is defined\n"); +#endif +#ifdef BAR + printf("BAR is defined\n"); +#endif +} +*/ +import "C" +import "fmt" + +func main() { + runPy() + f := &C.Foo{a: 1} + Foo(f) + Bar(f) + C.test_macros() + r := C.test_structs(&C.s4{a: 1}, &C.s8{a: 1, b: 2}, &C.s12{a: 1, b: 2, c: 3}, &C.s16{a: 1, b: 2, c: 3, d: 4}, &C.s20{a: 1, b: 2, c: 3, d: 4, e: 5}) + fmt.Println(r) + if r != 35 { + panic("test_structs failed") + } +} + +func runPy() { + Initialize() + defer Finalize() + Run("print('Hello, Python!')") +} diff --git a/_demo/cgofull/foo.c b/_demo/cgofull/foo.c new file mode 100644 index 00000000..b3f07e04 --- /dev/null +++ b/_demo/cgofull/foo.c @@ -0,0 +1,6 @@ +#include +#include "foo.h" + +void print_foo(Foo* f) { + printf("print_foo: %d\n", f->a); +} diff --git a/_demo/cgofull/foo.go b/_demo/cgofull/foo.go new file mode 100644 index 00000000..1d6aa3b4 --- /dev/null +++ b/_demo/cgofull/foo.go @@ -0,0 +1,16 @@ +package main + +/* +#cgo CFLAGS: -DFOO +#include +#include "foo.h" +static void foo(Foo* f) { + printf("foo in bar: %d\n", f->a); +} +*/ +import "C" + +func Foo(f *C.Foo) { + C.print_foo(f) + C.foo(f) +} diff --git a/_demo/cgofull/foo.h b/_demo/cgofull/foo.h new file mode 100644 index 00000000..f714fec6 --- /dev/null +++ b/_demo/cgofull/foo.h @@ -0,0 +1,7 @@ +#pragma once + +typedef struct { + int a; +} Foo; + +extern void print_foo(Foo* f); diff --git a/_demo/cgofull/py.go b/_demo/cgofull/py.go new file mode 100644 index 00000000..509d88ca --- /dev/null +++ b/_demo/cgofull/py.go @@ -0,0 +1,24 @@ +package main + +/* +#cgo pkg-config: python3-embed +#include +*/ +import "C" +import "fmt" + +func Initialize() { + C.Py_Initialize() +} + +func Finalize() { + C.Py_Finalize() +} + +func Run(code string) error { + if C.PyRun_SimpleString(C.CString(code)) != 0 { + C.PyErr_Print() + return fmt.Errorf("failed to run code") + } + return nil +} diff --git a/_demo/cgopython/cgopython.go b/_demo/cgopython/cgopython.go index 2b506b07..be57c5ec 100644 --- a/_demo/cgopython/cgopython.go +++ b/_demo/cgopython/cgopython.go @@ -6,11 +6,6 @@ package main */ import "C" -// TODO(lijie): workaround for cgo pkg-config not working -const ( - LLGoPackage = "link: $LLGO_LIB_PYTHON; $(pkg-config --libs python3-embed)" -) - func main() { C.Py_Initialize() defer C.Py_Finalize() diff --git a/cl/_testrt/strlen/out.ll b/cl/_testrt/strlen/out.ll index 917e6bd9..bd201f26 100644 --- a/cl/_testrt/strlen/out.ll +++ b/cl/_testrt/strlen/out.ll @@ -1,6 +1,7 @@ ; ModuleID = 'main' source_filename = "main" +@"github.com/goplus/llgo/internal/runtime.cgoAlwaysFalse" = external global i1, align 1 @main.format = global [10 x i8] zeroinitializer, align 1 @"main.init$guard" = global i1 false, align 1 @__llgo_argc = global i32 0, align 4 diff --git a/cl/_testrt/struct/out.ll b/cl/_testrt/struct/out.ll index b1cb9d64..bdf1e248 100644 --- a/cl/_testrt/struct/out.ll +++ b/cl/_testrt/struct/out.ll @@ -3,6 +3,7 @@ source_filename = "main" %main.Foo = type { i32, i1 } +@"github.com/goplus/llgo/internal/runtime.cgoAlwaysFalse" = external global i1, align 1 @main.format = global [10 x i8] zeroinitializer, align 1 @"main.init$guard" = global i1 false, align 1 @__llgo_argc = global i32 0, align 4 diff --git a/cl/_testrt/typalias/out.ll b/cl/_testrt/typalias/out.ll index 778f876d..beb826c5 100644 --- a/cl/_testrt/typalias/out.ll +++ b/cl/_testrt/typalias/out.ll @@ -1,6 +1,7 @@ ; ModuleID = 'main' source_filename = "main" +@"github.com/goplus/llgo/internal/runtime.cgoAlwaysFalse" = external global i1, align 1 @main.format = global [10 x i8] zeroinitializer, align 1 @"main.init$guard" = global i1 false, align 1 @__llgo_argc = global i32 0, align 4 diff --git a/cl/compile.go b/cl/compile.go index 5cbd7aa7..480b2868 100644 --- a/cl/compile.go +++ b/cl/compile.go @@ -114,6 +114,11 @@ type context struct { state pkgState inCFunc bool skipall bool + + cgoCalled bool + cgoArgs []llssa.Expr + cgoRet llssa.Expr + cgoFuncs map[string][]string } type pkgState byte @@ -163,7 +168,7 @@ func (p *context) compileMethods(pkg llssa.Package, typ types.Type) { func (p *context) compileGlobal(pkg llssa.Package, gbl *ssa.Global) { typ := globalType(gbl) name, vtype, define := p.varName(gbl.Pkg.Pkg, gbl) - if vtype == pyVar || ignoreName(name) || checkCgo(name) { + if vtype == pyVar || ignoreName(name) { return } if debugInstr { @@ -189,6 +194,10 @@ var ( argvTy = types.NewPointer(types.NewPointer(types.Typ[types.Int8])) ) +func isCgoCfunc(f *ssa.Function) bool { + return strings.HasPrefix(f.Name(), "_Cfunc_") +} + func (p *context) compileFuncDecl(pkg llssa.Package, f *ssa.Function) (llssa.Function, llssa.PyObjRef, int) { pkgTypes, name, ftype := p.funcName(f, true) if ftype != goFunc { @@ -242,9 +251,14 @@ func (p *context) compileFuncDecl(pkg llssa.Package, f *ssa.Function) (llssa.Fun fn.Inline(llssa.NoInline) } } - if nblk := len(f.Blocks); nblk > 0 { - fn.MakeBlocks(nblk) // to set fn.HasBody() = true + p.cgoCalled = false + p.cgoArgs = nil + if isCgoCfunc(f) { + fn.MakeBlocks(1) + } else { + fn.MakeBlocks(nblk) // to set fn.HasBody() = true + } if f.Recover != nil { // set recover block fn.SetRecover(fn.Block(f.Recover.Index)) } @@ -269,8 +283,15 @@ func (p *context) compileFuncDecl(pkg llssa.Package, f *ssa.Function) (llssa.Fun } p.bvals = make(map[ssa.Value]llssa.Expr) off := make([]int, len(f.Blocks)) - for i, block := range f.Blocks { - off[i] = p.compilePhis(b, block) + if isCgoCfunc(f) { + p.cgoArgs = make([]llssa.Expr, len(f.Params)) + for i, param := range f.Params { + p.cgoArgs[i] = p.compileValue(b, param) + } + } else { + for i, block := range f.Blocks { + off[i] = p.compilePhis(b, block) + } } p.blkInfos = blocks.Infos(f.Blocks) i := 0 @@ -279,6 +300,10 @@ func (p *context) compileFuncDecl(pkg llssa.Package, f *ssa.Function) (llssa.Fun doMainInit := (i == 0 && name == "main") doModInit := (i == 1 && isInit) p.compileBlock(b, block, off[i], doMainInit, doModInit) + if isCgoCfunc(f) { + // just process first block for performance + break + } if i = p.blkInfos[i].Next; i < 0 { break } @@ -381,14 +406,69 @@ func (p *context) compileBlock(b llssa.Builder, block *ssa.BasicBlock, n int, do callRuntimeInit(b, pkg) b.Call(pkg.FuncOf("main.init").Expr) } + fname := p.goProg.Fset.Position(block.Parent().Pos()).Filename + if p.cgoFuncs == nil { + p.cgoFuncs = make(map[string][]string) + } + var cgoFuncs []string + if funcs, ok := p.cgoFuncs[fname]; ok { + cgoFuncs = funcs + } + cgoReturned := false for i, instr := range instrs { if i == 1 && doModInit && p.state == pkgInPatch { // in patch package but no pkgFNoOldInit initFnNameOld := initFnNameOfHasPatch(p.fn.Name()) fnOld := pkg.NewFunc(initFnNameOld, llssa.NoArgsNoRet, llssa.InC) b.Call(fnOld.Expr) } - p.compileInstr(b, instr) + if isCgoCfunc(block.Parent()) { + switch instr := instr.(type) { + case *ssa.Alloc: + // return value allocation + p.compileInstr(b, instr) + case *ssa.UnOp: + // load cgo function pointer + if instr.Op == token.MUL && strings.HasPrefix(instr.X.Name(), "_cgo_") { + cgoFuncs = append(cgoFuncs, instr.X.Name()) + p.cgoFuncs[fname] = cgoFuncs + p.compileInstr(b, instr) + } + case *ssa.Call: + // call c function + p.compileInstr(b, instr) + p.cgoCalled = true + case *ssa.Return: + // return cgo function result + if len(instr.Results) > 0 { + b.Return(p.cgoRet) + } else { + b.Return(llssa.Nil) + } + cgoReturned = true + } + } else { + p.compileInstr(b, instr) + } } + // is cgo cfunc but not return yet, some funcs has multiple blocks + if isCgoCfunc(block.Parent()) && !cgoReturned { + if !p.cgoCalled { + panic("cgo cfunc not called") + } + for _, block := range block.Parent().Blocks { + for _, instr := range block.Instrs { + if instr, ok := instr.(*ssa.Return); ok { + if len(instr.Results) > 0 { + b.Return(p.cgoRet) + } else { + b.Return(llssa.Nil) + } + goto end + } + } + } + } +end: if pyModInit { jump := block.Instrs[n+last].(*ssa.Jump) jumpTo := p.jumpTo(jump) @@ -721,7 +801,7 @@ func (p *context) compileInstr(b llssa.Builder, instr ssa.Instruction) { case *ssa.Store: // skip cgo global variables if checkCgo(v.Addr.Name()) { - return + // return } va := v.Addr if va, ok := va.(*ssa.IndexAddr); ok { @@ -896,11 +976,12 @@ type Patches = map[string]Patch // NewPackage compiles a Go package to LLVM IR package. func NewPackage(prog llssa.Program, pkg *ssa.Package, files []*ast.File) (ret llssa.Package, err error) { - return NewPackageEx(prog, nil, pkg, files) + ret, _, err = NewPackageEx(prog, nil, pkg, files) + return } // NewPackageEx compiles a Go package to LLVM IR package. -func NewPackageEx(prog llssa.Program, patches Patches, pkg *ssa.Package, files []*ast.File) (ret llssa.Package, err error) { +func NewPackageEx(prog llssa.Program, patches Patches, pkg *ssa.Package, files []*ast.File) (ret llssa.Package, externs map[string][]string, err error) { pkgProg := pkg.Prog pkgTypes := pkg.Pkg oldTypes := pkgTypes @@ -964,6 +1045,7 @@ func NewPackageEx(prog llssa.Program, patches Patches, pkg *ssa.Package, files [ ctx.initAfter = nil fn() } + externs = ctx.cgoFuncs return } @@ -1003,7 +1085,7 @@ func processPkg(ctx *context, ret llssa.Package, pkg *ssa.Package) { case *ssa.Global: // skip cgo global variables if checkCgo(member.Name()) { - continue + // continue } ctx.compileGlobal(ret, member) } diff --git a/cl/import.go b/cl/import.go index 6a6a632b..d2a16f10 100644 --- a/cl/import.go +++ b/cl/import.go @@ -420,6 +420,8 @@ const ( llgoCgoGoBytes = llgoCgoBase + 0x4 llgoCgoCMalloc = llgoCgoBase + 0x5 llgoCgoCheckPointer = llgoCgoBase + 0x6 + llgoCgoCgocall = llgoCgoBase + 0x7 + llgoCgoUse = llgoCgoBase + 0x8 llgoAtomicOpLast = llgoAtomicOpBase + int(llssa.OpUMin) ) @@ -433,15 +435,16 @@ func (p *context) funcName(fn *ssa.Function, ignore bool) (*types.Package, strin orgName = funcName(pkg, origin, true) } else { fname := fn.Name() - if fname == "_cgoCheckPointer" { + if checkCgo(fname) { return nil, fname, llgoInstr } if strings.HasPrefix(fname, "_Cfunc_") { if _, ok := llgoInstrs[fname]; ok { return nil, fname, llgoInstr } - fname = fname[7:] - return nil, fname, cFunc + // fname = fname[7:] + // fn.WriteTo(os.Stdout) + // return nil, fname, cFunc } if fnPkg := fn.Pkg; fnPkg != nil { pkg = fnPkg.Pkg diff --git a/cl/instr.go b/cl/instr.go index 93ba5ae9..6ae03ea7 100644 --- a/cl/instr.go +++ b/cl/instr.go @@ -121,6 +121,20 @@ func (p *context) cgoCheckPointer(b llssa.Builder, args []ssa.Value) { // don't need to do anything } +// func _cgo_runtime_cgocall(fn unsafe.Pointer, arg unsafe.Pointer) int +func (p *context) cgoCgocall(b llssa.Builder, args []ssa.Value) (ret llssa.Expr) { + pfn := p.compileValue(b, args[0]) + pfn.Type = p.prog.Pointer(p.fn.Type) + fn := b.Load(pfn) + p.cgoRet = b.Call(fn, p.cgoArgs...) + return p.cgoRet +} + +// func _Cgo_use(v any) +func (p *context) cgoUse(b llssa.Builder, args []ssa.Value) { + // don't need to do anything +} + // ----------------------------------------------------------------------------- // func index(arr *T, idx int) T @@ -302,13 +316,15 @@ var llgoInstrs = map[string]int{ "atomicUMax": int(llgoAtomicUMax), "atomicUMin": int(llgoAtomicUMin), - "_Cfunc_CString": llgoCgoCString, - "_Cfunc_CBytes": llgoCgoCBytes, - "_Cfunc_GoString": llgoCgoGoString, - "_Cfunc_GoStringN": llgoCgoGoStringN, - "_Cfunc_GoBytes": llgoCgoGoBytes, - "_Cfunc__CMalloc": llgoCgoCMalloc, - "_cgoCheckPointer": llgoCgoCheckPointer, + "_Cfunc_CString": llgoCgoCString, + "_Cfunc_CBytes": llgoCgoCBytes, + "_Cfunc_GoString": llgoCgoGoString, + "_Cfunc_GoStringN": llgoCgoGoStringN, + "_Cfunc_GoBytes": llgoCgoGoBytes, + "_Cfunc__CMalloc": llgoCgoCMalloc, + "_cgoCheckPointer": llgoCgoCheckPointer, + "_cgo_runtime_cgocall": llgoCgoCgocall, + "_Cgo_use": llgoCgoUse, } // funcOf returns a function by name and set ftype = goFunc, cFunc, etc. @@ -450,6 +466,10 @@ func (p *context) call(b llssa.Builder, act llssa.DoAction, call *ssa.CallCommon ret = p.cgoCMalloc(b, args) case llgoCgoCheckPointer: p.cgoCheckPointer(b, args) + case llgoCgoCgocall: + p.cgoCgocall(b, args) + case llgoCgoUse: + p.cgoUse(b, args) case llgoAdvance: ret = p.advance(b, args) case llgoIndex: diff --git a/internal/build/build.go b/internal/build/build.go index 8a81727f..c74141fb 100644 --- a/internal/build/build.go +++ b/internal/build/build.go @@ -286,8 +286,14 @@ func buildAllPkgs(ctx *context, initial []*packages.Package, verbose bool) (pkgs pkg.ExportFile = "" case cl.PkgLinkIR, cl.PkgLinkExtern, cl.PkgPyModule: if len(pkg.GoFiles) > 0 { - buildPkg(ctx, aPkg, verbose) - pkg.ExportFile = " " + concatPkgLinkFiles(ctx, pkg, verbose) + " " + pkg.ExportFile + cgoParts, err := buildPkg(ctx, aPkg, verbose) + if err != nil { + panic(err) + } + linkParts := concatPkgLinkFiles(ctx, pkg, verbose) + allParts := append(linkParts, cgoParts...) + allParts = append(allParts, pkg.ExportFile) + pkg.ExportFile = " " + strings.Join(allParts, " ") } else { // panic("todo") // TODO(xsw): support packages out of llgo @@ -337,7 +343,12 @@ func buildAllPkgs(ctx *context, initial []*packages.Package, verbose bool) (pkgs } } default: - buildPkg(ctx, aPkg, verbose) + cgoParts, err := buildPkg(ctx, aPkg, verbose) + if err != nil { + panic(err) + } + allParts := append(cgoParts, pkg.ExportFile) + pkg.ExportFile = " " + strings.Join(allParts, " ") setNeedRuntimeOrPyInit(pkg, prog.NeedRuntime, prog.NeedPyInit) } } @@ -483,7 +494,7 @@ func linkMainPkg(ctx *context, pkg *packages.Package, pkgs []*aPackage, llFiles return } -func buildPkg(ctx *context, aPkg *aPackage, verbose bool) { +func buildPkg(ctx *context, aPkg *aPackage, verbose bool) (cgoParts []string, err error) { pkg := aPkg.Package pkgPath := pkg.PkgPath if debugBuild || verbose { @@ -503,12 +514,13 @@ func buildPkg(ctx *context, aPkg *aPackage, verbose bool) { cl.SetDebug(cl.DbgFlagAll) } - ret, err := cl.NewPackageEx(ctx.prog, ctx.patches, aPkg.SSA, syntax) + ret, externs, err := cl.NewPackageEx(ctx.prog, ctx.patches, aPkg.SSA, syntax) if showDetail { llssa.SetDebug(0) cl.SetDebug(0) } check(err) + cgoParts, err = parseCgo(ctx, aPkg, aPkg.Package.Syntax, externs, verbose) if needLLFile(ctx.mode) { pkg.ExportFile += ".ll" os.WriteFile(pkg.ExportFile, []byte(ret.String()), 0644) @@ -517,6 +529,7 @@ func buildPkg(ctx *context, aPkg *aPackage, verbose bool) { } } aPkg.LPkg = ret + return } const ( @@ -690,25 +703,11 @@ func isSingleLinkFile(ret string) bool { return len(ret) > 0 && ret[0] != ' ' } -func concatPkgLinkFiles(ctx *context, pkg *packages.Package, verbose bool) string { - var b strings.Builder - var ret string - var n int +func concatPkgLinkFiles(ctx *context, pkg *packages.Package, verbose bool) (parts []string) { llgoPkgLinkFiles(ctx, pkg, func(linkFile string) { - if n == 0 { - ret = linkFile - } else { - b.WriteByte(' ') - b.WriteString(linkFile) - } - n++ + parts = append(parts, linkFile) }, verbose) - if n > 1 { - b.WriteByte(' ') - b.WriteString(ret) - return b.String() - } - return ret + return } // const LLGoFiles = "file1; file2; ..." diff --git a/internal/build/cgo.go b/internal/build/cgo.go new file mode 100644 index 00000000..5b2a59af --- /dev/null +++ b/internal/build/cgo.go @@ -0,0 +1,295 @@ +/* + * 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" + "os" + "os/exec" + "path/filepath" + "regexp" + "strings" +) + +type cgoDecl struct { + platform 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 parseCgo(ctx *context, pkg *aPackage, files []*ast.File, externs map[string][]string, verbose bool) (cgoParts []string, err error) { + cfiles, preambles, cdecls, err := parseCgo_(pkg, files) + if err != nil { + return + } + cflags := []string{} + ldflags := []string{} + for _, cdecl := range cdecls { + cflags = append(cflags, cdecl.cflags) + 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) { + cgoParts = append(cgoParts, linkFile) + }, verbose) + } + re := regexp.MustCompile(`^(_cgo_[^_]+_Cfunc_)(.*)$`) + cgoFuncs := make(map[string]string) + mallocFix := false + for _, funcs := range externs { + for _, funcName := range funcs { + if m := re.FindStringSubmatch(funcName); len(m) > 0 { + cgoFuncs[funcName] = m[2] + // fix missing _cgo_9113e32b6599_Cfunc__Cmalloc + if !mallocFix { + pkgPrefix := m[1] + mallocName := pkgPrefix + "_Cmalloc" + cgoFuncs[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 := genExternDeclsByClang(code, cflags, cgoFuncs) + 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) { + cgoParts = append(cgoParts, linkFile) + }, verbose) + } + cgoParts = append(cgoParts, ldflags...) + 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(src string, cflags []string, cgoFuncs map[string]string) string { + tmpSrc, err := os.CreateTemp("", "cgo-src-*.c") + if err != nil { + return "" + } + defer os.Remove(tmpSrc.Name()) + if err := os.WriteFile(tmpSrc.Name(), []byte(src), 0644); err != nil { + return "" + } + args := append([]string{"-Xclang", "-ast-dump=json", "-fsyntax-only"}, cflags...) + args = append(args, tmpSrc.Name()) + cmd := exec.Command("clang", args...) + output, err := cmd.Output() + if err != nil { + return "" + } + var astRoot clangASTNode + if err := json.Unmarshal(output, &astRoot); err != nil { + return "" + } + // Extract just function names + funcNames := make(map[string]bool) + extractFuncNames(&astRoot, funcNames) + + b := strings.Builder{} + // Create a list of functions to remove + var toRemove []string + // Process cgoFuncs and build assignments + for cgoFunc, funcName := range cgoFuncs { + if funcNames[funcName] { + // Only generate the assignment, not the extern declaration + b.WriteString(fmt.Sprintf("void* %s = (void*)%s;\n", cgoFunc, funcName)) + // Mark this function for removal + toRemove = append(toRemove, cgoFunc) + } + } + // Remove processed functions from cgoFuncs + for _, funcName := range toRemove { + delete(cgoFuncs, funcName) + } + return b.String() +} + +// Simplified function to just collect function names +func extractFuncNames(node *clangASTNode, funcNames map[string]bool) { + if node.Kind == "FunctionDecl" && node.Name != "" { + // Skip functions that are likely internal/system functions + funcNames[node.Name] = true + } + // Recursively process inner nodes + for i := range node.Inner { + extractFuncNames(&node.Inner[i], funcNames) + } +} + +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 { + files, err := filepath.Glob(filepath.Join(dir, "*.c")) + if err != nil { + continue + } + cfiles = append(cfiles, files...) + } + + 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:]) + toks := strings.Split(decl, " ") + var platform, flag string + if len(toks) == 2 { + flag = toks[1] + } else if len(toks) == 3 { + platform, flag = toks[1], toks[2] + } else { + err = fmt.Errorf("invalid cgo directive: %v, toks: %v", line, toks) + return + } + 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{ + platform: platform, + cflags: strings.TrimSpace(string(cflags)), + ldflags: strings.TrimSpace(string(ldflags)), + }) + case "CFLAGS": + cgoDecls = append(cgoDecls, cgoDecl{ + platform: platform, + cflags: arg, + }) + case "LDFLAGS": + cgoDecls = append(cgoDecls, cgoDecl{ + platform: platform, + ldflags: arg, + }) + } + return +}