Merge pull request #788 from luoliwoshang/llcppsymg/classname

llcppsymg:refine class fetch,test case,-v
This commit is contained in:
xushiwei
2024-10-28 11:00:55 +08:00
committed by GitHub
15 changed files with 1668 additions and 263 deletions

View File

@@ -0,0 +1,119 @@
package main
import (
"fmt"
"os"
"github.com/goplus/llgo/c"
"github.com/goplus/llgo/c/clang"
"github.com/goplus/llgo/chore/_xtool/llcppsymg/clangutils"
)
func main() {
TestClangUtil()
}
func TestClangUtil() {
testCases := []struct {
name string
content string
isTemp bool
isCpp bool
}{
{
name: "C Header File",
content: `
int test_function(int a, int b);
void another_function(void);
`,
isTemp: false,
isCpp: false,
},
{
name: "C++ Temp File",
content: `
class TestClass {
public:
void test_method();
static int static_method(float f);
};
namespace TestNamespace {
void namespaced_function();
}
`,
isTemp: true,
isCpp: true,
},
}
for _, tc := range testCases {
fmt.Printf("=== Test Case: %s ===\n", tc.name)
var filePath string
var tempFile *os.File
if tc.isTemp {
filePath = tc.content
} else {
var err error
tempFile, err = os.CreateTemp("", "test_*.h")
if err != nil {
fmt.Printf("Failed to create temporary file: %v\n", err)
continue
}
_, err = tempFile.Write([]byte(tc.content))
if err != nil {
fmt.Printf("Failed to write to temporary file: %v\n", err)
tempFile.Close()
os.Remove(tempFile.Name())
continue
}
tempFile.Close()
filePath = tempFile.Name()
}
config := &clangutils.Config{
File: filePath,
Temp: tc.isTemp,
IsCpp: tc.isCpp,
}
index, unit, err := clangutils.CreateTranslationUnit(config)
if err != nil {
fmt.Printf("CreateTranslationUnit failed: %v\n", err)
continue
}
fmt.Println("CreateTranslationUnit succeeded")
cursor := unit.Cursor()
clangutils.VisitChildren(cursor, func(cursor, parent clang.Cursor) clang.ChildVisitResult {
switch cursor.Kind {
case clang.CursorFunctionDecl, clang.CursorCXXMethod:
funcName := cursor.String()
fmt.Printf("Function/Method: %s\n", c.GoString(funcName.CStr()))
parts := clangutils.BuildScopingParts(cursor)
fmt.Printf("Scoping parts: %v\n", parts)
funcName.Dispose()
case clang.CursorClassDecl:
className := cursor.String()
fmt.Printf("Class: %s\n", c.GoString(className.CStr()))
className.Dispose()
case clang.CursorNamespace:
namespaceName := cursor.String()
fmt.Printf("Namespace: %s\n", c.GoString(namespaceName.CStr()))
namespaceName.Dispose()
}
return clang.ChildVisit_Recurse
})
index.Dispose()
unit.Dispose()
if !tc.isTemp && tempFile != nil {
os.Remove(tempFile.Name())
}
fmt.Println()
}
}

View File

@@ -0,0 +1,23 @@
#stdout
=== Test Case: C Header File ===
CreateTranslationUnit succeeded
Function/Method: test_function
Scoping parts: [test_function]
Function/Method: another_function
Scoping parts: [another_function]
=== Test Case: C++ Temp File ===
CreateTranslationUnit succeeded
Class: TestClass
Function/Method: test_method
Scoping parts: [TestClass test_method]
Function/Method: static_method
Scoping parts: [TestClass static_method]
Namespace: TestNamespace
Function/Method: namespaced_function
Scoping parts: [TestNamespace namespaced_function]
#stderr
#exit 0

View File

@@ -0,0 +1,332 @@
package main
import (
"fmt"
"os"
"path/filepath"
"runtime"
"strings"
"github.com/goplus/llgo/chore/_xtool/llcppsymg/config"
"github.com/goplus/llgo/chore/_xtool/llcppsymg/config/cfgparse"
)
func main() {
TestGetConf()
TestParseLibs()
TestGenDylibPaths()
TestParseCFlags()
TestGenHeaderFilePath()
}
func TestGetConf() {
testCases := []struct {
name string
input string
}{
{
name: "SQLite configuration",
input: `{
"name": "sqlite",
"cflags": "-I/opt/homebrew/opt/sqlite/include",
"include": ["sqlite3.h"],
"libs": "-L/opt/homebrew/opt/sqlite/lib -lsqlite3",
"trimPrefixes": ["sqlite3_"],
"cplusplus": false
}`,
},
{
name: "Lua configuration",
input: `{
"name": "lua",
"cflags": "-I/opt/homebrew/include/lua",
"include": ["lua.h"],
"libs": "-L/opt/homebrew/lib -llua -lm",
"trimPrefixes": ["lua_", "lua_"],
"cplusplus": false
}`,
},
{
name: "Invalid JSON",
input: `{invalid json}`,
},
}
for _, tc := range testCases {
fmt.Printf("=== Test case: %s ===\n", tc.name)
result, err := config.GetConf([]byte(tc.input))
if err != nil {
fmt.Println("Error:", err.Error())
} else {
fmt.Println("Name:", result.Config.Name)
fmt.Println("CFlags:", result.Config.CFlags)
fmt.Println("Libs:", result.Config.Libs)
fmt.Println("Include:", strings.Join(result.Config.Include, ", "))
fmt.Println("TrimPrefixes:", strings.Join(result.Config.TrimPrefixes, ", "))
fmt.Println("Cplusplus:", result.Config.Cplusplus)
}
fmt.Println()
}
}
func TestParseLibs() {
fmt.Println("=== Test ParseLibs ===")
testCases := []struct {
name string
input string
}{
{
name: "Lua library",
input: "-L/opt/homebrew/lib -llua -lm",
},
{
name: "SQLite library",
input: "-L/opt/homebrew/opt/sqlite/lib -lsqlite3",
},
{
name: "INIReader library",
input: "-L/opt/homebrew/Cellar/inih/58/lib -lINIReader",
},
{
name: "Multiple library paths",
input: "-L/opt/homebrew/lib -L/usr/lib -llua",
},
{
name: "No valid library",
input: "-L/opt/homebrew/lib",
},
}
for _, tc := range testCases {
fmt.Printf("Test case: %s\n", tc.name)
fmt.Printf("Input: %s\n", tc.input)
conf := cfgparse.ParseLibs(tc.input)
fmt.Println("Paths:", conf.Paths)
fmt.Println("Names:", conf.Names)
}
}
func TestGenDylibPaths() {
fmt.Println("=== Test GenDylibPaths ===")
tempDir := os.TempDir()
tempDefaultPath := filepath.Join(tempDir, "symblib")
affix := ".dylib"
if runtime.GOOS == "linux" {
affix = ".so"
}
err := os.MkdirAll(tempDefaultPath, 0755)
if err != nil {
fmt.Printf("Failed to create temp default path: %v\n", err)
return
}
dylib1 := filepath.Join(tempDir, "libsymb1"+affix)
dylib2 := filepath.Join(tempDir, "libsymb2"+affix)
defaultDylib3 := filepath.Join(tempDefaultPath, "libsymb3"+affix)
os.Create(dylib1)
os.Create(dylib2)
os.Create(defaultDylib3)
defer os.Remove(dylib1)
defer os.Remove(dylib2)
defer os.Remove(defaultDylib3)
defer os.Remove(tempDefaultPath)
testCase := []struct {
name string
conf *cfgparse.Libs
defaultPaths []string
want []string
}{
{
name: "existing dylib",
conf: &cfgparse.Libs{
Names: []string{"symb1"},
Paths: []string{tempDir},
},
defaultPaths: []string{},
want: []string{dylib1},
},
{
name: "existing dylibs",
conf: &cfgparse.Libs{
Names: []string{"symb1", "symb2"},
Paths: []string{tempDir},
},
defaultPaths: []string{},
want: []string{dylib1, dylib2},
},
{
name: "existint default paths",
conf: &cfgparse.Libs{
Names: []string{"symb1", "symb3"},
Paths: []string{tempDir},
},
defaultPaths: []string{tempDefaultPath},
want: []string{dylib1, defaultDylib3},
},
{
name: "existint default paths & not found",
conf: &cfgparse.Libs{
Names: []string{"symb1", "symb3", "math"},
Paths: []string{tempDir},
},
defaultPaths: []string{tempDefaultPath},
want: []string{dylib1, defaultDylib3},
},
{
name: "no existing dylib",
conf: &cfgparse.Libs{
Names: []string{"notexist"},
Paths: []string{tempDir},
},
want: []string{},
},
}
for _, tc := range testCase {
fmt.Printf("Test case: %s\n", tc.name)
paths, notFounds, err := tc.conf.GenDylibPaths(tc.defaultPaths)
if len(notFounds) > 0 {
fmt.Println("notFounds", notFounds)
}
if err != nil {
fmt.Printf("Error: %v\n", err)
}
for _, path := range paths {
found := false
for _, wantPath := range tc.want {
if path == wantPath {
found = true
fileName := filepath.Base(path)
if runtime.GOOS == "linux" {
fileName = strings.TrimSuffix(fileName, ".so")
} else {
fileName = strings.TrimSuffix(fileName, ".dylib")
}
fmt.Printf("Path %s is in the expected paths\n", fileName)
break
}
}
if !found {
fmt.Printf("Path %s is not in the expected paths\n", path)
}
}
}
}
func TestParseCFlags() {
fmt.Println("=== Test ParseCFlags ===")
testCases := []struct {
name string
input string
}{
{
name: "Single include path",
input: "-I/usr/include",
},
{
name: "Multiple include paths",
input: "-I/usr/include -I/opt/homebrew/include",
},
{
name: "Include paths mixed with other flags",
input: "-I/usr/include -DDEBUG -I/opt/local/include -Wall",
},
{
name: "Empty input",
input: "",
},
}
for _, tc := range testCases {
fmt.Printf("Test case: %s\n", tc.name)
fmt.Printf("Input: %s\n", tc.input)
conf := cfgparse.ParseCFlags(tc.input)
fmt.Println("Paths:", conf.Paths)
}
}
func TestGenHeaderFilePath() {
fmt.Println("=== Test GenHeaderFilePath ===")
tempDir := os.TempDir()
temDir2 := filepath.Join(tempDir, "include")
tempFile1 := filepath.Join(tempDir, "test1.h")
tempFile2 := filepath.Join(tempDir, "test2.h")
tempFile3 := filepath.Join(temDir2, "test3.h")
os.MkdirAll(temDir2, 0755)
os.Create(tempFile1)
os.Create(tempFile2)
os.Create(tempFile3)
defer os.Remove(tempFile1)
defer os.Remove(tempFile2)
defer os.Remove(tempFile3)
defer os.Remove(temDir2)
testCases := []struct {
name string
cflags string
files []string
}{
{
name: "Valid files",
cflags: "-I" + tempDir,
files: []string{"test1.h", "test2.h"},
},
{
name: "Mixed existing and non-existing files",
cflags: "-I" + tempDir,
files: []string{"test1.h", "nonexistent.h"},
},
{
name: "Multiple include paths",
cflags: "-I" + tempDir + " -I" + temDir2,
files: []string{"test1.h", "test2.h", "test3.h"},
},
{
name: "No existing files",
cflags: "-I" + tempDir,
files: []string{"nonexistent1.h", "nonexistent2.h"},
},
{
name: "Empty file list",
cflags: "-I/usr/include",
files: []string{},
},
}
for _, tc := range testCases {
fmt.Printf("Test case: %s\n", tc.name)
fmt.Printf("Input files: %v\n", tc.files)
cflag := cfgparse.ParseCFlags(tc.cflags)
result, notFounds, err := cflag.GenHeaderFilePaths(tc.files)
if err != nil {
fmt.Printf("Error: %v\n", err)
}
if len(notFounds) > 0 {
fmt.Println("notFounds", notFounds)
}
if result != nil {
relativeResult := make([]string, len(result))
for i, path := range result {
relativeResult[i] = filepath.Base(path)
}
fmt.Printf("Output: %v\n", relativeResult)
}
fmt.Println()
}
}

View File

@@ -0,0 +1,97 @@
#stdout
=== Test case: SQLite configuration ===
Name: sqlite
CFlags: -I/opt/homebrew/opt/sqlite/include
Libs: -L/opt/homebrew/opt/sqlite/lib -lsqlite3
Include: sqlite3.h
TrimPrefixes: sqlite3_
Cplusplus: false
=== Test case: Lua configuration ===
Name: lua
CFlags: -I/opt/homebrew/include/lua
Libs: -L/opt/homebrew/lib -llua -lm
Include: lua.h
TrimPrefixes: lua_, lua_
Cplusplus: false
=== Test case: Invalid JSON ===
Error: failed to parse config
=== Test ParseLibs ===
Test case: Lua library
Input: -L/opt/homebrew/lib -llua -lm
Paths: [/opt/homebrew/lib]
Names: [lua m]
Test case: SQLite library
Input: -L/opt/homebrew/opt/sqlite/lib -lsqlite3
Paths: [/opt/homebrew/opt/sqlite/lib]
Names: [sqlite3]
Test case: INIReader library
Input: -L/opt/homebrew/Cellar/inih/58/lib -lINIReader
Paths: [/opt/homebrew/Cellar/inih/58/lib]
Names: [INIReader]
Test case: Multiple library paths
Input: -L/opt/homebrew/lib -L/usr/lib -llua
Paths: [/opt/homebrew/lib /usr/lib]
Names: [lua]
Test case: No valid library
Input: -L/opt/homebrew/lib
Paths: [/opt/homebrew/lib]
Names: []
=== Test GenDylibPaths ===
Test case: existing dylib
Path libsymb1 is in the expected paths
Test case: existing dylibs
Path libsymb1 is in the expected paths
Path libsymb2 is in the expected paths
Test case: existint default paths
Path libsymb1 is in the expected paths
Path libsymb3 is in the expected paths
Test case: existint default paths & not found
notFounds [math]
Path libsymb1 is in the expected paths
Path libsymb3 is in the expected paths
Test case: no existing dylib
notFounds [notexist]
Error: failed to find any libraries
=== Test ParseCFlags ===
Test case: Single include path
Input: -I/usr/include
Paths: [/usr/include]
Test case: Multiple include paths
Input: -I/usr/include -I/opt/homebrew/include
Paths: [/usr/include /opt/homebrew/include]
Test case: Include paths mixed with other flags
Input: -I/usr/include -DDEBUG -I/opt/local/include -Wall
Paths: [/usr/include /opt/local/include]
Test case: Empty input
Input:
Paths: []
=== Test GenHeaderFilePath ===
Test case: Valid files
Input files: [test1.h test2.h]
Output: [test1.h test2.h]
Test case: Mixed existing and non-existing files
Input files: [test1.h nonexistent.h]
notFounds [nonexistent.h]
Output: [test1.h]
Test case: Multiple include paths
Input files: [test1.h test2.h test3.h]
Output: [test1.h test2.h test3.h]
Test case: No existing files
Input files: [nonexistent1.h nonexistent2.h]
Error: failed to find any header files
notFounds [nonexistent1.h nonexistent2.h]
Test case: Empty file list
Input files: []
Error: failed to find any header files
#stderr
#exit 0

View File

@@ -0,0 +1,44 @@
#stdout
=== Test NewSymbolProcessor ===
Before: No prefixes After: Prefixes: [lua_ luaL_]
=== Test RemovePrefix ===
Before: lua_closethread After: closethread
Before: luaL_checknumber After: checknumber
=== Test ToGoName ===
Before: lua_closethread After: Closethread
Before: luaL_checknumber After: Checknumber
Before: sqlite3_close_v2 After: CloseV2
Before: sqlite3_callback After: Callback
Before: GetReal After: GetReal
Before: GetBoolean After: GetBoolean
Before: INIReader After: Reader
=== Test GenMethodName ===
Before: Class: INIReader, Name: INIReader After: (*INIReader).Init
Before: Class: INIReader, Name: INIReader After: (*INIReader).Dispose
Before: Class: INIReader, Name: HasValue After: (*INIReader).HasValue
=== Test AddSuffix ===
Before: Class: INIReader, Method: INIReader After: (*Reader).Init
Before: Class: INIReader, Method: INIReader After: (*Reader).Init__1
Before: Class: INIReader, Method: ParseError After: (*Reader).ParseError
Before: Class: INIReader, Method: HasValue After: (*Reader).HasValue
=== Test Case: C++ Class with Methods ===
Parsed Symbols:
Symbol Map GoName: (*Reader).Init__1, ProtoName In HeaderFile: INIReader::INIReader(const char *, int), MangledName: _ZN9INIReaderC1EPKci
Symbol Map GoName: (*Reader).Init, ProtoName In HeaderFile: INIReader::INIReader(const int &), MangledName: _ZN9INIReaderC1ERKi
Symbol Map GoName: (*Reader).Dispose, ProtoName In HeaderFile: INIReader::~INIReader(), MangledName: _ZN9INIReaderD1Ev
Symbol Map GoName: (*Reader).ParseError, ProtoName In HeaderFile: INIReader::ParseError(), MangledName: _ZNK9INIReader10ParseErrorEv
=== Test Case: C Functions ===
Parsed Symbols:
Symbol Map GoName: Compare, ProtoName In HeaderFile: lua_compare(lua_State *, int, int, int), MangledName: lua_compare
Symbol Map GoName: Rawequal, ProtoName In HeaderFile: lua_rawequal(lua_State *, int, int), MangledName: lua_rawequal
#stderr
#exit 0

View File

@@ -0,0 +1,165 @@
package main
import (
"fmt"
"sort"
"github.com/goplus/llgo/chore/_xtool/llcppsymg/parse"
)
func main() {
TestNewSymbolProcessor()
TestRemovePrefix()
TestToGoName()
TestGenMethodName()
TestAddSuffix()
TestParseHeaderFile()
}
func TestNewSymbolProcessor() {
fmt.Println("=== Test NewSymbolProcessor ===")
process := parse.NewSymbolProcessor([]string{"lua_", "luaL_"})
fmt.Printf("Before: No prefixes After: Prefixes: %v\n", process.Prefixes)
fmt.Println()
}
func TestRemovePrefix() {
fmt.Println("=== Test RemovePrefix ===")
process := parse.NewSymbolProcessor([]string{"lua_", "luaL_"})
testCases := []string{"lua_closethread", "luaL_checknumber"}
for _, input := range testCases {
result := process.TrimPrefixes(input)
fmt.Printf("Before: %s After: %s\n", input, result)
}
fmt.Println()
}
func TestToGoName() {
fmt.Println("=== Test ToGoName ===")
process1 := parse.NewSymbolProcessor([]string{"lua_", "luaL_"})
process2 := parse.NewSymbolProcessor([]string{"sqlite3_", "sqlite3_"})
process3 := parse.NewSymbolProcessor([]string{"INI"})
testCases := []struct {
processor *parse.SymbolProcessor
input string
}{
{process1, "lua_closethread"},
{process1, "luaL_checknumber"},
{process2, "sqlite3_close_v2"},
{process2, "sqlite3_callback"},
{process3, "GetReal"},
{process3, "GetBoolean"},
{process3, "INIReader"},
}
for _, tc := range testCases {
result := tc.processor.ToGoName(tc.input)
fmt.Printf("Before: %s After: %s\n", tc.input, result)
}
fmt.Println()
}
func TestGenMethodName() {
fmt.Println("=== Test GenMethodName ===")
process := &parse.SymbolProcessor{}
testCases := []struct {
class string
name string
isDestructor bool
}{
{"INIReader", "INIReader", false},
{"INIReader", "INIReader", true},
{"INIReader", "HasValue", false},
}
for _, tc := range testCases {
input := fmt.Sprintf("Class: %s, Name: %s", tc.class, tc.name)
result := process.GenMethodName(tc.class, tc.name, tc.isDestructor)
fmt.Printf("Before: %s After: %s\n", input, result)
}
fmt.Println()
}
func TestAddSuffix() {
fmt.Println("=== Test AddSuffix ===")
process := parse.NewSymbolProcessor([]string{"INI"})
methods := []string{
"INIReader",
"INIReader",
"ParseError",
"HasValue",
}
for _, method := range methods {
goName := process.ToGoName(method)
className := process.ToGoName("INIReader")
methodName := process.GenMethodName(className, goName, false)
finalName := process.AddSuffix(methodName)
input := fmt.Sprintf("Class: INIReader, Method: %s", method)
fmt.Printf("Before: %s After: %s\n", input, finalName)
}
fmt.Println()
}
func TestParseHeaderFile() {
testCases := []struct {
name string
content string
isCpp bool
prefixes []string
}{
{
name: "C++ Class with Methods",
content: `
class INIReader {
public:
INIReader(const std::string &filename);
INIReader(const char *buffer, size_t buffer_size);
~INIReader();
int ParseError() const;
private:
static std::string MakeKey(const std::string &section, const std::string &name);
};
`,
isCpp: true,
prefixes: []string{"INI"},
},
{
name: "C Functions",
content: `
typedef struct lua_State lua_State;
int(lua_rawequal)(lua_State *L, int idx1, int idx2);
int(lua_compare)(lua_State *L, int idx1, int idx2, int op);
`,
isCpp: false,
prefixes: []string{"lua_"},
},
}
for _, tc := range testCases {
fmt.Printf("=== Test Case: %s ===\n", tc.name)
symbolMap, err := parse.ParseHeaderFile([]string{tc.content}, tc.prefixes, tc.isCpp, true)
if err != nil {
fmt.Printf("Error: %v\n", err)
continue
}
fmt.Println("Parsed Symbols:")
var keys []string
for key := range symbolMap {
keys = append(keys, key)
}
sort.Strings(keys)
for _, key := range keys {
info := symbolMap[key]
fmt.Printf("Symbol Map GoName: %s, ProtoName In HeaderFile: %s, MangledName: %s\n", info.GoName, info.ProtoName, key)
}
fmt.Println()
}
}

View File

@@ -0,0 +1,46 @@
#stdout
=== Test GetCommonSymbols ===
Test Case: Lua symbols
Common Symbols (4):
Mangle: lua_absindex, CPP: lua_absindex(lua_State *, int), Go: Absindex
Mangle: lua_arith, CPP: lua_arith(lua_State *, int), Go: Arith
Mangle: lua_atpanic, CPP: lua_atpanic(lua_State *, lua_CFunction), Go: Atpanic
Mangle: lua_callk, CPP: lua_callk(lua_State *, int, int, lua_KContext, lua_KFunction), Go: Callk
Test Case: INIReader and Std library symbols
Common Symbols (3):
Mangle: _ZNK9INIReader12GetInteger64ERKNSt3__112basic_stringIcNS0_11char_traitsIcEENS0_9allocatorIcEEEES8_x, CPP: INIReader::GetInteger64(const std::string &, const std::string &, int64_t), Go: (*Reader).GetInteger64
Mangle: _ZNK9INIReader7GetRealERKNSt3__112basic_stringIcNS0_11char_traitsIcEENS0_9allocatorIcEEEES8_d, CPP: INIReader::GetReal(const std::string &, const std::string &, double), Go: (*Reader).GetReal
Mangle: _ZNK9INIReader10ParseErrorEv, CPP: INIReader::ParseError(), Go: (*Reader).ParseError
=== Test ReadExistingSymbolTable ===
Symbols read from the file:
Symbol Map GoName: (*Reader).Init__1, ProtoName In HeaderFile: INIReader::INIReader(const char *, size_t), MangledName: _ZN9INIReaderC1EPKcm
Symbol Map GoName: (*Reader).GetBoolean, ProtoName In HeaderFile: INIReader::GetBoolean(const std::string &, const std::string &, bool), MangledName: _ZNK9INIReader10GetBooleanERKNSt3__112basic_stringIcNS0_11char_traitsIcEENS0_9allocatorIcEEEES8_b
Symbol Map GoName: (*Reader).ParseError, ProtoName In HeaderFile: INIReader::ParseError(), MangledName: _ZNK9INIReader10ParseErrorEv
Havent existed symb file
=== Test GenSymbolTableData ===
[{
"mangle": "lua_absindex",
"c++": "lua_absindex(lua_State *, int)",
"go": "Absindex"
}, {
"mangle": "lua_arith",
"c++": "lua_arith(lua_State *, int)",
"go": "Arith"
}, {
"mangle": "lua_atpanic",
"c++": "lua_atpanic(lua_State *, lua_CFunction)",
"go": "Atpanic"
}, {
"mangle": "lua_callk",
"c++": "lua_callk(lua_State *, int, int, lua_KContext, lua_KFunction)",
"go": "ModifiedCallk"
}]
#stderr
#exit 0

View File

@@ -0,0 +1,152 @@
package main
import (
"fmt"
"os"
"sort"
"github.com/goplus/llgo/chore/_xtool/llcppsymg/parse"
"github.com/goplus/llgo/chore/_xtool/llcppsymg/symbol"
"github.com/goplus/llgo/chore/llcppg/types"
"github.com/goplus/llgo/xtool/nm"
)
func main() {
TestGetCommonSymbols()
TestReadExistingSymbolTable()
TestGenSymbolTableData()
}
func TestGetCommonSymbols() {
fmt.Println("=== Test GetCommonSymbols ===")
testCases := []struct {
name string
dylibSymbols []*nm.Symbol
headerSymbols map[string]*parse.SymbolInfo
}{
{
name: "Lua symbols",
dylibSymbols: []*nm.Symbol{
{Name: symbol.AddSymbolPrefixUnder("lua_absindex", false)},
{Name: symbol.AddSymbolPrefixUnder("lua_arith", false)},
{Name: symbol.AddSymbolPrefixUnder("lua_atpanic", false)},
{Name: symbol.AddSymbolPrefixUnder("lua_callk", false)},
{Name: symbol.AddSymbolPrefixUnder("lua_lib_nonexistent", false)},
},
headerSymbols: map[string]*parse.SymbolInfo{
"lua_absindex": {ProtoName: "lua_absindex(lua_State *, int)", GoName: "Absindex"},
"lua_arith": {ProtoName: "lua_arith(lua_State *, int)", GoName: "Arith"},
"lua_atpanic": {ProtoName: "lua_atpanic(lua_State *, lua_CFunction)", GoName: "Atpanic"},
"lua_callk": {ProtoName: "lua_callk(lua_State *, int, int, lua_KContext, lua_KFunction)", GoName: "Callk"},
"lua_header_nonexistent": {ProtoName: "lua_header_nonexistent()", GoName: "HeaderNonexistent"},
},
},
{
name: "INIReader and Std library symbols",
dylibSymbols: []*nm.Symbol{
{Name: symbol.AddSymbolPrefixUnder("ZNK9INIReader12GetInteger64ERKNSt3__112basic_stringIcNS0_11char_traitsIcEENS0_9allocatorIcEEEES8_x", true)},
{Name: symbol.AddSymbolPrefixUnder("ZNK9INIReader7GetRealERKNSt3__112basic_stringIcNS0_11char_traitsIcEENS0_9allocatorIcEEEES8_d", true)},
{Name: symbol.AddSymbolPrefixUnder("ZNK9INIReader10ParseErrorEv", true)},
},
headerSymbols: map[string]*parse.SymbolInfo{
"_ZNK9INIReader12GetInteger64ERKNSt3__112basic_stringIcNS0_11char_traitsIcEENS0_9allocatorIcEEEES8_x": {GoName: "(*Reader).GetInteger64", ProtoName: "INIReader::GetInteger64(const std::string &, const std::string &, int64_t)"},
"_ZNK9INIReader13GetUnsigned64ERKNSt3__112basic_stringIcNS0_11char_traitsIcEENS0_9allocatorIcEEEES8_y": {GoName: "(*Reader).GetUnsigned64", ProtoName: "INIReader::GetUnsigned64(const std::string &, const std::string &, uint64_t)"},
"_ZNK9INIReader7GetRealERKNSt3__112basic_stringIcNS0_11char_traitsIcEENS0_9allocatorIcEEEES8_d": {GoName: "(*Reader).GetReal", ProtoName: "INIReader::GetReal(const std::string &, const std::string &, double)"},
"_ZNK9INIReader10ParseErrorEv": {GoName: "(*Reader).ParseError", ProtoName: "INIReader::ParseError()"},
"_ZNK9INIReader10GetBooleanERKNSt3__112basic_stringIcNS0_11char_traitsIcEENS0_9allocatorIcEEEES8_b": {GoName: "(*Reader).GetBoolean", ProtoName: "INIReader::GetBoolean(const std::string &, const std::string &, bool)"},
},
},
}
for _, tc := range testCases {
fmt.Printf("\nTest Case: %s\n", tc.name)
commonSymbols := symbol.GetCommonSymbols(tc.dylibSymbols, tc.headerSymbols)
fmt.Printf("Common Symbols (%d):\n", len(commonSymbols))
for _, sym := range commonSymbols {
fmt.Printf("Mangle: %s, CPP: %s, Go: %s\n", sym.Mangle, sym.CPP, sym.Go)
}
}
fmt.Println()
}
func TestReadExistingSymbolTable() {
fmt.Println("=== Test ReadExistingSymbolTable ===")
tmpFile, err := os.CreateTemp("", "llcppg.symb.json")
if err != nil {
fmt.Printf("Failed to create temp file: %v\n", err)
return
}
defer os.Remove(tmpFile.Name())
testData := `[
{
"mangle": "_ZN9INIReaderC1EPKcm",
"c++": "INIReader::INIReader(const char *, size_t)",
"go": "(*Reader).Init__1"
},
{
"mangle": "_ZNK9INIReader10GetBooleanERKNSt3__112basic_stringIcNS0_11char_traitsIcEENS0_9allocatorIcEEEES8_b",
"c++": "INIReader::GetBoolean(const std::string &, const std::string &, bool)",
"go": "(*Reader).GetBoolean"
},
{
"mangle": "_ZNK9INIReader10ParseErrorEv",
"c++": "INIReader::ParseError()",
"go": "(*Reader).ParseError"
}
]`
if _, err := tmpFile.Write([]byte(testData)); err != nil {
fmt.Printf("Failed to write test data: %v\n", err)
return
}
tmpFile.Close()
symbols, exist := symbol.ReadExistingSymbolTable(tmpFile.Name())
if !exist {
fmt.Printf("ReadExistingSymbolTable failed")
return
}
fmt.Println("Symbols read from the file:")
var keys []string
for key := range symbols {
keys = append(keys, key)
}
sort.Strings(keys)
for _, key := range keys {
info := symbols[key]
fmt.Printf("Symbol Map GoName: %s, ProtoName In HeaderFile: %s, MangledName: %s\n",
info.Go, info.CPP, key)
}
_, exist = symbol.ReadExistingSymbolTable("other.json")
if !exist {
fmt.Println("Havent existed symb file")
}
fmt.Println()
}
func TestGenSymbolTableData() {
fmt.Println("=== Test GenSymbolTableData ===")
commonSymbols := []*types.SymbolInfo{
{Mangle: "lua_absindex", CPP: "lua_absindex(lua_State *, int)", Go: "Absindex"},
{Mangle: "lua_arith", CPP: "lua_arith(lua_State *, int)", Go: "Arith"},
{Mangle: "lua_atpanic", CPP: "lua_atpanic(lua_State *, lua_CFunction)", Go: "Atpanic"},
{Mangle: "lua_callk", CPP: "lua_callk(lua_State *, int, int, lua_KContext, lua_KFunction)", Go: "Callk"},
}
existingSymbols := map[string]types.SymbolInfo{
"lua_absindex": {Mangle: "lua_absindex", CPP: "lua_absindex(lua_State *, int)", Go: "Absindex"},
"lua_arith": {Mangle: "lua_arith", CPP: "lua_arith(lua_State *, int)", Go: "Arith"},
"lua_callk": {Mangle: "lua_callk", CPP: "lua_callk(lua_State *, int, int, lua_KContext, lua_KFunction)", Go: "ModifiedCallk"},
}
data, err := symbol.GenSymbolTableData(commonSymbols, existingSymbols)
if err != nil {
fmt.Printf("Error generating symbol table data: %v\n", err)
return
}
fmt.Println(string(data))
fmt.Println()
}

View File

@@ -0,0 +1,45 @@
#stdout
=== Test Case: inireader ===
[{
"mangle": "_ZN9INIReaderC1EPKc",
"c++": "INIReader::INIReader(const char *)",
"go": "(*Reader).Init"
}, {
"mangle": "_ZN9INIReaderC1EPKcl",
"c++": "INIReader::INIReader(const char *, long)",
"go": "(*Reader).Init__1"
}, {
"mangle": "_ZN9INIReaderD1Ev",
"c++": "INIReader::~INIReader()",
"go": "(*Reader).Dispose"
}, {
"mangle": "_ZNK9INIReader10ParseErrorEv",
"c++": "INIReader::ParseError()",
"go": "(*Reader).ModifyedParseError"
}, {
"mangle": "_ZNK9INIReader3GetEPKcS1_S1_",
"c++": "INIReader::Get(const char *, const char *, const char *)",
"go": "(*Reader).Get"
}]
=== Test Case: lua ===
[{
"mangle": "lua_error",
"c++": "lua_error(lua_State *)",
"go": "Error"
}, {
"mangle": "lua_next",
"c++": "lua_next(lua_State *, int)",
"go": "Next"
}, {
"mangle": "lua_concat",
"c++": "lua_concat(lua_State *, int)",
"go": "Concat"
}, {
"mangle": "lua_stringtonumber",
"c++": "lua_stringtonumber(lua_State *, const char *)",
"go": "Stringtonumber"
}]
#stderr
#exit 0

View File

@@ -0,0 +1,117 @@
package main
import (
"fmt"
"os"
"github.com/goplus/llgo/chore/_xtool/llcppsymg/parse"
"github.com/goplus/llgo/chore/_xtool/llcppsymg/symbol"
"github.com/goplus/llgo/xtool/nm"
)
func main() {
TestParseHeaderFile()
}
func TestParseHeaderFile() {
testCases := []struct {
name string
content string
isCpp bool
prefixes []string
dylibSymbols []*nm.Symbol
symbFileContent string
}{
{
name: "inireader",
content: `
#define INI_API __attribute__((visibility("default")))
class INIReader {
public:
__attribute__((visibility("default"))) explicit INIReader(const char *filename);
INI_API explicit INIReader(const char *buffer, long buffer_size);
~INIReader();
INI_API int ParseError() const;
INI_API const char * Get(const char *section, const char *name,
const char *default_value) const;
private:
static const char * MakeKey(const char *section, const char *name);
};
`,
isCpp: true,
prefixes: []string{"INI"},
dylibSymbols: []*nm.Symbol{
{Name: symbol.AddSymbolPrefixUnder("ZN9INIReaderC1EPKc", true)},
{Name: symbol.AddSymbolPrefixUnder("ZN9INIReaderC1EPKcl", true)},
{Name: symbol.AddSymbolPrefixUnder("ZN9INIReaderD1Ev", true)},
{Name: symbol.AddSymbolPrefixUnder("ZNK9INIReader10ParseErrorEv", true)},
{Name: symbol.AddSymbolPrefixUnder("ZNK9INIReader3GetEPKcS1_S1_", true)},
},
symbFileContent: `
[{
"mangle": "_ZN9INIReaderC1EPKc",
"c++": "INIReader::INIReader(const char *)",
"go": "(*Reader).Init"
}, {
"mangle": "_ZN9INIReaderC1EPKcl",
"c++": "INIReader::INIReader(const char *, long)",
"go": "(*Reader).Init__1"
}, {
"mangle": "_ZN9INIReaderD1Ev",
"c++": "INIReader::~INIReader()",
"go": "(*Reader).Dispose"
}, {
"mangle": "_ZNK9INIReader10ParseErrorEv",
"c++": "INIReader::ParseError()",
"go": "(*Reader).ModifyedParseError"
}]`,
},
{
name: "lua",
content: `
typedef struct lua_State lua_State;
LUA_API int(lua_error)(lua_State *L);
LUA_API int(lua_next)(lua_State *L, int idx);
LUA_API void(lua_concat)(lua_State *L, int n);
LUA_API void(lua_len)(lua_State *L, int idx);
LUA_API long unsigned int(lua_stringtonumber)(lua_State *L, const char *s);
LUA_API void(lua_setallocf)(lua_State *L, lua_Alloc f, void *ud);
LUA_API void(lua_toclose)(lua_State *L, int idx);
LUA_API void(lua_closeslot)(lua_State *L, int idx);
`,
isCpp: false,
prefixes: []string{"lua_"},
dylibSymbols: []*nm.Symbol{
{Name: symbol.AddSymbolPrefixUnder("lua_error", false)},
{Name: symbol.AddSymbolPrefixUnder("lua_next", false)},
{Name: symbol.AddSymbolPrefixUnder("lua_concat", false)},
{Name: symbol.AddSymbolPrefixUnder("lua_stringtonumber", false)},
},
},
}
for _, tc := range testCases {
fmt.Printf("=== Test Case: %s ===\n", tc.name)
headerSymbolMap, err := parse.ParseHeaderFile([]string{tc.content}, tc.prefixes, tc.isCpp, true)
if err != nil {
fmt.Println("Error:", err)
}
tmpFile, err := os.CreateTemp("", "llcppg.symb.json")
if err != nil {
fmt.Printf("Failed to create temp file: %v\n", err)
return
}
tmpFile.Write([]byte(tc.symbFileContent))
symbolData, err := symbol.GenerateAndUpdateSymbolTable(tc.dylibSymbols, headerSymbolMap, tmpFile.Name())
if err != nil {
fmt.Println("Error:", err)
}
fmt.Println(string(symbolData))
os.Remove(tmpFile.Name())
}
}

View File

@@ -16,6 +16,10 @@ type Config struct {
Index *clang.Index Index *clang.Index
} }
type Visitor func(cursor, parent clang.Cursor) clang.ChildVisitResult
const TEMP_FILE = "temp.h"
func CreateTranslationUnit(config *Config) (*clang.Index, *clang.TranslationUnit, error) { func CreateTranslationUnit(config *Config) (*clang.Index, *clang.TranslationUnit, error) {
// default use the c/c++ standard of clang; c:gnu17 c++:gnu++17 // default use the c/c++ standard of clang; c:gnu17 c++:gnu++17
// https://clang.llvm.org/docs/CommandGuide/clang.html // https://clang.llvm.org/docs/CommandGuide/clang.html
@@ -42,7 +46,7 @@ func CreateTranslationUnit(config *Config) (*clang.Index, *clang.TranslationUnit
if config.Temp { if config.Temp {
content := c.AllocaCStr(config.File) content := c.AllocaCStr(config.File)
tempFile := &clang.UnsavedFile{ tempFile := &clang.UnsavedFile{
Filename: c.Str("temp.h"), Filename: c.Str(TEMP_FILE),
Contents: content, Contents: content,
Length: c.Ulong(c.Strlen(content)), Length: c.Ulong(c.Strlen(content)),
} }
@@ -71,6 +75,11 @@ func CreateTranslationUnit(config *Config) (*clang.Index, *clang.TranslationUnit
return index, unit, nil return index, unit, nil
} }
func GetLocation(loc clang.SourceLocation) (file clang.File, line c.Uint, column c.Uint, offset c.Uint) {
loc.SpellingLocation(&file, &line, &column, &offset)
return
}
// Traverse up the semantic parents // Traverse up the semantic parents
func BuildScopingParts(cursor clang.Cursor) []string { func BuildScopingParts(cursor clang.Cursor) []string {
var parts []string var parts []string
@@ -83,3 +92,10 @@ func BuildScopingParts(cursor clang.Cursor) []string {
} }
return parts return parts
} }
func VisitChildren(cursor clang.Cursor, fn Visitor) c.Uint {
return clang.VisitChildren(cursor, func(cursor, parent clang.Cursor, clientData unsafe.Pointer) clang.ChildVisitResult {
cfn := *(*Visitor)(clientData)
return cfn(cursor, parent)
}, unsafe.Pointer(&fn))
}

View File

@@ -0,0 +1,109 @@
package cfgparse
import (
"fmt"
"os"
"path/filepath"
"runtime"
"strings"
)
// Note: This package is not placed under the 'config' package because 'config'
// depends on 'cjson'. The parsing of Libs and cflags is intended to be usable
// by both llgo and go, without introducing additional dependencies.
type Libs struct {
Paths []string // Dylib Path
Names []string
}
type CFlags struct {
Paths []string // Include Path
}
func ParseLibs(libs string) *Libs {
parts := strings.Fields(libs)
lbs := &Libs{}
for _, part := range parts {
if strings.HasPrefix(part, "-L") {
lbs.Paths = append(lbs.Paths, part[2:])
} else if strings.HasPrefix(part, "-l") {
lbs.Names = append(lbs.Names, part[2:])
}
}
return lbs
}
// searches for each library name in the provided paths and default paths,
// appending the appropriate file extension (.dylib for macOS, .so for Linux).
//
// Example: For "-L/opt/homebrew/lib -llua -lm":
// - It will search for liblua.dylib (on macOS) or liblua.so (on Linux)
// - System libs like -lm are ignored and included in notFound
//
// So error is returned if no libraries found at all.
func (l *Libs) GenDylibPaths(defaultPaths []string) ([]string, []string, error) {
var foundPaths []string
var notFound []string
affix := ".dylib"
if runtime.GOOS == "linux" {
affix = ".so"
}
searchPaths := append(l.Paths, defaultPaths...)
for _, name := range l.Names {
var foundPath string
for _, path := range searchPaths {
dylibPath := filepath.Join(path, "lib"+name+affix)
if _, err := os.Stat(dylibPath); err == nil {
foundPath = dylibPath
break
}
}
if foundPath != "" {
foundPaths = append(foundPaths, foundPath)
} else {
notFound = append(notFound, name)
}
}
if len(foundPaths) == 0 {
return nil, notFound, fmt.Errorf("failed to find any libraries")
}
return foundPaths, notFound, nil
}
func ParseCFlags(cflags string) *CFlags {
parts := strings.Fields(cflags)
cf := &CFlags{}
for _, part := range parts {
if strings.HasPrefix(part, "-I") {
cf.Paths = append(cf.Paths, part[2:])
}
}
return cf
}
func (cf *CFlags) GenHeaderFilePaths(files []string) ([]string, []string, error) {
var foundPaths []string
var notFound []string
for _, file := range files {
var found bool
for _, path := range cf.Paths {
fullPath := filepath.Join(path, file)
if _, err := os.Stat(fullPath); err == nil {
foundPaths = append(foundPaths, fullPath)
found = true
break
}
}
if !found {
notFound = append(notFound, file)
}
}
if len(foundPaths) == 0 {
return nil, notFound, fmt.Errorf("failed to find any header files")
}
return foundPaths, notFound, nil
}

View File

@@ -17,55 +17,74 @@
package main package main
import ( import (
"errors"
"fmt" "fmt"
"io" "io"
"os" "os"
"path/filepath"
"strings"
"unsafe"
"github.com/goplus/llgo/c" "github.com/goplus/llgo/chore/_xtool/llcppsymg/args"
"github.com/goplus/llgo/c/cjson"
"github.com/goplus/llgo/chore/_xtool/llcppsymg/config" "github.com/goplus/llgo/chore/_xtool/llcppsymg/config"
"github.com/goplus/llgo/chore/_xtool/llcppsymg/config/cfgparse"
"github.com/goplus/llgo/chore/_xtool/llcppsymg/parse" "github.com/goplus/llgo/chore/_xtool/llcppsymg/parse"
"github.com/goplus/llgo/chore/llcppg/types" "github.com/goplus/llgo/chore/_xtool/llcppsymg/symbol"
"github.com/goplus/llgo/xtool/nm"
) )
func main() { func main() {
cfgFile := "llcppg.cfg" symbFile := "llcppg.symb.json"
if len(os.Args) > 1 {
cfgFile = os.Args[1] ags, _ := args.ParseArgs(os.Args[1:], nil)
}
var data []byte var data []byte
var err error var err error
if cfgFile == "-" { if ags.UseStdin {
data, err = io.ReadAll(os.Stdin) data, err = io.ReadAll(os.Stdin)
} else { } else {
data, err = os.ReadFile(cfgFile) data, err = os.ReadFile(ags.CfgFile)
} }
check(err)
check(err)
conf, err := config.GetConf(data) conf, err := config.GetConf(data)
check(err) check(err)
defer conf.Delete() defer conf.Delete()
if err != nil { if ags.Verbose {
fmt.Fprintln(os.Stderr, "Failed to parse config file:", cfgFile) symbol.SetDebug(symbol.DbgFlagAll)
if ags.UseStdin {
fmt.Println("Config From Stdin")
} else {
fmt.Println("Config From File", ags.CfgFile)
}
fmt.Println("Name:", conf.Name)
fmt.Println("CFlags:", conf.CFlags)
fmt.Println("Libs:", conf.Libs)
fmt.Println("Include:", conf.Include)
fmt.Println("TrimPrefixes:", conf.TrimPrefixes)
fmt.Println("Cplusplus:", conf.Cplusplus)
} }
symbols, err := parseDylibSymbols(conf.Libs)
if err != nil {
fmt.Fprintln(os.Stderr, "Failed to parse config file:", ags.CfgFile)
}
symbols, err := symbol.ParseDylibSymbols(conf.Libs)
check(err) check(err)
filepaths := genHeaderFilePath(conf.CFlags, conf.Include) cflag := cfgparse.ParseCFlags(conf.CFlags)
headerInfos, err := parse.ParseHeaderFile(filepaths, conf.TrimPrefixes, conf.Cplusplus) filepaths, notFounds, err := cflag.GenHeaderFilePaths(conf.Include)
check(err) check(err)
symbolInfo := getCommonSymbols(symbols, headerInfos, conf.TrimPrefixes) if ags.Verbose {
fmt.Println("header file paths", filepaths)
if len(notFounds) > 0 {
fmt.Println("not found header files", notFounds)
}
}
err = genSymbolTableFile(symbolInfo) headerInfos, err := parse.ParseHeaderFile(filepaths, conf.TrimPrefixes, conf.Cplusplus, false)
check(err)
symbolData, err := symbol.GenerateAndUpdateSymbolTable(symbols, headerInfos, symbFile)
check(err)
err = os.WriteFile(symbFile, symbolData, 0644)
check(err) check(err)
} }
@@ -74,141 +93,3 @@ func check(err error) {
panic(err) panic(err)
} }
} }
func parseDylibSymbols(lib string) ([]*nm.Symbol, error) {
dylibPath, err := genDylibPath(lib)
if err != nil {
return nil, errors.New("failed to generate dylib path")
}
files, err := nm.New("").List(dylibPath)
if err != nil {
return nil, errors.New("failed to list symbols in dylib")
}
var symbols []*nm.Symbol
for _, file := range files {
symbols = append(symbols, file.Symbols...)
}
return symbols, nil
}
func genDylibPath(lib string) (string, error) {
output := lib
libPath := ""
libName := ""
for _, part := range strings.Fields(string(output)) {
if strings.HasPrefix(part, "-L") {
libPath = part[2:]
} else if strings.HasPrefix(part, "-l") {
libName = part[2:]
}
}
if libPath == "" || libName == "" {
return "", fmt.Errorf("failed to parse pkg-config output: %s", output)
}
dylibPath := filepath.Join(libPath, "lib"+libName+".dylib")
return dylibPath, nil
}
func genHeaderFilePath(cflags string, files []string) []string {
prefixPath := cflags
prefixPath = strings.TrimPrefix(prefixPath, "-I")
var includePaths []string
for _, file := range files {
includePaths = append(includePaths, filepath.Join(prefixPath, "/"+file))
}
return includePaths
}
func getCommonSymbols(dylibSymbols []*nm.Symbol, symbolMap map[string]*parse.SymbolInfo, prefix []string) []*types.SymbolInfo {
var commonSymbols []*types.SymbolInfo
for _, dylibSym := range dylibSymbols {
symName := strings.TrimPrefix(dylibSym.Name, "_")
if symInfo, ok := symbolMap[symName]; ok {
symbolInfo := &types.SymbolInfo{
Mangle: symName,
CPP: symInfo.ProtoName,
Go: symInfo.GoName,
}
commonSymbols = append(commonSymbols, symbolInfo)
}
}
return commonSymbols
}
func genSymbolTableFile(symbolInfos []*types.SymbolInfo) error {
fileName := "llcppg.symb.json"
existingSymbols, err := readExistingSymbolTable(fileName)
if err != nil {
return err
}
for i := range symbolInfos {
if existingSymbol, exists := existingSymbols[symbolInfos[i].Mangle]; exists {
symbolInfos[i].Go = existingSymbol.Go
}
}
root := cjson.Array()
defer root.Delete()
for _, symbol := range symbolInfos {
item := cjson.Object()
item.SetItem(c.Str("mangle"), cjson.String(c.AllocaCStr(symbol.Mangle)))
item.SetItem(c.Str("c++"), cjson.String(c.AllocaCStr(symbol.CPP)))
item.SetItem(c.Str("go"), cjson.String(c.AllocaCStr(symbol.Go)))
root.AddItem(item)
}
cStr := root.Print()
if cStr == nil {
return errors.New("symbol table is empty")
}
defer c.Free(unsafe.Pointer(cStr))
data := unsafe.Slice((*byte)(unsafe.Pointer(cStr)), c.Strlen(cStr))
if err := os.WriteFile(fileName, data, 0644); err != nil {
return errors.New("failed to write symbol table file")
}
return nil
}
func readExistingSymbolTable(fileName string) (map[string]types.SymbolInfo, error) {
existingSymbols := make(map[string]types.SymbolInfo)
if _, err := os.Stat(fileName); err != nil {
return existingSymbols, nil
}
data, err := os.ReadFile(fileName)
if err != nil {
return nil, errors.New("failed to read symbol table file")
}
parsedJSON := cjson.ParseBytes(data)
if parsedJSON == nil {
return nil, errors.New("failed to parse JSON")
}
arraySize := parsedJSON.GetArraySize()
for i := 0; i < int(arraySize); i++ {
item := parsedJSON.GetArrayItem(c.Int(i))
if item == nil {
continue
}
symbol := types.SymbolInfo{
Mangle: config.GetStringItem(item, "mangle", ""),
CPP: config.GetStringItem(item, "c++", ""),
Go: config.GetStringItem(item, "go", ""),
}
existingSymbols[symbol.Mangle] = symbol
}
return existingSymbols, nil
}

View File

@@ -2,6 +2,7 @@ package parse
import ( import (
"errors" "errors"
"runtime"
"strconv" "strconv"
"strings" "strings"
@@ -15,37 +16,27 @@ type SymbolInfo struct {
ProtoName string ProtoName string
} }
type Context struct { type SymbolProcessor struct {
namespaceName string Prefixes []string
className string SymbolMap map[string]*SymbolInfo
prefixes []string CurrentFile string
symbolMap map[string]*SymbolInfo NameCounts map[string]int
currentFile string
nameCounts map[string]int
} }
func newContext(prefixes []string) *Context { func NewSymbolProcessor(Prefixes []string) *SymbolProcessor {
return &Context{ return &SymbolProcessor{
prefixes: prefixes, Prefixes: Prefixes,
symbolMap: make(map[string]*SymbolInfo), SymbolMap: make(map[string]*SymbolInfo),
nameCounts: make(map[string]int), NameCounts: make(map[string]int),
} }
} }
func (c *Context) setNamespaceName(name string) { func (p *SymbolProcessor) setCurrentFile(filename string) {
c.namespaceName = name p.CurrentFile = filename
} }
func (c *Context) setClassName(name string) { func (p *SymbolProcessor) TrimPrefixes(str string) string {
c.className = name for _, prefix := range p.Prefixes {
}
func (c *Context) setCurrentFile(filename string) {
c.currentFile = filename
}
func (c *Context) removePrefix(str string) string {
for _, prefix := range c.prefixes {
if strings.HasPrefix(str, prefix) { if strings.HasPrefix(str, prefix) {
return strings.TrimPrefix(str, prefix) return strings.TrimPrefix(str, prefix)
} }
@@ -57,10 +48,10 @@ func toTitle(s string) string {
if s == "" { if s == "" {
return "" return ""
} }
return strings.ToUpper(s[:1]) + strings.ToLower(s[1:]) return strings.ToUpper(s[:1]) + (s[1:])
} }
func toCamel(originName string) string { func toUpperCamelCase(originName string) string {
if originName == "" { if originName == "" {
return "" return ""
} }
@@ -74,37 +65,44 @@ func toCamel(originName string) string {
// 1. remove prefix from config // 1. remove prefix from config
// 2. convert to camel case // 2. convert to camel case
func (c *Context) toGoName(name string) string { func (p *SymbolProcessor) ToGoName(name string) string {
name = c.removePrefix(name) return toUpperCamelCase(p.TrimPrefixes(name))
return toCamel(name)
} }
func (c *Context) genGoName(name string) string { func (p *SymbolProcessor) GenMethodName(class, name string, isDestructor bool) string {
class := c.toGoName(c.className)
name = c.toGoName(name)
var baseName string
if class == "" {
baseName = name
} else {
baseName = c.genMethodName(class, name)
}
return c.addSuffix(baseName)
}
func (c *Context) genMethodName(class, name string) string {
prefix := "(*" + class + ")." prefix := "(*" + class + ")."
if isDestructor {
return prefix + "Dispose"
}
if class == name { if class == name {
return prefix + "Init" return prefix + "Init"
} }
if name == "~"+class {
return prefix + "Dispose"
}
return prefix + name return prefix + name
} }
func (p *Context) genProtoName(cursor clang.Cursor) string { func (p *SymbolProcessor) genGoName(cursor clang.Cursor) string {
funcName := cursor.String()
defer funcName.Dispose()
originName := c.GoString(funcName.CStr())
isDestructor := cursor.Kind == clang.CursorDestructor
var convertedName string
if isDestructor {
convertedName = p.ToGoName(originName[1:])
} else {
convertedName = p.ToGoName(originName)
}
if parent := cursor.SemanticParent(); parent.Kind == clang.CursorClassDecl {
parentName := parent.String()
defer parentName.Dispose()
class := p.ToGoName(c.GoString(parentName.CStr()))
return p.AddSuffix(p.GenMethodName(class, convertedName, isDestructor))
}
return p.AddSuffix(convertedName)
}
func (p *SymbolProcessor) genProtoName(cursor clang.Cursor) string {
displayName := cursor.DisplayName() displayName := cursor.DisplayName()
defer displayName.Dispose() defer displayName.Dispose()
@@ -120,95 +118,73 @@ func (p *Context) genProtoName(cursor clang.Cursor) string {
return builder.String() return builder.String()
} }
func (c *Context) addSuffix(name string) string { func (p *SymbolProcessor) AddSuffix(name string) string {
c.nameCounts[name]++ p.NameCounts[name]++
count := c.nameCounts[name] if count := p.NameCounts[name]; count > 1 {
if count > 1 {
return name + "__" + strconv.Itoa(count-1) return name + "__" + strconv.Itoa(count-1)
} }
return name return name
} }
var context = newContext([]string{}) func (p *SymbolProcessor) collectFuncInfo(cursor clang.Cursor) {
func collectFuncInfo(cursor clang.Cursor) {
cursorStr := cursor.String()
symbol := cursor.Mangling() symbol := cursor.Mangling()
name := c.GoString(cursorStr.CStr())
symbolName := c.GoString(symbol.CStr())
if len(symbolName) >= 1 && symbolName[0] == '_' {
symbolName = symbolName[1:]
}
defer symbol.Dispose() defer symbol.Dispose()
defer cursorStr.Dispose()
context.symbolMap[symbolName] = &SymbolInfo{ // On Linux, C++ symbols typically have one leading underscore
GoName: context.genGoName(name), // On macOS, C++ symbols may have two leading underscores
ProtoName: context.genProtoName(cursor), // For consistency, we remove the first leading underscore on macOS
symbolName := c.GoString(symbol.CStr())
if runtime.GOOS == "darwin" {
symbolName = strings.TrimPrefix(symbolName, "_")
}
p.SymbolMap[symbolName] = &SymbolInfo{
GoName: p.genGoName(cursor),
ProtoName: p.genProtoName(cursor),
} }
} }
func visit(cursor, parent clang.Cursor, clientData c.Pointer) clang.ChildVisitResult { func (p *SymbolProcessor) visitTop(cursor, parent clang.Cursor) clang.ChildVisitResult {
switch cursor.Kind { switch cursor.Kind {
case clang.CursorNamespace, clang.CursorClassDecl: case clang.CursorNamespace, clang.CursorClassDecl:
nameStr := cursor.String() clangutils.VisitChildren(cursor, p.visitTop)
defer nameStr.Dispose()
name := c.GoString(nameStr.CStr())
if cursor.Kind == clang.CursorNamespace {
context.setNamespaceName(name)
} else {
context.setClassName(name)
}
clang.VisitChildren(cursor, visit, nil)
if cursor.Kind == clang.CursorNamespace {
context.setNamespaceName("")
} else {
context.setClassName("")
}
case clang.CursorCXXMethod, clang.CursorFunctionDecl, clang.CursorConstructor, clang.CursorDestructor: case clang.CursorCXXMethod, clang.CursorFunctionDecl, clang.CursorConstructor, clang.CursorDestructor:
loc := cursor.Location() loc := cursor.Location()
var file clang.File file, _, _, _ := clangutils.GetLocation(loc)
var line, column c.Uint
loc.SpellingLocation(&file, &line, &column, nil)
filename := file.FileName() filename := file.FileName()
defer filename.Dispose() defer filename.Dispose()
isCurrentFile := c.Strcmp(filename.CStr(), c.AllocaCStr(context.currentFile)) == 0 isCurrentFile := c.Strcmp(filename.CStr(), c.AllocaCStr(p.CurrentFile)) == 0
isPublicMethod := (cursor.CXXAccessSpecifier() == clang.CXXPublic) && cursor.Kind == clang.CursorCXXMethod || cursor.Kind == clang.CursorConstructor || cursor.Kind == clang.CursorDestructor isPublicMethod := (cursor.CXXAccessSpecifier() == clang.CXXPublic) && cursor.Kind == clang.CursorCXXMethod || cursor.Kind == clang.CursorConstructor || cursor.Kind == clang.CursorDestructor
if isCurrentFile && (cursor.Kind == clang.CursorFunctionDecl || isPublicMethod) { if isCurrentFile && (cursor.Kind == clang.CursorFunctionDecl || isPublicMethod) {
collectFuncInfo(cursor) p.collectFuncInfo(cursor)
} }
} }
return clang.ChildVisit_Continue return clang.ChildVisit_Continue
} }
func ParseHeaderFile(filepaths []string, prefixes []string, isCpp bool) (map[string]*SymbolInfo, error) { func ParseHeaderFile(files []string, Prefixes []string, isCpp bool, isTemp bool) (map[string]*SymbolInfo, error) {
context = newContext(prefixes) processer := NewSymbolProcessor(Prefixes)
index := clang.CreateIndex(0, 0) index := clang.CreateIndex(0, 0)
for _, filename := range filepaths { for _, file := range files {
_, unit, err := clangutils.CreateTranslationUnit(&clangutils.Config{ _, unit, err := clangutils.CreateTranslationUnit(&clangutils.Config{
File: filename, File: file,
Temp: false, Temp: isTemp,
IsCpp: isCpp, IsCpp: isCpp,
Index: index, Index: index,
}) })
if err != nil { if err != nil {
return nil, errors.New("Unable to parse translation unit for file " + filename) return nil, errors.New("Unable to parse translation unit for file " + file)
} }
cursor := unit.Cursor() cursor := unit.Cursor()
context.setCurrentFile(filename) if isTemp {
clang.VisitChildren(cursor, visit, nil) processer.setCurrentFile(clangutils.TEMP_FILE)
} else {
processer.setCurrentFile(file)
}
clangutils.VisitChildren(cursor, processer.visitTop)
unit.Dispose() unit.Dispose()
} }
index.Dispose() index.Dispose()
return context.symbolMap, nil return processer.SymbolMap, nil
} }

View File

@@ -0,0 +1,283 @@
package symbol
import (
"errors"
"fmt"
"os"
"runtime"
"strings"
"unsafe"
"github.com/goplus/llgo/c"
"github.com/goplus/llgo/c/cjson"
"github.com/goplus/llgo/chore/_xtool/llcppsymg/config"
"github.com/goplus/llgo/chore/_xtool/llcppsymg/config/cfgparse"
"github.com/goplus/llgo/chore/_xtool/llcppsymg/parse"
"github.com/goplus/llgo/chore/llcppg/types"
"github.com/goplus/llgo/xtool/nm"
)
type dbgFlags = int
const (
DbgSymbol dbgFlags = 1 << iota
DbgFlagAll = DbgSymbol
)
var (
debugSymbol bool
)
func SetDebug(dbgFlags dbgFlags) {
debugSymbol = (dbgFlags & DbgSymbol) != 0
}
// ParseDylibSymbols parses symbols from dynamic libraries specified in the lib string.
// It handles multiple libraries (e.g., -L/opt/homebrew/lib -llua -lm) and returns
// symbols if at least one library is successfully parsed. Errors from inaccessible
// libraries (like standard libs) are logged as warnings.
//
// Returns symbols and nil error if any symbols are found, or nil and error if none found.
func ParseDylibSymbols(lib string) ([]*nm.Symbol, error) {
if debugSymbol {
fmt.Println("ParseDylibSymbols:from", lib)
}
sysPaths := getSysLibPaths()
lbs := cfgparse.ParseLibs(lib)
if debugSymbol {
fmt.Println("ParseDylibSymbols:LibConfig Parse To")
fmt.Println("libs.Names: ", lbs.Names)
fmt.Println("libs.Paths: ", lbs.Paths)
}
dylibPaths, notFounds, err := lbs.GenDylibPaths(sysPaths)
if err != nil {
return nil, fmt.Errorf("failed to generate some dylib paths: %v", err)
}
if debugSymbol {
fmt.Println("ParseDylibSymbols:dylibPaths", dylibPaths)
if len(notFounds) > 0 {
fmt.Println("ParseDylibSymbols:not found libname", notFounds)
} else {
fmt.Println("ParseDylibSymbols:every library is found")
}
}
var symbols []*nm.Symbol
var parseErrors []string
for _, dylibPath := range dylibPaths {
if _, err := os.Stat(dylibPath); err != nil {
if debugSymbol {
fmt.Printf("ParseDylibSymbols:Failed to access dylib %s: %v\n", dylibPath, err)
}
continue
}
files, err := nm.New("").List(dylibPath)
if err != nil {
parseErrors = append(parseErrors, fmt.Sprintf("ParseDylibSymbols:Failed to list symbols in dylib %s: %v", dylibPath, err))
continue
}
for _, file := range files {
symbols = append(symbols, file.Symbols...)
}
}
if len(symbols) > 0 {
if debugSymbol {
if len(parseErrors) > 0 {
fmt.Printf("ParseDylibSymbols:Some libraries could not be parsed: %v\n", parseErrors)
}
fmt.Println("ParseDylibSymbols:", len(symbols), "symbols")
}
return symbols, nil
}
return nil, fmt.Errorf("no symbols found in any dylib. Errors: %v", parseErrors)
}
func getSysLibPaths() []string {
var paths []string
if runtime.GOOS == "linux" {
if debugSymbol {
fmt.Println("getSysLibPaths:find sys lib path from linux")
}
paths = []string{
"/usr/lib",
"/usr/local/lib",
}
paths = append(paths, getPath("/etc/ld.so.conf")...)
if debugSymbol && len(paths) == 0 {
fmt.Println("getSysLibPaths:/etc/ld.so.conf havent find any path")
}
confd := "/etc/ld.so.conf.d"
dir, err := os.Stat(confd)
if err != nil || !dir.IsDir() {
if debugSymbol {
fmt.Println("getSysLibPaths:/etc/ld.so.conf.d not found or not dir")
}
return paths
}
// todo(zzy) : wait llgo os.ReadDir support
// files, err := os.ReadDir(confd)
// if err == nil {
// for _, file := range files {
// filepath := filepath.Join(confd, file.Name())
// paths = append(paths, getPath(filepath)...)
// }
// }
}
return paths
}
func getPath(file string) []string {
if debugSymbol {
fmt.Println("getPath:from", file)
}
var paths []string
content, err := os.ReadFile(file)
if err != nil {
return paths
}
lines := strings.Split(string(content), "\n")
for _, line := range lines {
line = strings.TrimSpace(line)
if line != "" && !strings.HasPrefix(line, "#") {
if file, err := os.Stat(line); err == nil && file.IsDir() {
paths = append(paths, line)
}
}
}
return paths
}
// finds the intersection of symbols from the dynamic library's symbol table and the symbols parsed from header files.
// It returns a list of symbols that can be externally linked.
func GetCommonSymbols(dylibSymbols []*nm.Symbol, headerSymbols map[string]*parse.SymbolInfo) []*types.SymbolInfo {
var commonSymbols []*types.SymbolInfo
for _, dylibSym := range dylibSymbols {
symName := dylibSym.Name
if runtime.GOOS == "darwin" {
symName = strings.TrimPrefix(symName, "_")
}
if symInfo, ok := headerSymbols[symName]; ok {
symbolInfo := &types.SymbolInfo{
Mangle: symName,
CPP: symInfo.ProtoName,
Go: symInfo.GoName,
}
commonSymbols = append(commonSymbols, symbolInfo)
}
}
return commonSymbols
}
func ReadExistingSymbolTable(fileName string) (map[string]types.SymbolInfo, bool) {
if _, err := os.Stat(fileName); err != nil {
return nil, false
}
data, err := os.ReadFile(fileName)
if err != nil {
return nil, false
}
parsedJSON := cjson.ParseBytes(data)
if parsedJSON == nil {
return nil, false
}
existingSymbols := make(map[string]types.SymbolInfo)
arraySize := parsedJSON.GetArraySize()
for i := 0; i < int(arraySize); i++ {
item := parsedJSON.GetArrayItem(c.Int(i))
symbol := types.SymbolInfo{
Mangle: config.GetStringItem(item, "mangle", ""),
CPP: config.GetStringItem(item, "c++", ""),
Go: config.GetStringItem(item, "go", ""),
}
existingSymbols[symbol.Mangle] = symbol
}
return existingSymbols, true
}
func GenSymbolTableData(commonSymbols []*types.SymbolInfo, existingSymbols map[string]types.SymbolInfo) ([]byte, error) {
if len(existingSymbols) > 0 {
if debugSymbol {
fmt.Println("GenSymbolTableData:generate symbol table with exist symbol table")
}
for i := range commonSymbols {
if existingSymbol, exists := existingSymbols[commonSymbols[i].Mangle]; exists && commonSymbols[i].Go != existingSymbol.Go {
if debugSymbol {
fmt.Println("symbol", commonSymbols[i].Mangle, "already exist, use exist symbol", existingSymbol.Go)
}
commonSymbols[i].Go = existingSymbol.Go
} else {
if debugSymbol {
fmt.Println("new symbol", commonSymbols[i].Mangle, "-", commonSymbols[i].CPP, "-", commonSymbols[i].Go)
}
}
}
} else {
if debugSymbol {
fmt.Println("GenSymbolTableData:generate symbol table without symbol table")
for _, symbol := range commonSymbols {
fmt.Println("new symbol", symbol.Mangle, "-", symbol.CPP, "-", symbol.Go)
}
}
}
root := cjson.Array()
defer root.Delete()
for _, symbol := range commonSymbols {
item := cjson.Object()
item.SetItem(c.Str("mangle"), cjson.String(c.AllocaCStr(symbol.Mangle)))
item.SetItem(c.Str("c++"), cjson.String(c.AllocaCStr(symbol.CPP)))
item.SetItem(c.Str("go"), cjson.String(c.AllocaCStr(symbol.Go)))
root.AddItem(item)
}
cStr := root.Print()
if cStr == nil {
return nil, errors.New("symbol table is empty")
}
defer c.Free(unsafe.Pointer(cStr))
result := []byte(c.GoString(cStr))
return result, nil
}
func GenerateAndUpdateSymbolTable(symbols []*nm.Symbol, headerInfos map[string]*parse.SymbolInfo, symbFile string) ([]byte, error) {
commonSymbols := GetCommonSymbols(symbols, headerInfos)
if debugSymbol {
fmt.Println("GenerateAndUpdateSymbolTable:", len(commonSymbols), "common symbols")
}
existSymbols, exist := ReadExistingSymbolTable(symbFile)
if exist && debugSymbol {
fmt.Println("GenerateAndUpdateSymbolTable:current path have exist symbol table", symbFile)
}
symbolData, err := GenSymbolTableData(commonSymbols, existSymbols)
if err != nil {
return nil, err
}
return symbolData, nil
}
// For mutiple os test,the nm output's symbol name is different.
func AddSymbolPrefixUnder(name string, isCpp bool) string {
prefix := ""
if runtime.GOOS == "darwin" {
prefix = prefix + "_"
}
if isCpp {
prefix = prefix + "_"
}
return prefix + name
}