diff --git a/chore/_xtool/llcppsymg/_cmptest/clangutils_test/clangutils.go b/chore/_xtool/llcppsymg/_cmptest/clangutils_test/clangutils.go new file mode 100644 index 00000000..1dd06781 --- /dev/null +++ b/chore/_xtool/llcppsymg/_cmptest/clangutils_test/clangutils.go @@ -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() + } +} diff --git a/chore/_xtool/llcppsymg/_cmptest/clangutils_test/llgo.expect b/chore/_xtool/llcppsymg/_cmptest/clangutils_test/llgo.expect new file mode 100644 index 00000000..de8589e7 --- /dev/null +++ b/chore/_xtool/llcppsymg/_cmptest/clangutils_test/llgo.expect @@ -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 diff --git a/chore/_xtool/llcppsymg/_cmptest/config_test/config.go b/chore/_xtool/llcppsymg/_cmptest/config_test/config.go new file mode 100644 index 00000000..4b3bdbc1 --- /dev/null +++ b/chore/_xtool/llcppsymg/_cmptest/config_test/config.go @@ -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() + } +} diff --git a/chore/_xtool/llcppsymg/_cmptest/config_test/llgo.expect b/chore/_xtool/llcppsymg/_cmptest/config_test/llgo.expect new file mode 100644 index 00000000..a7c57a05 --- /dev/null +++ b/chore/_xtool/llcppsymg/_cmptest/config_test/llgo.expect @@ -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 diff --git a/chore/_xtool/llcppsymg/_cmptest/parse_test/llgo.expect b/chore/_xtool/llcppsymg/_cmptest/parse_test/llgo.expect new file mode 100644 index 00000000..819ff4b8 --- /dev/null +++ b/chore/_xtool/llcppsymg/_cmptest/parse_test/llgo.expect @@ -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 diff --git a/chore/_xtool/llcppsymg/_cmptest/parse_test/parse.go b/chore/_xtool/llcppsymg/_cmptest/parse_test/parse.go new file mode 100644 index 00000000..4a827b45 --- /dev/null +++ b/chore/_xtool/llcppsymg/_cmptest/parse_test/parse.go @@ -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 §ion, 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() + } +} diff --git a/chore/_xtool/llcppsymg/_cmptest/symbol_test/llgo.expect b/chore/_xtool/llcppsymg/_cmptest/symbol_test/llgo.expect new file mode 100644 index 00000000..8372439b --- /dev/null +++ b/chore/_xtool/llcppsymg/_cmptest/symbol_test/llgo.expect @@ -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 diff --git a/chore/_xtool/llcppsymg/_cmptest/symbol_test/symbol.go b/chore/_xtool/llcppsymg/_cmptest/symbol_test/symbol.go new file mode 100644 index 00000000..929921cd --- /dev/null +++ b/chore/_xtool/llcppsymg/_cmptest/symbol_test/symbol.go @@ -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() +} diff --git a/chore/_xtool/llcppsymg/_cmptest/symg_test/llgo.expect b/chore/_xtool/llcppsymg/_cmptest/symg_test/llgo.expect new file mode 100644 index 00000000..93bf120d --- /dev/null +++ b/chore/_xtool/llcppsymg/_cmptest/symg_test/llgo.expect @@ -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 diff --git a/chore/_xtool/llcppsymg/_cmptest/symg_test/symg.go b/chore/_xtool/llcppsymg/_cmptest/symg_test/symg.go new file mode 100644 index 00000000..ef90ca02 --- /dev/null +++ b/chore/_xtool/llcppsymg/_cmptest/symg_test/symg.go @@ -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()) + } +} diff --git a/chore/_xtool/llcppsymg/clangutils/clangutils.go b/chore/_xtool/llcppsymg/clangutils/clangutils.go index 95681d25..e5821b9d 100644 --- a/chore/_xtool/llcppsymg/clangutils/clangutils.go +++ b/chore/_xtool/llcppsymg/clangutils/clangutils.go @@ -16,6 +16,10 @@ type Config struct { 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) { // default use the c/c++ standard of clang; c:gnu17 c++:gnu++17 // https://clang.llvm.org/docs/CommandGuide/clang.html @@ -42,7 +46,7 @@ func CreateTranslationUnit(config *Config) (*clang.Index, *clang.TranslationUnit if config.Temp { content := c.AllocaCStr(config.File) tempFile := &clang.UnsavedFile{ - Filename: c.Str("temp.h"), + Filename: c.Str(TEMP_FILE), Contents: content, Length: c.Ulong(c.Strlen(content)), } @@ -71,6 +75,11 @@ func CreateTranslationUnit(config *Config) (*clang.Index, *clang.TranslationUnit 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 func BuildScopingParts(cursor clang.Cursor) []string { var parts []string @@ -83,3 +92,10 @@ func BuildScopingParts(cursor clang.Cursor) []string { } 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)) +} diff --git a/chore/_xtool/llcppsymg/config/cfgparse/parse.go b/chore/_xtool/llcppsymg/config/cfgparse/parse.go new file mode 100644 index 00000000..6bac7430 --- /dev/null +++ b/chore/_xtool/llcppsymg/config/cfgparse/parse.go @@ -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 +} diff --git a/chore/_xtool/llcppsymg/llcppsymg.go b/chore/_xtool/llcppsymg/llcppsymg.go index e9e0dc78..63ca3d4b 100644 --- a/chore/_xtool/llcppsymg/llcppsymg.go +++ b/chore/_xtool/llcppsymg/llcppsymg.go @@ -17,55 +17,74 @@ package main import ( - "errors" "fmt" "io" "os" - "path/filepath" - "strings" - "unsafe" - "github.com/goplus/llgo/c" - "github.com/goplus/llgo/c/cjson" + "github.com/goplus/llgo/chore/_xtool/llcppsymg/args" "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" + "github.com/goplus/llgo/chore/_xtool/llcppsymg/symbol" ) func main() { - cfgFile := "llcppg.cfg" - if len(os.Args) > 1 { - cfgFile = os.Args[1] - } + symbFile := "llcppg.symb.json" + + ags, _ := args.ParseArgs(os.Args[1:], nil) var data []byte var err error - if cfgFile == "-" { + if ags.UseStdin { data, err = io.ReadAll(os.Stdin) } else { - data, err = os.ReadFile(cfgFile) + data, err = os.ReadFile(ags.CfgFile) } - check(err) + check(err) conf, err := config.GetConf(data) check(err) defer conf.Delete() - if err != nil { - fmt.Fprintln(os.Stderr, "Failed to parse config file:", cfgFile) + if ags.Verbose { + 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) - filepaths := genHeaderFilePath(conf.CFlags, conf.Include) - headerInfos, err := parse.ParseHeaderFile(filepaths, conf.TrimPrefixes, conf.Cplusplus) + cflag := cfgparse.ParseCFlags(conf.CFlags) + filepaths, notFounds, err := cflag.GenHeaderFilePaths(conf.Include) 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) } @@ -74,141 +93,3 @@ func check(err error) { 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 -} diff --git a/chore/_xtool/llcppsymg/parse/parse.go b/chore/_xtool/llcppsymg/parse/parse.go index 98923b3a..b6d7072d 100644 --- a/chore/_xtool/llcppsymg/parse/parse.go +++ b/chore/_xtool/llcppsymg/parse/parse.go @@ -2,6 +2,7 @@ package parse import ( "errors" + "runtime" "strconv" "strings" @@ -15,37 +16,27 @@ type SymbolInfo struct { ProtoName string } -type Context struct { - namespaceName string - className string - prefixes []string - symbolMap map[string]*SymbolInfo - currentFile string - nameCounts map[string]int +type SymbolProcessor struct { + Prefixes []string + SymbolMap map[string]*SymbolInfo + CurrentFile string + NameCounts map[string]int } -func newContext(prefixes []string) *Context { - return &Context{ - prefixes: prefixes, - symbolMap: make(map[string]*SymbolInfo), - nameCounts: make(map[string]int), +func NewSymbolProcessor(Prefixes []string) *SymbolProcessor { + return &SymbolProcessor{ + Prefixes: Prefixes, + SymbolMap: make(map[string]*SymbolInfo), + NameCounts: make(map[string]int), } } -func (c *Context) setNamespaceName(name string) { - c.namespaceName = name +func (p *SymbolProcessor) setCurrentFile(filename string) { + p.CurrentFile = filename } -func (c *Context) setClassName(name string) { - c.className = name -} - -func (c *Context) setCurrentFile(filename string) { - c.currentFile = filename -} - -func (c *Context) removePrefix(str string) string { - for _, prefix := range c.prefixes { +func (p *SymbolProcessor) TrimPrefixes(str string) string { + for _, prefix := range p.Prefixes { if strings.HasPrefix(str, prefix) { return strings.TrimPrefix(str, prefix) } @@ -57,10 +48,10 @@ func toTitle(s string) string { if s == "" { 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 == "" { return "" } @@ -74,37 +65,44 @@ func toCamel(originName string) string { // 1. remove prefix from config // 2. convert to camel case -func (c *Context) toGoName(name string) string { - name = c.removePrefix(name) - return toCamel(name) +func (p *SymbolProcessor) ToGoName(name string) string { + return toUpperCamelCase(p.TrimPrefixes(name)) } -func (c *Context) genGoName(name string) 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 { +func (p *SymbolProcessor) GenMethodName(class, name string, isDestructor bool) string { prefix := "(*" + class + ")." + if isDestructor { + return prefix + "Dispose" + } if class == name { return prefix + "Init" } - if name == "~"+class { - return prefix + "Dispose" - } 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() defer displayName.Dispose() @@ -120,95 +118,73 @@ func (p *Context) genProtoName(cursor clang.Cursor) string { return builder.String() } -func (c *Context) addSuffix(name string) string { - c.nameCounts[name]++ - count := c.nameCounts[name] - if count > 1 { +func (p *SymbolProcessor) AddSuffix(name string) string { + p.NameCounts[name]++ + if count := p.NameCounts[name]; count > 1 { return name + "__" + strconv.Itoa(count-1) } return name } -var context = newContext([]string{}) - -func collectFuncInfo(cursor clang.Cursor) { - cursorStr := cursor.String() +func (p *SymbolProcessor) collectFuncInfo(cursor clang.Cursor) { 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 cursorStr.Dispose() - context.symbolMap[symbolName] = &SymbolInfo{ - GoName: context.genGoName(name), - ProtoName: context.genProtoName(cursor), + // On Linux, C++ symbols typically have one leading underscore + // On macOS, C++ symbols may have two leading underscores + // 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 { case clang.CursorNamespace, clang.CursorClassDecl: - nameStr := cursor.String() - 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("") - } - + clangutils.VisitChildren(cursor, p.visitTop) case clang.CursorCXXMethod, clang.CursorFunctionDecl, clang.CursorConstructor, clang.CursorDestructor: loc := cursor.Location() - var file clang.File - var line, column c.Uint - - loc.SpellingLocation(&file, &line, &column, nil) + file, _, _, _ := clangutils.GetLocation(loc) filename := file.FileName() 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 if isCurrentFile && (cursor.Kind == clang.CursorFunctionDecl || isPublicMethod) { - collectFuncInfo(cursor) + p.collectFuncInfo(cursor) } } - return clang.ChildVisit_Continue } -func ParseHeaderFile(filepaths []string, prefixes []string, isCpp bool) (map[string]*SymbolInfo, error) { - context = newContext(prefixes) +func ParseHeaderFile(files []string, Prefixes []string, isCpp bool, isTemp bool) (map[string]*SymbolInfo, error) { + processer := NewSymbolProcessor(Prefixes) index := clang.CreateIndex(0, 0) - for _, filename := range filepaths { + for _, file := range files { _, unit, err := clangutils.CreateTranslationUnit(&clangutils.Config{ - File: filename, - Temp: false, + File: file, + Temp: isTemp, IsCpp: isCpp, Index: index, }) 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() - context.setCurrentFile(filename) - clang.VisitChildren(cursor, visit, nil) + if isTemp { + processer.setCurrentFile(clangutils.TEMP_FILE) + } else { + processer.setCurrentFile(file) + } + clangutils.VisitChildren(cursor, processer.visitTop) unit.Dispose() } index.Dispose() - return context.symbolMap, nil + return processer.SymbolMap, nil } diff --git a/chore/_xtool/llcppsymg/symbol/symbol.go b/chore/_xtool/llcppsymg/symbol/symbol.go new file mode 100644 index 00000000..a6b360ae --- /dev/null +++ b/chore/_xtool/llcppsymg/symbol/symbol.go @@ -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 +}