496 lines
22 KiB
Python
496 lines
22 KiB
Python
#!/usr/bin/python
|
|
# -*- coding: utf-8 -*-
|
|
import sys
|
|
import os.path
|
|
import warnings
|
|
import re
|
|
from pygccxml import parser
|
|
from pygccxml import declarations
|
|
from module import Module
|
|
from typehandlers.codesink import FileCodeSink
|
|
from typehandlers.base import ReturnValue, Parameter
|
|
from enum import Enum
|
|
from function import Function
|
|
from cppclass import CppClass, CppConstructor, CppMethod
|
|
from pygccxml.declarations import type_traits
|
|
from pygccxml.declarations import cpptypes
|
|
from pygccxml.declarations import calldef
|
|
|
|
#from pygccxml.declarations.calldef import \
|
|
# destructor_t, constructor_t, member_function_t
|
|
from pygccxml.declarations.variable import variable_t
|
|
|
|
__all__ = ['ModuleScanner']
|
|
|
|
## ------------------------
|
|
|
|
class GccXmlTypeRegistry(object):
|
|
def __init__(self):
|
|
self.classes = {} # value is a (return_handler, parameter_handler) tuple
|
|
|
|
def register_class(self, cpp_class):
|
|
assert isinstance(cpp_class, CppClass)
|
|
if cpp_class.full_name.startswith('::'):
|
|
full_name = cpp_class.full_name
|
|
else:
|
|
full_name = '::' + cpp_class.full_name
|
|
self.classes[full_name] = cpp_class
|
|
|
|
def _get_class_type_traits(self, type_info):
|
|
assert isinstance(type_info, cpptypes.type_t)
|
|
|
|
decomposed = type_traits.decompose_type(type_info)
|
|
base_type = decomposed.pop()
|
|
is_const = False
|
|
is_reference = False
|
|
is_pointer = False
|
|
pointer_is_const = False
|
|
if isinstance(base_type, cpptypes.declarated_t):
|
|
try:
|
|
cpp_class = self.classes[base_type.decl_string]
|
|
except KeyError:
|
|
return (None, is_const, is_pointer, is_reference, pointer_is_const)
|
|
|
|
try:
|
|
type_tmp = decomposed.pop()
|
|
except IndexError:
|
|
return (cpp_class, is_const, is_pointer, is_reference, pointer_is_const)
|
|
|
|
if isinstance(type_tmp, cpptypes.const_t):
|
|
is_const = True
|
|
try:
|
|
type_tmp = decomposed.pop()
|
|
except IndexError:
|
|
return (cpp_class, is_const, is_pointer, is_reference, pointer_is_const)
|
|
|
|
if isinstance(type_tmp, cpptypes.reference_t):
|
|
is_reference = True
|
|
elif isinstance(type_tmp, cpptypes.pointer_t):
|
|
is_pointer = True
|
|
else:
|
|
raise AssertionError
|
|
|
|
try:
|
|
type_tmp = decomposed.pop()
|
|
except IndexError:
|
|
pass
|
|
else:
|
|
if isinstance(type_tmp, cpptypes.const_t):
|
|
assert is_pointer
|
|
pointer_is_const = True
|
|
else:
|
|
raise AssertionError
|
|
assert len(decomposed) == 0
|
|
return (cpp_class, is_const, is_pointer, is_reference, pointer_is_const)
|
|
return (None, is_const, is_pointer, is_reference, pointer_is_const)
|
|
|
|
def lookup_return(self, type_info, annotations={}):
|
|
assert isinstance(type_info, cpptypes.type_t)
|
|
cpp_class, is_const, is_pointer, is_reference, pointer_is_const = \
|
|
self._get_class_type_traits(type_info)
|
|
|
|
|
|
kwargs = {}
|
|
for name, value in annotations.iteritems():
|
|
if name == 'caller_owns_return':
|
|
kwargs['caller_owns_return'] = annotations_scanner.parse_boolean(value)
|
|
else:
|
|
warnings.warn("invalid annotation name %r" % name)
|
|
|
|
if cpp_class is None:
|
|
return ReturnValue.new(type_info.decl_string, **kwargs)
|
|
|
|
if not is_pointer and not is_reference:
|
|
return cpp_class.ThisClassReturn(type_info.decl_string)
|
|
if is_pointer and not is_reference:
|
|
if is_const:
|
|
## a pointer to const object usually means caller_owns_return=False
|
|
return cpp_class.ThisClassPtrReturn(type_info.decl_string, caller_owns_return=False,
|
|
**kwargs)
|
|
else:
|
|
## This will fail, "missing caller_owns_return
|
|
## parameter", but the lack of a const does not always
|
|
## imply caller_owns_return=True, so trying to guess
|
|
## here is a Bad Idea™
|
|
return cpp_class.ThisClassPtrReturn(type_info.decl_string, **kwargs)
|
|
if not is_pointer and is_reference:
|
|
return cpp_class.ThisClassRefReturn(type_info.decl_string, **kwargs)
|
|
assert 0, "this line should not be reached"
|
|
|
|
def lookup_parameter(self, type_info, param_name, annotations={}):
|
|
assert isinstance(type_info, cpptypes.type_t)
|
|
|
|
kwargs = {}
|
|
for name, value in annotations.iteritems():
|
|
if name == 'transfer_ownership':
|
|
kwargs['transfer_ownership'] = annotations_scanner.parse_boolean(value)
|
|
elif name == 'direction':
|
|
if value.lower() == 'in':
|
|
kwargs['direction'] = Parameter.DIRECTION_IN
|
|
elif value.lower() == 'out':
|
|
kwargs['direction'] = Parameter.DIRECTION_OUT
|
|
elif value.lower() == 'inout':
|
|
kwargs['direction'] = Parameter.DIRECTION_INOUT
|
|
else:
|
|
warnings.warn("invalid direction direction %r" % value)
|
|
else:
|
|
warnings.warn("invalid annotation name %r" % name)
|
|
|
|
cpp_class, is_const, is_pointer, is_reference, pointer_is_const = \
|
|
self._get_class_type_traits(type_info)
|
|
if cpp_class is None:
|
|
return Parameter.new(type_info.decl_string, param_name, **kwargs)
|
|
if not is_pointer and not is_reference:
|
|
return cpp_class.ThisClassParameter(type_info.decl_string, param_name, **kwargs)
|
|
if is_pointer and not is_reference:
|
|
if is_const:
|
|
## a pointer to const object usually means transfer_ownership=False
|
|
kwargs.setdefault('transfer_ownership', False)
|
|
return cpp_class.ThisClassPtrParameter(type_info.decl_string, param_name,
|
|
**kwargs)
|
|
else:
|
|
## This will fail, "missing param_name
|
|
## parameter", but the lack of a const does not always
|
|
## imply transfer_ownership=True, so trying to guess
|
|
## here is a Bad Idea™
|
|
return cpp_class.ThisClassPtrParameter(type_info.decl_string, param_name, **kwargs)
|
|
if not is_pointer and is_reference:
|
|
return cpp_class.ThisClassRefParameter(type_info.decl_string, param_name, **kwargs)
|
|
assert 0, "this line should not be reached"
|
|
|
|
type_registry = GccXmlTypeRegistry()
|
|
|
|
|
|
class AnnotationsScanner(object):
|
|
def __init__(self):
|
|
self.files = {} # file name -> list(lines)
|
|
self._comment_rx = re.compile(r"^\s*//\s+-\*-(.*)-\*-\s*")
|
|
self._global_annotation_rx = re.compile(r"(\w+)=([^\s;]+)")
|
|
self._param_annotation_rx = re.compile(r"@(\w+)\(([^;]+)\)")
|
|
|
|
def get_annotations(self, file_name, line_number):
|
|
"""
|
|
file_name -- absolute file name where the definition is
|
|
line_number -- line number of where the definition is within the file
|
|
"""
|
|
try:
|
|
lines = self.files[file_name]
|
|
except KeyError:
|
|
lines = file(file_name, "rt").readlines()
|
|
self.files[file_name] = lines
|
|
|
|
line_number -= 2
|
|
global_annotations = {}
|
|
parameter_annotations = {}
|
|
while 1:
|
|
line = lines[line_number]
|
|
line_number -= 1
|
|
m = self._comment_rx.match(line)
|
|
if m is None:
|
|
break
|
|
line = m.group(1).strip()
|
|
for annotation_str in line.split(';'):
|
|
annotation_str = annotation_str.strip()
|
|
m = self._global_annotation_rx.match(annotation_str)
|
|
if m is not None:
|
|
global_annotations[m.group(1)] = m.group(2)
|
|
continue
|
|
|
|
m = self._param_annotation_rx.match(annotation_str)
|
|
if m is not None:
|
|
param_annotation = {}
|
|
parameter_annotations[m.group(1)] = param_annotation
|
|
for param in m.group(2).split(','):
|
|
m = self._global_annotation_rx.match(param.strip())
|
|
if m is not None:
|
|
param_annotation[m.group(1)] = m.group(2)
|
|
else:
|
|
warnings.warn("%s:%i: could not parse %r as parameter annotation element" %
|
|
(file_name, line_number, param.strip()))
|
|
continue
|
|
warnings.warn("%s:%i: could not parse %r" %
|
|
(file_name, line_number, annotation_str))
|
|
return global_annotations, parameter_annotations
|
|
|
|
def parse_boolean(self, value):
|
|
if value.lower() in ['false', 'off']:
|
|
return False
|
|
elif value.lower() in ['true', 'on']:
|
|
return True
|
|
else:
|
|
warnings.warn("bad boolean value %r" % value)
|
|
|
|
|
|
annotations_scanner = AnnotationsScanner()
|
|
|
|
## ------------------------
|
|
|
|
|
|
class ModuleParser(object):
|
|
def __init__(self, module_name, module_namespace_name='::'):
|
|
"""
|
|
Creates an object that will be able parse header files and
|
|
create a pybindgen module definition.
|
|
|
|
module_name -- name of the Python module
|
|
module_namespace_name -- optional C++ namespace name; if
|
|
given, only definitions of this
|
|
namespace will be included in the
|
|
python module
|
|
"""
|
|
self.module_name = module_name
|
|
self.module_namespace_name = module_namespace_name
|
|
self.location_filter = None
|
|
self.header_files = None
|
|
|
|
def __location_match(self, decl):
|
|
return (decl.location.file_name in self.header_files)
|
|
|
|
def parse(self, header_files):
|
|
"""
|
|
parses a set of header files and returns a pybindgen Module instance.
|
|
"""
|
|
assert isinstance(header_files, list)
|
|
self.header_files = [os.path.abspath(f) for f in header_files]
|
|
self.location_filter = declarations.custom_matcher_t(self.__location_match)
|
|
|
|
config = parser.config_t()
|
|
decls = parser.parse(header_files, config)
|
|
if self.module_namespace_name == '::':
|
|
module_namespace = declarations.get_global_namespace(decls)
|
|
else:
|
|
module_namespace = declarations.get_global_namespace(decls).namespace(self.module_namespace_name)
|
|
module = Module(self.module_name, cpp_namespace=module_namespace.decl_string)
|
|
self._scan_namespace_types(module, module_namespace)
|
|
self._scan_namespace_functions(module, module_namespace)
|
|
|
|
return module
|
|
|
|
def _scan_namespace_types(self, module, module_namespace):
|
|
## scan enumerations
|
|
for enum in module_namespace.enums(function=self.location_filter, recursive=False, allow_empty=True):
|
|
if enum.name.startswith('__'):
|
|
continue
|
|
module.add_enum(Enum(enum.name, [name for name, dummy_val in enum.values]))
|
|
|
|
## scan classes
|
|
unregistered_classes = [cls for cls in
|
|
module_namespace.classes(function=self.location_filter,
|
|
recursive=False, allow_empty=True)
|
|
if not cls.name.startswith('__')]
|
|
registered_classes = {} # class_t -> CppClass
|
|
while unregistered_classes:
|
|
cls = unregistered_classes.pop(0)
|
|
if '<' in cls.name:
|
|
warnings.warn("Class %s ignored because it is templated; templates not yet supported"
|
|
% cls.decl_string)
|
|
continue
|
|
|
|
if len(cls.bases) > 1:
|
|
warnings.warn("Class %s ignored because it uses multiple "
|
|
"inheritance (not yet supported by pybindgen)"
|
|
% cls.decl_string)
|
|
continue
|
|
if cls.bases:
|
|
base_cls = cls.bases[0].related_class
|
|
try:
|
|
base_class_wrapper = registered_classes[base_cls]
|
|
except KeyError:
|
|
## base class not yet registered => postpone this class registration
|
|
if base_cls not in unregistered_classes:
|
|
warnings.warn("Class %s ignored because it uses has a base class (%s) "
|
|
"which is not declared."
|
|
% (cls.decl_string, base_cls.decl_string))
|
|
continue
|
|
unregistered_classes.append(cls)
|
|
continue
|
|
else:
|
|
base_class_wrapper = None
|
|
|
|
kwargs = {}
|
|
|
|
global_annotations, dummy_param_annotations = \
|
|
annotations_scanner.get_annotations(cls.location.file_name,
|
|
cls.location.line)
|
|
for name, value in global_annotations.iteritems():
|
|
if name == 'allow_subclassing':
|
|
kwargs.setdefault('allow_subclassing', annotations_scanner.parse_boolean(value))
|
|
elif name == 'is_singleton':
|
|
kwargs.setdefault('is_singleton', annotations_scanner.parse_boolean(value))
|
|
elif name == 'incref_method':
|
|
kwargs.setdefault('incref_method', value)
|
|
elif name == 'decref_method':
|
|
kwargs.setdefault('decref_method', value)
|
|
else:
|
|
warnings.warn("Class annotation %r ignored" % name)
|
|
|
|
if self._class_has_virtual_methods(cls):
|
|
kwargs.setdefault('allow_subclassing', True)
|
|
|
|
if not self._class_has_public_destructor(cls):
|
|
kwargs.setdefault('is_singleton', True)
|
|
|
|
|
|
class_wrapper = CppClass(cls.name, parent=base_class_wrapper, **kwargs)
|
|
module.add_class(class_wrapper)
|
|
registered_classes[cls] = class_wrapper
|
|
type_registry.register_class(class_wrapper)
|
|
assert cls.decl_string in type_registry.classes\
|
|
and type_registry.classes[cls.decl_string] == class_wrapper
|
|
|
|
for cls, class_wrapper in registered_classes.iteritems():
|
|
self._scan_methods(cls, class_wrapper)
|
|
|
|
## scan nested namespaces (mapped as python submodules)
|
|
for nested_namespace in module_namespace.namespaces(allow_empty=True, recursive=False):
|
|
if nested_namespace.name.startswith('__'):
|
|
continue
|
|
nested_module = Module(name=nested_namespace.name, parent=module, cpp_namespace=nested_namespace.name)
|
|
self._scan_namespace_types(nested_module, nested_namespace)
|
|
|
|
def _class_has_virtual_methods(self, cls):
|
|
"""return True if cls has at least one virtual method, else False"""
|
|
for member in cls.get_members('public'):
|
|
if isinstance(member, calldef.member_function_t):
|
|
if member.virtuality != calldef.VIRTUALITY_TYPES.NOT_VIRTUAL:
|
|
return True
|
|
return False
|
|
|
|
def _class_has_public_destructor(self, cls):
|
|
"""return True if cls has a public destructor, else False"""
|
|
for member in cls.get_members('public'):
|
|
if isinstance(member, calldef.destructor_t):
|
|
return True
|
|
return False
|
|
|
|
def _scan_methods(self, cls, class_wrapper):
|
|
for member in cls.get_members('public'):
|
|
|
|
dummy_global_annotations, parameter_annotations = \
|
|
annotations_scanner.get_annotations(member.location.file_name,
|
|
member.location.line)
|
|
|
|
if isinstance(member, calldef.member_function_t):
|
|
is_virtual = (member.virtuality != calldef.VIRTUALITY_TYPES.NOT_VIRTUAL)
|
|
pure_virtual = (member.virtuality == calldef.VIRTUALITY_TYPES.PURE_VIRTUAL)
|
|
|
|
try:
|
|
return_type = type_registry.lookup_return(member.return_type,
|
|
parameter_annotations.get('return', {}))
|
|
except (TypeError, KeyError), ex:
|
|
warnings.warn("Return value '%s' error (used in %s): %r"
|
|
% (member.return_type.decl_string, member, ex))
|
|
if pure_virtual:
|
|
class_wrapper.set_cannot_be_constructed(True)
|
|
continue
|
|
arguments = []
|
|
ok = True
|
|
for arg in member.arguments:
|
|
try:
|
|
arguments.append(type_registry.lookup_parameter(arg.type, arg.name,
|
|
parameter_annotations.get(arg.name, {})))
|
|
except (TypeError, KeyError), ex:
|
|
warnings.warn("Parameter '%s %s' error (used in %s): %r"
|
|
% (arg.type.decl_string, arg.name, member, ex))
|
|
ok = False
|
|
if not ok:
|
|
if pure_virtual:
|
|
class_wrapper.set_cannot_be_constructed(True)
|
|
continue
|
|
|
|
if pure_virtual and not class_wrapper.allow_subclassing:
|
|
class_wrapper.set_cannot_be_constructed(True)
|
|
|
|
method_wrapper = CppMethod(return_type, member.name, arguments,
|
|
is_const=member.has_const,
|
|
is_static=member.has_static,
|
|
is_virtual=(is_virtual and class_wrapper.allow_subclassing))
|
|
class_wrapper.add_method(method_wrapper)
|
|
|
|
elif isinstance(member, calldef.constructor_t):
|
|
arguments = []
|
|
for arg in member.arguments:
|
|
try:
|
|
arguments.append(type_registry.lookup_parameter(arg.type, arg.name))
|
|
except (TypeError, KeyError), ex:
|
|
warnings.warn("Parameter '%s %s' error (used in %s): %r"
|
|
% (arg.type.decl_string, arg.name, member, ex))
|
|
ok = False
|
|
break
|
|
else:
|
|
ok = True
|
|
if not ok:
|
|
continue
|
|
constructor_wrapper = CppConstructor(arguments)
|
|
class_wrapper.add_constructor(constructor_wrapper)
|
|
|
|
elif isinstance(member, variable_t):
|
|
try:
|
|
return_type = type_registry.lookup_return(member.type)
|
|
except (TypeError, KeyError), ex:
|
|
warnings.warn("Return value '%s' error (used in %s): %r"
|
|
% (member.type.decl_string, member, ex))
|
|
continue
|
|
if member.type_qualifiers.has_static:
|
|
class_wrapper.add_static_attribute(return_type, member.name)
|
|
else:
|
|
class_wrapper.add_instance_attribute(return_type, member.name)
|
|
|
|
elif isinstance(member, calldef.destructor_t):
|
|
pass
|
|
|
|
|
|
def _scan_namespace_functions(self, module, module_namespace):
|
|
for fun in module_namespace.free_functions(function=self.location_filter,
|
|
allow_empty=True, recursive=False):
|
|
if fun.name.startswith('__'):
|
|
continue
|
|
|
|
dummy_global_annotations, parameter_annotations = \
|
|
annotations_scanner.get_annotations(fun.location.file_name,
|
|
fun.location.line)
|
|
|
|
try:
|
|
return_type = type_registry.lookup_return(fun.return_type, parameter_annotations.get('return', {}))
|
|
except (TypeError, KeyError), ex:
|
|
warnings.warn("Return value '%s' error (used in %s): %r"
|
|
% (fun.return_type.decl_string, fun, ex))
|
|
continue
|
|
arguments = []
|
|
for arg in fun.arguments:
|
|
try:
|
|
arguments.append(type_registry.lookup_parameter(arg.type, arg.name,
|
|
parameter_annotations.get(arg.name, {})))
|
|
except (TypeError, KeyError), ex:
|
|
warnings.warn("Parameter '%s %s' error (used in %s): %r"
|
|
% (arg.type.decl_string, arg.name, fun, ex))
|
|
ok = False
|
|
break
|
|
else:
|
|
ok = True
|
|
if not ok:
|
|
continue
|
|
|
|
func_wrapper = Function(return_type, fun.name, arguments)
|
|
module.add_function(func_wrapper)
|
|
|
|
## scan nested namespaces (mapped as python submodules)
|
|
for nested_namespace in module_namespace.namespaces(allow_empty=True, recursive=False):
|
|
if nested_namespace.name.startswith('__'):
|
|
continue
|
|
nested_module = Module(name=nested_namespace.name, parent=module, cpp_namespace=nested_namespace.name)
|
|
self._scan_namespace_functions(nested_module, nested_namespace)
|
|
|
|
|
|
def _test():
|
|
module_parser = ModuleParser('foo', '::')
|
|
module = module_parser.parse(sys.argv[1:])
|
|
if 0:
|
|
out = FileCodeSink(sys.stdout)
|
|
import utils
|
|
utils.write_preamble(out)
|
|
module.generate(out)
|
|
|
|
if __name__ == '__main__':
|
|
_test()
|