1#!/usr/bin/python3 -i 2# 3# Copyright 2013-2024 The Khronos Group Inc. 4# 5# SPDX-License-Identifier: Apache-2.0 6"""Base class for source/header/doc generators, as well as some utility functions.""" 7 8from __future__ import unicode_literals 9 10import io 11import os 12import pdb 13import re 14import shutil 15import sys 16import tempfile 17try: 18 from pathlib import Path 19except ImportError: 20 from pathlib2 import Path # type: ignore 21 22from spec_tools.util import getElemName, getElemType 23 24 25def write(*args, **kwargs): 26 file = kwargs.pop('file', sys.stdout) 27 end = kwargs.pop('end', '\n') 28 file.write(' '.join(str(arg) for arg in args)) 29 file.write(end) 30 31 32def noneStr(s): 33 """Return string argument, or "" if argument is None. 34 35 Used in converting etree Elements into text. 36 s - string to convert""" 37 if s: 38 return s 39 return "" 40 41 42def enquote(s): 43 """Return string argument with surrounding quotes, 44 for serialization into Python code.""" 45 if s: 46 if isinstance(s, str): 47 return f"'{s}'" 48 else: 49 return s 50 return None 51 52 53def regSortCategoryKey(feature): 54 """Sort key for regSortFeatures. 55 Sorts by category of the feature name string: 56 57 - Core API features (those defined with a `<feature>` tag) 58 - (sort VKSC after VK - this is Vulkan-specific) 59 - ARB/KHR/OES (Khronos extensions) 60 - other (EXT/vendor extensions)""" 61 62 if feature.elem.tag == 'feature': 63 if feature.name.startswith('VKSC'): 64 return 0.5 65 else: 66 return 0 67 68 if feature.category.upper() in ['ARB', 'KHR', 'OES']: 69 return 1 70 71 return 2 72 73 74def regSortOrderKey(feature): 75 """Sort key for regSortFeatures - key is the sortorder attribute.""" 76 77 return feature.sortorder 78 79 80def regSortNameKey(feature): 81 """Sort key for regSortFeatures - key is the extension name.""" 82 83 return feature.name 84 85 86def regSortFeatureVersionKey(feature): 87 """Sort key for regSortFeatures - key is the feature version. 88 `<extension>` elements all have version number 0.""" 89 90 return float(feature.versionNumber) 91 92 93def regSortExtensionNumberKey(feature): 94 """Sort key for regSortFeatures - key is the extension number. 95 `<feature>` elements all have extension number 0.""" 96 97 return int(feature.number) 98 99 100def regSortFeatures(featureList): 101 """Default sort procedure for features. 102 103 - Sorts by explicit sort order (default 0) relative to other features 104 - then by feature category ('feature' or 'extension'), 105 - then by version number (for features) 106 - then by extension number (for extensions)""" 107 featureList.sort(key=regSortExtensionNumberKey) 108 featureList.sort(key=regSortFeatureVersionKey) 109 featureList.sort(key=regSortCategoryKey) 110 featureList.sort(key=regSortOrderKey) 111 112 113class MissingGeneratorOptionsError(RuntimeError): 114 """Error raised when a Generator tries to do something that requires GeneratorOptions but it is None.""" 115 116 def __init__(self, msg=None): 117 full_msg = 'Missing generator options object self.genOpts' 118 if msg: 119 full_msg += ': ' + msg 120 super().__init__(full_msg) 121 122 123class MissingRegistryError(RuntimeError): 124 """Error raised when a Generator tries to do something that requires a Registry object but it is None.""" 125 126 def __init__(self, msg=None): 127 full_msg = 'Missing Registry object self.registry' 128 if msg: 129 full_msg += ': ' + msg 130 super().__init__(full_msg) 131 132 133class MissingGeneratorOptionsConventionsError(RuntimeError): 134 """Error raised when a Generator tries to do something that requires a Conventions object but it is None.""" 135 136 def __init__(self, msg=None): 137 full_msg = 'Missing Conventions object self.genOpts.conventions' 138 if msg: 139 full_msg += ': ' + msg 140 super().__init__(full_msg) 141 142 143class GeneratorOptions: 144 """Base class for options used during header/documentation production. 145 146 These options are target language independent, and used by 147 Registry.apiGen() and by base OutputGenerator objects.""" 148 149 def __init__(self, 150 conventions=None, 151 filename=None, 152 directory='.', 153 genpath=None, 154 apiname=None, 155 mergeApiNames=None, 156 profile=None, 157 versions='.*', 158 emitversions='.*', 159 defaultExtensions=None, 160 addExtensions=None, 161 removeExtensions=None, 162 emitExtensions=None, 163 emitSpirv=None, 164 emitFormats=None, 165 reparentEnums=True, 166 sortProcedure=regSortFeatures, 167 requireCommandAliases=False, 168 requireDepends=True, 169 ): 170 """Constructor. 171 172 Arguments: 173 174 - conventions - may be mandatory for some generators: 175 an object that implements ConventionsBase 176 - filename - basename of file to generate, or None to write to stdout. 177 - directory - directory in which to generate filename 178 - genpath - path to previously generated files, such as apimap.py 179 - apiname - string matching `<api>` 'apiname' attribute, e.g. 'gl'. 180 - mergeApiNames - If not None, a comma separated list of API names 181 to merge into the API specified by 'apiname' 182 - profile - string specifying API profile , e.g. 'core', or None. 183 - versions - regex matching API versions to process interfaces for. 184 Normally `'.*'` or `'[0-9][.][0-9]'` to match all defined versions. 185 - emitversions - regex matching API versions to actually emit 186 interfaces for (though all requested versions are considered 187 when deciding which interfaces to generate). For GL 4.3 glext.h, 188 this might be `'1[.][2-5]|[2-4][.][0-9]'`. 189 - defaultExtensions - If not None, a string which must in its 190 entirety match the pattern in the "supported" attribute of 191 the `<extension>`. Defaults to None. Usually the same as apiname. 192 - addExtensions - regex matching names of additional extensions 193 to include. Defaults to None. 194 - removeExtensions - regex matching names of extensions to 195 remove (after defaultExtensions and addExtensions). Defaults 196 to None. 197 - emitExtensions - regex matching names of extensions to actually emit 198 interfaces for (though all requested versions are considered when 199 deciding which interfaces to generate). Defaults to None. 200 - emitSpirv - regex matching names of extensions and capabilities 201 to actually emit interfaces for. 202 - emitFormats - regex matching names of formats to actually emit 203 interfaces for. 204 - reparentEnums - move <enum> elements which extend an enumerated 205 type from <feature> or <extension> elements to the target <enums> 206 element. This is required for almost all purposes, but the 207 InterfaceGenerator relies on the list of interfaces in the <feature> 208 or <extension> being complete. Defaults to True. 209 - sortProcedure - takes a list of FeatureInfo objects and sorts 210 them in place to a preferred order in the generated output. 211 - requireCommandAliases - if True, treat command aliases 212 as required dependencies. 213 - requireDepends - whether to follow API dependencies when emitting 214 APIs. 215 216 Default is 217 - core API versions 218 - Khronos (ARB/KHR/OES) extensions 219 - All other extensions 220 - By core API version number or extension number in each group. 221 222 The regex patterns can be None or empty, in which case they match 223 nothing.""" 224 self.conventions = conventions 225 """may be mandatory for some generators: 226 an object that implements ConventionsBase""" 227 228 self.filename = filename 229 "basename of file to generate, or None to write to stdout." 230 231 self.genpath = genpath 232 """path to previously generated files, such as apimap.py""" 233 234 self.directory = directory 235 "directory in which to generate filename" 236 237 self.apiname = apiname 238 "string matching `<api>` 'apiname' attribute, e.g. 'gl'." 239 240 self.mergeApiNames = mergeApiNames 241 "comma separated list of API names to merge into the API specified by 'apiname'" 242 243 self.profile = profile 244 "string specifying API profile , e.g. 'core', or None." 245 246 self.versions = self.emptyRegex(versions) 247 """regex matching API versions to process interfaces for. 248 Normally `'.*'` or `'[0-9][.][0-9]'` to match all defined versions.""" 249 250 self.emitversions = self.emptyRegex(emitversions) 251 """regex matching API versions to actually emit 252 interfaces for (though all requested versions are considered 253 when deciding which interfaces to generate). For GL 4.3 glext.h, 254 this might be `'1[.][2-5]|[2-4][.][0-9]'`.""" 255 256 self.defaultExtensions = defaultExtensions 257 """If not None, a string which must in its 258 entirety match the pattern in the "supported" attribute of 259 the `<extension>`. Defaults to None. Usually the same as apiname.""" 260 261 self.addExtensions = self.emptyRegex(addExtensions) 262 """regex matching names of additional extensions 263 to include. Defaults to None.""" 264 265 self.removeExtensions = self.emptyRegex(removeExtensions) 266 """regex matching names of extensions to 267 remove (after defaultExtensions and addExtensions). Defaults 268 to None.""" 269 270 self.emitExtensions = self.emptyRegex(emitExtensions) 271 """regex matching names of extensions to actually emit 272 interfaces for (though all requested versions are considered when 273 deciding which interfaces to generate).""" 274 275 self.emitSpirv = self.emptyRegex(emitSpirv) 276 """regex matching names of extensions and capabilities 277 to actually emit interfaces for.""" 278 279 self.emitFormats = self.emptyRegex(emitFormats) 280 """regex matching names of formats 281 to actually emit interfaces for.""" 282 283 self.reparentEnums = reparentEnums 284 """boolean specifying whether to remove <enum> elements from 285 <feature> or <extension> when extending an <enums> type.""" 286 287 self.sortProcedure = sortProcedure 288 """takes a list of FeatureInfo objects and sorts 289 them in place to a preferred order in the generated output. 290 Default is core API versions, ARB/KHR/OES extensions, all 291 other extensions, alphabetically within each group.""" 292 293 self.codeGenerator = False 294 """True if this generator makes compilable code""" 295 296 self.registry = None 297 """Populated later with the registry object.""" 298 299 self.requireCommandAliases = requireCommandAliases 300 """True if alias= attributes of <command> tags are transitively 301 required.""" 302 303 self.requireDepends = requireDepends 304 """True if dependencies of API tags are transitively required.""" 305 306 def emptyRegex(self, pat): 307 """Substitute a regular expression which matches no version 308 or extension names for None or the empty string.""" 309 if not pat: 310 return '_nomatch_^' 311 312 return pat 313 314 315class OutputGenerator: 316 """Generate specified API interfaces in a specific style, such as a C header. 317 318 Base class for generating API interfaces. 319 Manages basic logic, logging, and output file control. 320 Derived classes actually generate formatted output. 321 """ 322 323 # categoryToPath - map XML 'category' to include file directory name 324 categoryToPath = { 325 'bitmask': 'flags', 326 'enum': 'enums', 327 'funcpointer': 'funcpointers', 328 'handle': 'handles', 329 'define': 'defines', 330 'basetype': 'basetypes', 331 } 332 333 def breakName(self, name, msg): 334 """Break into debugger if this is a special name""" 335 336 # List of string names to break on 337 bad = ( 338 ) 339 340 if name in bad and True: 341 print('breakName {}: {}'.format(name, msg)) 342 pdb.set_trace() 343 344 def __init__(self, errFile=sys.stderr, warnFile=sys.stderr, diagFile=sys.stdout): 345 """Constructor 346 347 - errFile, warnFile, diagFile - file handles to write errors, 348 warnings, diagnostics to. May be None to not write.""" 349 self.outFile = None 350 self.errFile = errFile 351 self.warnFile = warnFile 352 self.diagFile = diagFile 353 # Internal state 354 self.featureName = None 355 """The current feature name being generated.""" 356 357 self.genOpts = None 358 """The GeneratorOptions subclass instance.""" 359 360 self.registry = None 361 """The specification registry object.""" 362 363 self.featureDictionary = {} 364 """The dictionary of dictionaries of API features.""" 365 366 # Used for extension enum value generation 367 self.extBase = 1000000000 368 self.extBlockSize = 1000 369 self.madeDirs = {} 370 371 # API dictionary, which may be loaded by the beginFile method of 372 # derived generators. 373 self.apidict = None 374 375 # File suffix for generated files, set in beginFile below. 376 self.file_suffix = '' 377 378 def logMsg(self, level, *args): 379 """Write a message of different categories to different 380 destinations. 381 382 - `level` 383 - 'diag' (diagnostic, voluminous) 384 - 'warn' (warning) 385 - 'error' (fatal error - raises exception after logging) 386 387 - `*args` - print()-style arguments to direct to corresponding log""" 388 if level == 'error': 389 strfile = io.StringIO() 390 write('ERROR:', *args, file=strfile) 391 if self.errFile is not None: 392 write(strfile.getvalue(), file=self.errFile) 393 raise UserWarning(strfile.getvalue()) 394 elif level == 'warn': 395 if self.warnFile is not None: 396 write('WARNING:', *args, file=self.warnFile) 397 elif level == 'diag': 398 if self.diagFile is not None: 399 write('DIAG:', *args, file=self.diagFile) 400 else: 401 raise UserWarning( 402 '*** FATAL ERROR in Generator.logMsg: unknown level:' + level) 403 404 def enumToValue(self, elem, needsNum, bitwidth = 32, 405 forceSuffix = False, parent_for_alias_dereference=None): 406 """Parse and convert an `<enum>` tag into a value. 407 408 - elem - <enum> Element 409 - needsNum - generate a numeric representation of the element value 410 - bitwidth - size of the numeric representation in bits (32 or 64) 411 - forceSuffix - if True, always use a 'U' / 'ULL' suffix on integers 412 - parent_for_alias_dereference - if not None, an Element containing 413 the parent of elem, used to look for elements this is an alias of 414 415 Returns a list: 416 417 - first element - integer representation of the value, or None 418 if needsNum is False. The value must be a legal number 419 if needsNum is True. 420 - second element - string representation of the value 421 422 There are several possible representations of values. 423 424 - A 'value' attribute simply contains the value. 425 - A 'bitpos' attribute defines a value by specifying the bit 426 position which is set in that value. 427 - An 'offset','extbase','extends' triplet specifies a value 428 as an offset to a base value defined by the specified 429 'extbase' extension name, which is then cast to the 430 typename specified by 'extends'. This requires probing 431 the registry database, and imbeds knowledge of the 432 API extension enum scheme in this function. 433 - An 'alias' attribute contains the name of another enum 434 which this is an alias of. The other enum must be 435 declared first when emitting this enum.""" 436 if self.genOpts is None: 437 raise MissingGeneratorOptionsError() 438 if self.genOpts.conventions is None: 439 raise MissingGeneratorOptionsConventionsError() 440 441 name = elem.get('name') 442 numVal = None 443 if 'value' in elem.keys(): 444 value = elem.get('value') 445 # print('About to translate value =', value, 'type =', type(value)) 446 if needsNum: 447 numVal = int(value, 0) 448 # If there is a non-integer, numeric 'type' attribute (e.g. 'u' or 449 # 'ull'), append it to the string value. 450 # t = enuminfo.elem.get('type') 451 # if t is not None and t != '' and t != 'i' and t != 's': 452 # value += enuminfo.type 453 if forceSuffix: 454 if bitwidth == 64: 455 value = value + 'ULL' 456 else: 457 value = value + 'U' 458 self.logMsg('diag', 'Enum', name, '-> value [', numVal, ',', value, ']') 459 return [numVal, value] 460 if 'bitpos' in elem.keys(): 461 value = elem.get('bitpos') 462 bitpos = int(value, 0) 463 numVal = 1 << bitpos 464 value = '0x%08x' % numVal 465 if bitwidth == 64 or bitpos >= 32: 466 value = value + 'ULL' 467 elif forceSuffix: 468 value = value + 'U' 469 self.logMsg('diag', 'Enum', name, '-> bitpos [', numVal, ',', value, ']') 470 return [numVal, value] 471 if 'offset' in elem.keys(): 472 # Obtain values in the mapping from the attributes 473 enumNegative = False 474 offset = int(elem.get('offset'), 0) 475 extnumber = int(elem.get('extnumber'), 0) 476 extends = elem.get('extends') 477 if 'dir' in elem.keys(): 478 enumNegative = True 479 self.logMsg('diag', 'Enum', name, 'offset =', offset, 480 'extnumber =', extnumber, 'extends =', extends, 481 'enumNegative =', enumNegative) 482 # Now determine the actual enumerant value, as defined 483 # in the "Layers and Extensions" appendix of the spec. 484 numVal = self.extBase + (extnumber - 1) * self.extBlockSize + offset 485 if enumNegative: 486 numVal *= -1 487 value = '%d' % numVal 488 # More logic needed! 489 self.logMsg('diag', 'Enum', name, '-> offset [', numVal, ',', value, ']') 490 return [numVal, value] 491 if 'alias' in elem.keys(): 492 alias_of = elem.get('alias') 493 if parent_for_alias_dereference is None: 494 return (None, alias_of) 495 siblings = parent_for_alias_dereference.findall('enum') 496 for sib in siblings: 497 sib_name = sib.get('name') 498 if sib_name == alias_of: 499 return self.enumToValue(sib, needsNum) 500 raise RuntimeError("Could not find the aliased enum value") 501 return [None, None] 502 503 def checkDuplicateEnums(self, enums): 504 """Check enumerated values for duplicates. 505 506 - enums - list of `<enum>` Elements 507 508 returns the list with duplicates stripped""" 509 # Dictionaries indexed by name and numeric value. 510 # Entries are [ Element, numVal, strVal ] matching name or value 511 512 nameMap = {} 513 valueMap = {} 514 515 stripped = [] 516 for elem in enums: 517 name = elem.get('name') 518 (numVal, strVal) = self.enumToValue(elem, True) 519 520 if name in nameMap: 521 # Duplicate name found; check values 522 (name2, numVal2, strVal2) = nameMap[name] 523 524 # Duplicate enum values for the same name are benign. This 525 # happens when defining the same enum conditionally in 526 # several extension blocks. 527 if (strVal2 == strVal or (numVal is not None 528 and numVal == numVal2)): 529 True 530 # self.logMsg('info', 'checkDuplicateEnums: Duplicate enum (' + name + 531 # ') found with the same value:' + strVal) 532 else: 533 self.logMsg('warn', 'checkDuplicateEnums: Duplicate enum (' + name 534 + ') found with different values:' + strVal 535 + ' and ' + strVal2) 536 537 # Do not add the duplicate to the returned list 538 continue 539 elif numVal in valueMap: 540 # Duplicate value found (such as an alias); report it, but 541 # still add this enum to the list. 542 (name2, numVal2, strVal2) = valueMap[numVal] 543 544 msg = 'Two enums found with the same value: {} = {} = {}'.format( 545 name, name2.get('name'), strVal) 546 self.logMsg('error', msg) 547 548 # Track this enum to detect followon duplicates 549 nameMap[name] = [elem, numVal, strVal] 550 if numVal is not None: 551 valueMap[numVal] = [elem, numVal, strVal] 552 553 # Add this enum to the list 554 stripped.append(elem) 555 556 # Return the list 557 return stripped 558 559 def misracstyle(self): 560 return False; 561 562 def misracppstyle(self): 563 return False; 564 565 def buildEnumCDecl(self, expand, groupinfo, groupName): 566 """Generate the C declaration for an enum""" 567 if self.genOpts is None: 568 raise MissingGeneratorOptionsError() 569 if self.genOpts.conventions is None: 570 raise MissingGeneratorOptionsConventionsError() 571 572 groupElem = groupinfo.elem 573 574 # Determine the required bit width for the enum group. 575 # 32 is the default, which generates C enum types for the values. 576 bitwidth = 32 577 578 # If the constFlagBits preference is set, 64 is the default for bitmasks 579 if self.genOpts.conventions.constFlagBits and groupElem.get('type') == 'bitmask': 580 bitwidth = 64 581 582 # Check for an explicitly defined bitwidth, which will override any defaults. 583 if groupElem.get('bitwidth'): 584 try: 585 bitwidth = int(groupElem.get('bitwidth')) 586 except ValueError as ve: 587 self.logMsg('error', 'Invalid value for bitwidth attribute (', groupElem.get('bitwidth'), ') for ', groupName, ' - must be an integer value\n') 588 exit(1) 589 590 usebitmask = False 591 usedefine = False 592 593 # Bitmask flags can be generated as either "static const uint{32,64}_t" values, 594 # or as 32-bit C enums. 64-bit types must use uint64_t values. 595 if groupElem.get('type') == 'bitmask': 596 if bitwidth > 32 or self.misracppstyle(): 597 usebitmask = True 598 if self.misracstyle(): 599 usedefine = True 600 601 if usedefine or usebitmask: 602 # Validate the bitwidth and generate values appropriately 603 if bitwidth > 64: 604 self.logMsg('error', 'Invalid value for bitwidth attribute (', groupElem.get('bitwidth'), ') for bitmask type ', groupName, ' - must be less than or equal to 64\n') 605 exit(1) 606 else: 607 return self.buildEnumCDecl_BitmaskOrDefine(groupinfo, groupName, bitwidth, usedefine) 608 else: 609 # Validate the bitwidth and generate values appropriately 610 if bitwidth > 32: 611 self.logMsg('error', 'Invalid value for bitwidth attribute (', groupElem.get('bitwidth'), ') for enum type ', groupName, ' - must be less than or equal to 32\n') 612 exit(1) 613 else: 614 return self.buildEnumCDecl_Enum(expand, groupinfo, groupName) 615 616 def buildEnumCDecl_BitmaskOrDefine(self, groupinfo, groupName, bitwidth, usedefine): 617 """Generate the C declaration for an "enum" that is actually a 618 set of flag bits""" 619 groupElem = groupinfo.elem 620 flagTypeName = groupElem.get('name') 621 622 # Prefix 623 body = "// Flag bits for " + flagTypeName + "\n" 624 625 if bitwidth == 64: 626 body += "typedef VkFlags64 %s;\n" % flagTypeName; 627 else: 628 body += "typedef VkFlags %s;\n" % flagTypeName; 629 630 # Maximum allowable value for a flag (unsigned 64-bit integer) 631 maxValidValue = 2**(64) - 1 632 minValidValue = 0 633 634 # Get a list of nested 'enum' tags. 635 enums = groupElem.findall('enum') 636 637 # Check for and report duplicates, and return a list with them 638 # removed. 639 enums = self.checkDuplicateEnums(enums) 640 641 # Accumulate non-numeric enumerant values separately and append 642 # them following the numeric values, to allow for aliases. 643 # NOTE: this does not do a topological sort yet, so aliases of 644 # aliases can still get in the wrong order. 645 aliasText = '' 646 647 # Loop over the nested 'enum' tags. 648 for elem in enums: 649 # Convert the value to an integer and use that to track min/max. 650 # Values of form -(number) are accepted but nothing more complex. 651 # Should catch exceptions here for more complex constructs. Not yet. 652 (numVal, strVal) = self.enumToValue(elem, True, bitwidth, True) 653 name = elem.get('name') 654 655 # Range check for the enum value 656 if numVal is not None and (numVal > maxValidValue or numVal < minValidValue): 657 self.logMsg('error', 'Allowable range for flag types in C is [', minValidValue, ',', maxValidValue, '], but', name, 'flag has a value outside of this (', strVal, ')\n') 658 exit(1) 659 660 decl = self.genRequirements(name, mustBeFound = False) 661 662 if self.isEnumRequired(elem): 663 protect = elem.get('protect') 664 if protect is not None: 665 body += '#ifdef {}\n'.format(protect) 666 667 if usedefine: 668 decl += "#define {} {}\n".format(name, strVal) 669 elif self.misracppstyle(): 670 decl += "static constexpr {} {} {{{}}};\n".format(flagTypeName, name, strVal) 671 else: 672 # Some C compilers only allow initializing a 'static const' variable with a literal value. 673 # So initializing an alias from another 'static const' value would fail to compile. 674 # Work around this by chasing the aliases to get the actual value. 675 while numVal is None: 676 alias = self.registry.tree.find("enums/enum[@name='" + strVal + "']") 677 if alias is not None: 678 (numVal, strVal) = self.enumToValue(alias, True, bitwidth, True) 679 else: 680 self.logMsg('error', 'No such alias {} for enum {}'.format(strVal, name)) 681 decl += "static const {} {} = {};\n".format(flagTypeName, name, strVal) 682 683 if numVal is not None: 684 body += decl 685 else: 686 aliasText += decl 687 688 if protect is not None: 689 body += '#endif\n' 690 691 # Now append the non-numeric enumerant values 692 body += aliasText 693 694 # Postfix 695 696 return ("bitmask", body) 697 698 def buildEnumCDecl_Enum(self, expand, groupinfo, groupName): 699 """Generate the C declaration for an enumerated type""" 700 groupElem = groupinfo.elem 701 702 # Break the group name into prefix and suffix portions for range 703 # enum generation 704 expandName = re.sub(r'([0-9]+|[a-z_])([A-Z0-9])', r'\1_\2', groupName).upper() 705 expandPrefix = expandName 706 expandSuffix = '' 707 expandSuffixMatch = re.search(r'[A-Z][A-Z]+$', groupName) 708 if expandSuffixMatch: 709 expandSuffix = '_' + expandSuffixMatch.group() 710 # Strip off the suffix from the prefix 711 expandPrefix = expandName.rsplit(expandSuffix, 1)[0] 712 713 # Prefix 714 body = ["typedef enum %s {" % groupName] 715 716 # @@ Should use the type="bitmask" attribute instead 717 isEnum = ('FLAG_BITS' not in expandPrefix) 718 719 # Allowable range for a C enum - which is that of a signed 32-bit integer 720 maxValidValue = 2**(32 - 1) - 1 721 minValidValue = (maxValidValue * -1) - 1 722 723 # Get a list of nested 'enum' tags. 724 enums = groupElem.findall('enum') 725 726 # Check for and report duplicates, and return a list with them 727 # removed. 728 enums = self.checkDuplicateEnums(enums) 729 730 # Loop over the nested 'enum' tags. Keep track of the minimum and 731 # maximum numeric values, if they can be determined; but only for 732 # core API enumerants, not extension enumerants. This is inferred 733 # by looking for 'extends' attributes. 734 minName = None 735 736 # Accumulate non-numeric enumerant values separately and append 737 # them following the numeric values, to allow for aliases. 738 # NOTE: this does not do a topological sort yet, so aliases of 739 # aliases can still get in the wrong order. 740 aliasText = [] 741 742 maxName = None 743 minValue = None 744 maxValue = None 745 for elem in enums: 746 # Convert the value to an integer and use that to track min/max. 747 # Values of form -(number) are accepted but nothing more complex. 748 # Should catch exceptions here for more complex constructs. Not yet. 749 (numVal, strVal) = self.enumToValue(elem, True) 750 name = elem.get('name') 751 752 # Extension enumerants are only included if they are required 753 if self.isEnumRequired(elem): 754 decl = '' 755 756 protect = elem.get('protect') 757 if protect is not None: 758 decl += '#ifdef {}\n'.format(protect) 759 760 # Indent requirements comment, if there is one 761 requirements = self.genRequirements(name, mustBeFound = False) 762 if requirements != '': 763 requirements = ' ' + requirements 764 decl += requirements 765 decl += ' {} = {},'.format(name, strVal) 766 767 if protect is not None: 768 decl += '\n#endif' 769 770 if numVal is not None: 771 body.append(decl) 772 else: 773 aliasText.append(decl) 774 775 # Range check for the enum value 776 if numVal is not None and (numVal > maxValidValue or numVal < minValidValue): 777 self.logMsg('error', 'Allowable range for C enum types is [', minValidValue, ',', maxValidValue, '], but', name, 'has a value outside of this (', strVal, ')\n') 778 exit(1) 779 780 # Do not track min/max for non-numbers (numVal is None) 781 if isEnum and numVal is not None and elem.get('extends') is None: 782 if minName is None: 783 minName = maxName = name 784 minValue = maxValue = numVal 785 elif minValue is None or numVal < minValue: 786 minName = name 787 minValue = numVal 788 elif maxValue is None or numVal > maxValue: 789 maxName = name 790 maxValue = numVal 791 792 # Now append the non-numeric enumerant values 793 body.extend(aliasText) 794 795 # Generate min/max value tokens - legacy use case. 796 if isEnum and expand: 797 body.extend((f' {expandPrefix}_BEGIN_RANGE{expandSuffix} = {minName},', 798 f' {expandPrefix}_END_RANGE{expandSuffix} = {maxName},', 799 f' {expandPrefix}_RANGE_SIZE{expandSuffix} = ({maxName} - {minName} + 1),')) 800 801 # Generate a range-padding value to ensure the enum is 32 bits, but 802 # only in code generators, so it does not appear in documentation 803 if (self.genOpts.codeGenerator or 804 self.conventions.generate_max_enum_in_docs): 805 body.append(f' {expandPrefix}_MAX_ENUM{expandSuffix} = 0x7FFFFFFF') 806 807 # Postfix 808 body.append("} %s;" % groupName) 809 810 # Determine appropriate section for this declaration 811 if groupElem.get('type') == 'bitmask': 812 section = 'bitmask' 813 else: 814 section = 'group' 815 816 return (section, '\n'.join(body)) 817 818 def buildConstantCDecl(self, enuminfo, name, alias): 819 """Generate the C declaration for a constant (a single <enum> 820 value). 821 822 <enum> tags may specify their values in several ways, but are 823 usually just integers or floating-point numbers.""" 824 825 (_, strVal) = self.enumToValue(enuminfo.elem, False) 826 827 if self.misracppstyle() and enuminfo.elem.get('type') and not alias: 828 # Generate e.g.: static constexpr uint32_t x = ~static_cast<uint32_t>(1U); 829 # This appeases MISRA "underlying type" rules. 830 typeStr = enuminfo.elem.get('type'); 831 invert = '~' in strVal 832 number = strVal.strip("()~UL") 833 if typeStr != "float": 834 number += 'U' 835 strVal = "~" if invert else "" 836 strVal += "static_cast<" + typeStr + ">(" + number + ")" 837 body = 'static constexpr ' + typeStr.ljust(9) + name.ljust(33) + ' {' + strVal + '};' 838 elif enuminfo.elem.get('type') and not alias: 839 # Generate e.g.: #define x (~0ULL) 840 typeStr = enuminfo.elem.get('type'); 841 invert = '~' in strVal 842 paren = '(' in strVal 843 number = strVal.strip("()~UL") 844 if typeStr != "float": 845 if typeStr == "uint64_t": 846 number += 'ULL' 847 else: 848 number += 'U' 849 strVal = "~" if invert else "" 850 strVal += number 851 if paren: 852 strVal = "(" + strVal + ")"; 853 body = '#define ' + name.ljust(33) + ' ' + strVal; 854 else: 855 body = '#define ' + name.ljust(33) + ' ' + strVal 856 857 return body 858 859 def makeDir(self, path): 860 """Create a directory, if not already done. 861 862 Generally called from derived generators creating hierarchies.""" 863 self.logMsg('diag', 'OutputGenerator::makeDir(' + path + ')') 864 if path not in self.madeDirs: 865 # This can get race conditions with multiple writers, see 866 # https://stackoverflow.com/questions/273192/ 867 if not os.path.exists(path): 868 os.makedirs(path) 869 self.madeDirs[path] = None 870 871 def beginFile(self, genOpts): 872 """Start a new interface file 873 874 - genOpts - GeneratorOptions controlling what is generated and how""" 875 876 self.genOpts = genOpts 877 if self.genOpts is None: 878 raise MissingGeneratorOptionsError() 879 if self.genOpts.conventions is None: 880 raise MissingGeneratorOptionsConventionsError() 881 self.should_insert_may_alias_macro = \ 882 self.genOpts.conventions.should_insert_may_alias_macro(self.genOpts) 883 self.file_suffix = self.genOpts.conventions.file_suffix 884 885 # Try to import the API dictionary, apimap.py, if it exists. Nothing 886 # in apimap.py cannot be extracted directly from the XML, and in the 887 # future we should do that. 888 if self.genOpts.genpath is not None: 889 try: 890 sys.path.insert(0, self.genOpts.genpath) 891 import apimap 892 self.apidict = apimap 893 except ImportError: 894 self.apidict = None 895 896 self.conventions = genOpts.conventions 897 898 # Open a temporary file for accumulating output. 899 if self.genOpts.filename is not None: 900 self.outFile = tempfile.NamedTemporaryFile(mode='w', encoding='utf-8', newline='\n', delete=False) 901 else: 902 self.outFile = sys.stdout 903 904 def endFile(self): 905 if self.errFile: 906 self.errFile.flush() 907 if self.warnFile: 908 self.warnFile.flush() 909 if self.diagFile: 910 self.diagFile.flush() 911 if self.outFile: 912 self.outFile.flush() 913 if self.outFile != sys.stdout and self.outFile != sys.stderr: 914 self.outFile.close() 915 916 if self.genOpts is None: 917 raise MissingGeneratorOptionsError() 918 919 # On successfully generating output, move the temporary file to the 920 # target file. 921 if self.genOpts.filename is not None: 922 if sys.platform == 'win32': 923 directory = Path(self.genOpts.directory) 924 if not Path.exists(directory): 925 os.makedirs(directory) 926 shutil.copy(self.outFile.name, self.genOpts.directory + '/' + self.genOpts.filename) 927 os.remove(self.outFile.name) 928 self.genOpts = None 929 930 def beginFeature(self, interface, emit): 931 """Write interface for a feature and tag generated features as having been done. 932 933 - interface - element for the `<version>` / `<extension>` to generate 934 - emit - actually write to the header only when True""" 935 self.emit = emit 936 self.featureName = interface.get('name') 937 # If there is an additional 'protect' attribute in the feature, save it 938 self.featureExtraProtect = interface.get('protect') 939 940 def endFeature(self): 941 """Finish an interface file, closing it when done. 942 943 Derived classes responsible for emitting feature""" 944 self.featureName = None 945 self.featureExtraProtect = None 946 947 def genRequirements(self, name, mustBeFound = True): 948 """Generate text showing what core versions and extensions introduce 949 an API. This exists in the base Generator class because it is used by 950 the shared enumerant-generating interfaces (buildEnumCDecl, etc.). 951 Here it returns an empty string for most generators, but can be 952 overridden by e.g. DocGenerator. 953 954 - name - name of the API 955 - mustBeFound - If True, when requirements for 'name' cannot be 956 determined, a warning comment is generated. 957 """ 958 959 return '' 960 961 def validateFeature(self, featureType, featureName): 962 """Validate we are generating something only inside a `<feature>` tag""" 963 if self.featureName is None: 964 raise UserWarning('Attempt to generate', featureType, 965 featureName, 'when not in feature') 966 967 def genType(self, typeinfo, name, alias): 968 """Generate interface for a type 969 970 - typeinfo - TypeInfo for a type 971 972 Extend to generate as desired in your derived class.""" 973 self.validateFeature('type', name) 974 975 def genStruct(self, typeinfo, typeName, alias): 976 """Generate interface for a C "struct" type. 977 978 - typeinfo - TypeInfo for a type interpreted as a struct 979 980 Extend to generate as desired in your derived class.""" 981 self.validateFeature('struct', typeName) 982 983 # The mixed-mode <member> tags may contain no-op <comment> tags. 984 # It is convenient to remove them here where all output generators 985 # will benefit. 986 for member in typeinfo.elem.findall('.//member'): 987 for comment in member.findall('comment'): 988 member.remove(comment) 989 990 def genGroup(self, groupinfo, groupName, alias): 991 """Generate interface for a group of enums (C "enum") 992 993 - groupinfo - GroupInfo for a group. 994 995 Extend to generate as desired in your derived class.""" 996 997 self.validateFeature('group', groupName) 998 999 def genEnum(self, enuminfo, typeName, alias): 1000 """Generate interface for an enum (constant). 1001 1002 - enuminfo - EnumInfo for an enum 1003 - name - enum name 1004 1005 Extend to generate as desired in your derived class.""" 1006 self.validateFeature('enum', typeName) 1007 1008 def genCmd(self, cmd, cmdinfo, alias): 1009 """Generate interface for a command. 1010 1011 - cmdinfo - CmdInfo for a command 1012 1013 Extend to generate as desired in your derived class.""" 1014 self.validateFeature('command', cmdinfo) 1015 1016 def genSpirv(self, spirv, spirvinfo, alias): 1017 """Generate interface for a spirv element. 1018 1019 - spirvinfo - SpirvInfo for a command 1020 1021 Extend to generate as desired in your derived class.""" 1022 return 1023 1024 def genFormat(self, format, formatinfo, alias): 1025 """Generate interface for a format element. 1026 1027 - formatinfo - FormatInfo 1028 1029 Extend to generate as desired in your derived class.""" 1030 return 1031 1032 def genSyncStage(self, stageinfo): 1033 """Generate interface for a sync stage element. 1034 1035 - stageinfo - SyncStageInfo 1036 1037 Extend to generate as desired in your derived class.""" 1038 return 1039 1040 def genSyncAccess(self, accessinfo): 1041 """Generate interface for a sync stage element. 1042 1043 - accessinfo - AccessInfo 1044 1045 Extend to generate as desired in your derived class.""" 1046 return 1047 1048 def genSyncPipeline(self, pipelineinfo): 1049 """Generate interface for a sync stage element. 1050 1051 - pipelineinfo - SyncPipelineInfo 1052 1053 Extend to generate as desired in your derived class.""" 1054 return 1055 1056 def makeProtoName(self, name, tail): 1057 """Turn a `<proto>` `<name>` into C-language prototype 1058 and typedef declarations for that name. 1059 1060 - name - contents of `<name>` tag 1061 - tail - whatever text follows that tag in the Element""" 1062 if self.genOpts is None: 1063 raise MissingGeneratorOptionsError() 1064 return self.genOpts.apientry + name + tail 1065 1066 def makeTypedefName(self, name, tail): 1067 """Make the function-pointer typedef name for a command.""" 1068 if self.genOpts is None: 1069 raise MissingGeneratorOptionsError() 1070 return '(' + self.genOpts.apientryp + 'PFN_' + name + tail + ')' 1071 1072 def makeCParamDecl(self, param, aligncol): 1073 """Return a string which is an indented, formatted 1074 declaration for a `<param>` or `<member>` block (e.g. function parameter 1075 or structure/union member). 1076 1077 - param - Element (`<param>` or `<member>`) to format 1078 - aligncol - if non-zero, attempt to align the nested `<name>` element 1079 at this column""" 1080 if self.genOpts is None: 1081 raise MissingGeneratorOptionsError() 1082 if self.genOpts.conventions is None: 1083 raise MissingGeneratorOptionsConventionsError() 1084 indent = ' ' 1085 paramdecl = indent 1086 prefix = noneStr(param.text) 1087 1088 for elem in param: 1089 text = noneStr(elem.text) 1090 tail = noneStr(elem.tail) 1091 1092 if self.should_insert_may_alias_macro and self.genOpts.conventions.is_voidpointer_alias(elem.tag, text, tail): 1093 # OpenXR-specific macro insertion - but not in apiinc for the spec 1094 tail = self.genOpts.conventions.make_voidpointer_alias(tail) 1095 if elem.tag == 'name' and aligncol > 0: 1096 self.logMsg('diag', 'Aligning parameter', elem.text, 'to column', self.genOpts.alignFuncParam) 1097 # Align at specified column, if possible 1098 paramdecl = paramdecl.rstrip() 1099 oldLen = len(paramdecl) 1100 # This works around a problem where very long type names - 1101 # longer than the alignment column - would run into the tail 1102 # text. 1103 paramdecl = paramdecl.ljust(aligncol - 1) + ' ' 1104 newLen = len(paramdecl) 1105 self.logMsg('diag', 'Adjust length of parameter decl from', oldLen, 'to', newLen, ':', paramdecl) 1106 1107 if (self.misracppstyle() and prefix.find('const ') != -1): 1108 # Change pointer type order from e.g. "const void *" to "void const *". 1109 # If the string starts with 'const', reorder it to be after the first type. 1110 paramdecl += prefix.replace('const ', '') + text + ' const' + tail 1111 else: 1112 paramdecl += prefix + text + tail 1113 1114 # Clear prefix for subsequent iterations 1115 prefix = '' 1116 1117 paramdecl = paramdecl + prefix 1118 1119 if aligncol == 0: 1120 # Squeeze out multiple spaces other than the indentation 1121 paramdecl = indent + ' '.join(paramdecl.split()) 1122 return paramdecl 1123 1124 def getCParamTypeLength(self, param): 1125 """Return the length of the type field is an indented, formatted 1126 declaration for a `<param>` or `<member>` block (e.g. function parameter 1127 or structure/union member). 1128 1129 - param - Element (`<param>` or `<member>`) to identify""" 1130 if self.genOpts is None: 1131 raise MissingGeneratorOptionsError() 1132 if self.genOpts.conventions is None: 1133 raise MissingGeneratorOptionsConventionsError() 1134 1135 # Allow for missing <name> tag 1136 newLen = 0 1137 paramdecl = ' ' + noneStr(param.text) 1138 for elem in param: 1139 text = noneStr(elem.text) 1140 tail = noneStr(elem.tail) 1141 1142 if self.should_insert_may_alias_macro and self.genOpts.conventions.is_voidpointer_alias(elem.tag, text, tail): 1143 # OpenXR-specific macro insertion 1144 tail = self.genOpts.conventions.make_voidpointer_alias(tail) 1145 if elem.tag == 'name': 1146 # Align at specified column, if possible 1147 newLen = len(paramdecl.rstrip()) 1148 self.logMsg('diag', 'Identifying length of', elem.text, 'as', newLen) 1149 paramdecl += text + tail 1150 1151 return newLen 1152 1153 def getMaxCParamTypeLength(self, info): 1154 """Return the length of the longest type field for a member/parameter. 1155 1156 - info - TypeInfo or CommandInfo. 1157 """ 1158 lengths = (self.getCParamTypeLength(member) 1159 for member in info.getMembers()) 1160 return max(lengths) 1161 1162 def getHandleParent(self, typename): 1163 """Get the parent of a handle object.""" 1164 if self.registry is None: 1165 raise MissingRegistryError() 1166 1167 info = self.registry.typedict.get(typename) 1168 if info is None: 1169 return None 1170 1171 elem = info.elem 1172 if elem is not None: 1173 return elem.get('parent') 1174 1175 return None 1176 1177 def iterateHandleAncestors(self, typename): 1178 """Iterate through the ancestors of a handle type.""" 1179 current = self.getHandleParent(typename) 1180 while current is not None: 1181 yield current 1182 current = self.getHandleParent(current) 1183 1184 def getHandleAncestors(self, typename): 1185 """Get the ancestors of a handle object.""" 1186 return list(self.iterateHandleAncestors(typename)) 1187 1188 def getTypeCategory(self, typename): 1189 """Get the category of a type.""" 1190 if self.registry is None: 1191 raise MissingRegistryError() 1192 1193 info = self.registry.typedict.get(typename) 1194 if info is None: 1195 return None 1196 1197 elem = info.elem 1198 if elem is not None: 1199 return elem.get('category') 1200 return None 1201 1202 def isStructAlwaysValid(self, structname): 1203 """Try to do check if a structure is always considered valid (i.e. there is no rules to its acceptance).""" 1204 # A conventions object is required for this call. 1205 if not self.conventions: 1206 raise RuntimeError("To use isStructAlwaysValid, be sure your options include a Conventions object.") 1207 if self.registry is None: 1208 raise MissingRegistryError() 1209 1210 if self.conventions.type_always_valid(structname): 1211 return True 1212 1213 category = self.getTypeCategory(structname) 1214 if self.conventions.category_requires_validation(category): 1215 return False 1216 1217 info = self.registry.typedict.get(structname) 1218 if info is None: 1219 self.logMsg('error', f'isStructAlwaysValid({structname}) - structure not found in typedict') 1220 1221 members = info.getMembers() 1222 1223 for member in members: 1224 member_name = getElemName(member) 1225 if member_name in (self.conventions.structtype_member_name, 1226 self.conventions.nextpointer_member_name): 1227 return False 1228 1229 if member.get('noautovalidity'): 1230 return False 1231 1232 member_type = getElemType(member) 1233 1234 if member_type in ('void', 'char') or self.paramIsArray(member) or self.paramIsPointer(member): 1235 return False 1236 1237 if self.conventions.type_always_valid(member_type): 1238 continue 1239 1240 member_category = self.getTypeCategory(member_type) 1241 1242 if self.conventions.category_requires_validation(member_category): 1243 return False 1244 1245 if member_category in ('struct', 'union'): 1246 if self.isStructAlwaysValid(member_type) is False: 1247 return False 1248 1249 return True 1250 1251 def paramIsArray(self, param): 1252 """Check if the parameter passed in is a pointer to an array. 1253 1254 param the XML information for the param 1255 """ 1256 return param.get('len') is not None 1257 1258 def paramIsPointer(self, param): 1259 """Check if the parameter passed in is a pointer. 1260 1261 param the XML information for the param 1262 """ 1263 tail = param.find('type').tail 1264 return tail is not None and '*' in tail 1265 1266 def isEnumRequired(self, elem): 1267 """Return True if this `<enum>` element is 1268 required, False otherwise 1269 1270 - elem - `<enum>` element to test""" 1271 required = elem.get('required') is not None 1272 self.logMsg('diag', 'isEnumRequired:', elem.get('name'), 1273 '->', required) 1274 return required 1275 1276 # @@@ This code is overridden by equivalent code now run in 1277 # @@@ Registry.generateFeature 1278 1279 required = False 1280 1281 extname = elem.get('extname') 1282 if extname is not None: 1283 # 'supported' attribute was injected when the <enum> element was 1284 # moved into the <enums> group in Registry.parseTree() 1285 if self.genOpts.defaultExtensions == elem.get('supported'): 1286 required = True 1287 elif re.match(self.genOpts.addExtensions, extname) is not None: 1288 required = True 1289 elif elem.get('version') is not None: 1290 required = re.match(self.genOpts.emitversions, elem.get('version')) is not None 1291 else: 1292 required = True 1293 1294 return required 1295 1296 def makeCDecls(self, cmd): 1297 """Return C prototype and function pointer typedef for a 1298 `<command>` Element, as a two-element list of strings. 1299 1300 - cmd - Element containing a `<command>` tag""" 1301 if self.genOpts is None: 1302 raise MissingGeneratorOptionsError() 1303 proto = cmd.find('proto') 1304 params = cmd.findall('param') 1305 # Begin accumulating prototype and typedef strings 1306 pdecl = self.genOpts.apicall 1307 tdecl = 'typedef ' 1308 1309 # Insert the function return type/name. 1310 # For prototypes, add APIENTRY macro before the name 1311 # For typedefs, add (APIENTRY *<name>) around the name and 1312 # use the PFN_cmdnameproc naming convention. 1313 # Done by walking the tree for <proto> element by element. 1314 # etree has elem.text followed by (elem[i], elem[i].tail) 1315 # for each child element and any following text 1316 # Leading text 1317 pdecl += noneStr(proto.text) 1318 tdecl += noneStr(proto.text) 1319 # For each child element, if it is a <name> wrap in appropriate 1320 # declaration. Otherwise append its contents and tail contents. 1321 for elem in proto: 1322 text = noneStr(elem.text) 1323 tail = noneStr(elem.tail) 1324 if elem.tag == 'name': 1325 pdecl += self.makeProtoName(text, tail) 1326 tdecl += self.makeTypedefName(text, tail) 1327 else: 1328 pdecl += text + tail 1329 tdecl += text + tail 1330 1331 if self.genOpts.alignFuncParam == 0: 1332 # Squeeze out multiple spaces - there is no indentation 1333 pdecl = ' '.join(pdecl.split()) 1334 tdecl = ' '.join(tdecl.split()) 1335 1336 # Now add the parameter declaration list, which is identical 1337 # for prototypes and typedefs. Concatenate all the text from 1338 # a <param> node without the tags. No tree walking required 1339 # since all tags are ignored. 1340 # Uses: self.indentFuncProto 1341 # self.indentFuncPointer 1342 # self.alignFuncParam 1343 n = len(params) 1344 # Indented parameters 1345 if n > 0: 1346 indentdecl = '(\n' 1347 indentdecl += ',\n'.join(self.makeCParamDecl(p, self.genOpts.alignFuncParam) 1348 for p in params) 1349 indentdecl += ');' 1350 else: 1351 indentdecl = '(void);' 1352 # Non-indented parameters 1353 paramdecl = '(' 1354 if n > 0: 1355 paramnames = [] 1356 if self.misracppstyle(): 1357 for p in params: 1358 param = '' 1359 firstIter = True; 1360 for t in p.itertext(): 1361 if (firstIter): 1362 prefix = t 1363 firstIter = False 1364 else: 1365 # Change pointer type order from e.g. "const void *" to "void const *". 1366 # If the string starts with 'const', reorder it to be after the first type. 1367 if (prefix.find('const ') != -1): 1368 param += prefix.replace('const ', '') + t + ' const ' 1369 else: 1370 param += prefix + t 1371 # Clear prefix for subsequent iterations 1372 prefix = '' 1373 paramnames.append(param); 1374 else: 1375 paramnames = (''.join(t for t in p.itertext()) 1376 for p in params) 1377 paramdecl += ', '.join(paramnames) 1378 else: 1379 paramdecl += 'void' 1380 paramdecl += ");" 1381 return [pdecl + indentdecl, tdecl + paramdecl] 1382 1383 def newline(self): 1384 """Print a newline to the output file (utility function)""" 1385 write('', file=self.outFile) 1386 1387 def setRegistry(self, registry): 1388 self.registry = registry 1389