def rename(self, n): new_name = self.rename_types.get(n, None) if new_name: arg_type_order = self.wrapper_order.get(new_name, None) class_order = self.wrapper_order.get(self.current_class_name, None) # arg type is defined after current class if arg_type_order and class_order and arg_type_order > class_order: return T.Constant(new_name) if new_name == self.current_class_name: return T.Constant(new_name) return T.Name(new_name) return n
def parse_int(self, val): if val.startswith('0x'): return T.Constant(int(val, base=16)) try: return T.Constant(int(val)) except ValueError: pass try: return T.Constant(float(val)) except ValueError: pass return None
def generate_function(self, elem: Cursor, depth=0, **kwargs): """Generate a type alias Examples -------- >>> from tide.generators.clang_utils import parse_clang >>> tu, index = parse_clang('float add(float a, float b);') >>> module = BindingGenerator().generate(tu) >>> print(compact(unparse(module))) <BLANKLINE> add = _bind('add', [c_float, c_float], c_float, arg_names=['a', 'b']) <BLANKLINE> """ log.debug(f'{d(depth)}Generate function `{elem.spelling}`') definition: Cursor = elem.get_definition() if definition is None: definition = elem rtype = self.generate_type(definition.result_type, depth + 1) args = definition.get_arguments() docstring = get_comment(elem) pyargs = [] arg_names = [] for a in args: atype = self.generate_type(a.type, depth + 1) pyargs.append(atype) if a.spelling != '': arg_names.append(T.Constant(a.spelling)) funnane = definition.spelling if not pyargs: pyargs = T.Name('None') else: pyargs = T.List(elts=pyargs) kwargs = [] if len(docstring) > 0: kwargs.append( T.Keyword('docstring', T.Constant(docstring, docstring=True))) if arg_names: kwargs.append(T.Keyword('arg_names', T.List(arg_names))) binding_call = T.Call(T.Name('_bind'), [T.Constant(funnane), pyargs, rtype], kwargs) return T.Assign([T.Name(funnane)], binding_call)
def class_definition(self, class_def: T.ClassDef, depth): assert class_def.name in self.ctypes # Opaque struct: move on # if class_def.name[0] == '_': # return class_def self_wrap = self.wrappers.get(class_def.name) if self_wrap is None: if T.Name('enumeration') in class_def.decorator_list: return self.clean_up_enumeration(class_def) _, names = parse_sdl_name(class_def.name) cl_name = class_name(*names) self_wrap = T.ClassDef(cl_name) self.new_code.append((class_def.name, self_wrap)) self.wrappers[class_def.name] = self_wrap self.wrappers_2_ctypes[self_wrap.name] = class_def.name self.wrapper_order[self_wrap.name] = len(self.wrappers) self_type = T.Call(T.Name('POINTER'), [T.Name(class_def.name)]) self_wrap.body.append( T.AnnAssign(T.Name('handle'), self_type, T.Name('None'))) # Factory to build the wrapper form the ctype # create an uninitialized version of the object and set the handle from_ctype = T.FunctionDef('from_handle', decorator_list=[T.Name('staticmethod')]) from_ctype.args = T.Arguments(args=[T.Arg('a', self_type)]) from_ctype.returns = T.Constant(cl_name) from_ctype.body = [ T.Assign([T.Name('b')], T.Call(T.Attribute(T.Name('object'), '__new__'), [T.Name(cl_name)])), T.Assign([T.Attribute(T.Name('b'), 'handle')], T.Name('a')), T.Return(T.Name('b')) ] self_wrap.body.append(from_ctype) if not self.is_opaque_container(class_def.name): # default init allocate a dummy object for it default_init = T.FunctionDef('__init__') default_init.args = T.Arguments(args=[T.Arg('self')]) default_init.body = [ T.Assign([T.Attribute(T.Name('self'), '_value')], T.Call(T.Name(class_def.name), [])), T.Assign([T.Attribute(T.Name('self'), 'handle')], T.Call(T.Name('byref'), [T.Attribute(T.Name('self'), '_value')])) ] self_wrap.body.append(default_init) self.rename_types[T.Call(T.Name('POINTER'), [T.Name(class_def.name)])] = cl_name return class_def
def generate_integer(self, elem: Cursor, **kwargs): try: val = list(elem.get_tokens())[0].spelling val = val.replace('u', '') val = val.replace('ll', '') val = val.replace('U', '') val = val.replace('L', '') base = 10 if '0x' in val: base = 16 return T.Constant(int(val, base=base)) except IndexError: # this integer comes from __LINE__ macro # this is not correct at all, this should return the line on which it is called # but we cannot really do that in python I think return T.Constant(int(elem.location.line))
def parse_expression(self, depth=0): tok = self.peek() log.debug(f'{d(depth)} parse_expression {tok.kind} {tok.spelling}') if tok.spelling == '\\\n{': body = [] self.next() while tok.spelling != '\\\n}': p = T.Expr(self.parse_expression(depth + 1)) body.append(p) tok = self.peek() if tok.spelling == ';': self.next() tok = self.peek() self.next() return T.If(T.Constant(True), body) if is_operator(tok.spelling) and is_unary_operator(tok.spelling): p = self.parse_unary(depth + 1) else: p = self.parse_primary(depth + 1) tok = self.peek() if tok is None: return p # we are doing a cast or call (type(expr)) if tok.spelling == '(': self.next() expr = self.parse_call(p, depth + 1) if self.peek().spelling == ')': self.next() return expr # argument list do not try to parse operators if tok.spelling == ',' and self.is_call[-1]: return p if tok.spelling == ')': return p # cast (expr) <expr> if not is_operator(tok.spelling): return self.parse_cast(p, depth + 1) # if tok.spelling == ')': # return p return self.parse_expression_1(p, 0, depth + 1)
def process_builtin_macros(self, cursor: Cursor): tokens = list(cursor.get_tokens()) name, tok_args, tok_body = parse_macro(tokens) if len(tok_body) == 0: self.definitions[name.spelling] = T.Constant(True) return try: bods = {t.spelling for t in tok_body} if not bods.isdisjoint(self.unsupported_macros): raise UnsupportedExpression() py_body = parse_macro2(name, tok_args, tok_body) except UnsupportedExpression: self.unsupported_macros.add(name.spelling) self.definitions[name.spelling] = py_body
def parse_literal(self, tok: Token, depth): log.debug(f'{d(depth)} parse_literal {tok.kind} {tok.spelling}') val = None # 3243212132u # 0x13223u l = self.is_int_annotated(tok.spelling) if l > 0: val = self.parse_int(tok.spelling[:-l]) # 1232133 | 123.123 if val is None: val = self.parse_int(tok.spelling) # the rest if val is None: val = T.Constant(str(tok.spelling)) return val
def generate_field(self, body, attrs, attr, anonymous_renamed, depth, **kwargs): if attr.kind == CursorKind.FIELD_DECL: # Rename anonymous types uid = self.get_underlying_type_uid(attr.type) log.debug( f'{d(depth)}uid: {uid} {attr.type.spelling} {anonymous_renamed}' ) if uid in anonymous_renamed: parent, name = anonymous_renamed[uid] if parent: typename = T.Attribute(T.Name(parent), name) else: typename = T.Name(name) if attr.type.kind == TypeKind.POINTER: typename = T.Call(T.Name('POINTER'), [typename]) else: typename = self.generate_type(attr.type, depth + 1) pair = T.Tuple() pair.elts = [T.Constant(attr.spelling), typename] attrs.elts.append(pair) elif attr.kind in (CursorKind.UNION_DECL, CursorKind.STRUCT_DECL): nested_struct = self.generate_struct_union( attr, depth + 1, nested=True, rename=anonymous_renamed) body.append(nested_struct) elif attr.kind == CursorKind.PACKED_ATTR: for attr2 in attr.get_children(): self.generate_field(body, attrs, attr2, anonymous_renamed, depth + 1) # attrs.append(field) else: show_elem(attr) print('NESTED ', attr.kind) raise RuntimeError('')
def generate_string(self, elem, **kwargs): return T.Constant(elem.spelling)
def generate_enum(self, elem: Cursor, depth=0, **kwargs): """ Generate a enum class Notes ----- we cannot use enum.Enum because we need the class to be a ctype/c_int when they are used a arguments Examples -------- >>> from tide.generators.clang_utils import parse_clang >>> tu, index = parse_clang('enum Colors { Red, Green, Blue;};') >>> module = BindingGenerator().generate(tu) >>> print(compact(unparse(module))) <BLANKLINE> @enumeration class Colors(c_int): Red = 0 Green = 1 Blue = 2 <BLANKLINE> """ log.debug(f'{d(depth)}Generate Enum') name = self.get_name(elem) enum = [] ctypes = self.type_registry.get('int') common = self.find_duplicate_name(elem) if name == '': name = common if name[-1] == '_': name = name[:-1] if self.global_enum: enum.append(T.Assign([T.Name(name)], self.type_registry.get('int'))) def shorten_name(n): if len(n) == len(common): return n if self.short_enum_names: new_name = n[len(common):] if not new_name[0].isalpha(): return n # rename SDL_PIXELTYPE_UNKNOWN to SDL_PIXELTYPE.UNKNOWN # useful to get editor auto-completion to show relevant values self.renaming[n] = T.Attribute(T.Name(name), new_name) return new_name return n for value in elem.get_children(): if value.kind == CursorKind.ENUM_CONSTANT_DECL: n = shorten_name(self.get_name(value)) enum.append(T.Assign([T.Name(n)], T.Constant(value.enum_value))) else: log.error(f'Unexpected children {value.kind}') raise RuntimeError() if not self.global_enum: # this require renaming the code where the enum were used # not implemented yet self.import_enum = True enum_class = T.ClassDef(name, bases=[ctypes]) enum_class.body = enum enum_class.decorator_list.append(T.Name('enumeration')) return enum_class return enum
def generate_struct_union(self, elem: Cursor, depth=1, nested=False, rename=None, **kwargs): """Generate a struct or union alias Examples -------- >>> from tide.generators.clang_utils import parse_clang >>> tu, index = parse_clang('struct Point { float x, y;};') >>> module = BindingGenerator().generate(tu) >>> print(compact(unparse(module))) <BLANKLINE> class Point(Structure): pass <BLANKLINE> Point._fields_ = [('x', c_float), ('y', c_float)] <BLANKLINE> >>> from tide.generators.clang_utils import parse_clang >>> tu, index = parse_clang('union Point { float x; int y;};') >>> module = BindingGenerator().generate(tu) >>> print(compact(unparse(module))) <BLANKLINE> class Point(Union): pass <BLANKLINE> Point._fields_ = [('x', c_float), ('y', c_int)] <BLANKLINE> """ log.debug(f'{d(depth)}Generate struct `{elem.spelling}`') pyname = self.get_name(elem, rename=rename, depth=depth + 1) base = 'Structure' if elem.kind == CursorKind.UNION_DECL: base = 'Union' # For recursive data structure # log.debug(pyname) self.type_registry[f'struct {pyname}'] = T.Name(pyname) self.type_registry[f'const struct {pyname}'] = T.Name(pyname) parent = pyname anonymous_renamed = self.find_anonymous_fields(elem, parent, depth=depth + 1) # Docstring is the first element of the body # T.Constant(get_comment(elem)) body = [] docstring = get_comment(elem) if docstring: body.append(T.Expr(T.Constant(docstring, docstring=True))) attrs = T.List() need_pass = len(body) attr: Cursor for attr in elem.get_children(): self.generate_field(body, attrs, attr, anonymous_renamed, depth) # insert pass even if we have a docstring because # we will remove the docstring later if need_pass == len(body): body.append(ast.Pass()) if not attrs.elts: return T.ClassDef(name=pyname, bases=[T.Name(base)], body=body) fields = T.Assign([T.Attribute(T.Name(pyname), '_fields_')], attrs) return [ T.ClassDef(name=pyname, bases=[T.Name(base)], body=body), fields ]
def _generate_type(self, type: Type, depth=0): # print(type.kind, type.spelling, self.type_registry.get(type.spelling, 'NOT FOUND')) if type.kind == TypeKind.VOID: return T.Name('None') if type.kind == TypeKind.INT: return T.Name('c_int') if type.kind == TypeKind.POINTER and type.get_pointee( ).kind == TypeKind.VOID: return T.Name('c_void_p') if type.kind == TypeKind.POINTER and type.get_pointee( ).kind == TypeKind.CHAR_S: return T.Name('c_char_p') if type.kind == TypeKind.POINTER: pointee: Type = type.get_pointee() # Typedef use the name that it is aliased to if pointee.kind is TypeKind.TYPEDEF: pointee = get_typename(pointee) elif pointee.kind != TypeKind.VOID: pointee = self.generate_type(pointee, depth + 1) else: pointee = T.Name('c_void_p') # Function pointer do not need to be decorated by POINTER call if isinstance(pointee, T.Call) and isinstance( pointee.func, T.Name) and pointee.func.id == 'CFUNCTYPE': return pointee # for native types we need to keep ref because python will copy them return T.Call(T.Name('POINTER'), [pointee]) if type.kind == TypeKind.CHAR_S: return T.Name('c_char_p') # if type.is_const_qualified(): # return T.Name(get_typename(type)) if type.kind == TypeKind.TYPEDEF: return get_typename(type) if type.kind == TypeKind.FUNCTIONPROTO or ( type.kind == TypeKind.UNEXPOSED and type.get_canonical().kind == TypeKind.FUNCTIONPROTO): # SDL_HitTest = CFUNCTYPE(SDL_HitTestResult, POINTER(SDL_Window), POINTER(SDL_Point), c_void_p) canon = type if type.kind == TypeKind.UNEXPOSED: canon = type.get_canonical() rtype = canon.get_result() args = [] for arg in canon.argument_types(): args.append(self.generate_type(arg, depth + 1)) returntype = self.generate_type(rtype, depth + 1) cargs = [returntype] cargs.extend(args) return T.Call(T.Name('CFUNCTYPE'), args=cargs) if type.kind == TypeKind.CONSTANTARRAY: t = self.generate_type(type.element_type, depth + 1) return T.BinOp(t, ast.Mult(), T.Constant(type.element_count)) # Represents a C array with an unspecified size. if type.kind == TypeKind.INCOMPLETEARRAY: element_type = self.generate_type(type.element_type, depth + 1) # char *[] -> char ** return T.Call(T.Name('POINTER'), [element_type]) # struct <TYPENAME> if type.kind == TypeKind.ELABORATED: return T.Name(get_typename(type).id.replace('struct', '').strip()) if type.kind == TypeKind.ENUM: return get_typename(type) # print('gentype') show_elem(type, print_fun=log.debug) return get_typename(type)
def generate_float(self, elem, **kwargs): toks = [t.spelling for t in elem.get_tokens()] assert len(toks) == 1 return T.Constant(float(toks[0]))
def parse_expression_1(self, lhs, min_precedence, depth): lookahead = self.peek() precedence = fetch_precedence(lookahead.spelling) log.debug( f'{d(depth)} parse_expression_1 {lhs} {lookahead.kind} {lookahead.spelling}' ) while lookahead and precedence is not None and precedence >= min_precedence: op = lookahead self.next() rhs = self.parse_primary(depth + 1) lookahead = self.peek() if lookahead is None: break is_binary = is_binary_operator(lookahead.spelling) lookahead_pred = fetch_precedence(lookahead.spelling) is_right_asso = is_right_associative(lookahead.spelling) while lookahead and (is_binary and lookahead_pred > precedence ) or (is_right_asso and lookahead_pred == precedence): rhs = self.parse_expression_1(rhs, lookahead_pred, depth + 1) lookahead = self.peek() is_binary = is_binary_operator(lookahead.spelling) lookahead_pred = fetch_precedence(lookahead.spelling) is_right_asso = is_right_associative(lookahead.spelling) if lookahead.spelling == ')': break # the result of applying op with operands lhs and rhs pyop = fetch_python_op(op.spelling) if pyop is not None and not isinstance(pyop, str): if is_comparison(op.spelling): lhs = T.Compare(left=lhs, ops=[pyop()], comparators=[rhs]) elif is_bool(op.spelling): lhs = T.BoolOp(op=pyop(), values=[lhs, rhs]) else: lhs = T.BinOp(left=lhs, op=pyop(), right=rhs) elif pyop == 'if': raise UnsupportedExpression() elif pyop == 'assign': lhs = T.Assign(targets=[lhs], value=rhs) elif op.spelling == '[': lhs = T.Subscript(value=lhs, slice=T.Index(value=rhs), ctx=T.Load()) tok = self.peek() assert tok.spelling == ']' self.next() elif op.spelling == '->': if isinstance(rhs, T.Name): rhs = rhs.id lhs = T.Attribute(lhs, rhs, ctx=T.Load()) elif op.spelling == ',': lhs = T.If(T.Constant(True), [T.Expr(lhs), T.Return(rhs)]) else: show_elem(op) assert False precedence = fetch_precedence(lookahead.spelling) return lhs
tu = index.parse(filename, options=0x01) for diag in tu.diagnostics: print(diag.format()) for elem in tu.cursor.get_children(): show_elem(elem) if elem.spelling == '__GNUC__': break pass if __name__ == '__main__': # show_file('/usr/include/SDL2/SDL.h') from tide.generators.unparser_patch import unparse import tide.generators.nodes as T import ast from astunparse import dump mod = ast.parse(""" \"\"\"test\"\"\" """) mod = T.Constant("abc", docstring=True) # print(dump(mod)) print(unparse(mod))
class APIPass: """Generate a more friendly API for C bindings Notes ----- * removes the explicit library namespace from the name of functions and struct * Rewrites functions as method if first argument is a pointer to struct * Method use original c argument names * Method has the origin c docstring * Method has a short name removing the object name and the library namespace * enum values are now scoped inside an enum class * enum values have shorter names since name clashing cannot happen anymore * rewrites function that takes pointer arguments to return multiple values """ def __init__(self): self.dispatcher = { 'Expr': self.expression, 'Assign': self.assign, 'ClassDef': self.class_definition, 'FunctionDef': self.function_definition } self.pre_dispatcher = { 'Expr': self.pre_expression, 'Assign': self.pre_assign, 'ClassDef': self.pre_class_definition, } self.ctypes = dict() self.wrappers = dict() self.wrappers_2_ctypes = dict() # keep the order to know if we can annotate with Name of with string self.wrapper_order = dict() # we will remove wrappers that do not have methods # i.e they are data struct only self.wrapper_method_count = defaultdict(int) self.wrapper_ctor = defaultdict(list) self.ctypes_fields = dict() self.new_code = [] self.names = Trie() self.rename_types = dict() self.current_class_name = None self.new_names = set() def pre_expression(self, expr: T.Expr, depth): values = self.preprocess(expr.value, depth) if isinstance(values, (tuple, list)): return [T.Expr(v) for v in values] return T.Expr(values) def pre_class_definition(self, class_def: T.ClassDef, depth=0): self.ctypes[class_def.name] = class_def def pre_assign(self, expr: T.Assign, depth): # <c-type>._fields_ = [] if match(expr.targets[0], 'Attribute') and match( expr.value, 'List') and expr.targets[0].attr == '_fields_': ctype_name = expr.targets[0].value.id data = [] self.ctypes_fields[ctype_name] = data for elem in expr.value.elts: name = elem.elts[0].value ctype = elem.elts[1] data.append((name, ctype)) def preprocess(self, expr, depth): handler = self.pre_dispatcher.get(expr.__class__.__name__, None) if handler is not None: handler(expr, depth) def preprocessor(self, module: T.Module, depth=0): for expr in module.body: self.preprocess(expr, depth) def post_process_class_defintion(self, class_def: T.ClassDef): """Make a final pass over the generated class to improve function names""" # move the documentation over to our new class c_class_name = self.wrappers_2_ctypes[class_def.name] c_class_def = self.ctypes[c_class_name] docstring = fetch_docstring(c_class_def) if docstring: class_def.body.insert(0, docstring) # -- class_name = class_def.name.lower() names = defaultdict(int) for expr in class_def.body: if not isinstance(expr, T.FunctionDef): continue for n in expr.name.split('_'): if len(n) > 0: names[n] += 1 names = list(names.items()) # remove names that are not used that often names = sorted(filter(lambda x: x[1] > 2, names), key=lambda x: x[1]) # this is the magic filter that makes thing work # by forcing the name we are removing to be part of the type name # we are almost sure to remove duplicated data that is not useful # even when it appears multiple times it can make sense to have it duplicated # Example: # read_le16, read_le32, read_le64 # write_le16, write_le32, write_le64 names = list( map(lambda x: x[0], filter(lambda x: x[0] in class_name, names))) for expr in class_def.body: if not isinstance(expr, T.FunctionDef): continue for useless_name in names: expr.name = clean_name(expr.name, useless_name) def group_constant_to_enums(self): pass def clean_up_enumeration(self, class_def: T.ClassDef): """Removes superfluous prefix""" _, names = parse_sdl_name(class_def.name) cl_name = class_name(*names) # make a short alias of the enum (without the hardcoded c namespace) if cl_name != class_def.name: self.new_code.append( (None, T.Assign([T.Name(cl_name)], T.Name(class_def.name)))) t = Trie() counts = 0 for expr in class_def.body: if match(expr, 'Assign') and match(expr.targets[0], 'Name'): constant_name = expr.targets[0].id t.insert(constant_name) counts += 1 _ = list(t.redundant_prefix()) if len(_) > 1: # select the shortest prefix that is common to all most_likely = _[0] for c, n in _: if len(n) < len(most_likely[1]): most_likely = (c, n) # check that the prefix is common to all good = True for c, n in _: if not most_likely[1] in n: good = False break if good: _ = [most_likely] else: _ = [] if len(_) == 1: c, namespace = _[0] # SDL insert a None/Max enum that does not follow the pattern if counts != c + 1: return class_def for expr in class_def.body: if match(expr, 'Assign') and match(expr.targets[0], 'Name'): constant_name = expr.targets[0].id expr.targets[0].id = clean_name(constant_name, namespace) return class_def def is_opaque_container(self, name): # does this class define fields return self.ctypes_fields.get(name, None) is None def class_definition(self, class_def: T.ClassDef, depth): assert class_def.name in self.ctypes # Opaque struct: move on # if class_def.name[0] == '_': # return class_def self_wrap = self.wrappers.get(class_def.name) if self_wrap is None: if T.Name('enumeration') in class_def.decorator_list: return self.clean_up_enumeration(class_def) _, names = parse_sdl_name(class_def.name) cl_name = class_name(*names) self_wrap = T.ClassDef(cl_name) self.new_code.append((class_def.name, self_wrap)) self.wrappers[class_def.name] = self_wrap self.wrappers_2_ctypes[self_wrap.name] = class_def.name self.wrapper_order[self_wrap.name] = len(self.wrappers) self_type = T.Call(T.Name('POINTER'), [T.Name(class_def.name)]) self_wrap.body.append( T.AnnAssign(T.Name('handle'), self_type, T.Name('None'))) # Factory to build the wrapper form the ctype # create an uninitialized version of the object and set the handle from_ctype = T.FunctionDef('from_handle', decorator_list=[T.Name('staticmethod')]) from_ctype.args = T.Arguments(args=[T.Arg('a', self_type)]) from_ctype.returns = T.Constant(cl_name) from_ctype.body = [ T.Assign([T.Name('b')], T.Call(T.Attribute(T.Name('object'), '__new__'), [T.Name(cl_name)])), T.Assign([T.Attribute(T.Name('b'), 'handle')], T.Name('a')), T.Return(T.Name('b')) ] self_wrap.body.append(from_ctype) if not self.is_opaque_container(class_def.name): # default init allocate a dummy object for it default_init = T.FunctionDef('__init__') default_init.args = T.Arguments(args=[T.Arg('self')]) default_init.body = [ T.Assign([T.Attribute(T.Name('self'), '_value')], T.Call(T.Name(class_def.name), [])), T.Assign([T.Attribute(T.Name('self'), 'handle')], T.Call(T.Name('byref'), [T.Attribute(T.Name('self'), '_value')])) ] self_wrap.body.append(default_init) self.rename_types[T.Call(T.Name('POINTER'), [T.Name(class_def.name)])] = cl_name return class_def def function_definition(self, function_def: T.FunctionDef, depth): return function_def SELF_HANDLE = T.Attribute(T.Name('self'), 'handle') def rename(self, n): new_name = self.rename_types.get(n, None) if new_name: arg_type_order = self.wrapper_order.get(new_name, None) class_order = self.wrapper_order.get(self.current_class_name, None) # arg type is defined after current class if arg_type_order and class_order and arg_type_order > class_order: return T.Constant(new_name) if new_name == self.current_class_name: return T.Constant(new_name) return T.Name(new_name) return n def make_wrapping_call( self, call: BindCall, replace_arg_names: Optional[List] = None, replace_args: Optional[List] = None) -> Tuple[T.Arguments, T.Call]: if replace_arg_names is None: replace_arg_names = [] if replace_args is None: replace_args = [] assert len(replace_args) == len(replace_arg_names) fun_name = call.function_name offset = len(replace_args) ctype_args = list(call.arguments[offset:]) arg_names = list(call.argument_names[offset:]) fun_args = T.Arguments( args=replace_arg_names + [T.Arg(n, self.rename(t)) for n, t in zip(arg_names, ctype_args)]) fun_call = T.Call( T.Name(fun_name), replace_args + [T.Name(arg_names[i]) for i in range(len(ctype_args))]) return fun_args, fun_call def add_docstring(self, new_fun: T.FunctionDef, binding: BindCall, indent_level: int): docstring = binding.docstring if docstring: docstring.value = docstring.value.replace( '\n ', '\n' + ' ' * indent_level) new_fun.body.insert(0, T.Expr(docstring)) def generate_constructor(self, class_def: T.ClassDef, call: BindCall): self.current_class_name = class_def.name cl_name = class_def.name # because we can have multiple constructors we have a to generate a constructor as a static method fun_name = call.function_name ctype_return = call.return_type _, names = parse_sdl_name(fun_name) args, c_call = self.make_wrapping_call(call) new_fun = T.FunctionDef(function_name(*names), decorator_list=[T.Name('staticmethod')]) new_fun.returns = self.rename(ctype_return) new_fun.args = args new_fun.body = [ T.Assign([T.Name('b')], T.Call(T.Attribute(T.Name('object'), '__new__'), [T.Name(cl_name)])), T.Assign([T.Attribute(T.Name('b'), 'handle')], c_call), T.Return(T.Name('b')) ] self.add_docstring(new_fun, call, 2) call.clear_kwargs() class_def.body.append(new_fun) self.current_class_name = None def get_arg_names(self, call: T.Call): arg_names = get_kwarg_arg('arg_names', call.keywords, None) if arg_names is not None: return [a.value.lower() for a in arg_names.elts] else: return self.ARGS def generate_method(self, call: BindCall, self_wrap: T.ClassDef): fun_name = call.function_name _, names = parse_sdl_name(fun_name) self.current_class_name = self_wrap.name args, c_call = self.make_wrapping_call( call, replace_arg_names=[T.Arg('self')], replace_args=[self.SELF_HANDLE]) new_fun = T.FunctionDef(function_name(*names)) new_fun.returns = self.rename(call.return_type) new_fun.args = args # does the return type need to be wrapped if new_fun.returns != call.return_type: cast_to = new_fun.returns if isinstance(new_fun.returns, T.Constant): cast_to = T.Name(new_fun.returns.value) c_call = T.Call(T.Attribute(cast_to, 'from_handle'), [c_call]) new_fun.body = [T.Return(c_call)] self.add_docstring(new_fun, call, 2) call.clear_kwargs() if self.is_multi_output(new_fun, offset=1): new_fun = self.rewrite_multi_output_function(new_fun, offset=1) self_wrap.body.append(new_fun) # we cannot automatically add a destructor because we do not know # which object is owning what # # try to find destructor and constructor functions # for i, n in enumerate(names): # arg_n = len(new_fun.args.args) # # # check if the function is named destroy + class_name # if arg_n == 1 and n == 'destroy' and i + 2 == len(names) and names[i + 1] == self_wrap.name.lower(): # destructor = copy.deepcopy(new_fun) # destructor.name = '__del__' # self_wrap.body.append(destructor) # break # # # sdl is not consistent with that one # if n == 'free': # destructor = copy.deepcopy(new_fun) # destructor.name = '__del__' # self_wrap.body.append(destructor) self.wrapper_method_count[self.current_class_name] += 1 self.current_class_name = None def generate_function_wrapper(self, call: BindCall): fun_name = call.function_name _, names = parse_sdl_name(fun_name) args, c_call = self.make_wrapping_call(call) new_name = function_name(*names) if new_name not in self.new_names: self.new_names.add(new_name) else: log.warning( f'{new_name} already exist in this scope cannot rename {fun_name}' ) new_name = fun_name new_fun = T.FunctionDef(new_name) new_fun.returns = self.rename(call.return_type) new_fun.args = args new_fun.body = [T.Return(c_call)] self.add_docstring(new_fun, call, 1) call.clear_kwargs() if self.is_multi_output(new_fun, offset=0): new_fun = self.rewrite_multi_output_function(new_fun, offset=0) self.new_code.append((None, new_fun)) return def process_bind_call(self, call: BindCall, parent): """Try to find the class this function belongs to""" fun_name = call.function_name # constructor self_name = call.is_constructor() if self_name and call.is_method() is None: class_def: T.ClassDef = self.wrappers.get(self_name) if class_def: return self.generate_constructor(class_def, call) return self.generate_function_wrapper(call) # --- self_name = call.is_method() # not a method if self_name is None: self.generate_function_wrapper(call) return parent self_def = self.ctypes.get(self_name) if not self_def: log.debug(f'Method `{fun_name}` does not have c-class') self.generate_function_wrapper(call) return parent # not a known class if not match(self_def, 'ClassDef'): self.generate_function_wrapper(call) return parent # Generate our new python class that will wrap the ctypes self_wrap: T.ClassDef = self.wrappers.get(self_name) if self_wrap is None and match(self_def, 'ClassDef'): log.debug( f'Method `{fun_name}` does not have wrapped class `{self_name}`' ) return parent self.generate_method(call, self_wrap) return parent handle_is_not_none = T.Assert(test=T.Compare( left=T.Attribute(value=T.Name(id='self', ctx=T.Load()), attr='handle', ctx=T.Load()), ops=[T.IsNot()], comparators=[T.Constant(value=None, kind=None)]), msg=None) def is_multi_output(self, func: T.FunctionDef, offset): expr = func.body[0] if not match(expr, 'Expr', ('value', 'Constant')): return False expr: T.Constant = func.body[0].value if not expr.docstring: return False # if doc string specify it returns an error # or if function is known to return nothing if (':return 0 on success, or -1' not in expr.value) and (match( func.returns, 'Name') and func.returns.id != 'None'): return False if len(func.args.args[offset:]) == 0: return False count = 0 # multi output should group all the output at the end of the function call previous_was_ptr = False for arg in func.args.args[offset:]: if match(arg.annotation, 'Call', ('func', 'Name')) and arg.annotation.func.id == 'POINTER': count += 1 previous_was_ptr = True elif previous_was_ptr: return False if count > 0: log.debug(f'{func.name} has multiple outputs') return True return False def rewrite_multi_output_function(self, func: T.FunctionDef, offset): """ Notes ----- Outputs should be grouped at the end of the function to differentiate between an output and an array input """ new_func = T.FunctionDef(func.name) new_func.body = [func.body[0]] arg_len = len(func.args.args[offset:]) if arg_len == 0: return func output_args = [] remaining_args = [] arg_type = dict() # Filter output arguments for i, arg in enumerate(func.args.args[offset:]): if match(arg.annotation, 'Call', ('func', 'Name')) and arg.annotation.func.id == 'POINTER': # Generate the result argument var_type = arg.annotation.args[0] new_func.body.append( T.Assign([T.Name(arg.arg)], T.Call(var_type))) output_args.append(arg) arg_type[i] = 'O' else: remaining_args.append(arg) arg_type[i] = 'I' original_call: T.Call = func.body[1].value for i in range(len(original_call.args[offset:])): if arg_type[i] == 'O': original_call.args[i + offset] = T.Call( T.Name('byref'), [T.Name(func.args.args[i + offset].arg)]) new_func.body.append(T.Assign([T.Name('error')], original_call)) returns = [T.Name(arg.arg) for arg in output_args] returns_types = [arg.annotation.args[0] for arg in output_args] if len(returns) == 1: new_func.body.append(T.Return(returns[0])) new_func.returns = returns_types[0] else: new_func.body.append(T.Return(T.Tuple(returns))) # new_func.returns = T.Call(T.Name('Tuple'), returns_types) new_func.returns = T.Subscript(T.Name('Tuple'), T.ExtSlice(dims=returns_types), T.Load()) if offset == 1: remaining_args = [T.Arg('self')] + remaining_args new_func.args = T.Arguments(args=remaining_args) return new_func def struct_fields(self, expr: T.Assign): ctype_name = expr.targets[0].value.id assert ctype_name in self.ctypes_fields def assign(self, expr: T.Assign, depth): # <function> = _bind('c-function', [cargs], rtype, docstring=, arg_names=) if BindCall.is_binding_call(expr.value): expr = self.process_bind_call(BindCall(expr.value), expr) # <c-type>._fields_ = [] elif match(expr.targets[0], 'Attribute') and match( expr.value, 'List') and expr.targets[0].attr == '_fields_': self.struct_fields(expr) # <Name> = <Name> elif match(expr.value, 'Name') and match(expr.targets[0], 'Name'): alias_name = expr.targets[0].id aliased_name = expr.value.id aliased_ctype = self.ctypes.get(aliased_name) aliased_wrapped = self.wrappers.get(aliased_name) self.rename_types[aliased_name] = alias_name self.ctypes[alias_name] = aliased_ctype self.wrappers[alias_name] = aliased_wrapped return expr def expression(self, expr: T.Expr, depth): values = self.dispatch(expr.value, depth) if isinstance(values, (tuple, list)): return [T.Expr(v) for v in values] return T.Expr(values) def generate(self, module: T.Module, depth=0): new_module: T.Module = ast.Module() new_module.body = [] self.preprocessor(module) for expr in module.body: self.dispatch(expr, depth + 1) # insert our new bindings at the end for k, v in self.new_code: if isinstance(v, T.ClassDef): if self.wrapper_method_count.get(v.name, 0) > 0: self.post_process_class_defintion(v) module.body.append(v) elif v.name in self.wrappers_2_ctypes: c_struct = self.wrappers_2_ctypes.get(v.name) # make it an alias for the ctype module.body.append( T.Expr(T.Assign([T.Name(v.name)], T.Name(c_struct)))) else: module.body.append(T.Expr(v)) return module def dispatch(self, expr, depth) -> T.Expr: handler = self.dispatcher.get(expr.__class__.__name__, None) if handler is None: print(expr) assert False return handler(expr, depth)