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 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 <stdio.h>
#include "foo.h" #include "foo.h"
typedef struct { typedef struct {
@@ -50,6 +55,21 @@ static void test_macros() {
#ifdef BAR #ifdef BAR
printf("BAR is defined\n"); printf("BAR is defined\n");
#endif #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" import "C"

View File

@@ -799,10 +799,6 @@ func (p *context) compileInstr(b llssa.Builder, instr ssa.Instruction) {
} }
switch v := instr.(type) { switch v := instr.(type) {
case *ssa.Store: case *ssa.Store:
// skip cgo global variables
if checkCgo(v.Addr.Name()) {
// return
}
va := v.Addr va := v.Addr
if va, ok := va.(*ssa.IndexAddr); ok { if va, ok := va.(*ssa.IndexAddr); ok {
if args, ok := p.isVArgs(va.X); ok { // varargs: this is a varargs store 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: case *ssa.Type:
ctx.compileType(ret, member) ctx.compileType(ret, member)
case *ssa.Global: case *ssa.Global:
// skip cgo global variables
if checkCgo(member.Name()) {
// continue
}
ctx.compileGlobal(ret, member) 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) { if checkCgo(fname) {
return nil, fname, llgoInstr return nil, fname, llgoInstr
} }
if strings.HasPrefix(fname, "_Cfunc_") { if isCgoCfunc(fn) {
if _, ok := llgoInstrs[fname]; ok { if _, ok := llgoInstrs[fname]; ok {
return nil, fname, llgoInstr return nil, fname, llgoInstr
} }
// fname = fname[7:]
// fn.WriteTo(os.Stdout)
// return nil, fname, cFunc
} }
if fnPkg := fn.Pkg; fnPkg != nil { if fnPkg := fn.Pkg; fnPkg != nil {
pkg = fnPkg.Pkg pkg = fnPkg.Pkg

View File

@@ -201,7 +201,7 @@ func Do(args []string, conf *Config) {
env := llvm.New("") env := llvm.New("")
os.Setenv("PATH", env.BinDir()+":"+os.Getenv("PATH")) // TODO(xsw): check windows 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) pkgs := buildAllPkgs(ctx, initial, verbose)
var llFiles []string var llFiles []string
@@ -249,6 +249,7 @@ const (
type context struct { type context struct {
env *llvm.Env env *llvm.Env
conf *packages.Config
progSSA *ssa.Program progSSA *ssa.Program
prog llssa.Program prog llssa.Program
dedup packages.Deduper dedup packages.Deduper
@@ -520,7 +521,7 @@ func buildPkg(ctx *context, aPkg *aPackage, verbose bool) (cgoParts []string, er
cl.SetDebug(0) cl.SetDebug(0)
} }
check(err) 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) { if needLLFile(ctx.mode) {
pkg.ExportFile += ".ll" pkg.ExportFile += ".ll"
os.WriteFile(pkg.ExportFile, []byte(ret.String()), 0644) os.WriteFile(pkg.ExportFile, []byte(ret.String()), 0644)

View File

@@ -26,12 +26,14 @@ import (
"path/filepath" "path/filepath"
"regexp" "regexp"
"strings" "strings"
"github.com/goplus/llgo/internal/buildtags"
) )
type cgoDecl struct { type cgoDecl struct {
platform string tag string
cflags string cflags string
ldflags string ldflags string
} }
type cgoPreamble struct { 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) cfiles, preambles, cdecls, err := parseCgo_(pkg, files)
if err != nil { if err != nil {
return 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{} cflags := []string{}
ldflags := []string{} ldflags := []string{}
for _, cdecl := range cdecls { for _, cdecl := range cdecls {
cflags = append(cflags, cdecl.cflags) if cdecl.tag == "" || tagUsed[cdecl.tag] {
ldflags = append(ldflags, cdecl.ldflags) if cdecl.cflags != "" {
cflags = append(cflags, cdecl.cflags)
}
if cdecl.ldflags != "" {
ldflags = append(ldflags, cdecl.ldflags)
}
}
} }
incDirs := make(map[string]none) incDirs := make(map[string]none)
for _, preamble := range preambles { 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) return nil, fmt.Errorf("failed to create temp file: %v", err)
} }
tmpName := tmpFile.Name() tmpName := tmpFile.Name()
// defer os.Remove(tmpName) defer os.Remove(tmpName)
code := cgoHeader + "\n\n" + preamble.src code := cgoHeader + "\n\n" + preamble.src
externDecls := genExternDeclsByClang(code, cflags, cgoFuncs) externDecls := genExternDeclsByClang(code, cflags, cgoFuncs)
if err = os.WriteFile(tmpName, []byte(code+"\n\n"+externDecls), 0644); err != nil { 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 { if err := json.Unmarshal(output, &astRoot); err != nil {
return "" return ""
} }
// Extract just function names
funcNames := make(map[string]bool) funcNames := make(map[string]bool)
extractFuncNames(&astRoot, funcNames) extractFuncNames(&astRoot, funcNames)
b := strings.Builder{} b := strings.Builder{}
// Create a list of functions to remove
var toRemove []string var toRemove []string
// Process cgoFuncs and build assignments
for cgoFunc, funcName := range cgoFuncs { for cgoFunc, funcName := range cgoFuncs {
if funcNames[funcName] { if funcNames[funcName] {
// Only generate the assignment, not the extern declaration
b.WriteString(fmt.Sprintf("void* %s = (void*)%s;\n", cgoFunc, funcName)) b.WriteString(fmt.Sprintf("void* %s = (void*)%s;\n", cgoFunc, funcName))
// Mark this function for removal
toRemove = append(toRemove, cgoFunc) toRemove = append(toRemove, cgoFunc)
} }
} }
// Remove processed functions from cgoFuncs
for _, funcName := range toRemove { for _, funcName := range toRemove {
delete(cgoFuncs, funcName) delete(cgoFuncs, funcName)
} }
return b.String() return b.String()
} }
// Simplified function to just collect function names
func extractFuncNames(node *clangASTNode, funcNames map[string]bool) { func extractFuncNames(node *clangASTNode, funcNames map[string]bool) {
if node.Kind == "FunctionDecl" && node.Name != "" { for _, inner := range node.Inner {
// Skip functions that are likely internal/system functions if inner.Kind == "FunctionDecl" && inner.Name != "" {
funcNames[node.Name] = true funcNames[inner.Name] = true
} }
// Recursively process inner nodes
for i := range node.Inner {
extractFuncNames(&node.Inner[i], funcNames)
} }
} }
@@ -251,18 +257,29 @@ func parseCgoDecl(line string) (cgoDecls []cgoDecl, err error) {
err = fmt.Errorf("invalid cgo format: %v", line) err = fmt.Errorf("invalid cgo format: %v", line)
return return
} }
decl := strings.TrimSpace(line[:idx]) decl := strings.TrimSpace(line[:idx])
arg := strings.TrimSpace(line[idx+1:]) arg := strings.TrimSpace(line[idx+1:])
toks := strings.Split(decl, " ")
var platform, flag string // Split on first space to remove #cgo
if len(toks) == 2 { parts := strings.SplitN(decl, " ", 2)
flag = toks[1] if len(parts) < 2 {
} else if len(toks) == 3 { err = fmt.Errorf("invalid cgo directive: %v", line)
platform, flag = toks[1], toks[2]
} else {
err = fmt.Errorf("invalid cgo directive: %v, toks: %v", line, toks)
return 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 { switch flag {
case "pkg-config": case "pkg-config":
ldflags, e := exec.Command("pkg-config", "--libs", arg).Output() ldflags, e := exec.Command("pkg-config", "--libs", arg).Output()
@@ -276,19 +293,19 @@ func parseCgoDecl(line string) (cgoDecls []cgoDecl, err error) {
return return
} }
cgoDecls = append(cgoDecls, cgoDecl{ cgoDecls = append(cgoDecls, cgoDecl{
platform: platform, tag: tag,
cflags: strings.TrimSpace(string(cflags)), cflags: strings.TrimSpace(string(cflags)),
ldflags: strings.TrimSpace(string(ldflags)), ldflags: strings.TrimSpace(string(ldflags)),
}) })
case "CFLAGS": case "CFLAGS":
cgoDecls = append(cgoDecls, cgoDecl{ cgoDecls = append(cgoDecls, cgoDecl{
platform: platform, tag: tag,
cflags: arg, cflags: arg,
}) })
case "LDFLAGS": case "LDFLAGS":
cgoDecls = append(cgoDecls, cgoDecl{ cgoDecls = append(cgoDecls, cgoDecl{
platform: platform, tag: tag,
ldflags: arg, ldflags: arg,
}) })
} }
return 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)
}
})
}
}