Repository navigation
Expand file tree
/
Copy pathcodegen.py
More file actions
311 lines (232 loc) · 9.94 KB
/
Copy pathcodegen.py
File metadata and controls
311 lines (232 loc) · 9.94 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
# C++ code generation using clang
import clang.cindex
import cProfile
import mako.template
import os
from typing import List, Iterable, Optional
# Directory that the codegen script file is stored in
TemplateDir = os.path.normpath(os.path.join(os.path.dirname(__file__), 'data'))
# [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', '-nostdinc', '-nostdinc++', '-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' or 'k' prefix from the name
if len(self.Name) >= 2 and (self.Name[0] is 'e' or self.Name[0] is 'k') 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, SourceRoot: str, OutputRoot: str):
""" Returns the path to store the auto-generated code for this source file """
abspath = os.path.abspath(self.FilePath)
relpath = os.path.relpath(abspath, SourceRoot)
reldir, filename = os.path.split(relpath)
filename_no_ext, ext = os.path.splitext(filename)
if len(ext):
ext = ext[1:]
return os.path.join(OutputRoot, reldir, filename_no_ext + '_' + ext + '_codegen.cpp')
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("")
def Generate(self, OutputPath: str):
if self.Enums:
with open("%s/template.mako" % TemplateDir, "r") as MakoTemplateFile:
MakoTemplateText = MakoTemplateFile.read()
MakoTemplate = mako.template.Template(MakoTemplateText)
GeneratedCode = MakoTemplate.render(Enums=self.Enums, IncludeFile=self.FilePath,
ForwardDeclares=self.RootDeclare.Children)
OutDir = os.path.dirname(OutputPath)
os.makedirs(OutDir, exist_ok=True)
with open(OutputPath, "w") as file:
file.write(GeneratedCode)
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(file_path: str, include_paths: Iterable[str], lib_clang_path: str, source_root: str, output_root: str):
file = GetAnalyzedSourceFile(file_path, include_paths, lib_clang_path)
output_path = file.GetCodegenFile(source_root, output_root)
file.Generate(output_path)
# if EnableProfiling:
# cProfile.run('RunCodegen()')
# else:
# RunCodegen()
def GetAnalyzedSourceFile(FilePath: str, IncludePaths: Iterable[str], LibClangPath: str) -> SourceFile:
# Clang index
clang.cindex.Config.set_library_file(LibClangPath)
ClangIndex = clang.cindex.Index.create()
# C++ environment
CompileEnvironment = CxxCompileEnvironment(IncludePaths)
NewFile = SourceFile(FilePath)
NewFile.Analyze(ClangIndex, CompileEnvironment)
return NewFile
def get_output_files(
source_file: str,
include_paths: Iterable[str],
lib_clang_path: str,
source_root: str,
output_root: str,
cache_path: Optional[str]
) -> List[str]:
file = GetAnalyzedSourceFile(source_file, include_paths, lib_clang_path)
if not file.Enums:
output_files = []
else:
output_files = [file.GetCodegenFile(source_root, output_root)]
if cache_path:
cache_output_files(cache_path, source_root, source_file, output_files)
return output_files
def cache_output_files(cache_path: str, source_root: str, file_path: str, output_files: List[str]) -> None:
cache_file_path = get_output_files_cache_filename(cache_path, source_root, file_path)
cache_file_dir = os.path.dirname(cache_file_path)
os.makedirs(cache_file_dir, exist_ok=True)
with open(cache_file_path, 'w') as f:
f.write(';'.join(output_files))
def get_output_files_cache_filename(cache_path: str, source_root: str, file_path: str) -> str:
abspath = os.path.abspath(file_path)
relpath = os.path.relpath(abspath, source_root)
return os.path.normpath(os.path.join(cache_path, relpath) + '.outputs')