blob: c57459861d9a8c7f42b35ee03e03e571b0a8b311 [file] [log] [blame]
Mike Stroyandee76ef2016-01-07 15:35:37 -07001#!/usr/bin/python3 -i
Dustin Graves3ff520c2016-03-28 16:17:38 -06002#
Mark Lobodzinskidbe7dce2018-01-08 08:17:24 -07003# Copyright (c) 2013-2018 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 Lobodzinskibc54f2c2017-03-21 10:12:31 -060017from __future__ import unicode_literals
Mark Lobodzinskicbaa2cd2016-12-19 09:41:16 -070018import io,os,re,sys
Mike Stroyandee76ef2016-01-07 15:35:37 -070019
20def write( *args, **kwargs ):
Mark Lobodzinskibc54f2c2017-03-21 10:12:31 -060021 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)
Mike Stroyandee76ef2016-01-07 15:35:37 -070025
26# noneStr - returns string argument, or "" if argument is None.
Mike Stroyan3c5a6e22016-04-05 16:40:30 -060027# Used in converting etree Elements into text.
Mike Stroyandee76ef2016-01-07 15:35:37 -070028# 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
Mark Lobodzinskic97e2642016-10-13 08:58:38 -060043# 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
Mike Stroyandee76ef2016-01-07 15:35:37 -070048# 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
Mark Lobodzinskic97e2642016-10-13 08:58:38 -060093# filename - basename of file to generate, or None to write to stdout.
94# directory - directory in which to generate filename
Mike Stroyandee76ef2016-01-07 15:35:37 -070095# 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,
Mark Lobodzinskic97e2642016-10-13 08:58:38 -0600121 directory = '.',
Mike Stroyandee76ef2016-01-07 15:35:37 -0700122 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
Mark Lobodzinskic97e2642016-10-13 08:58:38 -0600131 self.directory = directory
Mike Stroyandee76ef2016-01-07 15:35:37 -0700132 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
Mike Stroyandee76ef2016-01-07 15:35:37 -0700149# 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.
Mark Lobodzinskic97e2642016-10-13 08:58:38 -0600163# makeDir(directory) - create a directory, if not already done.
164# Generally called from derived generators creating hierarchies.
Mike Stroyandee76ef2016-01-07 15:35:37 -0700165# 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
Mark Lobodzinskic97e2642016-10-13 08:58:38 -0600184# isEnumRequired(enumElem) - return True if this <enum> element is required
185# elem - <enum> element to test
Mike Stroyandee76ef2016-01-07 15:35:37 -0700186# 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"""
Mark Lobodzinskic97e2642016-10-13 08:58:38 -0600193 #
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
Mike Stroyandee76ef2016-01-07 15:35:37 -0700205 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
Mark Lobodzinskic97e2642016-10-13 08:58:38 -0600220 self.madeDirs = {}
Mike Stroyandee76ef2016-01-07 15:35:37 -0700221 #
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 #
Mark Lobodzinskic97e2642016-10-13 08:58:38 -0600307 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 #
Mike Stroyandee76ef2016-01-07 15:35:37 -0700316 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):
Mark Lobodzinski2d589822016-12-12 09:44:34 -0700322 filename = self.genOpts.directory + '/' + self.genOpts.filename
Mark Lobodzinskicbaa2cd2016-12-19 09:41:16 -0700323 self.outFile = io.open(filename, 'w', encoding='utf-8')
Mike Stroyandee76ef2016-01-07 15:35:37 -0700324 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)
Mark Lobodzinskie4f2c5f2017-07-17 14:26:47 -0600358
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)
Mike Stroyandee76ef2016-01-07 15:35:37 -0700365 #
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)
Mark Lobodzinskic97e2642016-10-13 08:58:38 -0600403 # 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) + ' '
Mike Stroyandee76ef2016-01-07 15:35:37 -0700407 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 #
Mark Lobodzinskic97e2642016-10-13 08:58:38 -0600428 # 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 #
Mike Stroyandee76ef2016-01-07 15:35:37 -0700436 # 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.
Mike Stroyan3c5a6e22016-04-05 16:40:30 -0600452 # etree has elem.text followed by (elem[i], elem[i].tail)
Mike Stroyandee76ef2016-01-07 15:35:37 -0700453 # 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 #