mirror of
https://github.com/AxioDL/CodeGen.git
synced 2026-07-11 06:18:36 -07:00
361 lines
11 KiB
Python
361 lines
11 KiB
Python
# C++ code generation using clang
|
|
import clang.cindex
|
|
import cProfile
|
|
import glob
|
|
import mako.template
|
|
import os
|
|
import sys
|
|
|
|
# Directory that the codegen script file is stored in
|
|
ScriptDir = os.path.dirname(__file__)
|
|
|
|
# [debug] Whether to enable profiling
|
|
EnableProfiling = False
|
|
|
|
# [debug] Whether to print the AST for each processed source file
|
|
PrintAST = False
|
|
|
|
# [debug] Path to store auto-generated codegen source files
|
|
ExportPath = "build\\codegen"
|
|
|
|
# [debug] Path to store the final output cpp file
|
|
OutputCppSource = "%s\\auto_codegen.cpp" % ExportPath
|
|
|
|
# [debug] Path to find the source files
|
|
SourceRoot = "."
|
|
|
|
# [debug] Whether to parse parameters from the commandline
|
|
ParseCmdLine = True
|
|
|
|
# [debug] Whether to parse out source files and include paths from the command line
|
|
InputViaCommandLine = True
|
|
|
|
# Whether to always do a full regen
|
|
ForceFullRegen = False
|
|
|
|
def GetCursorFullyQualifiedName(Cursor):
|
|
""" Return the fully qualified name of the object represented by Cursor, including any parent scopes """
|
|
OutName = Cursor.spelling
|
|
Cursor = Cursor.semantic_parent
|
|
|
|
# not 100% sure this is correct?
|
|
while Cursor.kind != clang.cindex.CursorKind.TRANSLATION_UNIT:
|
|
OutName = "%s::%s" % (Cursor.spelling, OutName)
|
|
Cursor = Cursor.semantic_parent
|
|
|
|
return OutName
|
|
|
|
def GetCursorAnnotations(Cursor):
|
|
""" Return all annotation strings assigned to this cursor """
|
|
return [Child.displayname for Child in Cursor.get_children() if Child.kind is clang.cindex.CursorKind.ANNOTATE_ATTR]
|
|
|
|
def DebugPrintCursorRecursive(Cursor, SourceFile, Depth=0):
|
|
Padding = ""
|
|
for i in range(0, Depth):
|
|
Padding += " "
|
|
|
|
print("%s%s (%s)" % (Padding, Cursor.displayname, Cursor.kind))
|
|
|
|
for c in Cursor.get_children():
|
|
if c.location.file and SourceFile in c.location.file.name:
|
|
DebugPrintCursorRecursive(c, SourceFile, Depth+1)
|
|
|
|
class CxxCompileEnvironment:
|
|
""" Compilation environment used to configure clang """
|
|
def __init__(self, InIncludePaths):
|
|
# Disabling standard includes massively speeds up parsing time.
|
|
# We still need custom includes for things like the FOURCC macro, but standard includes are generally not needed.
|
|
self.CompilerArgs = ['-x', 'c++', '-std=c++17', '-nobuiltininc', '--no-standard-includes', '-DIS_CODEGEN_SCRIPT=1']
|
|
self.IncludePaths = InIncludePaths
|
|
|
|
def GetClangArgs(self):
|
|
return self.CompilerArgs + ["-I" + Path for Path in self.IncludePaths]
|
|
|
|
class CxxEnumConstant:
|
|
""" A C++ enum constant """
|
|
def __init__(self, Cursor):
|
|
self.Cursor = Cursor
|
|
self.Name = Cursor.spelling
|
|
self.FullName = GetCursorFullyQualifiedName(Cursor)
|
|
self.Value = Cursor.enum_value
|
|
|
|
# Remove 'e' prefix from the name
|
|
if len(self.Name) >= 2 and self.Name[0] is 'e' and not self.Name[1].islower():
|
|
self.Name = self.Name[1:]
|
|
|
|
class CxxEnum:
|
|
""" A C++ enum type """
|
|
def __init__(self, Cursor):
|
|
self.Cursor = Cursor
|
|
self.Name = Cursor.spelling
|
|
self.FullName = GetCursorFullyQualifiedName(Cursor)
|
|
self.Constants = []
|
|
self.ErrorValue = -1
|
|
|
|
for Child in Cursor.get_children():
|
|
if Child.kind is clang.cindex.CursorKind.ENUM_CONSTANT_DECL:
|
|
Constant = CxxEnumConstant(Child)
|
|
self.Constants.append(Constant)
|
|
|
|
LowercaseName = Constant.Name.lower()
|
|
if Constant.Value == -1 or "invalid" in LowercaseName or "unknown" in LowercaseName:
|
|
self.ErrorValue = Constant.Value
|
|
|
|
def DebugPrint(self):
|
|
print("Enum: %s (%s)" % (GetCursorFullyQualifiedName(self.Cursor), self.Cursor.location.file))
|
|
|
|
for Const in self.Constants:
|
|
print("- %s = %d" % (Const.Name, Const.Value))
|
|
|
|
print("")
|
|
|
|
class ScopedDeclare:
|
|
""" Recursive class that represents a scoped forward declare in the generated source code """
|
|
def __init__(self, TypeName, Name):
|
|
self.TypeName = TypeName
|
|
self.Name = Name
|
|
self.Children = []
|
|
|
|
def AddChild(self, TypeName, Name):
|
|
for Child in self.Children:
|
|
if Child.TypeName == TypeName and Child.Name == Name:
|
|
return Child
|
|
|
|
NewDeclare = ScopedDeclare(TypeName, Name)
|
|
self.Children.append(NewDeclare)
|
|
return self.Children[-1]
|
|
|
|
def GenerateText(self, Indentation):
|
|
OutText = ""
|
|
|
|
if self.Name:
|
|
IndentText = '\t' * Indentation
|
|
LineEnd = " {" if self.Children else ";"
|
|
OutText = "%s%s %s%s\n" % (IndentText, self.TypeName, self.Name, LineEnd)
|
|
|
|
for Child in self.Children:
|
|
OutText += Child.GenerateText(Indentation + 1)
|
|
|
|
if self.Children:
|
|
OutText += '\t' * Indentation
|
|
OutText += '}'
|
|
|
|
if self.TypeName is "struct":
|
|
OutText += ';'
|
|
|
|
if Indentation > 0:
|
|
OutText += '\n'
|
|
|
|
return OutText
|
|
|
|
def DebugPrint(self):
|
|
print( self.GenerateText(0) )
|
|
|
|
class SourceFile:
|
|
""" A C++ source code file """
|
|
def __init__(self, FilePath):
|
|
self.FilePath = FilePath
|
|
self.Enums = []
|
|
self.RootDeclare = ScopedDeclare("", "")
|
|
|
|
def GetCodegenFile(self):
|
|
""" Returns the path to store the auto-generated code for this source file """
|
|
if os.path.isabs(self.FilePath):
|
|
RelativePath = self.FilePath[3:]
|
|
return "%s\\%s.codegen.inl" % (ExportPath, RelativePath)
|
|
else:
|
|
return "%s\\%s.codegen.inl" % (ExportPath, self.FilePath)
|
|
|
|
def LastModifiedTime(self):
|
|
""" Returns the last modified time for this source file """
|
|
return os.path.getmtime( self.FilePath )
|
|
|
|
def Analyze(self, ClangIndex, CompileEnvironment):
|
|
""" Analyzes the AST for any components that need generated code """
|
|
TranslationUnit = ClangIndex.parse(self.FilePath, CompileEnvironment.GetClangArgs())
|
|
self.UsedSymbols = set()
|
|
self.CursorRecurse(TranslationUnit.cursor, 0)
|
|
|
|
if PrintAST:
|
|
DebugPrintCursorRecursive(TranslationUnit.cursor, self.FilePath)
|
|
print("")
|
|
|
|
# Generate code
|
|
if self.Enums:
|
|
MakoTemplateFile = open("%s/template.mako" % ScriptDir, "r")
|
|
MakoTemplateText = MakoTemplateFile.read()
|
|
MakoTemplateFile.close()
|
|
|
|
MakoTemplate = mako.template.Template(MakoTemplateText)
|
|
GeneratedCode = MakoTemplate.render(Enums=self.Enums, IncludeFile=self.FilePath, ForwardDeclares=self.RootDeclare.Children)
|
|
|
|
OutPath = self.GetCodegenFile()
|
|
OutDir = os.path.dirname(OutPath)
|
|
os.makedirs(OutDir, exist_ok=True)
|
|
|
|
OutCodegenFile = open(OutPath, "w")
|
|
OutCodegenFile.write(GeneratedCode)
|
|
OutCodegenFile.close()
|
|
|
|
def CursorRecurse(self, Cursor, Depth):
|
|
""" Recursive function that performs the actual analysis work of the AST """
|
|
for Child in Cursor.get_children():
|
|
# skip if this cursor isn't from the real source file
|
|
# if this check passes it means the cursor is from an include file, we don't care about that
|
|
if not Child.location.file or not Child.location.file.name.endswith(self.FilePath):
|
|
continue
|
|
|
|
# check for enum; anonymous enums are not supported
|
|
if Child.kind is clang.cindex.CursorKind.ENUM_DECL and Child.spelling:
|
|
# avoid the same enum being registered twice. This happens if you declare an enum and a variable of that enum simultaneously
|
|
QualifiedName = GetCursorFullyQualifiedName(Child)
|
|
|
|
if QualifiedName in self.UsedSymbols:
|
|
continue
|
|
else:
|
|
self.UsedSymbols.add(QualifiedName)
|
|
|
|
NewEnum = CxxEnum(Child)
|
|
self.Enums.append(NewEnum)
|
|
|
|
# Register any needed forward declares
|
|
Parent = Cursor
|
|
DeclarationCursors = []
|
|
|
|
while Parent is not None:
|
|
if Parent.kind is clang.cindex.CursorKind.CLASS_DECL or Parent.kind is clang.cindex.CursorKind.STRUCT_DECL:
|
|
DeclarationCursors.append(Parent)
|
|
|
|
elif Parent.kind is clang.cindex.CursorKind.NAMESPACE:
|
|
DeclarationCursors.append(Parent)
|
|
|
|
elif Parent.kind is clang.cindex.CursorKind.TRANSLATION_UNIT:
|
|
break
|
|
|
|
Parent = Parent.semantic_parent
|
|
|
|
if DeclarationCursors:
|
|
Declaration = self.RootDeclare
|
|
|
|
for i in range(len(DeclarationCursors)-1, -1, -1):
|
|
Decl = DeclarationCursors[i]
|
|
TypeName = "namespace" if Decl.kind is clang.cindex.CursorKind.NAMESPACE else "struct"
|
|
Declaration = Declaration.AddChild(TypeName, Decl.spelling)
|
|
|
|
Declaration.AddChild("enum", NewEnum.Name)
|
|
|
|
# currently we're only testing enums - recurse for non-enum types to look for more enums
|
|
else:
|
|
self.CursorRecurse(Child, Depth + 1)
|
|
|
|
def RunCodegen():
|
|
# Source files to parse
|
|
SourceFiles = []
|
|
IncludePaths = []
|
|
OutCodegenSource = ""
|
|
|
|
if ParseCmdLine:
|
|
global InputViaCommandLine
|
|
|
|
for i in range(0, len(sys.argv)):
|
|
arg = sys.argv[i]
|
|
|
|
if InputViaCommandLine is True and arg == "-sourcefiles" or arg == "-include":
|
|
i += 1
|
|
|
|
while i < len(sys.argv) and sys.argv[i][0] != '-':
|
|
File = sys.argv[i]
|
|
|
|
if arg == "-sourcefiles":
|
|
# this isn't the best way to handle this since the output file is provided via commandline arg
|
|
if "auto_codegen" not in File:
|
|
SourceFiles.append(File)
|
|
else:
|
|
IncludePaths.append(File)
|
|
|
|
i += 1
|
|
|
|
i -= 1
|
|
|
|
# The only time we don't use commandline input is for debugging
|
|
if arg == "-cmdinput":
|
|
InputViaCommandLine = True
|
|
|
|
if arg == "-full":
|
|
global ForceFullRegen
|
|
ForceFullRegen = True
|
|
|
|
if arg == "-o":
|
|
i += 1
|
|
OutCodegenSource = sys.argv[i]
|
|
|
|
global ExportPath
|
|
ExportPath = os.path.dirname(OutCodegenSource)
|
|
|
|
if arg == "-pwd":
|
|
i += 1
|
|
WorkingDirectory = sys.argv[i]
|
|
os.chdir(WorkingDirectory)
|
|
|
|
if not InputViaCommandLine:
|
|
SourceFiles = glob.glob("%s\\**\\*.cpp" % SourceRoot, recursive=True)
|
|
SourceFiles += glob.glob("%s\\**\\*.hpp" % SourceRoot, recursive=True)
|
|
SourceFiles += glob.glob("%s\\**\\*.h" % SourceRoot, recursive=True)
|
|
OutCodegenSource = OutputCppSource
|
|
|
|
print("Codegen Input: " + " ".join(SourceFiles))
|
|
print("Build Dir: " + ExportPath)
|
|
|
|
# Clang index
|
|
#@todo - this definitely isn't portable??? how should I be setting this???
|
|
clang.cindex.Config.set_library_file('C:\\Program Files\\LLVM\\bin\\libclang.dll')
|
|
ClangIndex = clang.cindex.Index.create()
|
|
|
|
# C++ environment
|
|
CompileEnvironment = CxxCompileEnvironment(IncludePaths)
|
|
|
|
# Process all source files
|
|
LastRegenTime = os.path.getmtime(OutCodegenSource) if os.path.isfile(OutCodegenSource) else 0
|
|
OutFiles = []
|
|
|
|
for FilePath in SourceFiles:
|
|
# If the file doesn't exist, just skip it.
|
|
# This mirrors qmake behavior.
|
|
if not os.path.isfile(FilePath):
|
|
continue
|
|
|
|
NewFile = SourceFile(FilePath)
|
|
NewFileOut = NewFile.GetCodegenFile()
|
|
|
|
# Only run codegen on this file if it has been modified since last time
|
|
if ForceFullRegen or NewFile.LastModifiedTime() > LastRegenTime:
|
|
NewFile.Analyze(ClangIndex, CompileEnvironment)
|
|
|
|
if NewFile.Enums:
|
|
# Register this as a file that needs to be included in the output source
|
|
OutFiles.append(NewFileOut)
|
|
else:
|
|
# Delete codegen file if it exists
|
|
if os.path.isfile(NewFileOut):
|
|
os.remove(NewFileOut)
|
|
|
|
# If the file hasn't been modified, we still want to make sure it gets included in the output source
|
|
else:
|
|
if os.path.isfile(NewFileOut):
|
|
OutFiles.append(NewFileOut)
|
|
|
|
# Create final output cpp
|
|
OutDir = os.path.dirname(OutCodegenSource)
|
|
os.makedirs(OutDir, exist_ok=True)
|
|
|
|
OutFile = open(OutCodegenSource, "w")
|
|
OutFile.write("#pragma warning( push )\n")
|
|
OutFile.write("#pragma warning( disable : 4146 )\n") # Suppress C4146: unary minus operator applied to unsigned type, result still unsgined
|
|
[OutFile.write("#include \"%s\"\n" % os.path.normpath(CodegenFile)) for CodegenFile in OutFiles]
|
|
OutFile.write("#pragma warning( pop )\n")
|
|
OutFile.close()
|
|
|
|
if EnableProfiling:
|
|
cProfile.run('RunCodegen()')
|
|
else:
|
|
RunCodegen() |