1#!/usr/bin/python3 -i 2# 3# Copyright (c) 2013-2019 The Khronos Group Inc. 4# 5# Licensed under the Apache License, Version 2.0 (the "License"); 6# you may not use this file except in compliance with the License. 7# You may obtain a copy of the License at 8# 9# http://www.apache.org/licenses/LICENSE-2.0 10# 11# Unless required by applicable law or agreed to in writing, software 12# distributed under the License is distributed on an "AS IS" BASIS, 13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14# See the License for the specific language governing permissions and 15# limitations under the License. 16 17from __future__ import unicode_literals 18import io,os,re,sys,pdb 19 20def write( *args, **kwargs ): 21 file = kwargs.pop('file',sys.stdout) 22 end = kwargs.pop('end','\n') 23 file.write(' '.join([str(arg) for arg in args])) 24 file.write(end) 25 26# noneStr - returns string argument, or "" if argument is None. 27# Used in converting etree Elements into text. 28# str - string to convert 29def noneStr(str): 30 if (str): 31 return str 32 else: 33 return "" 34 35# enquote - returns string argument with surrounding quotes, 36# for serialization into Python code. 37def enquote(str): 38 if (str): 39 return "'" + str + "'" 40 else: 41 return None 42 43# apiName - returns True if name is a Vulkan name (vk/Vk/VK prefix, or a 44# function pointer type), False otherwise. 45def apiName(str): 46 return str[0:2].lower() == 'vk' or str[0:3] == 'PFN' 47 48# Primary sort key for regSortFeatures. 49# Sorts by category of the feature name string: 50# Core API features (those defined with a <feature> tag) 51# ARB/KHR/OES (Khronos extensions) 52# other (EXT/vendor extensions) 53# This will need changing for Vulkan! 54def regSortCategoryKey(feature): 55 if (feature.elem.tag == 'feature'): 56 return 0 57 elif (feature.category == 'ARB' or 58 feature.category == 'KHR' or 59 feature.category == 'OES'): 60 return 1 61 else: 62 return 2 63 64# Secondary sort key for regSortFeatures. 65# Sorts by extension name. 66def regSortNameKey(feature): 67 return feature.name 68 69# Second sort key for regSortFeatures. 70# Sorts by feature version. <extension> elements all have version number "0" 71def regSortFeatureVersionKey(feature): 72 return float(feature.versionNumber) 73 74# Tertiary sort key for regSortFeatures. 75# Sorts by extension number. <feature> elements all have extension number 0. 76def regSortExtensionNumberKey(feature): 77 return int(feature.number) 78 79# regSortFeatures - default sort procedure for features. 80# Sorts by primary key of feature category ('feature' or 'extension') 81# then by version number (for features) 82# then by extension number (for extensions) 83def regSortFeatures(featureList): 84 featureList.sort(key = regSortExtensionNumberKey) 85 featureList.sort(key = regSortFeatureVersionKey) 86 featureList.sort(key = regSortCategoryKey) 87 88# GeneratorOptions - base class for options used during header production 89# These options are target language independent, and used by 90# Registry.apiGen() and by base OutputGenerator objects. 91# 92# Members 93# filename - basename of file to generate, or None to write to stdout. 94# directory - directory in which to generate filename 95# apiname - string matching <api> 'apiname' attribute, e.g. 'gl'. 96# profile - string specifying API profile , e.g. 'core', or None. 97# versions - regex matching API versions to process interfaces for. 98# Normally '.*' or '[0-9]\.[0-9]' to match all defined versions. 99# emitversions - regex matching API versions to actually emit 100# interfaces for (though all requested versions are considered 101# when deciding which interfaces to generate). For GL 4.3 glext.h, 102# this might be '1\.[2-5]|[2-4]\.[0-9]'. 103# defaultExtensions - If not None, a string which must in its 104# entirety match the pattern in the "supported" attribute of 105# the <extension>. Defaults to None. Usually the same as apiname. 106# addExtensions - regex matching names of additional extensions 107# to include. Defaults to None. 108# removeExtensions - regex matching names of extensions to 109# remove (after defaultExtensions and addExtensions). Defaults 110# to None. 111# emitExtensions - regex matching names of extensions to actually emit 112# interfaces for (though all requested versions are considered when 113# deciding which interfaces to generate). 114# sortProcedure - takes a list of FeatureInfo objects and sorts 115# them in place to a preferred order in the generated output. 116# Default is core API versions, ARB/KHR/OES extensions, all 117# other extensions, alphabetically within each group. 118# The regex patterns can be None or empty, in which case they match 119# nothing. 120class GeneratorOptions: 121 """Represents options during header production from an API registry""" 122 def __init__(self, 123 filename = None, 124 directory = '.', 125 apiname = None, 126 profile = None, 127 versions = '.*', 128 emitversions = '.*', 129 defaultExtensions = None, 130 addExtensions = None, 131 removeExtensions = None, 132 emitExtensions = None, 133 sortProcedure = regSortFeatures): 134 self.filename = filename 135 self.directory = directory 136 self.apiname = apiname 137 self.profile = profile 138 self.versions = self.emptyRegex(versions) 139 self.emitversions = self.emptyRegex(emitversions) 140 self.defaultExtensions = defaultExtensions 141 self.addExtensions = self.emptyRegex(addExtensions) 142 self.removeExtensions = self.emptyRegex(removeExtensions) 143 self.emitExtensions = self.emptyRegex(emitExtensions) 144 self.sortProcedure = sortProcedure 145 # 146 # Substitute a regular expression which matches no version 147 # or extension names for None or the empty string. 148 def emptyRegex(self,pat): 149 if (pat == None or pat == ''): 150 return '_nomatch_^' 151 else: 152 return pat 153 154# OutputGenerator - base class for generating API interfaces. 155# Manages basic logic, logging, and output file control 156# Derived classes actually generate formatted output. 157# 158# ---- methods ---- 159# OutputGenerator(errFile, warnFile, diagFile) 160# errFile, warnFile, diagFile - file handles to write errors, 161# warnings, diagnostics to. May be None to not write. 162# logMsg(level, *args) - log messages of different categories 163# level - 'error', 'warn', or 'diag'. 'error' will also 164# raise a UserWarning exception 165# *args - print()-style arguments 166# setExtMap(map) - specify a dictionary map from extension names to 167# numbers, used in creating values for extension enumerants. 168# makeDir(directory) - create a directory, if not already done. 169# Generally called from derived generators creating hierarchies. 170# beginFile(genOpts) - start a new interface file 171# genOpts - GeneratorOptions controlling what's generated and how 172# endFile() - finish an interface file, closing it when done 173# beginFeature(interface, emit) - write interface for a feature 174# and tag generated features as having been done. 175# interface - element for the <version> / <extension> to generate 176# emit - actually write to the header only when True 177# endFeature() - finish an interface. 178# genType(typeinfo,name,alias) - generate interface for a type 179# typeinfo - TypeInfo for a type 180# genStruct(typeinfo,name,alias) - generate interface for a C "struct" type. 181# typeinfo - TypeInfo for a type interpreted as a struct 182# genGroup(groupinfo,name,alias) - generate interface for a group of enums (C "enum") 183# groupinfo - GroupInfo for a group 184# genEnum(enuminfo,name,alias) - generate interface for an enum (constant) 185# enuminfo - EnumInfo for an enum 186# name - enum name 187# genCmd(cmdinfo,name,alias) - generate interface for a command 188# cmdinfo - CmdInfo for a command 189# isEnumRequired(enumElem) - return True if this <enum> element is required 190# elem - <enum> element to test 191# makeCDecls(cmd) - return C prototype and function pointer typedef for a 192# <command> Element, as a list of two strings 193# cmd - Element for the <command> 194# newline() - print a newline to the output file (utility function) 195# 196class OutputGenerator: 197 """Generate specified API interfaces in a specific style, such as a C header""" 198 # 199 # categoryToPath - map XML 'category' to include file directory name 200 categoryToPath = { 201 'bitmask' : 'flags', 202 'enum' : 'enums', 203 'funcpointer' : 'funcpointers', 204 'handle' : 'handles', 205 'define' : 'defines', 206 'basetype' : 'basetypes', 207 } 208 # 209 # Constructor 210 def __init__(self, 211 errFile = sys.stderr, 212 warnFile = sys.stderr, 213 diagFile = sys.stdout): 214 self.outFile = None 215 self.errFile = errFile 216 self.warnFile = warnFile 217 self.diagFile = diagFile 218 # Internal state 219 self.featureName = None 220 self.genOpts = None 221 self.registry = None 222 # Used for extension enum value generation 223 self.extBase = 1000000000 224 self.extBlockSize = 1000 225 self.madeDirs = {} 226 # 227 # logMsg - write a message of different categories to different 228 # destinations. 229 # level - 230 # 'diag' (diagnostic, voluminous) 231 # 'warn' (warning) 232 # 'error' (fatal error - raises exception after logging) 233 # *args - print()-style arguments to direct to corresponding log 234 def logMsg(self, level, *args): 235 """Log a message at the given level. Can be ignored or log to a file""" 236 if (level == 'error'): 237 strfile = io.StringIO() 238 write('ERROR:', *args, file=strfile) 239 if (self.errFile != None): 240 write(strfile.getvalue(), file=self.errFile) 241 raise UserWarning(strfile.getvalue()) 242 elif (level == 'warn'): 243 if (self.warnFile != None): 244 write('WARNING:', *args, file=self.warnFile) 245 elif (level == 'diag'): 246 if (self.diagFile != None): 247 write('DIAG:', *args, file=self.diagFile) 248 else: 249 raise UserWarning( 250 '*** FATAL ERROR in Generator.logMsg: unknown level:' + level) 251 # 252 # enumToValue - parses and converts an <enum> tag into a value. 253 # Returns a list 254 # first element - integer representation of the value, or None 255 # if needsNum is False. The value must be a legal number 256 # if needsNum is True. 257 # second element - string representation of the value 258 # There are several possible representations of values. 259 # A 'value' attribute simply contains the value. 260 # A 'bitpos' attribute defines a value by specifying the bit 261 # position which is set in that value. 262 # A 'offset','extbase','extends' triplet specifies a value 263 # as an offset to a base value defined by the specified 264 # 'extbase' extension name, which is then cast to the 265 # typename specified by 'extends'. This requires probing 266 # the registry database, and imbeds knowledge of the 267 # Vulkan extension enum scheme in this function. 268 # A 'alias' attribute contains the name of another enum 269 # which this is an alias of. The other enum must be 270 # declared first when emitting this enum. 271 def enumToValue(self, elem, needsNum): 272 name = elem.get('name') 273 numVal = None 274 if ('value' in elem.keys()): 275 value = elem.get('value') 276 # print('About to translate value =', value, 'type =', type(value)) 277 if (needsNum): 278 numVal = int(value, 0) 279 # If there's a non-integer, numeric 'type' attribute (e.g. 'u' or 280 # 'ull'), append it to the string value. 281 # t = enuminfo.elem.get('type') 282 # if (t != None and t != '' and t != 'i' and t != 's'): 283 # value += enuminfo.type 284 self.logMsg('diag', 'Enum', name, '-> value [', numVal, ',', value, ']') 285 return [numVal, value] 286 if ('bitpos' in elem.keys()): 287 value = elem.get('bitpos') 288 numVal = int(value, 0) 289 numVal = 1 << numVal 290 value = '0x%08x' % numVal 291 self.logMsg('diag', 'Enum', name, '-> bitpos [', numVal, ',', value, ']') 292 return [numVal, value] 293 if ('offset' in elem.keys()): 294 # Obtain values in the mapping from the attributes 295 enumNegative = False 296 offset = int(elem.get('offset'),0) 297 extnumber = int(elem.get('extnumber'),0) 298 extends = elem.get('extends') 299 if ('dir' in elem.keys()): 300 enumNegative = True 301 self.logMsg('diag', 'Enum', name, 'offset =', offset, 302 'extnumber =', extnumber, 'extends =', extends, 303 'enumNegative =', enumNegative) 304 # Now determine the actual enumerant value, as defined 305 # in the "Layers and Extensions" appendix of the spec. 306 numVal = self.extBase + (extnumber - 1) * self.extBlockSize + offset 307 if (enumNegative): 308 numVal = -numVal 309 value = '%d' % numVal 310 # More logic needed! 311 self.logMsg('diag', 'Enum', name, '-> offset [', numVal, ',', value, ']') 312 return [numVal, value] 313 if 'alias' in elem.keys(): 314 return [None, elem.get('alias')] 315 return [None, None] 316 # 317 # checkDuplicateEnums - sanity check for enumerated values 318 # enums - list of <enum> Elements 319 # returns the list with duplicates stripped 320 def checkDuplicateEnums(self, enums): 321 # Dictionaries indexed by name and numeric value. 322 # Entries are [ Element, numVal, strVal ] matching name or value 323 324 nameMap = {} 325 valueMap = {} 326 327 stripped = [] 328 for elem in enums: 329 name = elem.get('name') 330 (numVal, strVal) = self.enumToValue(elem, True) 331 332 if name in nameMap: 333 # Duplicate name found; check values 334 (name2, numVal2, strVal2) = nameMap[name] 335 336 # Duplicate enum values for the same name are benign. This 337 # happens when defining the same enum conditionally in 338 # several extension blocks. 339 if (strVal2 == strVal or (numVal != None and 340 numVal == numVal2)): 341 True 342 # self.logMsg('info', 'checkDuplicateEnums: Duplicate enum (' + name + 343 # ') found with the same value:' + strVal) 344 else: 345 self.logMsg('warn', 'checkDuplicateEnums: Duplicate enum (' + name + 346 ') found with different values:' + strVal + 347 ' and ' + strVal2) 348 349 # Don't add the duplicate to the returned list 350 continue 351 elif numVal in valueMap: 352 # Duplicate value found (such as an alias); report it, but 353 # still add this enum to the list. 354 (name2, numVal2, strVal2) = valueMap[numVal] 355 356 try: 357 self.logMsg('warn', 'Two enums found with the same value: ' 358 + name + ' = ' + name2.get('name') + ' = ' + strVal) 359 except: 360 pdb.set_trace() 361 362 # Track this enum to detect followon duplicates 363 nameMap[name] = [ elem, numVal, strVal ] 364 if numVal != None: 365 valueMap[numVal] = [ elem, numVal, strVal ] 366 367 # Add this enum to the list 368 stripped.append(elem) 369 370 # Return the list 371 return stripped 372 # 373 def makeDir(self, path): 374 self.logMsg('diag', 'OutputGenerator::makeDir(' + path + ')') 375 if not (path in self.madeDirs.keys()): 376 # This can get race conditions with multiple writers, see 377 # https://stackoverflow.com/questions/273192/ 378 if not os.path.exists(path): 379 os.makedirs(path) 380 self.madeDirs[path] = None 381 # 382 def beginFile(self, genOpts): 383 self.genOpts = genOpts 384 # 385 # Open specified output file. Not done in constructor since a 386 # Generator can be used without writing to a file. 387 if (self.genOpts.filename != None): 388 filename = self.genOpts.directory + '/' + self.genOpts.filename 389 self.outFile = io.open(filename, 'w', encoding='utf-8') 390 else: 391 self.outFile = sys.stdout 392 def endFile(self): 393 self.errFile and self.errFile.flush() 394 self.warnFile and self.warnFile.flush() 395 self.diagFile and self.diagFile.flush() 396 self.outFile.flush() 397 if (self.outFile != sys.stdout and self.outFile != sys.stderr): 398 self.outFile.close() 399 self.genOpts = None 400 # 401 def beginFeature(self, interface, emit): 402 self.emit = emit 403 self.featureName = interface.get('name') 404 # If there's an additional 'protect' attribute in the feature, save it 405 self.featureExtraProtect = interface.get('protect') 406 def endFeature(self): 407 # Derived classes responsible for emitting feature 408 self.featureName = None 409 self.featureExtraProtect = None 410 # Utility method to validate we're generating something only inside a 411 # <feature> tag 412 def validateFeature(self, featureType, featureName): 413 if (self.featureName == None): 414 raise UserWarning('Attempt to generate', featureType, 415 featureName, 'when not in feature') 416 # 417 # Type generation 418 def genType(self, typeinfo, name, alias): 419 self.validateFeature('type', name) 420 # 421 # Struct (e.g. C "struct" type) generation 422 def genStruct(self, typeinfo, name, alias): 423 self.validateFeature('struct', name) 424 425 # The mixed-mode <member> tags may contain no-op <comment> tags. 426 # It is convenient to remove them here where all output generators 427 # will benefit. 428 for member in typeinfo.elem.findall('.//member'): 429 for comment in member.findall('comment'): 430 member.remove(comment) 431 # 432 # Group (e.g. C "enum" type) generation 433 def genGroup(self, groupinfo, name, alias): 434 self.validateFeature('group', name) 435 # 436 # Enumerant (really, constant) generation 437 def genEnum(self, enuminfo, name, alias): 438 self.validateFeature('enum', name) 439 # 440 # Command generation 441 def genCmd(self, cmd, name, alias): 442 self.validateFeature('command', name) 443 # 444 # Utility functions - turn a <proto> <name> into C-language prototype 445 # and typedef declarations for that name. 446 # name - contents of <name> tag 447 # tail - whatever text follows that tag in the Element 448 def makeProtoName(self, name, tail): 449 return self.genOpts.apientry + name + tail 450 def makeTypedefName(self, name, tail): 451 return '(' + self.genOpts.apientryp + 'PFN_' + name + tail + ')' 452 # 453 # makeCParamDecl - return a string which is an indented, formatted 454 # declaration for a <param> or <member> block (e.g. function parameter 455 # or structure/union member). 456 # param - Element (<param> or <member>) to format 457 # aligncol - if non-zero, attempt to align the nested <name> element 458 # at this column 459 def makeCParamDecl(self, param, aligncol): 460 paramdecl = ' ' + noneStr(param.text) 461 for elem in param: 462 text = noneStr(elem.text) 463 tail = noneStr(elem.tail) 464 if (elem.tag == 'name' and aligncol > 0): 465 self.logMsg('diag', 'Aligning parameter', elem.text, 'to column', self.genOpts.alignFuncParam) 466 # Align at specified column, if possible 467 paramdecl = paramdecl.rstrip() 468 oldLen = len(paramdecl) 469 # This works around a problem where very long type names - 470 # longer than the alignment column - would run into the tail 471 # text. 472 paramdecl = paramdecl.ljust(aligncol-1) + ' ' 473 newLen = len(paramdecl) 474 self.logMsg('diag', 'Adjust length of parameter decl from', oldLen, 'to', newLen, ':', paramdecl) 475 paramdecl += text + tail 476 return paramdecl 477 # 478 # getCParamTypeLength - return the length of the type field is an indented, formatted 479 # declaration for a <param> or <member> block (e.g. function parameter 480 # or structure/union member). 481 # param - Element (<param> or <member>) to identify 482 def getCParamTypeLength(self, param): 483 paramdecl = ' ' + noneStr(param.text) 484 for elem in param: 485 text = noneStr(elem.text) 486 tail = noneStr(elem.tail) 487 if (elem.tag == 'name'): 488 # Align at specified column, if possible 489 newLen = len(paramdecl.rstrip()) 490 self.logMsg('diag', 'Identifying length of', elem.text, 'as', newLen) 491 paramdecl += text + tail 492 return newLen 493 # 494 # isEnumRequired(elem) - return True if this <enum> element is 495 # required, False otherwise 496 # elem - <enum> element to test 497 def isEnumRequired(self, elem): 498 required = elem.get('required') != None 499 self.logMsg('diag', 'isEnumRequired:', elem.get('name'), 500 '->', required) 501 return required 502 503 #@@@ This code is overridden by equivalent code now run in 504 #@@@ Registry.generateFeature 505 506 required = False 507 508 extname = elem.get('extname') 509 if extname is not None: 510 # 'supported' attribute was injected when the <enum> element was 511 # moved into the <enums> group in Registry.parseTree() 512 if self.genOpts.defaultExtensions == elem.get('supported'): 513 required = True 514 elif re.match(self.genOpts.addExtensions, extname) is not None: 515 required = True 516 elif elem.get('version') is not None: 517 required = re.match(self.genOpts.emitversions, elem.get('version')) is not None 518 else: 519 required = True 520 521 return required 522 523 # 524 # makeCDecls - return C prototype and function pointer typedef for a 525 # command, as a two-element list of strings. 526 # cmd - Element containing a <command> tag 527 def makeCDecls(self, cmd): 528 """Generate C function pointer typedef for <command> Element""" 529 proto = cmd.find('proto') 530 params = cmd.findall('param') 531 # Begin accumulating prototype and typedef strings 532 pdecl = self.genOpts.apicall 533 tdecl = 'typedef ' 534 # 535 # Insert the function return type/name. 536 # For prototypes, add APIENTRY macro before the name 537 # For typedefs, add (APIENTRY *<name>) around the name and 538 # use the PFN_cmdnameproc naming convention. 539 # Done by walking the tree for <proto> element by element. 540 # etree has elem.text followed by (elem[i], elem[i].tail) 541 # for each child element and any following text 542 # Leading text 543 pdecl += noneStr(proto.text) 544 tdecl += noneStr(proto.text) 545 # For each child element, if it's a <name> wrap in appropriate 546 # declaration. Otherwise append its contents and tail contents. 547 for elem in proto: 548 text = noneStr(elem.text) 549 tail = noneStr(elem.tail) 550 if (elem.tag == 'name'): 551 pdecl += self.makeProtoName(text, tail) 552 tdecl += self.makeTypedefName(text, tail) 553 else: 554 pdecl += text + tail 555 tdecl += text + tail 556 # Now add the parameter declaration list, which is identical 557 # for prototypes and typedefs. Concatenate all the text from 558 # a <param> node without the tags. No tree walking required 559 # since all tags are ignored. 560 # Uses: self.indentFuncProto 561 # self.indentFuncPointer 562 # self.alignFuncParam 563 # Might be able to doubly-nest the joins, e.g. 564 # ','.join(('_'.join([l[i] for i in range(0,len(l))]) 565 n = len(params) 566 # Indented parameters 567 if n > 0: 568 indentdecl = '(\n' 569 for i in range(0,n): 570 paramdecl = self.makeCParamDecl(params[i], self.genOpts.alignFuncParam) 571 if (i < n - 1): 572 paramdecl += ',\n' 573 else: 574 paramdecl += ');' 575 indentdecl += paramdecl 576 else: 577 indentdecl = '(void);' 578 # Non-indented parameters 579 paramdecl = '(' 580 if n > 0: 581 for i in range(0,n): 582 paramdecl += ''.join([t for t in params[i].itertext()]) 583 if (i < n - 1): 584 paramdecl += ', ' 585 else: 586 paramdecl += 'void' 587 paramdecl += ");"; 588 return [ pdecl + indentdecl, tdecl + paramdecl ] 589 # 590 def newline(self): 591 write('', file=self.outFile) 592 593 def setRegistry(self, registry): 594 self.registry = registry 595 # 596