cgo: support full cgo tags

This commit is contained in:
Li Jie
2024-11-15 12:24:46 +08:00
parent a64f4219e9
commit 9f0b3963cb
7 changed files with 344 additions and 50 deletions

View File

@@ -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 <stdio.h>
#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"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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