spec-tool: add python based xml to hpp converter

XSLT1 is too hard to work with, and Khronos have an annoying tendency to
generate their spec such that it's not in dependency order. So we're
switching to python, and performing basic depedency analysis within a
new tool.

This should allow us to do more extensive type modifications and
annotations.
This commit is contained in:
Danny Robson 2017-09-04 15:08:36 +10:00
parent 79752421cb
commit 863cdf4a35
2 changed files with 424 additions and 9 deletions

View File

@ -14,10 +14,7 @@ endif ()
##-----------------------------------------------------------------------------
include (FindLibXslt)
if (NOT LIBXSLT_XSLTPROC_EXECUTABLE)
message (FATAL_ERROR "xsltproc is required to generate the API headers")
endif()
find_package(PythonInterp 3 REQUIRED)
###############################################################################
@ -29,14 +26,14 @@ add_custom_command (
OUTPUT
"${CMAKE_CURRENT_BINARY_DIR}/vk.hpp"
COMMENT
"[xsltproc] vk.hpp"
"[spec.py] vk.hpp"
COMMAND
"${LIBXSLT_XSLTPROC_EXECUTABLE}"
--output "${CMAKE_CURRENT_BINARY_DIR}/vk.hpp"
"${CMAKE_CURRENT_SOURCE_DIR}/vk.xsl"
"${PYTHON_EXECUTABLE}"
"${CMAKE_CURRENT_SOURCE_DIR}/tools/spec.py"
"${CMAKE_CURRENT_SOURCE_DIR}/vk.xml"
"${CMAKE_CURRENT_BINARY_DIR}/vk.hpp"
DEPENDS
"${CMAKE_CURRENT_SOURCE_DIR}/vk.xsl"
"${CMAKE_CURRENT_SOURCE_DIR}/tools/spec.py"
"${CMAKE_CURRENT_SOURCE_DIR}/vk.xml"
)

418
tools/spec.py Executable file
View File

@ -0,0 +1,418 @@
#!/usr/bin/env python3
import logging
import xml.etree.ElementTree as ET
###############################################################################
class type(object):
def __init__(self, name):
self.name = name
def depends(self):
return []
def declare(self):
return ""
def define(self):
return ""
##-----------------------------------------------------------------------------
class basetype(type):
def __init__(self, node):
assert(node.tag == 'type')
assert(node.attrib['category'] == 'basetype')
super().__init__(node.find('name').text)
self._type = node.find('type').text
def depends(self):
return [self._type]
def define(self):
return "using %s = %s;" % (self.name, self._type)
##-----------------------------------------------------------------------------
class handle(type):
def __init__(self, node):
assert (node.tag == "type")
assert (node.attrib['category'] == 'handle')
super().__init__(node.find('name').text)
def declare(self):
return "using %s = uintptr_t;" % self.name
##-----------------------------------------------------------------------------
class constant(type):
def __init__(self,node):
assert(node.tag == 'enum')
super().__init__(node.attrib['name'])
self._value = node.attrib['value']
def define(self):
return "constexpr auto %s = %s;" % (self.name, self._value)
##-----------------------------------------------------------------------------
# a high level 'bitmask' enum that doesn't necessarily define anything.
# inherits the values from something else instead. super fucking weird..
class bitmask(type):
def __init__(self, node):
assert(node.tag == 'type')
assert(node.attrib['category'] == 'bitmask')
super().__init__(node.find('name').text)
self._requires = node.attrib.get('requires', None)
self._type = node.find('type').text
def declare(self):
return ""
def depends(self):
if self._requires:
return [self._type, self._requires]
else:
return [self._type]
def define(self):
return "using %s = %s;" % (self.name, self._type)
##-----------------------------------------------------------------------------
# an enum with bit values that will be used by a bitmask enum. weirdly...
class bitflag(type):
def __init__(self, node):
assert(node.tag == "enums")
assert(node.attrib['type'] == 'bitmask')
name = node.attrib['name']
super ().__init__(name)
self._depends = node.attrib.get('requires', None)
self.values = []
for v in node.findall("./enum"):
if 'value' in v.attrib:
self.values.append({'name': v.attrib['name'], 'value': v.attrib['value']})
elif 'bitpos' in v.attrib:
self.values.append({'name': v.attrib['name'], 'value': '1 << %s' % v.attrib['bitpos']})
else:
assert False, "unhandled bitmask type"
def depends(self):
if self._depends:
return [self._depends]
else:
return []
def declare(self):
return ""
def define(self):
values = map(lambda x: "%(name)s = %(value)s" % x, self.values)
return "enum %s { %s };" % (self.name, ", ".join(values))
##-----------------------------------------------------------------------------
class enum(type):
def __init__(self, node):
assert(node.tag == "enums")
name = node.attrib['name']
super().__init__(name)
self.values = map(
lambda x: {
'name' : x.attrib['name'],
'value': x.attrib['value']
},
node.findall('./enum')
)
def declare(self):
return ""
def define(self):
values = map(lambda x: "%(name)s = %(value)s" % x, self.values)
return "enum %s { %s };" % (
self.name,
", ".join(values)
)
##-----------------------------------------------------------------------------
class funcpointer(type):
def __init__(self, node):
assert(node.tag == 'type')
assert(node.attrib['category'] == "funcpointer")
name = node.find('name').text
super().__init__(name)
self.params = list(map(lambda x: x.text, node.findall('./type')))
self.text = "".join(node.itertext())
def depends(self):
return ['VkBool32'] + self.params
def declare(self):
return self.text
def define(self):
return "";
##-----------------------------------------------------------------------------
class pod(type):
def __init__(self,node):
assert(node.tag == 'type')
assert(node.attrib['category'] in ['struct', 'union'])
super().__init__(node.attrib['name'])
self._category = node.attrib['category']
# sometimes there are enums hiding in the member fields being used as array sizes
self._depends = []
self._depends += list(e.text for e in node.findall('.//enum'))
self._depends += list(t.text for t in node.findall('.//type'))
self._members = list(map(
lambda x: {
'type': x.find('type').text,
'name': x.find('name').text,
# we must include a space separator otherwise we get run-on
# types/names in the member definitions.
'code': " ".join(x.itertext())
},
node.findall('./member'))
)
def depends(self):
return self._depends
def declare(self):
return "%s %s;" % (self._category, self.name)
def define(self):
return "%(category)s %(name)s { %(members)s };" % {
'category': self._category,
'name': self.name,
'members': "\n".join(m['code'] + ';' for m in self._members)
}
##-----------------------------------------------------------------------------
class struct(pod):
def __init__(self,node):
super().__init__(node)
##-----------------------------------------------------------------------------
class union(pod):
def __init__(self,node):
super().__init__(node)
###############################################################################
def parse_types(nodes):
types = []
for n in nodes:
# we need to parse too much unstructured text to actually do anything
# useful with these declarations
for t in n.findall("./type[@category='handle']"):
types.append(handle(t))
for t in n.findall('./type[@category="bitmask"]'):
types.append(bitmask(t))
for t in n.findall('./type[@category="basetype"]'):
types.append(basetype(t))
for t in n.findall("./type[@category='funcpointer']"):
types.append(funcpointer(t))
for t in n.findall("./type[@category='union']"):
types.append(union(t))
for t in n.findall("./type[@category='struct']"):
types.append(struct(t))
return types
###############################################################################
def parse_enums(nodes):
enums = []
for n in nodes:
if n.attrib['name'] == "API Constants":
for c in n.findall('./enum'):
enums.append(constant(c))
elif n.attrib['type'] == 'bitmask':
enums.append(bitflag(n))
elif n.attrib['type'] == 'enum':
enums.append(enum(n))
else:
assert False, "unhandled enum type"
return enums
###############################################################################
class command(type):
def __init__(self, node):
assert(node.tag == "command")
proto = node.find('proto')
super().__init__(proto.find('name').text)
self._result = proto.find('type').text
self._params = []
self._depends = []
for p in node.findall('./param'):
self._depends.append(p.find('type').text)
self._params.append("".join(p.itertext()))
def depends(self):
return [self._result] + self._depends
def declare(self):
return "%(result)s %(name)s (%(params)s) noexcept;" % {
'name': self.name,
'result': self._result,
'params': ", ".join(self._params)
}
##-----------------------------------------------------------------------------
def parse_commands(nodes):
commands = []
for n in nodes:
for c in n.findall('./command'):
commands.append(command(c))
return commands
###############################################################################
def write_type(dst, types, t):
logging.info("writing: %s", t.name)
for d in t.depends():
if d in types:
write_type(dst, types, types[d])
if t.name in types:
dst.write(t.declare())
dst.write(t.define())
dst.write('\n')
del types[t.name]
##-----------------------------------------------------------------------------
def write_types(dst, types):
keys = list(types.keys())
for k in keys:
if k in types:
write_type(dst, types, types[k])
##-----------------------------------------------------------------------------
def write_root(dst, node):
dst.write ("""
#ifndef __VK_HPP
#define __VK_HPP
#include <cstdint>
#include <cstddef>
#if defined(__cplusplus)
extern "C" {
#endif
struct Display;
struct VisualID;
using Window = unsigned long;
struct ANativeWindow;
struct MirConnection;
struct MirSurface;
struct wl_display;
struct wl_surface;
using HANDLE = void*;
using HINSTANCE = HANDLE;
using HWND = HANDLE;
using SECURITY_ATTRIBUTES = HANDLE;
using DWORD = uint32_t;
using LPCWSTR = char16_t*;
struct xcb_connection_t;
struct xcb_visualid_t;
using xcb_window_t = uint32_t;
using XID = unsigned long;
using RROutput = XID;
#define VKAPI_PTR /*VKAPI_PTR*/
// because, in its wisdom, the spec doesn't actually allow us to
// extract the value for VK_NULL_HANDLE from the XML we'll just
// hard code it here.
#define VK_NULL_HANDLE uintptr_t(0)
""")
types = []
types += parse_types(node.findall('./types'))
types += parse_enums(node.findall('./enums'))
types += parse_commands(node.findall('./commands'))
types = dict((t.name,t) for t in types)
write_types(dst, types)
#dst.writelines("\n".join(map(lambda x: x.declare(), types)))
#dst.writelines("\n".join(map(lambda x: x.define(), types)))
#dst.writelines("\n".join(map(lambda x: x.declare(), commands)))
dst.write ("""
#if defined(__cplusplus)
}
#endif
#endif
""")
###############################################################################
import argparse
##-----------------------------------------------------------------------------
if __name__ == '__main__':
#logging.getLogger().setLevel(logging.INFO)
parser = argparse.ArgumentParser(description='Transform XML API specification into C++ headers')
parser.add_argument('src', type=str, help='the path to the XML file to transform')
parser.add_argument('dst', type=str, help='the output path for the result')
args = parser.parse_args()
src = open(args.src, 'r')
dst = open(args.dst, 'w')
tree = ET.parse(src)
root = tree.getroot()
write_root(dst, root)