• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/python3 -i
2#
3# Copyright (c) 2013-2018 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
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.version)
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#   sortProcedure - takes a list of FeatureInfo objects and sorts
112#     them in place to a preferred order in the generated output.
113#     Default is core API versions, ARB/KHR/OES extensions, all
114#     other extensions, alphabetically within each group.
115# The regex patterns can be None or empty, in which case they match
116#   nothing.
117class GeneratorOptions:
118    """Represents options during header production from an API registry"""
119    def __init__(self,
120                 filename = None,
121                 directory = '.',
122                 apiname = None,
123                 profile = None,
124                 versions = '.*',
125                 emitversions = '.*',
126                 defaultExtensions = None,
127                 addExtensions = None,
128                 removeExtensions = None,
129                 sortProcedure = regSortFeatures):
130        self.filename          = filename
131        self.directory         = directory
132        self.apiname           = apiname
133        self.profile           = profile
134        self.versions          = self.emptyRegex(versions)
135        self.emitversions      = self.emptyRegex(emitversions)
136        self.defaultExtensions = defaultExtensions
137        self.addExtensions     = self.emptyRegex(addExtensions)
138        self.removeExtensions  = self.emptyRegex(removeExtensions)
139        self.sortProcedure     = sortProcedure
140    #
141    # Substitute a regular expression which matches no version
142    # or extension names for None or the empty string.
143    def emptyRegex(self,pat):
144        if (pat == None or pat == ''):
145            return '_nomatch_^'
146        else:
147            return pat
148
149# OutputGenerator - base class for generating API interfaces.
150# Manages basic logic, logging, and output file control
151# Derived classes actually generate formatted output.
152#
153# ---- methods ----
154# OutputGenerator(errFile, warnFile, diagFile)
155#   errFile, warnFile, diagFile - file handles to write errors,
156#     warnings, diagnostics to. May be None to not write.
157# logMsg(level, *args) - log messages of different categories
158#   level - 'error', 'warn', or 'diag'. 'error' will also
159#     raise a UserWarning exception
160#   *args - print()-style arguments
161# setExtMap(map) - specify a dictionary map from extension names to
162#   numbers, used in creating values for extension enumerants.
163# makeDir(directory) - create a directory, if not already done.
164#   Generally called from derived generators creating hierarchies.
165# beginFile(genOpts) - start a new interface file
166#   genOpts - GeneratorOptions controlling what's generated and how
167# endFile() - finish an interface file, closing it when done
168# beginFeature(interface, emit) - write interface for a feature
169# and tag generated features as having been done.
170#   interface - element for the <version> / <extension> to generate
171#   emit - actually write to the header only when True
172# endFeature() - finish an interface.
173# genType(typeinfo,name) - generate interface for a type
174#   typeinfo - TypeInfo for a type
175# genStruct(typeinfo,name) - generate interface for a C "struct" type.
176#   typeinfo - TypeInfo for a type interpreted as a struct
177# genGroup(groupinfo,name) - generate interface for a group of enums (C "enum")
178#   groupinfo - GroupInfo for a group
179# genEnum(enuminfo, name) - generate interface for an enum (constant)
180#   enuminfo - EnumInfo for an enum
181#   name - enum name
182# genCmd(cmdinfo) - generate interface for a command
183#   cmdinfo - CmdInfo for a command
184# isEnumRequired(enumElem) - return True if this <enum> element is required
185#   elem - <enum> element to test
186# makeCDecls(cmd) - return C prototype and function pointer typedef for a
187#     <command> Element, as a list of two strings
188#   cmd - Element for the <command>
189# newline() - print a newline to the output file (utility function)
190#
191class OutputGenerator:
192    """Generate specified API interfaces in a specific style, such as a C header"""
193    #
194    # categoryToPath - map XML 'category' to include file directory name
195    categoryToPath = {
196        'bitmask'      : 'flags',
197        'enum'         : 'enums',
198        'funcpointer'  : 'funcpointers',
199        'handle'       : 'handles',
200        'define'       : 'defines',
201        'basetype'     : 'basetypes',
202    }
203    #
204    # Constructor
205    def __init__(self,
206                 errFile = sys.stderr,
207                 warnFile = sys.stderr,
208                 diagFile = sys.stdout):
209        self.outFile = None
210        self.errFile = errFile
211        self.warnFile = warnFile
212        self.diagFile = diagFile
213        # Internal state
214        self.featureName = None
215        self.genOpts = None
216        self.registry = None
217        # Used for extension enum value generation
218        self.extBase      = 1000000000
219        self.extBlockSize = 1000
220        self.madeDirs = {}
221    #
222    # logMsg - write a message of different categories to different
223    #   destinations.
224    # level -
225    #   'diag' (diagnostic, voluminous)
226    #   'warn' (warning)
227    #   'error' (fatal error - raises exception after logging)
228    # *args - print()-style arguments to direct to corresponding log
229    def logMsg(self, level, *args):
230        """Log a message at the given level. Can be ignored or log to a file"""
231        if (level == 'error'):
232            strfile = io.StringIO()
233            write('ERROR:', *args, file=strfile)
234            if (self.errFile != None):
235                write(strfile.getvalue(), file=self.errFile)
236            raise UserWarning(strfile.getvalue())
237        elif (level == 'warn'):
238            if (self.warnFile != None):
239                write('WARNING:', *args, file=self.warnFile)
240        elif (level == 'diag'):
241            if (self.diagFile != None):
242                write('DIAG:', *args, file=self.diagFile)
243        else:
244            raise UserWarning(
245                '*** FATAL ERROR in Generator.logMsg: unknown level:' + level)
246    #
247    # enumToValue - parses and converts an <enum> tag into a value.
248    # Returns a list
249    #   first element - integer representation of the value, or None
250    #       if needsNum is False. The value must be a legal number
251    #       if needsNum is True.
252    #   second element - string representation of the value
253    # There are several possible representations of values.
254    #   A 'value' attribute simply contains the value.
255    #   A 'bitpos' attribute defines a value by specifying the bit
256    #       position which is set in that value.
257    #   A 'offset','extbase','extends' triplet specifies a value
258    #       as an offset to a base value defined by the specified
259    #       'extbase' extension name, which is then cast to the
260    #       typename specified by 'extends'. This requires probing
261    #       the registry database, and imbeds knowledge of the
262    #       Vulkan extension enum scheme in this function.
263    def enumToValue(self, elem, needsNum):
264        name = elem.get('name')
265        numVal = None
266        if ('value' in elem.keys()):
267            value = elem.get('value')
268            # print('About to translate value =', value, 'type =', type(value))
269            if (needsNum):
270                numVal = int(value, 0)
271            # If there's a non-integer, numeric 'type' attribute (e.g. 'u' or
272            # 'ull'), append it to the string value.
273            # t = enuminfo.elem.get('type')
274            # if (t != None and t != '' and t != 'i' and t != 's'):
275            #     value += enuminfo.type
276            self.logMsg('diag', 'Enum', name, '-> value [', numVal, ',', value, ']')
277            return [numVal, value]
278        if ('bitpos' in elem.keys()):
279            value = elem.get('bitpos')
280            numVal = int(value, 0)
281            numVal = 1 << numVal
282            value = '0x%08x' % numVal
283            self.logMsg('diag', 'Enum', name, '-> bitpos [', numVal, ',', value, ']')
284            return [numVal, value]
285        if ('offset' in elem.keys()):
286            # Obtain values in the mapping from the attributes
287            enumNegative = False
288            offset = int(elem.get('offset'),0)
289            extnumber = int(elem.get('extnumber'),0)
290            extends = elem.get('extends')
291            if ('dir' in elem.keys()):
292                enumNegative = True
293            self.logMsg('diag', 'Enum', name, 'offset =', offset,
294                'extnumber =', extnumber, 'extends =', extends,
295                'enumNegative =', enumNegative)
296            # Now determine the actual enumerant value, as defined
297            # in the "Layers and Extensions" appendix of the spec.
298            numVal = self.extBase + (extnumber - 1) * self.extBlockSize + offset
299            if (enumNegative):
300                numVal = -numVal
301            value = '%d' % numVal
302            # More logic needed!
303            self.logMsg('diag', 'Enum', name, '-> offset [', numVal, ',', value, ']')
304            return [numVal, value]
305        return [None, None]
306    #
307    def makeDir(self, path):
308        self.logMsg('diag', 'OutputGenerator::makeDir(' + path + ')')
309        if not (path in self.madeDirs.keys()):
310            # This can get race conditions with multiple writers, see
311            # https://stackoverflow.com/questions/273192/
312            if not os.path.exists(path):
313                os.makedirs(path)
314            self.madeDirs[path] = None
315    #
316    def beginFile(self, genOpts):
317        self.genOpts = genOpts
318        #
319        # Open specified output file. Not done in constructor since a
320        # Generator can be used without writing to a file.
321        if (self.genOpts.filename != None):
322            filename = self.genOpts.directory + '/' + self.genOpts.filename
323            self.outFile = io.open(filename, 'w', encoding='utf-8')
324        else:
325            self.outFile = sys.stdout
326    def endFile(self):
327        self.errFile and self.errFile.flush()
328        self.warnFile and self.warnFile.flush()
329        self.diagFile and self.diagFile.flush()
330        self.outFile.flush()
331        if (self.outFile != sys.stdout and self.outFile != sys.stderr):
332            self.outFile.close()
333        self.genOpts = None
334    #
335    def beginFeature(self, interface, emit):
336        self.emit = emit
337        self.featureName = interface.get('name')
338        # If there's an additional 'protect' attribute in the feature, save it
339        self.featureExtraProtect = interface.get('protect')
340    def endFeature(self):
341        # Derived classes responsible for emitting feature
342        self.featureName = None
343        self.featureExtraProtect = None
344    # Utility method to validate we're generating something only inside a
345    # <feature> tag
346    def validateFeature(self, featureType, featureName):
347        if (self.featureName == None):
348            raise UserWarning('Attempt to generate', featureType, name,
349                    'when not in feature')
350    #
351    # Type generation
352    def genType(self, typeinfo, name):
353        self.validateFeature('type', name)
354    #
355    # Struct (e.g. C "struct" type) generation
356    def genStruct(self, typeinfo, name):
357        self.validateFeature('struct', name)
358
359        # The mixed-mode <member> tags may contain no-op <comment> tags.
360        # It is convenient to remove them here where all output generators
361        # will benefit.
362        for member in typeinfo.elem.findall('.//member'):
363            for comment in member.findall('comment'):
364                member.remove(comment)
365    #
366    # Group (e.g. C "enum" type) generation
367    def genGroup(self, groupinfo, name):
368        self.validateFeature('group', name)
369    #
370    # Enumerant (really, constant) generation
371    def genEnum(self, enuminfo, name):
372        self.validateFeature('enum', name)
373    #
374    # Command generation
375    def genCmd(self, cmd, name):
376        self.validateFeature('command', name)
377    #
378    # Utility functions - turn a <proto> <name> into C-language prototype
379    # and typedef declarations for that name.
380    # name - contents of <name> tag
381    # tail - whatever text follows that tag in the Element
382    def makeProtoName(self, name, tail):
383        return self.genOpts.apientry + name + tail
384    def makeTypedefName(self, name, tail):
385       return '(' + self.genOpts.apientryp + 'PFN_' + name + tail + ')'
386    #
387    # makeCParamDecl - return a string which is an indented, formatted
388    # declaration for a <param> or <member> block (e.g. function parameter
389    # or structure/union member).
390    # param - Element (<param> or <member>) to format
391    # aligncol - if non-zero, attempt to align the nested <name> element
392    #   at this column
393    def makeCParamDecl(self, param, aligncol):
394        paramdecl = '    ' + noneStr(param.text)
395        for elem in param:
396            text = noneStr(elem.text)
397            tail = noneStr(elem.tail)
398            if (elem.tag == 'name' and aligncol > 0):
399                self.logMsg('diag', 'Aligning parameter', elem.text, 'to column', self.genOpts.alignFuncParam)
400                # Align at specified column, if possible
401                paramdecl = paramdecl.rstrip()
402                oldLen = len(paramdecl)
403                # This works around a problem where very long type names -
404                # longer than the alignment column - would run into the tail
405                # text.
406                paramdecl = paramdecl.ljust(aligncol-1) + ' '
407                newLen = len(paramdecl)
408                self.logMsg('diag', 'Adjust length of parameter decl from', oldLen, 'to', newLen, ':', paramdecl)
409            paramdecl += text + tail
410        return paramdecl
411    #
412    # getCParamTypeLength - return the length of the type field is an indented, formatted
413    # declaration for a <param> or <member> block (e.g. function parameter
414    # or structure/union member).
415    # param - Element (<param> or <member>) to identify
416    def getCParamTypeLength(self, param):
417        paramdecl = '    ' + noneStr(param.text)
418        for elem in param:
419            text = noneStr(elem.text)
420            tail = noneStr(elem.tail)
421            if (elem.tag == 'name'):
422                # Align at specified column, if possible
423                newLen = len(paramdecl.rstrip())
424                self.logMsg('diag', 'Identifying length of', elem.text, 'as', newLen)
425            paramdecl += text + tail
426        return newLen
427    #
428    # isEnumRequired(elem) - return True if this <enum> element is
429    # required, False otherwise
430    # elem - <enum> element to test
431    def isEnumRequired(self, elem):
432        return (elem.get('extname') is None or
433                re.match(self.genOpts.addExtensions, elem.get('extname')) is not None or
434                self.genOpts.defaultExtensions == elem.get('supported'))
435    #
436    # makeCDecls - return C prototype and function pointer typedef for a
437    #   command, as a two-element list of strings.
438    # cmd - Element containing a <command> tag
439    def makeCDecls(self, cmd):
440        """Generate C function pointer typedef for <command> Element"""
441        proto = cmd.find('proto')
442        params = cmd.findall('param')
443        # Begin accumulating prototype and typedef strings
444        pdecl = self.genOpts.apicall
445        tdecl = 'typedef '
446        #
447        # Insert the function return type/name.
448        # For prototypes, add APIENTRY macro before the name
449        # For typedefs, add (APIENTRY *<name>) around the name and
450        #   use the PFN_cmdnameproc naming convention.
451        # Done by walking the tree for <proto> element by element.
452        # etree has elem.text followed by (elem[i], elem[i].tail)
453        #   for each child element and any following text
454        # Leading text
455        pdecl += noneStr(proto.text)
456        tdecl += noneStr(proto.text)
457        # For each child element, if it's a <name> wrap in appropriate
458        # declaration. Otherwise append its contents and tail contents.
459        for elem in proto:
460            text = noneStr(elem.text)
461            tail = noneStr(elem.tail)
462            if (elem.tag == 'name'):
463                pdecl += self.makeProtoName(text, tail)
464                tdecl += self.makeTypedefName(text, tail)
465            else:
466                pdecl += text + tail
467                tdecl += text + tail
468        # Now add the parameter declaration list, which is identical
469        # for prototypes and typedefs. Concatenate all the text from
470        # a <param> node without the tags. No tree walking required
471        # since all tags are ignored.
472        # Uses: self.indentFuncProto
473        # self.indentFuncPointer
474        # self.alignFuncParam
475        # Might be able to doubly-nest the joins, e.g.
476        #   ','.join(('_'.join([l[i] for i in range(0,len(l))])
477        n = len(params)
478        # Indented parameters
479        if n > 0:
480            indentdecl = '(\n'
481            for i in range(0,n):
482                paramdecl = self.makeCParamDecl(params[i], self.genOpts.alignFuncParam)
483                if (i < n - 1):
484                    paramdecl += ',\n'
485                else:
486                    paramdecl += ');'
487                indentdecl += paramdecl
488        else:
489            indentdecl = '(void);'
490        # Non-indented parameters
491        paramdecl = '('
492        if n > 0:
493            for i in range(0,n):
494                paramdecl += ''.join([t for t in params[i].itertext()])
495                if (i < n - 1):
496                    paramdecl += ', '
497        else:
498            paramdecl += 'void'
499        paramdecl += ");";
500        return [ pdecl + indentdecl, tdecl + paramdecl ]
501    #
502    def newline(self):
503        write('', file=self.outFile)
504
505    def setRegistry(self, registry):
506        self.registry = registry
507        #
508