1#!/usr/bin/python3 -i
2#
3# Copyright (c) 2013-2020 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
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        return "'{}'".format(s)
47    return None
48
49
50def regSortCategoryKey(feature):
51    """Sort key for regSortFeatures.
52    Sorts by category of the feature name string:
53
54    - Core API features (those defined with a `<feature>` tag)
55    - ARB/KHR/OES (Khronos extensions)
56    - other       (EXT/vendor extensions)"""
57
58    if feature.elem.tag == 'feature':
59        return 0
60    if (feature.category == 'ARB'
61        or feature.category == 'KHR'
62            or feature.category == 'OES'):
63        return 1
64
65    return 2
66
67
68def regSortOrderKey(feature):
69    """Sort key for regSortFeatures - key is the sortorder attribute."""
70
71    # print("regSortOrderKey {} -> {}".format(feature.name, feature.sortorder))
72    return feature.sortorder
73
74
75def regSortFeatureVersionKey(feature):
76    """Sort key for regSortFeatures - key is the feature version.
77    `<extension>` elements all have version number 0."""
78
79    return float(feature.versionNumber)
80
81
82def regSortExtensionNumberKey(feature):
83    """Sort key for regSortFeatures - key is the extension number.
84    `<feature>` elements all have extension number 0."""
85
86    return int(feature.number)
87
88
89def regSortFeatures(featureList):
90    """Default sort procedure for features.
91
92    - Sorts by explicit sort order (default 0) relative to other features
93    - then by feature category ('feature' or 'extension'),
94    - then by version number (for features)
95    - then by extension number (for extensions)"""
96    featureList.sort(key=regSortExtensionNumberKey)
97    featureList.sort(key=regSortFeatureVersionKey)
98    featureList.sort(key=regSortCategoryKey)
99    featureList.sort(key=regSortOrderKey)
100
101
102class GeneratorOptions:
103    """Base class for options used during header/documentation production.
104
105    These options are target language independent, and used by
106    Registry.apiGen() and by base OutputGenerator objects."""
107
108    def __init__(self,
109                 conventions=None,
110                 filename=None,
111                 directory='.',
112                 genpath=None,
113                 apiname=None,
114                 profile=None,
115                 versions='.*',
116                 emitversions='.*',
117                 defaultExtensions=None,
118                 addExtensions=None,
119                 removeExtensions=None,
120                 emitExtensions=None,
121                 reparentEnums=True,
122                 sortProcedure=regSortFeatures):
123        """Constructor.
124
125        Arguments:
126
127        - conventions - may be mandatory for some generators:
128        an object that implements ConventionsBase
129        - filename - basename of file to generate, or None to write to stdout.
130        - directory - directory in which to generate files
131        - genpath - path to previously generated files, such as api.py
132        - apiname - string matching `<api>` 'apiname' attribute, e.g. 'gl'.
133        - profile - string specifying API profile , e.g. 'core', or None.
134        - versions - regex matching API versions to process interfaces for.
135        Normally `'.*'` or `'[0-9][.][0-9]'` to match all defined versions.
136        - emitversions - regex matching API versions to actually emit
137        interfaces for (though all requested versions are considered
138        when deciding which interfaces to generate). For GL 4.3 glext.h,
139        this might be `'1[.][2-5]|[2-4][.][0-9]'`.
140        - defaultExtensions - If not None, a string which must in its
141        entirety match the pattern in the "supported" attribute of
142        the `<extension>`. Defaults to None. Usually the same as apiname.
143        - addExtensions - regex matching names of additional extensions
144        to include. Defaults to None.
145        - removeExtensions - regex matching names of extensions to
146        remove (after defaultExtensions and addExtensions). Defaults
147        to None.
148        - emitExtensions - regex matching names of extensions to actually emit
149        interfaces for (though all requested versions are considered when
150        deciding which interfaces to generate).
151        - reparentEnums - move <enum> elements which extend an enumerated
152        type from <feature> or <extension> elements to the target <enums>
153        element. This is required for almost all purposes, but the
154        InterfaceGenerator relies on the list of interfaces in the <feature>
155        or <extension> being complete. Defaults to True.
156        - sortProcedure - takes a list of FeatureInfo objects and sorts
157        them in place to a preferred order in the generated output.
158        Default is core API versions, ARB/KHR/OES extensions, all other
159        extensions, by core API version number or extension number in each
160        group.
161
162        The regex patterns can be None or empty, in which case they match
163        nothing."""
164        self.conventions = conventions
165        """may be mandatory for some generators:
166        an object that implements ConventionsBase"""
167
168        self.filename = filename
169        "basename of file to generate, or None to write to stdout."
170
171        self.genpath = genpath
172        """path to previously generated files, such as api.py"""
173
174        self.directory = directory
175        "directory in which to generate filename"
176
177        self.apiname = apiname
178        "string matching `<api>` 'apiname' attribute, e.g. 'gl'."
179
180        self.profile = profile
181        "string specifying API profile , e.g. 'core', or None."
182
183        self.versions = self.emptyRegex(versions)
184        """regex matching API versions to process interfaces for.
185        Normally `'.*'` or `'[0-9][.][0-9]'` to match all defined versions."""
186
187        self.emitversions = self.emptyRegex(emitversions)
188        """regex matching API versions to actually emit
189        interfaces for (though all requested versions are considered
190        when deciding which interfaces to generate). For GL 4.3 glext.h,
191        this might be `'1[.][2-5]|[2-4][.][0-9]'`."""
192
193        self.defaultExtensions = defaultExtensions
194        """If not None, a string which must in its
195        entirety match the pattern in the "supported" attribute of
196        the `<extension>`. Defaults to None. Usually the same as apiname."""
197
198        self.addExtensions = self.emptyRegex(addExtensions)
199        """regex matching names of additional extensions
200        to include. Defaults to None."""
201
202        self.removeExtensions = self.emptyRegex(removeExtensions)
203        """regex matching names of extensions to
204        remove (after defaultExtensions and addExtensions). Defaults
205        to None."""
206
207        self.emitExtensions = self.emptyRegex(emitExtensions)
208        """regex matching names of extensions to actually emit
209        interfaces for (though all requested versions are considered when
210        deciding which interfaces to generate)."""
211
212        self.reparentEnums = reparentEnums
213        """boolean specifying whether to remove <enum> elements from
214        <feature> or <extension> when extending an <enums> type."""
215
216        self.sortProcedure = sortProcedure
217        """takes a list of FeatureInfo objects and sorts
218        them in place to a preferred order in the generated output.
219        Default is core API versions, ARB/KHR/OES extensions, all
220        other extensions, alphabetically within each group."""
221
222        self.codeGenerator = False
223        """True if this generator makes compilable code"""
224
225    def emptyRegex(self, pat):
226        """Substitute a regular expression which matches no version
227        or extension names for None or the empty string."""
228        if not pat:
229            return '_nomatch_^'
230
231        return pat
232
233
234class OutputGenerator:
235    """Generate specified API interfaces in a specific style, such as a C header.
236
237    Base class for generating API interfaces.
238    Manages basic logic, logging, and output file control.
239    Derived classes actually generate formatted output.
240    """
241
242    # categoryToPath - map XML 'category' to include file directory name
243    categoryToPath = {
244        'bitmask': 'flags',
245        'enum': 'enums',
246        'funcpointer': 'funcpointers',
247        'handle': 'handles',
248        'define': 'defines',
249        'basetype': 'basetypes',
250    }
251
252    def __init__(self, errFile=sys.stderr, warnFile=sys.stderr, diagFile=sys.stdout):
253        """Constructor
254
255        - errFile, warnFile, diagFile - file handles to write errors,
256          warnings, diagnostics to. May be None to not write."""
257        self.outFile = None
258        self.errFile = errFile
259        self.warnFile = warnFile
260        self.diagFile = diagFile
261        # Internal state
262        self.featureName = None
263        self.genOpts = None
264        self.registry = None
265        self.featureDictionary = {}
266        # Used for extension enum value generation
267        self.extBase = 1000000000
268        self.extBlockSize = 1000
269        self.madeDirs = {}
270
271        # API dictionary, which may be loaded by the beginFile method of
272        # derived generators.
273        self.apidict = None
274
275    def logMsg(self, level, *args):
276        """Write a message of different categories to different
277        destinations.
278
279        - `level`
280          - 'diag' (diagnostic, voluminous)
281          - 'warn' (warning)
282          - 'error' (fatal error - raises exception after logging)
283
284        - `*args` - print()-style arguments to direct to corresponding log"""
285        if level == 'error':
286            strfile = io.StringIO()
287            write('ERROR:', *args, file=strfile)
288            if self.errFile is not None:
289                write(strfile.getvalue(), file=self.errFile)
290            raise UserWarning(strfile.getvalue())
291        elif level == 'warn':
292            if self.warnFile is not None:
293                write('WARNING:', *args, file=self.warnFile)
294        elif level == 'diag':
295            if self.diagFile is not None:
296                write('DIAG:', *args, file=self.diagFile)
297        else:
298            raise UserWarning(
299                '*** FATAL ERROR in Generator.logMsg: unknown level:' + level)
300
301    def enumToValue(self, elem, needsNum):
302        """Parse and convert an `<enum>` tag into a value.
303
304        Returns a list:
305
306        - first element - integer representation of the value, or None
307          if needsNum is False. The value must be a legal number
308          if needsNum is True.
309        - second element - string representation of the value
310
311        There are several possible representations of values.
312
313        - A 'value' attribute simply contains the value.
314        - A 'bitpos' attribute defines a value by specifying the bit
315          position which is set in that value.
316        - An 'offset','extbase','extends' triplet specifies a value
317          as an offset to a base value defined by the specified
318          'extbase' extension name, which is then cast to the
319          typename specified by 'extends'. This requires probing
320          the registry database, and imbeds knowledge of the
321          API extension enum scheme in this function.
322        - An 'alias' attribute contains the name of another enum
323          which this is an alias of. The other enum must be
324          declared first when emitting this enum."""
325        name = elem.get('name')
326        numVal = None
327        if 'value' in elem.keys():
328            value = elem.get('value')
329            # print('About to translate value =', value, 'type =', type(value))
330            if needsNum:
331                numVal = int(value, 0)
332            # If there's a non-integer, numeric 'type' attribute (e.g. 'u' or
333            # 'ull'), append it to the string value.
334            # t = enuminfo.elem.get('type')
335            # if t is not None and t != '' and t != 'i' and t != 's':
336            #     value += enuminfo.type
337            self.logMsg('diag', 'Enum', name, '-> value [', numVal, ',', value, ']')
338            return [numVal, value]
339        if 'bitpos' in elem.keys():
340            value = elem.get('bitpos')
341            bitpos = int(value, 0)
342            numVal = 1 << bitpos
343            value = '0x%08x' % numVal
344            if bitpos >= 32:
345                value = value + 'ULL'
346            self.logMsg('diag', 'Enum', name, '-> bitpos [', numVal, ',', value, ']')
347            return [numVal, value]
348        if 'offset' in elem.keys():
349            # Obtain values in the mapping from the attributes
350            enumNegative = False
351            offset = int(elem.get('offset'), 0)
352            extnumber = int(elem.get('extnumber'), 0)
353            extends = elem.get('extends')
354            if 'dir' in elem.keys():
355                enumNegative = True
356            self.logMsg('diag', 'Enum', name, 'offset =', offset,
357                        'extnumber =', extnumber, 'extends =', extends,
358                        'enumNegative =', enumNegative)
359            # Now determine the actual enumerant value, as defined
360            # in the "Layers and Extensions" appendix of the spec.
361            numVal = self.extBase + (extnumber - 1) * self.extBlockSize + offset
362            if enumNegative:
363                numVal *= -1
364            value = '%d' % numVal
365            # More logic needed!
366            self.logMsg('diag', 'Enum', name, '-> offset [', numVal, ',', value, ']')
367            return [numVal, value]
368        if 'alias' in elem.keys():
369            return [None, elem.get('alias')]
370        return [None, None]
371
372    def checkDuplicateEnums(self, enums):
373        """Sanity check enumerated values.
374
375        -  enums - list of `<enum>` Elements
376
377        returns the list with duplicates stripped"""
378        # Dictionaries indexed by name and numeric value.
379        # Entries are [ Element, numVal, strVal ] matching name or value
380
381        nameMap = {}
382        valueMap = {}
383
384        stripped = []
385        for elem in enums:
386            name = elem.get('name')
387            (numVal, strVal) = self.enumToValue(elem, True)
388
389            if name in nameMap:
390                # Duplicate name found; check values
391                (name2, numVal2, strVal2) = nameMap[name]
392
393                # Duplicate enum values for the same name are benign. This
394                # happens when defining the same enum conditionally in
395                # several extension blocks.
396                if (strVal2 == strVal or (numVal is not None
397                                          and numVal == numVal2)):
398                    True
399                    # self.logMsg('info', 'checkDuplicateEnums: Duplicate enum (' + name +
400                    #             ') found with the same value:' + strVal)
401                else:
402                    self.logMsg('warn', 'checkDuplicateEnums: Duplicate enum (' + name
403                                + ') found with different values:' + strVal
404                                + ' and ' + strVal2)
405
406                # Don't add the duplicate to the returned list
407                continue
408            elif numVal in valueMap:
409                # Duplicate value found (such as an alias); report it, but
410                # still add this enum to the list.
411                (name2, numVal2, strVal2) = valueMap[numVal]
412
413                msg = 'Two enums found with the same value: {} = {} = {}'.format(
414                    name, name2.get('name'), strVal)
415                self.logMsg('error', msg)
416
417            # Track this enum to detect followon duplicates
418            nameMap[name] = [elem, numVal, strVal]
419            if numVal is not None:
420                valueMap[numVal] = [elem, numVal, strVal]
421
422            # Add this enum to the list
423            stripped.append(elem)
424
425        # Return the list
426        return stripped
427
428    def buildEnumCDecl(self, expand, groupinfo, groupName):
429        """Generate the C declaration for an enum"""
430        groupElem = groupinfo.elem
431
432        # Determine the required bit width for the enum group.
433        # 32 is the default, which generates C enum types for the values.
434        bitwidth = 32
435
436        # If the constFlagBits preference is set, 64 is the default for bitmasks
437        if self.genOpts.conventions.constFlagBits and groupElem.get('type') == 'bitmask':
438            bitwidth = 64
439
440        # Check for an explicitly defined bitwidth, which will override any defaults.
441        if groupElem.get('bitwidth'):
442            try:
443                bitwidth = int(groupElem.get('bitwidth'))
444            except ValueError as ve:
445                self.logMsg('error', 'Invalid value for bitwidth attribute (', groupElem.get('bitwidth'), ') for ', groupName, ' - must be an integer value\n')
446                exit(1)
447
448        # Bitmask types support 64-bit flags, so have different handling
449        if groupElem.get('type') == 'bitmask':
450
451            # Validate the bitwidth and generate values appropriately
452            # Bitmask flags up to 64-bit are generated as static const uint64_t values
453            # Bitmask flags up to 32-bit are generated as C enum values
454            if bitwidth > 64:
455                self.logMsg('error', 'Invalid value for bitwidth attribute (', groupElem.get('bitwidth'), ') for bitmask type ', groupName, ' - must be less than or equal to 64\n')
456                exit(1)
457            elif bitwidth > 32:
458                return self.buildEnumCDecl_Bitmask(groupinfo, groupName)
459            else:
460                return self.buildEnumCDecl_Enum(expand, groupinfo, groupName)
461        else:
462            # Validate the bitwidth and generate values appropriately
463            # Enum group types up to 32-bit are generated as C enum values
464            if bitwidth > 32:
465                self.logMsg('error', 'Invalid value for bitwidth attribute (', groupElem.get('bitwidth'), ') for enum type ', groupName, ' - must be less than or equal to 32\n')
466                exit(1)
467            else:
468                return self.buildEnumCDecl_Enum(expand, groupinfo, groupName)
469
470    def buildEnumCDecl_Bitmask(self, groupinfo, groupName):
471        """Generate the C declaration for an "enum" that is actually a
472        set of flag bits"""
473        groupElem = groupinfo.elem
474        flagTypeName = groupinfo.flagType.elem.get('name')
475
476        # Prefix
477        body = "// Flag bits for " + flagTypeName + "\n"
478
479        # Maximum allowable value for a flag (unsigned 64-bit integer)
480        maxValidValue = 2**(64) - 1
481        minValidValue = 0
482
483        # Loop over the nested 'enum' tags.
484        for elem in groupElem.findall('enum'):
485            # Convert the value to an integer and use that to track min/max.
486            # Values of form -(number) are accepted but nothing more complex.
487            # Should catch exceptions here for more complex constructs. Not yet.
488            (numVal, strVal) = self.enumToValue(elem, True)
489            name = elem.get('name')
490
491            # Range check for the enum value
492            if numVal is not None and (numVal > maxValidValue or numVal < minValidValue):
493                self.logMsg('error', 'Allowable range for flag types in C is [', minValidValue, ',', maxValidValue, '], but', name, 'flag has a value outside of this (', strVal, ')\n')
494                exit(1)
495
496            body += "static const {} {} = {};\n".format(flagTypeName, name, strVal)
497
498        # Postfix
499
500        return ("bitmask", body)
501
502    def buildEnumCDecl_Enum(self, expand, groupinfo, groupName):
503        """Generate the C declaration for an enumerated type"""
504        groupElem = groupinfo.elem
505
506        # Break the group name into prefix and suffix portions for range
507        # enum generation
508        expandName = re.sub(r'([0-9a-z_])([A-Z0-9])', r'\1_\2', groupName).upper()
509        expandPrefix = expandName
510        expandSuffix = ''
511        expandSuffixMatch = re.search(r'[A-Z][A-Z]+$', groupName)
512        if expandSuffixMatch:
513            expandSuffix = '_' + expandSuffixMatch.group()
514            # Strip off the suffix from the prefix
515            expandPrefix = expandName.rsplit(expandSuffix, 1)[0]
516
517        # Prefix
518        body = ["typedef enum %s {" % groupName]
519
520        # @@ Should use the type="bitmask" attribute instead
521        isEnum = ('FLAG_BITS' not in expandPrefix)
522
523        # Allowable range for a C enum - which is that of a signed 32-bit integer
524        maxValidValue = 2**(32 - 1) - 1
525        minValidValue = (maxValidValue * -1) - 1
526
527
528        # Get a list of nested 'enum' tags.
529        enums = groupElem.findall('enum')
530
531        # Check for and report duplicates, and return a list with them
532        # removed.
533        enums = self.checkDuplicateEnums(enums)
534
535        # Loop over the nested 'enum' tags. Keep track of the minimum and
536        # maximum numeric values, if they can be determined; but only for
537        # core API enumerants, not extension enumerants. This is inferred
538        # by looking for 'extends' attributes.
539        minName = None
540
541        # Accumulate non-numeric enumerant values separately and append
542        # them following the numeric values, to allow for aliases.
543        # NOTE: this doesn't do a topological sort yet, so aliases of
544        # aliases can still get in the wrong order.
545        aliasText = []
546
547        for elem in enums:
548            # Convert the value to an integer and use that to track min/max.
549            # Values of form -(number) are accepted but nothing more complex.
550            # Should catch exceptions here for more complex constructs. Not yet.
551            (numVal, strVal) = self.enumToValue(elem, True)
552            name = elem.get('name')
553
554            # Extension enumerants are only included if they are required
555            if self.isEnumRequired(elem):
556                decl = "    {} = {},".format(name, strVal)
557                if numVal is not None:
558                    body.append(decl)
559                else:
560                    aliasText.append(decl)
561
562            # Range check for the enum value
563            if numVal is not None and (numVal > maxValidValue or numVal < minValidValue):
564                self.logMsg('error', 'Allowable range for C enum types is [', minValidValue, ',', maxValidValue, '], but', name, 'has a value outside of this (', strVal, ')\n')
565                exit(1)
566
567
568            # Don't track min/max for non-numbers (numVal is None)
569            if isEnum and numVal is not None and elem.get('extends') is None:
570                if minName is None:
571                    minName = maxName = name
572                    minValue = maxValue = numVal
573                elif numVal < minValue:
574                    minName = name
575                    minValue = numVal
576                elif numVal > maxValue:
577                    maxName = name
578                    maxValue = numVal
579
580        # Now append the non-numeric enumerant values
581        body.extend(aliasText)
582
583        # Generate min/max value tokens - legacy use case.
584        if isEnum and expand:
585            body.extend(("    {}_BEGIN_RANGE{} = {},".format(expandPrefix, expandSuffix, minName),
586                         "    {}_END_RANGE{} = {},".format(
587                             expandPrefix, expandSuffix, maxName),
588                         "    {}_RANGE_SIZE{} = ({} - {} + 1),".format(expandPrefix, expandSuffix, maxName, minName)))
589
590        # Generate a range-padding value to ensure the enum is 32 bits, but
591        # only in code generators, so it doesn't appear in documentation
592        if (self.genOpts.codeGenerator or
593            self.conventions.generate_max_enum_in_docs):
594            body.append("    {}_MAX_ENUM{} = 0x7FFFFFFF".format(
595                expandPrefix, expandSuffix))
596
597        # Postfix
598        body.append("} %s;" % groupName)
599
600        # Determine appropriate section for this declaration
601        if groupElem.get('type') == 'bitmask':
602            section = 'bitmask'
603        else:
604            section = 'group'
605
606        return (section, '\n'.join(body))
607
608    def makeDir(self, path):
609        """Create a directory, if not already done.
610
611        Generally called from derived generators creating hierarchies."""
612        self.logMsg('diag', 'OutputGenerator::makeDir(' + path + ')')
613        if path not in self.madeDirs:
614            # This can get race conditions with multiple writers, see
615            # https://stackoverflow.com/questions/273192/
616            if not os.path.exists(path):
617                os.makedirs(path)
618            self.madeDirs[path] = None
619
620    def beginFile(self, genOpts):
621        """Start a new interface file
622
623        - genOpts - GeneratorOptions controlling what's generated and how"""
624        self.genOpts = genOpts
625        self.should_insert_may_alias_macro = \
626            self.genOpts.conventions.should_insert_may_alias_macro(self.genOpts)
627
628        # Try to import the API dictionary, api.py, if it exists. Nothing in
629        # api.py cannot be extracted directly from the XML, and in the
630        # future we should do that.
631        if self.genOpts.genpath is not None:
632            try:
633                sys.path.insert(0, self.genOpts.genpath)
634                import api
635                self.apidict = api
636            except ImportError:
637                self.apidict = None
638
639        self.conventions = genOpts.conventions
640
641        # Open a temporary file for accumulating output.
642        if self.genOpts.filename is not None:
643            self.outFile = tempfile.NamedTemporaryFile(mode='w', encoding='utf-8', newline='\n', delete=False)
644        else:
645            self.outFile = sys.stdout
646
647    def endFile(self):
648        if self.errFile:
649            self.errFile.flush()
650        if self.warnFile:
651            self.warnFile.flush()
652        if self.diagFile:
653            self.diagFile.flush()
654        self.outFile.flush()
655        if self.outFile != sys.stdout and self.outFile != sys.stderr:
656            self.outFile.close()
657
658        # On successfully generating output, move the temporary file to the
659        # target file.
660        if self.genOpts.filename is not None:
661            if sys.platform == 'win32':
662                directory = Path(self.genOpts.directory)
663                if not Path.exists(directory):
664                    os.makedirs(directory)
665            shutil.copy(self.outFile.name, self.genOpts.directory + '/' + self.genOpts.filename)
666            os.remove(self.outFile.name)
667        self.genOpts = None
668
669    def beginFeature(self, interface, emit):
670        """Write interface for a feature and tag generated features as having been done.
671
672        - interface - element for the `<version>` / `<extension>` to generate
673        - emit - actually write to the header only when True"""
674        self.emit = emit
675        self.featureName = interface.get('name')
676        # If there's an additional 'protect' attribute in the feature, save it
677        self.featureExtraProtect = interface.get('protect')
678
679    def endFeature(self):
680        """Finish an interface file, closing it when done.
681
682        Derived classes responsible for emitting feature"""
683        self.featureName = None
684        self.featureExtraProtect = None
685
686    def validateFeature(self, featureType, featureName):
687        """Validate we're generating something only inside a `<feature>` tag"""
688        if self.featureName is None:
689            raise UserWarning('Attempt to generate', featureType,
690                              featureName, 'when not in feature')
691
692    def genType(self, typeinfo, name, alias):
693        """Generate interface for a type
694
695        - typeinfo - TypeInfo for a type
696
697        Extend to generate as desired in your derived class."""
698        self.validateFeature('type', name)
699
700    def genStruct(self, typeinfo, typeName, alias):
701        """Generate interface for a C "struct" type.
702
703        - typeinfo - TypeInfo for a type interpreted as a struct
704
705        Extend to generate as desired in your derived class."""
706        self.validateFeature('struct', typeName)
707
708        # The mixed-mode <member> tags may contain no-op <comment> tags.
709        # It is convenient to remove them here where all output generators
710        # will benefit.
711        for member in typeinfo.elem.findall('.//member'):
712            for comment in member.findall('comment'):
713                member.remove(comment)
714
715    def genGroup(self, groupinfo, groupName, alias):
716        """Generate interface for a group of enums (C "enum")
717
718        - groupinfo - GroupInfo for a group.
719
720        Extend to generate as desired in your derived class."""
721
722        self.validateFeature('group', groupName)
723
724    def genEnum(self, enuminfo, typeName, alias):
725        """Generate interface for an enum (constant).
726
727        - enuminfo - EnumInfo for an enum
728        - name - enum name
729
730        Extend to generate as desired in your derived class."""
731        self.validateFeature('enum', typeName)
732
733    def genCmd(self, cmd, cmdinfo, alias):
734        """Generate interface for a command.
735
736        - cmdinfo - CmdInfo for a command
737
738        Extend to generate as desired in your derived class."""
739        self.validateFeature('command', cmdinfo)
740
741    def makeProtoName(self, name, tail):
742        """Turn a `<proto>` `<name>` into C-language prototype
743        and typedef declarations for that name.
744
745        - name - contents of `<name>` tag
746        - tail - whatever text follows that tag in the Element"""
747        return self.genOpts.apientry + name + tail
748
749    def makeTypedefName(self, name, tail):
750        """Make the function-pointer typedef name for a command."""
751        return '(' + self.genOpts.apientryp + 'PFN_' + name + tail + ')'
752
753    def makeCParamDecl(self, param, aligncol):
754        """Return a string which is an indented, formatted
755        declaration for a `<param>` or `<member>` block (e.g. function parameter
756        or structure/union member).
757
758        - param - Element (`<param>` or `<member>`) to format
759        - aligncol - if non-zero, attempt to align the nested `<name>` element
760          at this column"""
761        indent = '    '
762        paramdecl = indent + noneStr(param.text)
763        for elem in param:
764            text = noneStr(elem.text)
765            tail = noneStr(elem.tail)
766
767            if self.should_insert_may_alias_macro and self.genOpts.conventions.is_voidpointer_alias(elem.tag, text, tail):
768                # OpenXR-specific macro insertion - but not in apiinc for the spec
769                tail = self.genOpts.conventions.make_voidpointer_alias(tail)
770            if elem.tag == 'name' and aligncol > 0:
771                self.logMsg('diag', 'Aligning parameter', elem.text, 'to column', self.genOpts.alignFuncParam)
772                # Align at specified column, if possible
773                paramdecl = paramdecl.rstrip()
774                oldLen = len(paramdecl)
775                # This works around a problem where very long type names -
776                # longer than the alignment column - would run into the tail
777                # text.
778                paramdecl = paramdecl.ljust(aligncol - 1) + ' '
779                newLen = len(paramdecl)
780                self.logMsg('diag', 'Adjust length of parameter decl from', oldLen, 'to', newLen, ':', paramdecl)
781            paramdecl += text + tail
782        if aligncol == 0:
783            # Squeeze out multiple spaces other than the indentation
784            paramdecl = indent + ' '.join(paramdecl.split())
785        return paramdecl
786
787    def getCParamTypeLength(self, param):
788        """Return the length of the type field is an indented, formatted
789        declaration for a `<param>` or `<member>` block (e.g. function parameter
790        or structure/union member).
791
792        - param - Element (`<param>` or `<member>`) to identify"""
793
794        # Allow for missing <name> tag
795        newLen = 0
796        paramdecl = '    ' + noneStr(param.text)
797        for elem in param:
798            text = noneStr(elem.text)
799            tail = noneStr(elem.tail)
800
801            if self.should_insert_may_alias_macro and self.genOpts.conventions.is_voidpointer_alias(elem.tag, text, tail):
802                # OpenXR-specific macro insertion
803                tail = self.genOpts.conventions.make_voidpointer_alias(tail)
804            if elem.tag == 'name':
805                # Align at specified column, if possible
806                newLen = len(paramdecl.rstrip())
807                self.logMsg('diag', 'Identifying length of', elem.text, 'as', newLen)
808            paramdecl += text + tail
809
810        return newLen
811
812    def getMaxCParamTypeLength(self, info):
813        """Return the length of the longest type field for a member/parameter.
814
815        - info - TypeInfo or CommandInfo.
816        """
817        lengths = (self.getCParamTypeLength(member)
818                   for member in info.getMembers())
819        return max(lengths)
820
821    def getHandleParent(self, typename):
822        """Get the parent of a handle object."""
823        info = self.registry.typedict.get(typename)
824        if info is None:
825            return None
826
827        elem = info.elem
828        if elem is not None:
829            return elem.get('parent')
830
831        return None
832
833    def iterateHandleAncestors(self, typename):
834        """Iterate through the ancestors of a handle type."""
835        current = self.getHandleParent(typename)
836        while current is not None:
837            yield current
838            current = self.getHandleParent(current)
839
840    def getHandleAncestors(self, typename):
841        """Get the ancestors of a handle object."""
842        return list(self.iterateHandleAncestors(typename))
843
844    def getTypeCategory(self, typename):
845        """Get the category of a type."""
846        info = self.registry.typedict.get(typename)
847        if info is None:
848            return None
849
850        elem = info.elem
851        if elem is not None:
852            return elem.get('category')
853        return None
854
855    def isStructAlwaysValid(self, structname):
856        """Try to do check if a structure is always considered valid (i.e. there's no rules to its acceptance)."""
857        # A conventions object is required for this call.
858        if not self.conventions:
859            raise RuntimeError("To use isStructAlwaysValid, be sure your options include a Conventions object.")
860
861        if self.conventions.type_always_valid(structname):
862            return True
863
864        category = self.getTypeCategory(structname)
865        if self.conventions.category_requires_validation(category):
866            return False
867
868        info = self.registry.typedict.get(structname)
869        assert(info is not None)
870
871        members = info.getMembers()
872
873        for member in members:
874            member_name = getElemName(member)
875            if member_name in (self.conventions.structtype_member_name,
876                               self.conventions.nextpointer_member_name):
877                return False
878
879            if member.get('noautovalidity'):
880                return False
881
882            member_type = getElemType(member)
883
884            if member_type in ('void', 'char') or self.paramIsArray(member) or self.paramIsPointer(member):
885                return False
886
887            if self.conventions.type_always_valid(member_type):
888                continue
889
890            member_category = self.getTypeCategory(member_type)
891
892            if self.conventions.category_requires_validation(member_category):
893                return False
894
895            if member_category in ('struct', 'union'):
896                if self.isStructAlwaysValid(member_type) is False:
897                    return False
898
899        return True
900
901    def isEnumRequired(self, elem):
902        """Return True if this `<enum>` element is
903        required, False otherwise
904
905        - elem - `<enum>` element to test"""
906        required = elem.get('required') is not None
907        self.logMsg('diag', 'isEnumRequired:', elem.get('name'),
908                    '->', required)
909        return required
910
911        # @@@ This code is overridden by equivalent code now run in
912        # @@@ Registry.generateFeature
913
914        required = False
915
916        extname = elem.get('extname')
917        if extname is not None:
918            # 'supported' attribute was injected when the <enum> element was
919            # moved into the <enums> group in Registry.parseTree()
920            if self.genOpts.defaultExtensions == elem.get('supported'):
921                required = True
922            elif re.match(self.genOpts.addExtensions, extname) is not None:
923                required = True
924        elif elem.get('version') is not None:
925            required = re.match(self.genOpts.emitversions, elem.get('version')) is not None
926        else:
927            required = True
928
929        return required
930
931    def makeCDecls(self, cmd):
932        """Return C prototype and function pointer typedef for a
933        `<command>` Element, as a two-element list of strings.
934
935        - cmd - Element containing a `<command>` tag"""
936        proto = cmd.find('proto')
937        params = cmd.findall('param')
938        # Begin accumulating prototype and typedef strings
939        pdecl = self.genOpts.apicall
940        tdecl = 'typedef '
941
942        # Insert the function return type/name.
943        # For prototypes, add APIENTRY macro before the name
944        # For typedefs, add (APIENTRY *<name>) around the name and
945        #   use the PFN_cmdnameproc naming convention.
946        # Done by walking the tree for <proto> element by element.
947        # etree has elem.text followed by (elem[i], elem[i].tail)
948        #   for each child element and any following text
949        # Leading text
950        pdecl += noneStr(proto.text)
951        tdecl += noneStr(proto.text)
952        # For each child element, if it's a <name> wrap in appropriate
953        # declaration. Otherwise append its contents and tail contents.
954        for elem in proto:
955            text = noneStr(elem.text)
956            tail = noneStr(elem.tail)
957            if elem.tag == 'name':
958                pdecl += self.makeProtoName(text, tail)
959                tdecl += self.makeTypedefName(text, tail)
960            else:
961                pdecl += text + tail
962                tdecl += text + tail
963
964        if self.genOpts.alignFuncParam == 0:
965            # Squeeze out multiple spaces - there is no indentation
966            pdecl = ' '.join(pdecl.split())
967            tdecl = ' '.join(tdecl.split())
968
969        # Now add the parameter declaration list, which is identical
970        # for prototypes and typedefs. Concatenate all the text from
971        # a <param> node without the tags. No tree walking required
972        # since all tags are ignored.
973        # Uses: self.indentFuncProto
974        # self.indentFuncPointer
975        # self.alignFuncParam
976        n = len(params)
977        # Indented parameters
978        if n > 0:
979            indentdecl = '(\n'
980            indentdecl += ',\n'.join(self.makeCParamDecl(p, self.genOpts.alignFuncParam)
981                                     for p in params)
982            indentdecl += ');'
983        else:
984            indentdecl = '(void);'
985        # Non-indented parameters
986        paramdecl = '('
987        if n > 0:
988            paramnames = (''.join(t for t in p.itertext())
989                          for p in params)
990            paramdecl += ', '.join(paramnames)
991        else:
992            paramdecl += 'void'
993        paramdecl += ");"
994        return [pdecl + indentdecl, tdecl + paramdecl]
995
996    def newline(self):
997        """Print a newline to the output file (utility function)"""
998        write('', file=self.outFile)
999
1000    def setRegistry(self, registry):
1001        self.registry = registry
1002