blob: 709ac14827089d6c6abc1d82b53a3f09d5d99113 [file] [log] [blame]
Mike Stroyandee76ef2016-01-07 15:35:37 -07001#!/usr/bin/python3 -i
Dustin Graves3ff520c2016-03-28 16:17:38 -06002#
Mark Lobodzinski36c33862017-02-13 10:15:53 -07003# Copyright (c) 2013-2017 The Khronos Group Inc.
Dustin Graves3ff520c2016-03-28 16:17:38 -06004#
Jon Ashburn3ebf1252016-04-19 11:30:31 -06005# 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
Dustin Graves3ff520c2016-03-28 16:17:38 -06008#
Jon Ashburn3ebf1252016-04-19 11:30:31 -06009# http://www.apache.org/licenses/LICENSE-2.0
Dustin Graves3ff520c2016-03-28 16:17:38 -060010#
Jon Ashburn3ebf1252016-04-19 11:30:31 -060011# 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.
Dustin Graves3ff520c2016-03-28 16:17:38 -060016
Mark Lobodzinskicbaa2cd2016-12-19 09:41:16 -070017import io,os,re,sys
Mike Stroyandee76ef2016-01-07 15:35:37 -070018
19def write( *args, **kwargs ):
Mark Lobodzinskicbaa2cd2016-12-19 09:41:16 -070020 file = kwargs.pop(u'file',sys.stdout)
21 end = kwargs.pop( u'end',u'\n')
22 file.write( u' '.join([str(arg) for arg in args]) )
Mike Stroyandee76ef2016-01-07 15:35:37 -070023 file.write( end )
24
25# noneStr - returns string argument, or "" if argument is None.
Mike Stroyan3c5a6e22016-04-05 16:40:30 -060026# Used in converting etree Elements into text.
Mike Stroyandee76ef2016-01-07 15:35:37 -070027# 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
Mark Lobodzinskic97e2642016-10-13 08:58:38 -060042# 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
Mike Stroyandee76ef2016-01-07 15:35:37 -070047# 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
Mark Lobodzinskic97e2642016-10-13 08:58:38 -060092# filename - basename of file to generate, or None to write to stdout.
93# directory - directory in which to generate filename
Mike Stroyandee76ef2016-01-07 15:35:37 -070094# 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,
Mark Lobodzinskic97e2642016-10-13 08:58:38 -0600120 directory = '.',
Mike Stroyandee76ef2016-01-07 15:35:37 -0700121 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
Mark Lobodzinskic97e2642016-10-13 08:58:38 -0600130 self.directory = directory
Mike Stroyandee76ef2016-01-07 15:35:37 -0700131 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
Mike Stroyandee76ef2016-01-07 15:35:37 -0700148# 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.
Mark Lobodzinskic97e2642016-10-13 08:58:38 -0600162# makeDir(directory) - create a directory, if not already done.
163# Generally called from derived generators creating hierarchies.
Mike Stroyandee76ef2016-01-07 15:35:37 -0700164# 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
Mark Lobodzinskic97e2642016-10-13 08:58:38 -0600183# isEnumRequired(enumElem) - return True if this <enum> element is required
184# elem - <enum> element to test
Mike Stroyandee76ef2016-01-07 15:35:37 -0700185# 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"""
Mark Lobodzinskic97e2642016-10-13 08:58:38 -0600192 #
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
Mike Stroyandee76ef2016-01-07 15:35:37 -0700204 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
Mark Lobodzinskic97e2642016-10-13 08:58:38 -0600219 self.madeDirs = {}
Mike Stroyandee76ef2016-01-07 15:35:37 -0700220 #
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 #
Mark Lobodzinskic97e2642016-10-13 08:58:38 -0600306 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 #
Mike Stroyandee76ef2016-01-07 15:35:37 -0700315 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):
Mark Lobodzinski2d589822016-12-12 09:44:34 -0700321 filename = self.genOpts.directory + '/' + self.genOpts.filename
Mark Lobodzinskicbaa2cd2016-12-19 09:41:16 -0700322 self.outFile = io.open(filename, 'w', encoding='utf-8')
Mike Stroyandee76ef2016-01-07 15:35:37 -0700323 else:
324 self.outFile = sys.stdout
325 def endFile(self):
326 self.errFile and self.errFile.flush()
327 self.warnFile and self.warnFile.flush()
328 self.diagFile and self.diagFile.flush()
329 self.outFile.flush()
330 if (self.outFile != sys.stdout and self.outFile != sys.stderr):
331 self.outFile.close()
332 self.genOpts = None
333 #
334 def beginFeature(self, interface, emit):
335 self.emit = emit
336 self.featureName = interface.get('name')
337 # If there's an additional 'protect' attribute in the feature, save it
338 self.featureExtraProtect = interface.get('protect')
339 def endFeature(self):
340 # Derived classes responsible for emitting feature
341 self.featureName = None
342 self.featureExtraProtect = None
343 # Utility method to validate we're generating something only inside a
344 # <feature> tag
345 def validateFeature(self, featureType, featureName):
346 if (self.featureName == None):
347 raise UserWarning('Attempt to generate', featureType, name,
348 'when not in feature')
349 #
350 # Type generation
351 def genType(self, typeinfo, name):
352 self.validateFeature('type', name)
353 #
354 # Struct (e.g. C "struct" type) generation
355 def genStruct(self, typeinfo, name):
356 self.validateFeature('struct', name)
357 #
358 # Group (e.g. C "enum" type) generation
359 def genGroup(self, groupinfo, name):
360 self.validateFeature('group', name)
361 #
362 # Enumerant (really, constant) generation
363 def genEnum(self, enuminfo, name):
364 self.validateFeature('enum', name)
365 #
366 # Command generation
367 def genCmd(self, cmd, name):
368 self.validateFeature('command', name)
369 #
370 # Utility functions - turn a <proto> <name> into C-language prototype
371 # and typedef declarations for that name.
372 # name - contents of <name> tag
373 # tail - whatever text follows that tag in the Element
374 def makeProtoName(self, name, tail):
375 return self.genOpts.apientry + name + tail
376 def makeTypedefName(self, name, tail):
377 return '(' + self.genOpts.apientryp + 'PFN_' + name + tail + ')'
378 #
379 # makeCParamDecl - return a string which is an indented, formatted
380 # declaration for a <param> or <member> block (e.g. function parameter
381 # or structure/union member).
382 # param - Element (<param> or <member>) to format
383 # aligncol - if non-zero, attempt to align the nested <name> element
384 # at this column
385 def makeCParamDecl(self, param, aligncol):
386 paramdecl = ' ' + noneStr(param.text)
387 for elem in param:
388 text = noneStr(elem.text)
389 tail = noneStr(elem.tail)
390 if (elem.tag == 'name' and aligncol > 0):
391 self.logMsg('diag', 'Aligning parameter', elem.text, 'to column', self.genOpts.alignFuncParam)
392 # Align at specified column, if possible
393 paramdecl = paramdecl.rstrip()
394 oldLen = len(paramdecl)
Mark Lobodzinskic97e2642016-10-13 08:58:38 -0600395 # This works around a problem where very long type names -
396 # longer than the alignment column - would run into the tail
397 # text.
398 paramdecl = paramdecl.ljust(aligncol-1) + ' '
Mike Stroyandee76ef2016-01-07 15:35:37 -0700399 newLen = len(paramdecl)
400 self.logMsg('diag', 'Adjust length of parameter decl from', oldLen, 'to', newLen, ':', paramdecl)
401 paramdecl += text + tail
402 return paramdecl
403 #
404 # getCParamTypeLength - return the length of the type field is an indented, formatted
405 # declaration for a <param> or <member> block (e.g. function parameter
406 # or structure/union member).
407 # param - Element (<param> or <member>) to identify
408 def getCParamTypeLength(self, param):
409 paramdecl = ' ' + noneStr(param.text)
410 for elem in param:
411 text = noneStr(elem.text)
412 tail = noneStr(elem.tail)
413 if (elem.tag == 'name'):
414 # Align at specified column, if possible
415 newLen = len(paramdecl.rstrip())
416 self.logMsg('diag', 'Identifying length of', elem.text, 'as', newLen)
417 paramdecl += text + tail
418 return newLen
419 #
Mark Lobodzinskic97e2642016-10-13 08:58:38 -0600420 # isEnumRequired(elem) - return True if this <enum> element is
421 # required, False otherwise
422 # elem - <enum> element to test
423 def isEnumRequired(self, elem):
424 return (elem.get('extname') is None or
425 re.match(self.genOpts.addExtensions, elem.get('extname')) is not None or
426 self.genOpts.defaultExtensions == elem.get('supported'))
427 #
Mike Stroyandee76ef2016-01-07 15:35:37 -0700428 # makeCDecls - return C prototype and function pointer typedef for a
429 # command, as a two-element list of strings.
430 # cmd - Element containing a <command> tag
431 def makeCDecls(self, cmd):
432 """Generate C function pointer typedef for <command> Element"""
433 proto = cmd.find('proto')
434 params = cmd.findall('param')
435 # Begin accumulating prototype and typedef strings
436 pdecl = self.genOpts.apicall
437 tdecl = 'typedef '
438 #
439 # Insert the function return type/name.
440 # For prototypes, add APIENTRY macro before the name
441 # For typedefs, add (APIENTRY *<name>) around the name and
442 # use the PFN_cmdnameproc naming convention.
443 # Done by walking the tree for <proto> element by element.
Mike Stroyan3c5a6e22016-04-05 16:40:30 -0600444 # etree has elem.text followed by (elem[i], elem[i].tail)
Mike Stroyandee76ef2016-01-07 15:35:37 -0700445 # for each child element and any following text
446 # Leading text
447 pdecl += noneStr(proto.text)
448 tdecl += noneStr(proto.text)
449 # For each child element, if it's a <name> wrap in appropriate
450 # declaration. Otherwise append its contents and tail contents.
451 for elem in proto:
452 text = noneStr(elem.text)
453 tail = noneStr(elem.tail)
454 if (elem.tag == 'name'):
455 pdecl += self.makeProtoName(text, tail)
456 tdecl += self.makeTypedefName(text, tail)
457 else:
458 pdecl += text + tail
459 tdecl += text + tail
460 # Now add the parameter declaration list, which is identical
461 # for prototypes and typedefs. Concatenate all the text from
462 # a <param> node without the tags. No tree walking required
463 # since all tags are ignored.
464 # Uses: self.indentFuncProto
465 # self.indentFuncPointer
466 # self.alignFuncParam
467 # Might be able to doubly-nest the joins, e.g.
468 # ','.join(('_'.join([l[i] for i in range(0,len(l))])
469 n = len(params)
470 # Indented parameters
471 if n > 0:
472 indentdecl = '(\n'
473 for i in range(0,n):
474 paramdecl = self.makeCParamDecl(params[i], self.genOpts.alignFuncParam)
475 if (i < n - 1):
476 paramdecl += ',\n'
477 else:
478 paramdecl += ');'
479 indentdecl += paramdecl
480 else:
481 indentdecl = '(void);'
482 # Non-indented parameters
483 paramdecl = '('
484 if n > 0:
485 for i in range(0,n):
486 paramdecl += ''.join([t for t in params[i].itertext()])
487 if (i < n - 1):
488 paramdecl += ', '
489 else:
490 paramdecl += 'void'
491 paramdecl += ");";
492 return [ pdecl + indentdecl, tdecl + paramdecl ]
493 #
494 def newline(self):
495 write('', file=self.outFile)
496
497 def setRegistry(self, registry):
498 self.registry = registry
499 #