483 lines
20 KiB
Python
483 lines
20 KiB
Python
#!/usr/bin/python3 -i
|
|
#
|
|
# Copyright 2013-2021 The Khronos Group Inc.
|
|
#
|
|
# SPDX-License-Identifier: Apache-2.0
|
|
|
|
from pathlib import Path
|
|
|
|
from generator import GeneratorOptions, OutputGenerator, noneStr, write
|
|
|
|
ENUM_TABLE_PREFIX = """
|
|
[cols=",",options="header",]
|
|
|=======================================================================
|
|
|Enum |Description"""
|
|
|
|
ENUM_TABLE_SUFFIX = """|======================================================================="""
|
|
|
|
FLAG_BLOCK_PREFIX = """.Flag Descriptions
|
|
****"""
|
|
|
|
FLAG_BLOCK_SUFFIX = """****"""
|
|
|
|
def orgLevelKey(name):
|
|
# Sort key for organization levels of features / extensions
|
|
# From highest to lowest, core versions, KHR extensions, EXT extensions,
|
|
# and vendor extensions
|
|
|
|
prefixes = (
|
|
'VK_VERSION_',
|
|
'VKSC_VERSION_',
|
|
'VK_KHR_',
|
|
'VK_EXT_')
|
|
|
|
i = 0
|
|
for prefix in prefixes:
|
|
if name.startswith(prefix):
|
|
return i
|
|
i += 1
|
|
|
|
# Everything else (e.g. vendor extensions) is least important
|
|
return i
|
|
|
|
class DocGeneratorOptions(GeneratorOptions):
|
|
"""DocGeneratorOptions - subclass of GeneratorOptions for
|
|
generating declaration snippets for the spec.
|
|
|
|
Shares many members with CGeneratorOptions, since
|
|
both are writing C-style declarations."""
|
|
|
|
def __init__(self,
|
|
prefixText="",
|
|
apicall='',
|
|
apientry='',
|
|
apientryp='',
|
|
indentFuncProto=True,
|
|
indentFuncPointer=False,
|
|
alignFuncParam=0,
|
|
secondaryInclude=False,
|
|
expandEnumerants=True,
|
|
extEnumerantAdditions=False,
|
|
extEnumerantFormatString=" (Added by the {} extension)",
|
|
**kwargs):
|
|
"""Constructor.
|
|
|
|
Since this generator outputs multiple files at once,
|
|
the filename is just a "stamp" to indicate last generation time.
|
|
|
|
Shares many parameters/members with CGeneratorOptions, since
|
|
both are writing C-style declarations:
|
|
|
|
- prefixText - list of strings to prefix generated header with
|
|
(usually a copyright statement + calling convention macros).
|
|
- apicall - string to use for the function declaration prefix,
|
|
such as APICALL on Windows.
|
|
- apientry - string to use for the calling convention macro,
|
|
in typedefs, such as APIENTRY.
|
|
- apientryp - string to use for the calling convention macro
|
|
in function pointer typedefs, such as APIENTRYP.
|
|
- indentFuncProto - True if prototype declarations should put each
|
|
parameter on a separate line
|
|
- indentFuncPointer - True if typedefed function pointers should put each
|
|
parameter on a separate line
|
|
- alignFuncParam - if nonzero and parameters are being put on a
|
|
separate line, align parameter names at the specified column
|
|
|
|
Additional parameters/members:
|
|
|
|
- expandEnumerants - if True, add BEGIN/END_RANGE macros in enumerated
|
|
type declarations
|
|
- secondaryInclude - if True, add secondary (no xref anchor) versions
|
|
of generated files
|
|
- extEnumerantAdditions - if True, include enumerants added by extensions
|
|
in comment tables for core enumeration types.
|
|
- extEnumerantFormatString - A format string for any additional message for
|
|
enumerants from extensions if extEnumerantAdditions is True. The correctly-
|
|
marked-up extension name will be passed.
|
|
"""
|
|
GeneratorOptions.__init__(self, **kwargs)
|
|
self.prefixText = prefixText
|
|
"""list of strings to prefix generated header with (usually a copyright statement + calling convention macros)."""
|
|
|
|
self.apicall = apicall
|
|
"""string to use for the function declaration prefix, such as APICALL on Windows."""
|
|
|
|
self.apientry = apientry
|
|
"""string to use for the calling convention macro, in typedefs, such as APIENTRY."""
|
|
|
|
self.apientryp = apientryp
|
|
"""string to use for the calling convention macro in function pointer typedefs, such as APIENTRYP."""
|
|
|
|
self.indentFuncProto = indentFuncProto
|
|
"""True if prototype declarations should put each parameter on a separate line"""
|
|
|
|
self.indentFuncPointer = indentFuncPointer
|
|
"""True if typedefed function pointers should put each parameter on a separate line"""
|
|
|
|
self.alignFuncParam = alignFuncParam
|
|
"""if nonzero and parameters are being put on a separate line, align parameter names at the specified column"""
|
|
|
|
self.secondaryInclude = secondaryInclude
|
|
"""if True, add secondary (no xref anchor) versions of generated files"""
|
|
|
|
self.expandEnumerants = expandEnumerants
|
|
"""if True, add BEGIN/END_RANGE macros in enumerated type declarations"""
|
|
|
|
self.extEnumerantAdditions = extEnumerantAdditions
|
|
"""if True, include enumerants added by extensions in comment tables for core enumeration types."""
|
|
|
|
self.extEnumerantFormatString = extEnumerantFormatString
|
|
"""A format string for any additional message for
|
|
enumerants from extensions if extEnumerantAdditions is True. The correctly-
|
|
marked-up extension name will be passed."""
|
|
|
|
|
|
class DocOutputGenerator(OutputGenerator):
|
|
"""DocOutputGenerator - subclass of OutputGenerator.
|
|
|
|
Generates AsciiDoc includes with C-language API interfaces, for reference
|
|
pages and the corresponding specification. Similar to COutputGenerator,
|
|
but each interface is written into a different file as determined by the
|
|
options, only actual C types are emitted, and none of the boilerplate
|
|
preprocessor code is emitted."""
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
# Keep track of all extension numbers
|
|
self.extension_numbers = set()
|
|
|
|
def beginFile(self, genOpts):
|
|
OutputGenerator.beginFile(self, genOpts)
|
|
|
|
# This should be a separate conventions property rather than an
|
|
# inferred type name pattern for different APIs.
|
|
self.result_type = genOpts.conventions.type_prefix + "Result"
|
|
|
|
def endFile(self):
|
|
OutputGenerator.endFile(self)
|
|
|
|
def beginFeature(self, interface, emit):
|
|
# Start processing in superclass
|
|
OutputGenerator.beginFeature(self, interface, emit)
|
|
|
|
# Decide if we're in a core <feature> or an <extension>
|
|
self.in_core = (interface.tag == 'feature')
|
|
|
|
# Verify that each <extension> has a unique number during doc
|
|
# generation
|
|
# TODO move this to consistency_tools
|
|
if not self.in_core:
|
|
extension_number = interface.get('number')
|
|
if extension_number is not None and extension_number != "0":
|
|
if extension_number in self.extension_numbers:
|
|
self.logMsg('error', 'Duplicate extension number ', extension_number, ' detected in feature ', interface.get('name'), '\n')
|
|
exit(1)
|
|
else:
|
|
self.extension_numbers.add(extension_number)
|
|
|
|
def endFeature(self):
|
|
# Finish processing in superclass
|
|
OutputGenerator.endFeature(self)
|
|
|
|
def genRequirements(self, name, mustBeFound = True):
|
|
"""Generate text showing what core versions and extensions introduce
|
|
an API. This relies on the map in api.py, which may be loaded at
|
|
runtime into self.apidict. If not present, no message is
|
|
generated.
|
|
|
|
- name - name of the API
|
|
- mustBeFound - If True, when requirements for 'name' cannot be
|
|
determined, a warning comment is generated.
|
|
"""
|
|
|
|
if self.apidict:
|
|
if name in self.apidict.requiredBy:
|
|
# It's possible to get both 'A with B' and 'B with A' for
|
|
# the same API.
|
|
# To simplify this, sort the (base,dependency) requirements
|
|
# and put them in a set to ensure they're unique.
|
|
features = set()
|
|
for (base,dependency) in self.apidict.requiredBy[name]:
|
|
if dependency is not None:
|
|
l = sorted(
|
|
sorted((base, dependency)),
|
|
key=orgLevelKey)
|
|
features.add(' with '.join(l))
|
|
else:
|
|
features.add(base)
|
|
# Sort the overall dependencies so core versions are first
|
|
provider = ', '.join(sorted(features, key=orgLevelKey))
|
|
return f'// Provided by {provider}\n'
|
|
else:
|
|
if mustBeFound:
|
|
self.logMsg('warn', 'genRequirements: API {} not found'.format(name))
|
|
return ''
|
|
else:
|
|
# No API dictionary available, return nothing
|
|
return ''
|
|
|
|
def writeInclude(self, directory, basename, contents):
|
|
"""Generate an include file.
|
|
|
|
- directory - subdirectory to put file in
|
|
- basename - base name of the file
|
|
- contents - contents of the file (Asciidoc boilerplate aside)"""
|
|
# Create subdirectory, if needed
|
|
directory = self.genOpts.directory + '/' + directory
|
|
self.makeDir(directory)
|
|
|
|
# Create file
|
|
filename = directory + '/' + basename + '.txt'
|
|
self.logMsg('diag', '# Generating include file:', filename)
|
|
fp = open(filename, 'w', encoding='utf-8')
|
|
|
|
# Asciidoc anchor
|
|
write(self.genOpts.conventions.warning_comment, file=fp)
|
|
write('[[{0},{0}]]'.format(basename), file=fp)
|
|
|
|
if self.genOpts.conventions.generate_index_terms:
|
|
index_terms = []
|
|
if basename.startswith(self.conventions.command_prefix):
|
|
index_terms.append(basename[2:] + " (function)")
|
|
elif basename.startswith(self.conventions.type_prefix):
|
|
index_terms.append(basename[2:] + " (type)")
|
|
elif basename.startswith(self.conventions.api_prefix):
|
|
index_terms.append(basename[len(self.conventions.api_prefix):] + " (define)")
|
|
index_terms.append(basename)
|
|
write('indexterm:[{}]'.format(','.join(index_terms)), file=fp)
|
|
|
|
write('[source,c++]', file=fp)
|
|
write('----', file=fp)
|
|
write(contents, file=fp)
|
|
write('----', file=fp)
|
|
fp.close()
|
|
|
|
if self.genOpts.secondaryInclude:
|
|
# Create secondary no cross-reference include file
|
|
filename = directory + '/' + basename + '.no-xref.txt'
|
|
self.logMsg('diag', '# Generating include file:', filename)
|
|
fp = open(filename, 'w', encoding='utf-8')
|
|
|
|
# Asciidoc anchor
|
|
write(self.genOpts.conventions.warning_comment, file=fp)
|
|
write('// Include this no-xref version without cross reference id for multiple includes of same file', file=fp)
|
|
write('[source,c++]', file=fp)
|
|
write('----', file=fp)
|
|
write(contents, file=fp)
|
|
write('----', file=fp)
|
|
fp.close()
|
|
|
|
def writeTable(self, basename, values):
|
|
"""Output a table of enumerants."""
|
|
directory = Path(self.genOpts.directory) / 'enums'
|
|
self.makeDir(str(directory))
|
|
|
|
filename = str(directory / '{}.comments.txt'.format(basename))
|
|
self.logMsg('diag', '# Generating include file:', filename)
|
|
|
|
with open(filename, 'w', encoding='utf-8') as fp:
|
|
write(self.conventions.warning_comment, file=fp)
|
|
write(ENUM_TABLE_PREFIX, file=fp)
|
|
|
|
for data in values:
|
|
write("|ename:{}".format(data['name']), file=fp)
|
|
write("|{}".format(data['comment']), file=fp)
|
|
|
|
write(ENUM_TABLE_SUFFIX, file=fp)
|
|
|
|
def writeFlagBox(self, basename, values):
|
|
"""Output a box of flag bit comments."""
|
|
directory = Path(self.genOpts.directory) / 'enums'
|
|
self.makeDir(str(directory))
|
|
|
|
filename = str(directory / '{}.comments.txt'.format(basename))
|
|
self.logMsg('diag', '# Generating include file:', filename)
|
|
|
|
with open(filename, 'w', encoding='utf-8') as fp:
|
|
write(self.conventions.warning_comment, file=fp)
|
|
write(FLAG_BLOCK_PREFIX, file=fp)
|
|
|
|
for data in values:
|
|
write("* ename:{} -- {}".format(data['name'],
|
|
data['comment']),
|
|
file=fp)
|
|
|
|
write(FLAG_BLOCK_SUFFIX, file=fp)
|
|
|
|
def genType(self, typeinfo, name, alias):
|
|
"""Generate type."""
|
|
OutputGenerator.genType(self, typeinfo, name, alias)
|
|
typeElem = typeinfo.elem
|
|
# If the type is a struct type, traverse the embedded <member> tags
|
|
# generating a structure. Otherwise, emit the tag text.
|
|
category = typeElem.get('category')
|
|
|
|
if category in ('struct', 'union'):
|
|
# If the type is a struct type, generate it using the
|
|
# special-purpose generator.
|
|
self.genStruct(typeinfo, name, alias)
|
|
elif category not in OutputGenerator.categoryToPath:
|
|
# If there's no path, don't write output
|
|
self.logMsg('diag', 'NOT writing include for {} category {}'.format(
|
|
name, category))
|
|
else:
|
|
body = self.genRequirements(name)
|
|
if alias:
|
|
# If the type is an alias, just emit a typedef declaration
|
|
body += 'typedef ' + alias + ' ' + name + ';\n'
|
|
self.writeInclude(OutputGenerator.categoryToPath[category],
|
|
name, body)
|
|
else:
|
|
# Replace <apientry /> tags with an APIENTRY-style string
|
|
# (from self.genOpts). Copy other text through unchanged.
|
|
# If the resulting text is an empty string, don't emit it.
|
|
body += noneStr(typeElem.text)
|
|
for elem in typeElem:
|
|
if elem.tag == 'apientry':
|
|
body += self.genOpts.apientry + noneStr(elem.tail)
|
|
else:
|
|
body += noneStr(elem.text) + noneStr(elem.tail)
|
|
|
|
if body:
|
|
self.writeInclude(OutputGenerator.categoryToPath[category],
|
|
name, body + '\n')
|
|
else:
|
|
self.logMsg('diag', 'NOT writing empty include file for type', name)
|
|
|
|
def genStruct(self, typeinfo, typeName, alias):
|
|
"""Generate struct."""
|
|
OutputGenerator.genStruct(self, typeinfo, typeName, alias)
|
|
|
|
typeElem = typeinfo.elem
|
|
|
|
body = self.genRequirements(typeName)
|
|
if alias:
|
|
body += 'typedef ' + alias + ' ' + typeName + ';\n'
|
|
else:
|
|
body += 'typedef ' + typeElem.get('category') + ' ' + typeName + ' {\n'
|
|
|
|
targetLen = self.getMaxCParamTypeLength(typeinfo)
|
|
for member in typeElem.findall('.//member'):
|
|
body += self.makeCParamDecl(member, targetLen + 4)
|
|
body += ';\n'
|
|
body += '} ' + typeName + ';'
|
|
|
|
self.writeInclude('structs', typeName, body)
|
|
|
|
def genEnumTable(self, groupinfo, groupName):
|
|
"""Generate tables of enumerant values and short descriptions from
|
|
the XML."""
|
|
|
|
values = []
|
|
got_comment = False
|
|
missing_comments = []
|
|
for elem in groupinfo.elem.findall('enum'):
|
|
if not elem.get('required'):
|
|
continue
|
|
name = elem.get('name')
|
|
|
|
data = {
|
|
'name': name,
|
|
}
|
|
|
|
(numVal, strVal) = self.enumToValue(elem, True)
|
|
data['value'] = numVal
|
|
|
|
extname = elem.get('extname')
|
|
|
|
added_by_extension_to_core = (extname is not None and self.in_core)
|
|
if added_by_extension_to_core and not self.genOpts.extEnumerantAdditions:
|
|
# We're skipping such values
|
|
continue
|
|
|
|
comment = elem.get('comment')
|
|
if comment:
|
|
got_comment = True
|
|
elif name.endswith('_UNKNOWN') and numVal == 0:
|
|
# This is a placeholder for 0-initialization to be clearly invalid.
|
|
# Just skip this silently
|
|
continue
|
|
else:
|
|
# Skip but record this in case it's an odd-one-out missing a comment.
|
|
missing_comments.append(name)
|
|
continue
|
|
|
|
if added_by_extension_to_core and self.genOpts.extEnumerantFormatString:
|
|
# Add a note to the comment
|
|
comment += self.genOpts.extEnumerantFormatString.format(
|
|
self.conventions.formatExtension(extname))
|
|
|
|
data['comment'] = comment
|
|
values.append(data)
|
|
|
|
if got_comment:
|
|
# If any had a comment, output it.
|
|
|
|
if missing_comments:
|
|
self.logMsg('warn', 'The following values for', groupName,
|
|
'were omitted from the table due to missing comment attributes:',
|
|
', '.join(missing_comments))
|
|
|
|
group_type = groupinfo.elem.get('type')
|
|
if groupName == self.result_type:
|
|
# Split this into success and failure
|
|
self.writeTable(groupName + '.success',
|
|
(data for data in values
|
|
if data['value'] >= 0))
|
|
self.writeTable(groupName + '.error',
|
|
(data for data in values
|
|
if data['value'] < 0))
|
|
elif group_type == 'bitmask':
|
|
self.writeFlagBox(groupName, values)
|
|
elif group_type == 'enum':
|
|
self.writeTable(groupName, values)
|
|
else:
|
|
raise RuntimeError("Unrecognized enums type: " + str(group_type))
|
|
|
|
def genGroup(self, groupinfo, groupName, alias):
|
|
"""Generate group (e.g. C "enum" type)."""
|
|
OutputGenerator.genGroup(self, groupinfo, groupName, alias)
|
|
|
|
body = self.genRequirements(groupName)
|
|
if alias:
|
|
# If the group name is aliased, just emit a typedef declaration
|
|
# for the alias.
|
|
body += 'typedef ' + alias + ' ' + groupName + ';\n'
|
|
else:
|
|
expand = self.genOpts.expandEnumerants
|
|
(_, enumbody) = self.buildEnumCDecl(expand, groupinfo, groupName)
|
|
body += enumbody
|
|
if self.genOpts.conventions.generate_enum_table:
|
|
self.genEnumTable(groupinfo, groupName)
|
|
|
|
self.writeInclude('enums', groupName, body)
|
|
|
|
def genEnum(self, enuminfo, name, alias):
|
|
"""Generate the C declaration for a constant (a single <enum> value)."""
|
|
|
|
OutputGenerator.genEnum(self, enuminfo, name, alias)
|
|
|
|
body = self.buildConstantCDecl(enuminfo, name, alias)
|
|
|
|
self.writeInclude('enums', name, body)
|
|
|
|
def genCmd(self, cmdinfo, name, alias):
|
|
"Generate command."
|
|
OutputGenerator.genCmd(self, cmdinfo, name, alias)
|
|
|
|
return_type = cmdinfo.elem.find('proto/type')
|
|
if self.genOpts.conventions.requires_error_validation(return_type):
|
|
# This command returns an API result code, so check that it
|
|
# returns at least the required errors.
|
|
# TODO move this to consistency_tools
|
|
required_errors = set(self.genOpts.conventions.required_errors)
|
|
errorcodes = cmdinfo.elem.get('errorcodes').split(',')
|
|
if not required_errors.issubset(set(errorcodes)):
|
|
self.logMsg('error', 'Missing required error code for command: ', name, '\n')
|
|
exit(1)
|
|
|
|
body = self.genRequirements(name)
|
|
decls = self.makeCDecls(cmdinfo.elem)
|
|
body += decls[0]
|
|
self.writeInclude('protos', name, body)
|