diff --git a/chore/_xtool/llcppsymg/clangutils/clangutils.go b/chore/_xtool/llcppsymg/clangutils/clangutils.go index 95681d25..79770006 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)), } @@ -83,3 +87,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/llcppsymg.go b/chore/_xtool/llcppsymg/llcppsymg.go index e9e0dc78..67cf03a1 100644 --- a/chore/_xtool/llcppsymg/llcppsymg.go +++ b/chore/_xtool/llcppsymg/llcppsymg.go @@ -47,7 +47,6 @@ func main() { data, err = os.ReadFile(cfgFile) } check(err) - conf, err := config.GetConf(data) check(err) defer conf.Delete() @@ -60,7 +59,7 @@ func main() { check(err) filepaths := genHeaderFilePath(conf.CFlags, conf.Include) - headerInfos, err := parse.ParseHeaderFile(filepaths, conf.TrimPrefixes, conf.Cplusplus) + headerInfos, err := parse.ParseHeaderFile(filepaths, conf.TrimPrefixes, conf.Cplusplus, false) check(err) symbolInfo := getCommonSymbols(symbols, headerInfos, conf.TrimPrefixes) diff --git a/chore/_xtool/llcppsymg/parse/parse.go b/chore/_xtool/llcppsymg/parse/parse.go index 1b5a11a7..292a8ff7 100644 --- a/chore/_xtool/llcppsymg/parse/parse.go +++ b/chore/_xtool/llcppsymg/parse/parse.go @@ -15,27 +15,27 @@ type SymbolInfo struct { ProtoName string } -type Context struct { - 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) setCurrentFile(filename string) { - c.currentFile = filename +func (p *SymbolProcessor) setCurrentFile(filename string) { + p.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) } @@ -47,10 +47,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 "" } @@ -64,38 +64,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 (p *Context) genGoName(cursor clang.Cursor) string { - funcName := cursor.String() - defer funcName.Dispose() - - name := p.toGoName(c.GoString(funcName.CStr())) - 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, name)) - } - - return p.addSuffix(name) -} - -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() @@ -111,36 +117,32 @@ 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) { +func (p *SymbolProcessor) collectFuncInfo(cursor clang.Cursor) { symbol := cursor.Mangling() + defer symbol.Dispose() symbolName := c.GoString(symbol.CStr()) if len(symbolName) >= 1 && symbolName[0] == '_' { symbolName = symbolName[1:] } - defer symbol.Dispose() - - context.symbolMap[symbolName] = &SymbolInfo{ - GoName: context.genGoName(cursor), - ProtoName: context.genProtoName(cursor), + 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: - clang.VisitChildren(cursor, visit, nil) + clangutils.VisitChildren(cursor, p.visitTop) case clang.CursorCXXMethod, clang.CursorFunctionDecl, clang.CursorConstructor, clang.CursorDestructor: loc := cursor.Location() var file clang.File @@ -148,35 +150,38 @@ func visit(cursor, parent clang.Cursor, clientData c.Pointer) clang.ChildVisitRe 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/parse/parse_test/llgo.expect b/chore/_xtool/llcppsymg/parse/parse_test/llgo.expect new file mode 100644 index 00000000..819ff4b8 --- /dev/null +++ b/chore/_xtool/llcppsymg/parse/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/parse/parse_test/parse.go b/chore/_xtool/llcppsymg/parse/parse_test/parse.go new file mode 100644 index 00000000..4a827b45 --- /dev/null +++ b/chore/_xtool/llcppsymg/parse/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() + } +}