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