diff --git a/chore/_xtool/llcppsymg/common/common.go b/chore/_xtool/llcppsymg/common/common.go new file mode 100644 index 00000000..35fed532 --- /dev/null +++ b/chore/_xtool/llcppsymg/common/common.go @@ -0,0 +1,29 @@ +package common + +type CPPSymbol struct { + Symbol string `json:"symbol"` + Type string `json:"type"` + Name string `json:"name"` +} + +type ASTInformation struct { + Namespace string `json:"namespace"` + Class string `json:"class"` + Name string `json:"name"` + BaseClasses []string `json:"baseClasses"` + ReturnType string `json:"returnType"` + Location string `json:"location"` + Parameters []Parameter `json:"parameters"` + Symbol string `json:"symbol"` +} + +type Parameter struct { + Name string `json:"name"` + Type string `json:"type"` +} + +type SymbolInfo struct { + Mangle string `json:"mangle"` // C++ Symbol + CPP string `json:"c++"` // C++ function name + Go string `json:"go"` // Go function name +} diff --git a/chore/_xtool/llcppsymg/llcppg.cfg b/chore/_xtool/llcppsymg/llcppg.cfg new file mode 100644 index 00000000..b5335e2c --- /dev/null +++ b/chore/_xtool/llcppsymg/llcppg.cfg @@ -0,0 +1,10 @@ +{ + "name": "inih", + "cflags": "$(pkg-config --cflags INIReader)", + "include": [ + "INIReader.h", + "AnotherHeaderFile.h" + ], + "libs": "$(pkg-config --libs INIReader)", + "trimPrefixes": ["Ini", "INI"] +} \ No newline at end of file diff --git a/chore/_xtool/llcppsymg/llcppsymg.go b/chore/_xtool/llcppsymg/llcppsymg.go index ea60dffe..db4bc1e4 100644 --- a/chore/_xtool/llcppsymg/llcppsymg.go +++ b/chore/_xtool/llcppsymg/llcppsymg.go @@ -17,12 +17,20 @@ package main import ( + "bufio" + "bytes" + "encoding/json" + "errors" "fmt" + "github.com/goplus/llgo/chore/_xtool/llcppsymg/common" + "github.com/goplus/llgo/chore/llcppg/types" "io" "os" - - "github.com/goplus/llgo/c" - "github.com/goplus/llgo/c/cjson" + "os/exec" + "path/filepath" + "regexp" + "strconv" + "strings" ) func main() { @@ -40,14 +48,26 @@ func main() { } check(err) - conf := cjson.ParseBytes(data) - if conf == nil { - fmt.Fprintln(os.Stderr, "Failed to parse config file:", cfgFile) - os.Exit(1) - } - defer conf.Delete() + var config types.Config + err = json.Unmarshal(data, &config) + check(err) + + symbols, err := parseDylibSymbols(config.Libs) + check(err) + + files, err := parseHeaderFile(config) + check(err) + + symbolInfo := getCommonSymbols(symbols, files) + + jsonData, err := json.MarshalIndent(symbolInfo, "", " ") + check(err) + + // 写入文件 + fileName := "llcppg.symb.json" + err = os.WriteFile(fileName, jsonData, 0644) // 使用 0644 权限 + check(err) - c.Printf(c.Str("%s"), conf.Print()) } func check(err error) { @@ -55,3 +75,212 @@ func check(err error) { panic(err) } } + +func parseDylibSymbols(lib string) ([]common.CPPSymbol, error) { + dylibPath, _ := generateDylibPath(lib) + nmCmd := exec.Command("nm", "-gU", dylibPath) + nmOutput, err := nmCmd.Output() + if err != nil { + return nil, errors.New("failed to execute nm command") + } + + symbols := parseNmOutput(nmOutput) + + for i, sym := range symbols { + decodedName, err := decodeSymbolName(sym.Name) + if err != nil { + return nil, err + } + symbols[i].Name = decodedName + } + + return symbols, nil +} + +func generateDylibPath(lib string) (string, error) { + // 执行pkg-config命令 + output := expandEnv(lib) + // 解析输出 + libPath := "" + libName := "" + for _, part := range strings.Fields(string(output)) { + if strings.HasPrefix(part, "-L") { + libPath = part[2:] // 去掉-L前缀 + } else if strings.HasPrefix(part, "-l") { + libName = part[2:] // 去掉-l前缀 + } + } + + if libPath == "" || libName == "" { + return "", fmt.Errorf("failed to parse pkg-config output: %s", output) + } + + // 构造dylib路径 + dylibPath := filepath.Join(libPath, "lib"+libName+".dylib") + return dylibPath, nil +} + +func parseNmOutput(output []byte) []common.CPPSymbol { + scanner := bufio.NewScanner(bytes.NewReader(output)) + var symbols []common.CPPSymbol + + for scanner.Scan() { + line := scanner.Text() + fields := strings.Fields(line) + if len(fields) < 3 { + continue + } + symbolName := fields[2] + // Check if the symbol name starts with an underscore and remove it if present + if strings.HasPrefix(symbolName, "_") { + symbolName = symbolName[1:] + } + symbols = append(symbols, common.CPPSymbol{ + Symbol: symbolName, + Type: fields[1], + Name: fields[2], + }) + } + + return symbols +} + +func decodeSymbolName(symbolName string) (string, error) { + cppfiltCmd := exec.Command("c++filt", symbolName) + cppfiltOutput, err := cppfiltCmd.Output() + if err != nil { + return "", errors.New("failed to execute c++filt command") + } + + decodedName := strings.TrimSpace(string(cppfiltOutput)) + // 将特定的模板类型转换为 std::string + decodedName = strings.ReplaceAll(decodedName, "std::__1::basic_string, std::__1::allocator > const", "std::string") + return decodedName, nil +} + +// parseHeaderFile +func parseHeaderFile(config types.Config) ([]common.ASTInformation, error) { + files := generateHeaderFilePath(config.CFlags, config.Include) + headerFileCmd := exec.Command("llcppinfofetch", files...) + headerFileOutput, err := headerFileCmd.Output() + if err != nil { + return nil, errors.New("failed to execute header file command") + } + t := make([]common.ASTInformation, 0) + err = json.Unmarshal(headerFileOutput, &t) + if err != nil { + return nil, err + } + return t, nil +} + +func generateHeaderFilePath(cflags string, files []string) []string { + // 执行pkg-config命令 + prefixPath := expandEnv(cflags) + if strings.HasPrefix(prefixPath, "-I") { + prefixPath = prefixPath[2:] + } + // 去掉首尾空白字符(包括换行符) + prefixPath = strings.TrimSpace(prefixPath) + var includePaths []string + for _, file := range files { + includePaths = append(includePaths, filepath.Join(prefixPath, "/"+file)) + } + return includePaths +} + +func getCommonSymbols(dylibSymbols []common.CPPSymbol, astInfoList []common.ASTInformation) []common.SymbolInfo { + var commonSymbols []common.SymbolInfo + functionNameMap := make(map[string]int) + + for _, astInfo := range astInfoList { + for _, dylibSym := range dylibSymbols { + if dylibSym.Symbol == astInfo.Symbol { + cppName := generateCPPName(astInfo) + functionNameMap[cppName]++ + symbolInfo := common.SymbolInfo{ + Mangle: dylibSym.Symbol, + CPP: cppName, + Go: generateMangle(astInfo, functionNameMap[cppName]), + } + commonSymbols = append(commonSymbols, symbolInfo) + break + } + } + } + + return commonSymbols +} + +func generateCPPName(astInfo common.ASTInformation) string { + cppName := astInfo.Name + if astInfo.Class != "" { + cppName = astInfo.Class + "::" + astInfo.Name + } + return cppName +} + +func generateMangle(astInfo common.ASTInformation, count int) string { + res := "" + if astInfo.Class != "" { + if astInfo.Class == astInfo.Name { + res = "(*" + astInfo.Class + ")." + "Init" + if count > 1 { + res += "__" + strconv.Itoa(count-1) + } + } else if astInfo.Name == "~"+astInfo.Class { + res = "(*" + astInfo.Class + ")." + "Dispose" + if count > 1 { + res += "__" + strconv.Itoa(count-1) + } + } else { + res = "(*" + astInfo.Class + ")." + astInfo.Name + "__" + string(rune(count)) + } + } else { + res = astInfo.Name + if count > 0 { + res += "__" + strconv.Itoa(count-1) + } + } + return res +} + +var ( + reSubcmd = regexp.MustCompile(`\$\([^)]+\)`) + reFlag = regexp.MustCompile(`[^ \t\n]+`) +) + +func expandEnv(s string) string { + return expandEnvWithCmd(s) +} + +func expandEnvWithCmd(s string) string { + expanded := reSubcmd.ReplaceAllStringFunc(s, func(m string) string { + subcmd := strings.TrimSpace(s[2 : len(s)-1]) + + args := parseSubcmd(subcmd) + + cmd := args[0] + + if cmd != "pkg-config" && cmd != "llvm-config" { + fmt.Fprintf(os.Stderr, "expand cmd only support pkg-config and llvm-config: '%s'\n", subcmd) + return "" + } + + var out []byte + var err error + out, err = exec.Command(cmd, args[1:]...).Output() + + if err != nil { + // TODO(kindy): log in verbose mode + return "" + } + + return string(out) + }) + return os.Expand(expanded, os.Getenv) +} + +func parseSubcmd(s string) []string { + return reFlag.FindAllString(s, -1) +}