From 9f0b3963cbb2033b241d17d949ed1a06cf58ed24 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Fri, 15 Nov 2024 12:24:46 +0800 Subject: [PATCH] cgo: support full cgo tags --- _demo/cgofull/cgofull.go | 20 ++++ cl/compile.go | 8 -- cl/import.go | 5 +- internal/build/build.go | 5 +- internal/build/cgo.go | 89 +++++++++------- internal/buildtags/buildtags.go | 114 ++++++++++++++++++++ internal/buildtags/buildtags_test.go | 153 +++++++++++++++++++++++++++ 7 files changed, 344 insertions(+), 50 deletions(-) create mode 100644 internal/buildtags/buildtags.go create mode 100644 internal/buildtags/buildtags_test.go diff --git a/_demo/cgofull/cgofull.go b/_demo/cgofull/cgofull.go index 37f07a48..82c5e0f6 100644 --- a/_demo/cgofull/cgofull.go +++ b/_demo/cgofull/cgofull.go @@ -1,6 +1,11 @@ package main /* +#cgo windows,!amd64 CFLAGS: -D_WIN32 +#cgo !windows CFLAGS: -D_POSIX +#cgo windows,amd64 CFLAGS: -D_WIN64 +#cgo linux,amd64 CFLAGS: -D_LINUX64 +#cgo !windows,amd64 CFLAGS: -D_UNIX64 #include #include "foo.h" typedef struct { @@ -50,6 +55,21 @@ static void test_macros() { #ifdef BAR printf("BAR is defined\n"); #endif +#ifdef _WIN32 + printf("WIN32 is defined\n"); +#endif +#ifdef _POSIX + printf("POSIX is defined\n"); +#endif +#ifdef _WIN64 + printf("WIN64 is defined\n"); +#endif +#ifdef _LINUX64 + printf("LINUX64 is defined\n"); +#endif +#ifdef _UNIX64 + printf("UNIX64 is defined\n"); +#endif } */ import "C" diff --git a/cl/compile.go b/cl/compile.go index 480b2868..28a92470 100644 --- a/cl/compile.go +++ b/cl/compile.go @@ -799,10 +799,6 @@ func (p *context) compileInstr(b llssa.Builder, instr ssa.Instruction) { } switch v := instr.(type) { case *ssa.Store: - // skip cgo global variables - if checkCgo(v.Addr.Name()) { - // return - } va := v.Addr if va, ok := va.(*ssa.IndexAddr); ok { if args, ok := p.isVArgs(va.X); ok { // varargs: this is a varargs store @@ -1083,10 +1079,6 @@ func processPkg(ctx *context, ret llssa.Package, pkg *ssa.Package) { case *ssa.Type: ctx.compileType(ret, member) case *ssa.Global: - // skip cgo global variables - if checkCgo(member.Name()) { - // continue - } ctx.compileGlobal(ret, member) } } diff --git a/cl/import.go b/cl/import.go index d2a16f10..fd241849 100644 --- a/cl/import.go +++ b/cl/import.go @@ -438,13 +438,10 @@ func (p *context) funcName(fn *ssa.Function, ignore bool) (*types.Package, strin if checkCgo(fname) { return nil, fname, llgoInstr } - if strings.HasPrefix(fname, "_Cfunc_") { + if isCgoCfunc(fn) { if _, ok := llgoInstrs[fname]; ok { return nil, fname, llgoInstr } - // fname = fname[7:] - // fn.WriteTo(os.Stdout) - // return nil, fname, cFunc } if fnPkg := fn.Pkg; fnPkg != nil { pkg = fnPkg.Pkg diff --git a/internal/build/build.go b/internal/build/build.go index c74141fb..ca7cd220 100644 --- a/internal/build/build.go +++ b/internal/build/build.go @@ -201,7 +201,7 @@ func Do(args []string, conf *Config) { env := llvm.New("") os.Setenv("PATH", env.BinDir()+":"+os.Getenv("PATH")) // TODO(xsw): check windows - ctx := &context{env, progSSA, prog, dedup, patches, make(map[string]none), initial, mode, 0} + ctx := &context{env, cfg, progSSA, prog, dedup, patches, make(map[string]none), initial, mode, 0} pkgs := buildAllPkgs(ctx, initial, verbose) var llFiles []string @@ -249,6 +249,7 @@ const ( type context struct { env *llvm.Env + conf *packages.Config progSSA *ssa.Program prog llssa.Program dedup packages.Deduper @@ -520,7 +521,7 @@ func buildPkg(ctx *context, aPkg *aPackage, verbose bool) (cgoParts []string, er cl.SetDebug(0) } check(err) - cgoParts, err = parseCgo(ctx, aPkg, aPkg.Package.Syntax, externs, verbose) + cgoParts, err = buildCgo(ctx, aPkg, aPkg.Package.Syntax, externs, verbose) if needLLFile(ctx.mode) { pkg.ExportFile += ".ll" os.WriteFile(pkg.ExportFile, []byte(ret.String()), 0644) diff --git a/internal/build/cgo.go b/internal/build/cgo.go index 5b2a59af..02d72ed6 100644 --- a/internal/build/cgo.go +++ b/internal/build/cgo.go @@ -26,12 +26,14 @@ import ( "path/filepath" "regexp" "strings" + + "github.com/goplus/llgo/internal/buildtags" ) type cgoDecl struct { - platform string - cflags string - ldflags string + tag string + cflags string + ldflags string } type cgoPreamble struct { @@ -48,16 +50,29 @@ static void* _Cmalloc(size_t size) { ` ) -func parseCgo(ctx *context, pkg *aPackage, files []*ast.File, externs map[string][]string, verbose bool) (cgoParts []string, err error) { +func buildCgo(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 } + 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 { - cflags = append(cflags, cdecl.cflags) - ldflags = append(ldflags, cdecl.ldflags) + if cdecl.tag == "" || tagUsed[cdecl.tag] { + if cdecl.cflags != "" { + cflags = append(cflags, cdecl.cflags) + } + if cdecl.ldflags != "" { + ldflags = append(ldflags, cdecl.ldflags) + } + } } incDirs := make(map[string]none) for _, preamble := range preambles { @@ -95,7 +110,7 @@ func parseCgo(ctx *context, pkg *aPackage, files []*ast.File, externs map[string return nil, fmt.Errorf("failed to create temp file: %v", err) } tmpName := tmpFile.Name() - // defer os.Remove(tmpName) + 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 { @@ -136,38 +151,29 @@ func genExternDeclsByClang(src string, cflags []string, cgoFuncs map[string]stri 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) + for _, inner := range node.Inner { + if inner.Kind == "FunctionDecl" && inner.Name != "" { + funcNames[inner.Name] = true + } } } @@ -251,18 +257,29 @@ func parseCgoDecl(line string) (cgoDecls []cgoDecl, err error) { 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) + + // 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() @@ -276,19 +293,19 @@ func parseCgoDecl(line string) (cgoDecls []cgoDecl, err error) { return } cgoDecls = append(cgoDecls, cgoDecl{ - platform: platform, - cflags: strings.TrimSpace(string(cflags)), - ldflags: strings.TrimSpace(string(ldflags)), + tag: tag, + cflags: strings.TrimSpace(string(cflags)), + ldflags: strings.TrimSpace(string(ldflags)), }) case "CFLAGS": cgoDecls = append(cgoDecls, cgoDecl{ - platform: platform, - cflags: arg, + tag: tag, + cflags: arg, }) case "LDFLAGS": cgoDecls = append(cgoDecls, cgoDecl{ - platform: platform, - ldflags: arg, + tag: tag, + ldflags: arg, }) } return diff --git a/internal/buildtags/buildtags.go b/internal/buildtags/buildtags.go new file mode 100644 index 00000000..8688e572 --- /dev/null +++ b/internal/buildtags/buildtags.go @@ -0,0 +1,114 @@ +/* + * 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 buildtags + +import ( + "fmt" + "go/build" + "io" + "io/fs" + "strings" +) + +// checkTags checks which build tags are valid by creating virtual test files +// and using build.Context.MatchFile to verify them +func CheckTags(buildFlags []string, testTags map[string]bool) { + buildCtx := build.Default + buildCtx.BuildTags = parseBuildTags(buildFlags) + + // Create virtual filesystem + vfs := &virtualFS{ + files: make(map[string]virtualFile), + } + + // Generate virtual files for each test tag + i := 0 + fileToTag := make(map[string]string) // Map to track which file corresponds to which tag + for tag := range testTags { + fileName := fmt.Sprintf("a%02d.go", i) + content := fmt.Sprintf("// +build %s\n\npackage check\n", tag) + vfs.files[fileName] = virtualFile{ + name: fileName, + content: content, + dir: ".", + } + fileToTag[fileName] = tag + i++ + } + + // Override OpenFile to return our virtual file contents + buildCtx.OpenFile = func(name string) (io.ReadCloser, error) { + if file, ok := vfs.files[name]; ok { + return io.NopCloser(strings.NewReader(file.content)), nil + } + return nil, fs.ErrNotExist + } + + // Check each file against build context + for fileName, tag := range fileToTag { + match, err := buildCtx.MatchFile(".", fileName) + if err == nil && match { + testTags[tag] = true + } + } +} + +// virtualFile represents a virtual build tag check file +type virtualFile struct { + name string + content string + dir string +} + +// virtualFS implements a virtual filesystem for build tag checking +type virtualFS struct { + files map[string]virtualFile +} + +func parseBuildTags(buildFlags []string) []string { + buildTags := make([]string, 0) + // Extract tags from buildFlags + for i := 0; i < len(buildFlags); i++ { + flag := buildFlags[i] + if flag == "-tags" && i+1 < len(buildFlags) { + // Handle "-tags xxx" format + tags := strings.FieldsFunc(buildFlags[i+1], func(r rune) bool { + return r == ',' || r == ' ' + }) + buildTags = append(buildTags, tags...) + i++ // Skip the next item since we've processed it + } else if strings.HasPrefix(flag, "-tags=") { + // Handle "-tags=xxx" format + value := strings.TrimPrefix(flag, "-tags=") + tags := strings.FieldsFunc(value, func(r rune) bool { + return r == ',' || r == ' ' + }) + buildTags = append(buildTags, tags...) + } + } + + // Remove duplicates from tags + seen := make(map[string]bool) + uniqueBuildTags := make([]string, 0, len(buildTags)) + for _, tag := range buildTags { + if !seen[tag] { + seen[tag] = true + uniqueBuildTags = append(uniqueBuildTags, tag) + } + } + return uniqueBuildTags +} diff --git a/internal/buildtags/buildtags_test.go b/internal/buildtags/buildtags_test.go new file mode 100644 index 00000000..70d7c37d --- /dev/null +++ b/internal/buildtags/buildtags_test.go @@ -0,0 +1,153 @@ +package buildtags + +import ( + "reflect" + "runtime" + "testing" +) + +func TestCheckTags(t *testing.T) { + tests := []struct { + name string + buildFlags []string + testTags map[string]bool + want map[string]bool + }{ + { + name: "mywindows tags", + buildFlags: []string{"-tags", "mywindows"}, + testTags: map[string]bool{ + "mywindows": false, + "!mywindows": false, + "mywindows,myamd64": false, + }, + want: map[string]bool{ + "mywindows": true, + "!mywindows": false, + "mywindows,myamd64": runtime.GOARCH == "myamd64", + }, + }, + { + name: "non-mywindows tags", + buildFlags: []string{"-tags", "mylinux"}, + testTags: map[string]bool{ + "mywindows": false, + "!mywindows": false, + "mylinux,myamd64": false, + "!mywindows,myamd64": false, + }, + want: map[string]bool{ + "mywindows": false, + "!mywindows": true, + "mylinux,myamd64": runtime.GOARCH == "myamd64", + "!mywindows,myamd64": runtime.GOARCH == "myamd64", + }, + }, + { + name: "multiple tags", + buildFlags: []string{"-tags", "mywindows,myamd64"}, + testTags: map[string]bool{ + "mywindows": false, + "myamd64": false, + "mywindows,myamd64": false, + "mylinux,myamd64": false, + }, + want: map[string]bool{ + "mywindows": true, + "myamd64": true, + "mywindows,myamd64": true, + "mylinux,myamd64": false, + }, + }, + { + name: "tags with equals format", + buildFlags: []string{"-tags=mywindows,myamd64"}, + testTags: map[string]bool{ + "mywindows": false, + "myamd64": false, + "mywindows,myamd64": false, + }, + want: map[string]bool{ + "mywindows": true, + "myamd64": true, + "mywindows,myamd64": true, + }, + }, + { + name: "complex tag combinations", + buildFlags: []string{"-tags", "mylinux,myamd64"}, + testTags: map[string]bool{ + "mywindows": false, + "!mywindows": false, + "mylinux": false, + "mylinux,myamd64": false, + "!mywindows,myamd64": false, + }, + want: map[string]bool{ + "mywindows": false, + "!mywindows": true, + "mylinux": true, + "mylinux,myamd64": true, + "!mywindows,myamd64": true, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + testTags := make(map[string]bool) + for k := range tt.testTags { + testTags[k] = false + } + + CheckTags(tt.buildFlags, testTags) + + if !reflect.DeepEqual(testTags, tt.want) { + t.Errorf("CheckTags() = %v, want %v", testTags, tt.want) + } + }) + } +} + +func TestParseBuildTags(t *testing.T) { + tests := []struct { + name string + buildFlags []string + want []string + }{ + { + name: "space separated tags", + buildFlags: []string{"-tags", "mywindows myamd64"}, + want: []string{"mywindows", "myamd64"}, + }, + { + name: "equals format", + buildFlags: []string{"-tags=mywindows,myamd64"}, + want: []string{"mywindows", "myamd64"}, + }, + { + name: "multiple -tags flags", + buildFlags: []string{"-tags", "mywindows", "-tags", "myamd64"}, + want: []string{"mywindows", "myamd64"}, + }, + { + name: "duplicate tags", + buildFlags: []string{"-tags", "mywindows myamd64", "-tags=mywindows"}, + want: []string{"mywindows", "myamd64"}, + }, + { + name: "empty tags", + buildFlags: []string{}, + want: []string{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := parseBuildTags(tt.buildFlags) + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("name: %v, parseBuildTags() = %v, want %v", tt.name, got, tt.want) + } + }) + } +}