Mercurial > hgrepos > Python2 > PyMuPDF
diff mupdf-source/scripts/wrap/cpp.py @ 2:b50eed0cc0ef upstream
ADD: MuPDF v1.26.7: the MuPDF source as downloaded by a default build of PyMuPDF 1.26.4.
The directory name has changed: no version number in the expanded directory now.
| author | Franz Glasner <fzglas.hg@dom66.de> |
|---|---|
| date | Mon, 15 Sep 2025 11:43:07 +0200 |
| parents | |
| children |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mupdf-source/scripts/wrap/cpp.py Mon Sep 15 11:43:07 2025 +0200 @@ -0,0 +1,5880 @@ +''' +Functions for generating source code for the C++ bindings. +''' + +import io +import os +import pickle +import re +import textwrap + +import jlib + +from . import classes +from . import csharp +from . import parse +from . import python +from . import rename +from . import state +from . import util + + +def _make_top_level( text, top_level='::'): + if text == 'string': + # This is a hack; for some reason we often end up with `string` when it + # it should be `std::string`. + text = 'std::string' + initial_prefix = [''] + def handle_prefix( text, prefix): + if text.startswith( prefix): + initial_prefix[0] += prefix + return text[ len(prefix):] + return text + text = handle_prefix( text, 'const ') + text = handle_prefix( text, 'struct ') + if text.startswith( ('fz_', 'pdf_')): + text = f'{top_level}{text}' + text = f'{initial_prefix[0]}{text}' + return text + + +def declaration_text( + type_, + name, + nest=0, + name_is_simple=True, + verbose=False, + expand_typedef=True, + top_level='::', + ): + ''' + Returns text for C++ declaration of <type_> called <name>. + + type: + a clang.cindex.Type. + name: + name of type; can be empty. + nest: + for internal diagnostics. + name_is_simple: + true iff <name> is an identifier. + + If name_is_simple is false, we surround <name> with (...) if type is a + function. + ''' + # clang can give unhelpful spelling for anonymous structs. + assert 'struct (unnamed at ' not in type_.spelling, f'type_.spelling={type_.spelling}' + if verbose: + jlib.log( '{nest=} {name=} {type_.spelling=} {type_.get_declaration().get_usr()=}') + jlib.log( '{type_.kind=} {type_.get_array_size()=} {expand_typedef=}') + + array_n = type_.get_array_size() + if verbose: + jlib.log( '{array_n=}') + if array_n >= 0 or type_.kind == state.clang.cindex.TypeKind.INCOMPLETEARRAY: + if verbose: jlib.log( '{array_n=}') + if array_n < 0: + array_n = '' + ret = declaration_text( + type_.get_array_element_type(), + f'{name}[{array_n}]', + nest+1, + name_is_simple, + verbose=verbose, + expand_typedef=expand_typedef, + top_level=top_level, + ) + if verbose: + jlib.log( 'returning {ret=}') + return ret + + pointee = type_.get_pointee() + if pointee and pointee.spelling: + if type_.kind == state.clang.cindex.TypeKind.LVALUEREFERENCE: + pointee_type = '&' + elif type_.kind == state.clang.cindex.TypeKind.POINTER: + pointee_type = '*' + else: + assert 0, f'Unrecognised pointer kind {type_.kind=}.' + if verbose: jlib.log( '{type_=} {type_.kind=} {pointee.spelling=}') + ret = declaration_text( + pointee, + f'{pointee_type}{name}', + nest+1, + name_is_simple=False, + verbose=verbose, + expand_typedef=expand_typedef, + top_level=top_level, + ) + if verbose: + jlib.log( 'returning {ret=}') + return ret + + if expand_typedef and type_.get_typedef_name(): + if verbose: jlib.log( '{type_.get_typedef_name()=}') + const = 'const ' if type_.is_const_qualified() else '' + ret = f'{const}{_make_top_level(type_.get_typedef_name(), top_level)} {name}' + if verbose: + jlib.log( 'returning {ret=}') + return ret + + # On MacOS type `size_t` returns true from get_result() and is + # state.clang.cindex.TypeKind.ELABORATED. + # + if ( type_.get_result().spelling + and type_.kind not in + ( + state.clang.cindex.TypeKind.FUNCTIONNOPROTO, + state.clang.cindex.TypeKind.ELABORATED, + ) + ): + # <type> is a function. We call ourselves with type=type_.get_result() + # and name=<name>(<args>). + # + assert type_.kind == state.clang.cindex.TypeKind.FUNCTIONPROTO, \ + f'{type_.spelling=} {type_.kind=}' + ret = '' + sep = '' + for arg in type_.argument_types(): + ret += sep + ret += declaration_text( + arg, + '', + nest+1, + top_level=top_level, + verbose=verbose, + expand_typedef=expand_typedef, + ) + sep = ', ' + if verbose: jlib.log( '{ret!r=}') + if not name_is_simple: + # If name isn't a simple identifier, put it inside braces, e.g. + # this crudely allows function pointers to work. + name = f'({name})' + ret = f'{name}({ret})' + if verbose: jlib.log( '{type_.get_result()=}') + ret = declaration_text( + type_.get_result(), + ret, + nest+1, + name_is_simple=False, + verbose=verbose, + expand_typedef=expand_typedef, + top_level=top_level, + ) + if verbose: + jlib.log( 'returning {ret=}') + return ret + + ret = f'{_make_top_level(type_.spelling, top_level)} {name}' + assert not 'struct (unnamed at ' in ret, f'Bad clang name for anonymous struct: {ret}' + if verbose: jlib.log( 'returning {ret=}') + return ret + + +def write_call_arg( + tu, + arg, + classname, + have_used_this, + out_cpp, + verbose=False, + python=False, + ): + ''' + Write an arg of a function call, translating between raw and wrapping + classes as appropriate. + + If the required type is a fz_ struct that we wrap, we assume that arg.name + is a reference to an instance of the wrapper class. If the wrapper class + is the same as <classname>, we use 'this->' instead of <name>. We also + generate slightly different code depending on whether the wrapper class is + pod or inline pod. + + arg: + Arg from get_args(). + classname: + Name of wrapper class available as 'this'. + have_used_this: + If true, we never use 'this->...'. + out_cpp: + . + python: + If true, we write python code, not C. + + Returns True if we have used 'this->...', else return <have_used_this>. + ''' + assert isinstance( arg, parse.Arg) + assert isinstance( arg.cursor, state.clang.cindex.Cursor) + if not arg.alt: + # Arg is a normal type; no conversion necessary. + if python: + out_cpp.write( arg.name_python) + else: + out_cpp.write( arg.name) + return have_used_this + + if verbose: + jlib.log( '{=arg.name arg.alt.spelling classname}') + type_ = state.get_name_canonical( arg.cursor.type) + ptr = '*' + #log( '{=arg.name arg.alt.spelling classname type_.spelling}') + if type_.kind == state.clang.cindex.TypeKind.POINTER: + type_ = state.get_name_canonical( type_.get_pointee()) + ptr = '' + #log( '{=arg.name arg.alt.spelling classname type_.spelling}') + extras = parse.get_fz_extras( tu, type_.spelling) + assert extras, f'No extras for type_.spelling={type_.spelling}' + if verbose: + jlib.log( 'param is fz: {type_.spelling=} {extras2.pod=}') + assert extras.pod != 'none' \ + 'Cannot pass wrapper for {type_.spelling} as arg because pod is "none" so we cannot recover struct.' + if python: + if extras.pod == 'inline': + out_cpp.write( f'{arg.name_python}.internal()') + elif extras.pod: + out_cpp.write( f'{arg.name_python}.m_internal') + else: + out_cpp.write( f'{arg.name_python}.m_internal') + + elif extras.pod == 'inline': + # We use the address of the first class member, casting it to a pointer + # to the wrapped type. Not sure this is guaranteed safe, but should + # work in practise. + name_ = f'{arg.name}.' + if not have_used_this and rename.class_(arg.alt.type.spelling) == classname: + have_used_this = True + name_ = 'this->' + field0 = parse.get_field0(type_).spelling + out_cpp.write( f'{ptr} {name_}internal()') + else: + if verbose: + jlib.log( '{=arg state.get_name_canonical(arg.cursor.type).kind classname extras}') + if extras.pod and state.get_name_canonical( arg.cursor.type).kind == state.clang.cindex.TypeKind.POINTER: + out_cpp.write( '&') + elif not extras.pod and state.get_name_canonical( arg.cursor.type).kind != state.clang.cindex.TypeKind.POINTER: + out_cpp.write( '*') + elif arg.out_param: + out_cpp.write( '&') + if not have_used_this and rename.class_(arg.alt.type.spelling) == classname: + have_used_this = True + out_cpp.write( 'this->') + else: + out_cpp.write( f'{arg.name}.') + out_cpp.write( 'm_internal') + + return have_used_this + + +def make_fncall( tu, cursor, return_type, fncall, out, refcheck_if, trace_if): + ''' + Writes a low-level function call to <out>, using fz_context_s from + internal_context_get() and with fz_try...fz_catch that converts to C++ + exceptions by calling throw_exception(). + + return_type: + Text return type of function, e.g. 'void' or 'double'. + fncall: + Text containing function call, e.g. 'function(a, b, 34)'. + out: + Stream to which we write generated code. + ''' + uses_fz_context = False; + + # Setting this to False is a hack to elide all fz_try/fz_catch code. This + # has a very small effect on mupdfpy test suite performance - e.g. reduce + # time from 548.1s to 543.2s. + # + use_fz_try = True + + if cursor.spelling in ( + 'pdf_specifics', + ): + # This fn takes a fz_context* but never throws, so we can omit + # `fz_try()...fz_catch()`, which might give a small performance + # improvement. + use_fz_try = False + uses_fz_context = True + else: + for arg in parse.get_args( tu, cursor, include_fz_context=True): + if parse.is_pointer_to( arg.cursor.type, 'fz_context'): + uses_fz_context = True + break + if uses_fz_context: + context_get = rename.internal( 'context_get') + throw_exception = rename.internal( 'throw_exception') + out.write( f' fz_context* auto_ctx = {context_get}();\n') + + # Output code that writes diagnostics to std::cerr if $MUPDF_trace is set. + # + def varname_enable(): + for t in 'fz_keep_', 'fz_drop_', 'pdf_keep_', 'pdf_drop_': + if cursor.spelling.startswith( t): + return 's_trace_keepdrop' + return 's_trace > 1' + + out.write( f' {trace_if}\n') + out.write( f' if ({varname_enable()}) {{\n') + out.write( f' std::cerr << __FILE__ << ":" << __LINE__ << ":" << __FUNCTION__ << "(): calling {cursor.spelling}():";\n') + for arg in parse.get_args( tu, cursor, include_fz_context=True): + if parse.is_pointer_to( arg.cursor.type, 'fz_context'): + out.write( f' if ({varname_enable()}) std::cerr << " auto_ctx=" << auto_ctx;\n') + elif arg.out_param: + out.write( f' if ({varname_enable()}) std::cerr << " {arg.name}=" << (void*) {arg.name};\n') + elif arg.alt: + # If not a pod, there will not be an operator<<, so just show + # the address of this arg. + # + extras = parse.get_fz_extras( tu, arg.alt.type.spelling) + assert extras.pod != 'none' \ + 'Cannot pass wrapper for {type_.spelling} as arg because pod is "none" so we cannot recover struct.' + if extras.pod: + out.write( f' std::cerr << " {arg.name}=" << {arg.name};\n') + elif arg.cursor.type.kind == state.clang.cindex.TypeKind.POINTER: + out.write( f' if ({varname_enable()}) std::cerr << " {arg.name}=" << {arg.name};\n') + elif arg.cursor.type.kind == state.clang.cindex.TypeKind.LVALUEREFERENCE: + out.write( f' if ({varname_enable()}) std::cerr << " &{arg.name}=" << &{arg.name};\n') + else: + out.write( f' std::cerr << " &{arg.name}=" << &{arg.name};\n') + elif parse.is_pointer_to(arg.cursor.type, 'char') and state.get_name_canonical( arg.cursor.type.get_pointee()).is_const_qualified(): + # 'const char*' is assumed to be zero-terminated string. But we + # need to protect against trying to write nullptr because this + # appears to kill std::cerr on Linux. + out.write( f' if ({arg.name}) std::cerr << " {arg.name}=\'" << {arg.name} << "\'";\n') + out.write( f' else std::cerr << " {arg.name}:null";\n') + elif parse.is_( arg.cursor.type, 'va_list'): + out.write( f' std::cerr << " {arg.name}:va_list";\n') + elif (0 + or parse.is_( arg.cursor.type, 'signed char') + or parse.is_( arg.cursor.type, 'unsigned char') + ): + # Typically used for raw data, so not safe to treat as text. + out.write( f' std::cerr << " {arg.name}=" << ((int) {arg.name});\n') + elif (0 + or parse.is_pointer_to(arg.cursor.type, 'signed char') + or parse.is_pointer_to(arg.cursor.type, 'unsigned char') + ): + # Typically used for raw data, so not safe to treat as text. + out.write( f' std::cerr << " {arg.name}=" << ((void*) {arg.name});\n') + elif arg.cursor.type.kind == state.clang.cindex.TypeKind.POINTER: + # Don't assume non-const 'char*' is a zero-terminated string. + out.write( f' if ({varname_enable()}) std::cerr << " {arg.name}=" << (void*) {arg.name};\n') + elif arg.cursor.type.kind == state.clang.cindex.TypeKind.LVALUEREFERENCE: + out.write( f' if ({varname_enable()}) std::cerr << " &{arg.name}=" << &{arg.name};\n') + else: + out.write( f' std::cerr << " {arg.name}=" << {arg.name};\n') + out.write( f' std::cerr << "\\n";\n') + out.write( f' }}\n') + out.write( f' #endif\n') + + if uses_fz_context: + out.write( f' {refcheck_if}\n') + out.write( f' long stack0;\n') + out.write( f' if (s_check_error_stack)\n') + out.write( f' {{\n') + out.write( f' stack0 = auto_ctx->error.top - auto_ctx->error.stack_base;\n') + out.write( f' }}\n') + out.write( f' #endif\n') + + # Now output the function call. + # + if return_type != 'void': + out.write( f' {return_type} ret;\n') + + if cursor.spelling == 'fz_warn': + out.write( ' va_list ap;\n') + out.write( ' fz_var(ap);\n') + + indent = '' + if uses_fz_context and use_fz_try: + out.write( f' fz_try(auto_ctx) {{\n') + indent = ' ' + + if cursor.spelling == 'fz_warn': + out.write( f' {indent}va_start(ap, fmt);\n') + out.write( f' {indent}fz_vwarn(auto_ctx, fmt, ap);\n') + else: + if not uses_fz_context: + out.write( f' /* No fz_context* arg, so no need for fz_try()/fz_catch() to convert MuPDF exceptions into C++ exceptions. */\n') + out.write( f' {indent}') + if return_type != 'void': + out.write( f'ret = ') + out.write( f'{fncall};\n') + + if uses_fz_context and use_fz_try: + out.write( f' }}\n') + + if cursor.spelling == 'fz_warn': + if use_fz_try: + out.write( f' fz_always(auto_ctx) {{\n') + out.write( f' va_end(ap);\n') + out.write( f' }}\n') + else: + out.write( f' va_end(ap);\n') + + if uses_fz_context and use_fz_try: + out.write( f' fz_catch(auto_ctx) {{\n') + out.write( f' {trace_if}\n') + out.write( f' if (s_trace_exceptions) {{\n') + out.write( f' std::cerr << __FILE__ << ":" << __LINE__ << ":" << __FUNCTION__ << "(): fz_catch() has caught exception.\\n";\n') + out.write( f' }}\n') + out.write( f' #endif\n') + out.write( f' {throw_exception}(auto_ctx);\n') + out.write( f' }}\n') + + if uses_fz_context: + out.write( f' {refcheck_if}\n') + out.write( f' if (s_check_error_stack)\n') + out.write( f' {{\n') + out.write( f' long stack1 = auto_ctx->error.top - auto_ctx->error.stack_base;\n') + out.write( f' if (stack1 != stack0)\n') + out.write( f' std::cerr << __FILE__ << ":" << __LINE__ << ":" << __FUNCTION__ << "(): MuPDF error stack size changed by {cursor.spelling}(): " << stack0 << " -> " << stack1 << "\\n";\n') + out.write( f' }}\n') + out.write( f' #endif\n') + + if return_type != 'void': + out.write( f' return ret;\n') + + +def to_pickle( obj, path): + ''' + Pickles <obj> to file <path>. + ''' + with open( path, 'wb') as f: + pickle.dump( obj, f) + +def from_pickle( path): + ''' + Returns contents of file <path> unpickled. + ''' + with open( path, 'rb') as f: + return pickle.load( f) + +class Generated: + ''' + Stores information generated when we parse headers using clang. + ''' + def __init__( self): + self.h_files = [] + self.cpp_files = [] + self.fn_usage_filename = None + self.container_classnames = [] + self.to_string_structnames = [] + self.fn_usage = dict() + self.output_param_fns = [] + self.c_functions = [] + self.c_globals = [] + self.c_enums = [] + self.c_structs = [] + self.swig_cpp = io.StringIO() + self.swig_cpp_python = io.StringIO() + self.swig_python = io.StringIO() + self.swig_python_exceptions = io.StringIO() + self.swig_python_set_error_classes = io.StringIO() + self.swig_csharp = io.StringIO() + self.virtual_fnptrs = [] # List of extra wrapper class names with virtual fnptrs. + self.cppyy_extra = '' + + def save( self, dirpath): + ''' + Saves state to .pickle file, to be loaded later via pickle.load(). + ''' + to_pickle( self, f'{dirpath}/generated.pickle') + + +def make_outparam_helper( + tu, + cursor, + fnname, + fnname_wrapper, + generated, + ): + ''' + Create extra C++, Python and C# code to make tuple-returning wrapper of + specified function. + + We write Python code to generated.swig_python and C++ code to + generated.swig_cpp. + ''' + verbose = False + main_name = rename.ll_fn(cursor.spelling) + generated.swig_cpp.write( '\n') + + # Write struct. + generated.swig_cpp.write( 'namespace mupdf\n') + generated.swig_cpp.write('{\n') + generated.swig_cpp.write(f' /* Out-params helper class for {cursor.spelling}(). */\n') + generated.swig_cpp.write(f' struct {main_name}_outparams\n') + generated.swig_cpp.write(f' {{\n') + for arg in parse.get_args( tu, cursor): + if not arg.out_param: + continue + decl = declaration_text( arg.cursor.type, arg.name, verbose=verbose) + if verbose: + jlib.log( '{decl=}') + assert arg.cursor.type.kind == state.clang.cindex.TypeKind.POINTER + + # We use state.get_name_canonical() here because, for example, it + # converts int64_t to 'long long', which seems to be handled better by + # swig - swig maps int64_t to mupdf.SWIGTYPE_p_int64_t which can't be + # treated or converted to an integer. + # + # We also value-initialise in case the underlying mupdf function also + # reads the supplied value - i.e. treats it as an in-parm as well as an + # out-param; this is particularly important for pointer out-params. + # + pointee = state.get_name_canonical( arg.cursor.type.get_pointee()) + generated.swig_cpp.write(f' {declaration_text( pointee, arg.name)} = {{}};\n') + generated.swig_cpp.write(f' }};\n') + generated.swig_cpp.write('\n') + + # Write function definition. + name_args = f'{main_name}_outparams_fn(' + sep = '' + for arg in parse.get_args( tu, cursor): + if arg.out_param: + continue + name_args += sep + name_args += declaration_text( arg.cursor.type, arg.name, verbose=verbose) + sep = ', ' + name_args += f'{sep}{main_name}_outparams* outparams' + name_args += ')' + generated.swig_cpp.write(f' /* Out-params function for {cursor.spelling}(). */\n') + generated.swig_cpp.write(f' {declaration_text( cursor.result_type, name_args)}\n') + generated.swig_cpp.write( ' {\n') + return_void = (cursor.result_type.spelling == 'void') + generated.swig_cpp.write(f' ') + if not return_void: + generated.swig_cpp.write(f'{declaration_text(cursor.result_type, "ret")} = ') + generated.swig_cpp.write(f'{rename.ll_fn(cursor.spelling)}(') + sep = '' + for arg in parse.get_args( tu, cursor): + generated.swig_cpp.write(sep) + if arg.out_param: + generated.swig_cpp.write(f'&outparams->{arg.name}') + else: + generated.swig_cpp.write(f'{arg.name}') + sep = ', ' + generated.swig_cpp.write(');\n') + if not return_void: + generated.swig_cpp.write(' return ret;\n') + generated.swig_cpp.write(' }\n') + generated.swig_cpp.write('}\n') + + # Write Python wrapper. + python.make_outparam_helper_python(tu, cursor, fnname, fnname_wrapper, generated, main_name) + + # Write C# wrapper. + csharp.make_outparam_helper_csharp(tu, cursor, fnname, fnname_wrapper, generated, main_name) + + +def make_python_class_method_outparam_override( + tu, + cursor, + fnname, + generated, + structname, + classname, + return_type, + ): + ''' + Writes Python code to `generated.swig_python` that monkey-patches Python + function or method to make it call the underlying MuPDF function's Python + wrapper, which will return out-params in a tuple. + + This is necessary because C++ doesn't support out-params so the C++ API + supports wrapper class out-params by taking references to a dummy wrapper + class instances, whose m_internal is then changed to point to the out-param + struct (with suitable calls to keep/drop to manage the destruction of the + dummy instance). + + In Python, we could create dummy wrapper class instances (e.g. passing + nullptr to constructor) and return them, but instead we make our own call + to the underlying MuPDF function and wrap the out-params into wrapper + classes. + ''' + out = generated.swig_python + # Underlying fn. + main_name = rename.ll_fn(cursor.spelling) + + if structname: + name_new = f'{classname}_{rename.method(structname, cursor.spelling)}_outparams_fn' + else: + name_new = f'{rename.fn(cursor.spelling)}_outparams_fn' + + # Define an internal Python function that will become the class method. + # + out.write( f'def {name_new}(') + if structname: + out.write( ' self') + comma = ', ' + else: + comma = '' + for arg in parse.get_args( tu, cursor): + if arg.out_param: + continue + if structname and parse.is_pointer_to( arg.cursor.type, structname): + continue + out.write(f'{comma}{arg.name_python}') + comma = ', ' + out.write('):\n') + out.write( ' """\n') + if structname: + out.write(f' Helper for out-params of class method {structname}::{main_name}() [{cursor.spelling}()].\n') + else: + out.write(f' Class-aware helper for out-params of {fnname}() [{cursor.spelling}()].\n') + out.write( ' """\n') + + # ret, a, b, ... = foo::bar(self.m_internal, p, q, r, ...) + out.write(f' ') + sep = '' + if cursor.result_type.spelling != 'void': + out.write( 'ret') + sep = ', ' + for arg in parse.get_args( tu, cursor): + if not arg.out_param: + continue + out.write( f'{sep}{arg.name_python}') + sep = ', ' + out.write( f' = {main_name}(') + sep = '' + if structname: + out.write( f' self.m_internal') + sep = ', ' + for arg in parse.get_args( tu, cursor): + if arg.out_param: + continue + if structname and parse.is_pointer_to( arg.cursor.type, structname): + continue + out.write( sep) + write_call_arg( tu, arg, classname, have_used_this=False, out_cpp=out, python=True) + sep = ', ' + out.write( ')\n') + + # return ret, a, b. + # + # We convert returned items to wrapper classes if they are MuPDF types. + # + out.write( ' return ') + sep = '' + if cursor.result_type.spelling != 'void': + if return_type: + #out.write( f'{return_type}(ret)') + # Return type is a class wrapper. + return_ll_type = cursor.result_type + do_keep = False + if cursor.result_type.kind == state.clang.cindex.TypeKind.POINTER: + return_ll_type = return_ll_type.get_pointee() + if parse.has_refs( tu, return_ll_type): + return_ll_type = return_ll_type.spelling + return_ll_type = util.clip( return_ll_type, ('struct ', 'const ')) + assert return_ll_type.startswith( ( 'fz_', 'pdf_')) + for prefix in ( 'fz_', 'pdf_'): + if return_ll_type.startswith( prefix): + break + else: + assert 0, f'Unexpected arg type: {return_ll_type}' + return_extra = classes.classextras.get( tu, return_ll_type) + if not function_name_implies_kept_references( fnname): + do_keep = True + else: + if 'char' in return_ll_type.spelling: + jlib.log('### Function returns {cursor.result_type.spelling=} -> {return_ll_type.spelling=}: {fnname}. {function_name_implies_kept_references(fnname)=}') + if do_keep: + keepfn = f'{prefix}keep_{return_ll_type[ len(prefix):]}' + keepfn = rename.ll_fn( keepfn) + out.write( f'{return_type}( {keepfn}( ret))') + else: + out.write( f'{return_type}(ret)') + else: + out.write( 'ret') + sep = ', ' + for arg in parse.get_args( tu, cursor): + if not arg.out_param: + continue + if arg.alt: + name = util.clip( arg.alt.type.spelling, ('struct ', 'const ')) + for prefix in ( 'fz_', 'pdf_'): + if name.startswith( prefix): + break + else: + assert 0, f'Unexpected arg type: {name}' + if function_name_implies_kept_references( fnname): + out.write( f'{sep}{rename.class_(name)}( {arg.name_python})') + else: + keepfn = f'{prefix}keep_{name[ len(prefix):]}' + keepfn = rename.ll_fn( keepfn) + out.write( f'{sep}{rename.class_(name)}({keepfn}( {arg.name_python}))') + else: + out.write( f'{sep}{arg.name_python}') + sep = ', ' + out.write('\n') + out.write('\n') + + # foo.bar = foo_bar_outparams_fn + if structname: + out.write(f'{classname}.{rename.method(structname, cursor.spelling)} = {name_new}\n') + else: + out.write(f'{rename.fn( cursor.spelling)} = {name_new}\n') + out.write('\n') + out.write('\n') + + +def make_wrapper_comment( + tu, + cursor, + fnname, + fnname_wrapper, + indent, + is_method, + is_low_level, + ): + ret = io.StringIO() + def write(text): + text = text.replace('\n', f'\n{indent}') + ret.write( text) + + num_out_params = 0 + for arg in parse.get_args( + tu, + cursor, + include_fz_context=False, + skip_first_alt=is_method, + ): + if arg.out_param: + num_out_params += 1 + + if is_low_level: + write( f'Low-level wrapper for `{rename.c_fn(cursor.spelling)}()`.') + else: + write( f'Class-aware wrapper for `{rename.c_fn(cursor.spelling)}()`.') + if num_out_params: + tuple_size = num_out_params + if cursor.result_type.spelling != 'void': + tuple_size += 1 + write( f'\n') + write( f'\n') + write( f'This {"method" if is_method else "function"} has out-params. Python/C# wrappers look like:\n') + write( f' `{fnname_wrapper}(') + sep = '' + for arg in parse.get_args( tu, cursor, include_fz_context=False, skip_first_alt=is_method): + if arg.alt or not arg.out_param: + write( f'{sep}{declaration_text( arg.cursor.type, arg.name)}') + sep = ', ' + write(')` => ') + if tuple_size > 1: + write( '`(') + sep = '' + if cursor.result_type.spelling != 'void': + write( f'{cursor.result_type.spelling}') + sep = ', ' + for arg in parse.get_args( tu, cursor, include_fz_context=False, skip_first_alt=is_method): + if not arg.alt and arg.out_param: + write( f'{sep}{declaration_text( arg.cursor.type.get_pointee(), arg.name)}') + sep = ', ' + if tuple_size > 1: + write( ')`') + write( f'\n') + else: + write( ' ') + + return ret.getvalue() + + +def function_wrapper( + tu, + cursor, + fnname, + fnname_wrapper, + out_h, + out_cpp, + generated, + refcheck_if, + trace_if, + ): + ''' + Writes low-level C++ wrapper fn, converting any fz_try..fz_catch exception + into a C++ exception. + + cursor: + Clang cursor for function to wrap. + fnname: + Name of wrapped function. + fnname_wrapper: + Name of function to create. + out_h: + Stream to which we write header output. + out_cpp: + Stream to which we write cpp output. + generated: + A Generated instance. + refcheck_if: + A '#if*' statement that determines whether extra checks are compiled + in. + trace_if: + A '#if*' statement that determines whether runtime diagnostics are + compiled in. + + Example generated function: + + fz_band_writer * mupdf_new_band_writer_of_size(fz_context *ctx, size_t size, fz_output *out) + { + fz_band_writer * ret; + fz_try(ctx) { + ret = fz_new_band_writer_of_size(ctx, size, out); + } + fz_catch(ctx) { + mupdf_throw_exception(ctx); + } + return ret; + } + ''' + assert cursor.kind == state.clang.cindex.CursorKind.FUNCTION_DECL + if cursor.type.is_function_variadic() and fnname != 'fz_warn': + jlib.log( 'Not writing low-level wrapper because variadic: {fnname=}', 1) + return + + verbose = state.state_.show_details( fnname) + if verbose: + jlib.log( 'Wrapping {fnname}') + num_out_params = 0 + for arg in parse.get_args( tu, cursor, include_fz_context=True): + if parse.is_pointer_to(arg.cursor.type, 'fz_context'): + continue + if arg.out_param: + num_out_params += 1 + + # Write first line: <result_type> <fnname_wrapper> (<args>...) + # + comment = make_wrapper_comment( tu, cursor, fnname, fnname_wrapper, indent='', is_method=False, is_low_level=True) + comment = f'/** {comment}*/\n' + for out in out_h, out_cpp: + out.write( comment) + + # Copy any comment into .h file before declaration. + if cursor.raw_comment: + # On Windows, carriage returns can appear in cursor.raw_comment on + # due to line ending inconsistencies in our generated extra.cpp and + # extra.h, and can cause spurious differences in our generated C++ + # code, which in turn causes unnecessary rebuilds. + # + # It would probably better to fix line endings in our generation of + # extra.*. + raw_comment = cursor.raw_comment.replace('\r', '') + out_h.write(raw_comment) + if not raw_comment.endswith( '\n'): + out_h.write( '\n') + + # Write declaration and definition. + name_args_h = f'{fnname_wrapper}(' + name_args_cpp = f'{fnname_wrapper}(' + comma = '' + for arg in parse.get_args( tu, cursor, include_fz_context=True): + if verbose: + jlib.log( '{arg.cursor=} {arg.name=} {arg.separator=} {arg.alt=} {arg.out_param=}') + if parse.is_pointer_to(arg.cursor.type, 'fz_context'): + continue + decl = declaration_text( arg.cursor.type, arg.name, verbose=verbose) + if verbose: + jlib.log( '{decl=}') + name_args_h += f'{comma}{decl}' + decl = declaration_text( arg.cursor.type, arg.name) + name_args_cpp += f'{comma}{decl}' + comma = ', ' + + if cursor.type.is_function_variadic(): + name_args_h += f'{comma}...' + name_args_cpp += f'{comma}...' + + name_args_h += ')' + name_args_cpp += ')' + declaration_h = declaration_text( cursor.result_type, name_args_h, verbose=verbose) + declaration_cpp = declaration_text( cursor.result_type, name_args_cpp, verbose=verbose) + out_h.write( f'FZ_FUNCTION {declaration_h};\n') + out_h.write( '\n') + + # Write function definition. + # + out_cpp.write( f'FZ_FUNCTION {declaration_cpp}\n') + out_cpp.write( '{\n') + return_type = cursor.result_type.spelling + fncall = '' + fncall += f'{rename.c_fn(cursor.spelling)}(' + for arg in parse.get_args( tu, cursor, include_fz_context=True): + if parse.is_pointer_to( arg.cursor.type, 'fz_context'): + fncall += f'{arg.separator}auto_ctx' + else: + fncall += f'{arg.separator}{arg.name}' + fncall += ')' + make_fncall( tu, cursor, return_type, fncall, out_cpp, refcheck_if, trace_if) + out_cpp.write( '}\n') + out_cpp.write( '\n') + + if num_out_params: + make_outparam_helper( + tu, + cursor, + fnname, + fnname_wrapper, + generated, + ) + + +def make_namespace_open( namespace, out): + if namespace: + out.write( '\n') + out.write( f'namespace {namespace}\n') + out.write( '{\n') + + +def make_namespace_close( namespace, out): + if namespace: + out.write( '\n') + out.write( f'}} /* End of namespace {namespace}. */\n') + + +# libclang can't always find headers so we define our own `std::string` +# and `std::vector<>` that work well enough for the generation of the +# C++ API. +# +# We also define extra raw functions to aid SWIG-generated code. These +# are implemented in C++, and should be excluded from the generated +# windows_def file later on, otherwise we get link errors on Windows. +# +g_extra_declarations = textwrap.dedent(f''' + + #ifdef MUPDF_WRAP_LIBCLANG + + namespace std + {{ + template<typename T> + struct vector + {{ + }}; + + struct string + {{ + }}; + }} + + #else + + #include <string> + #include <vector> + + #endif + + #include "mupdf/fitz.h" + #include "mupdf/pdf.h" + + /** + C++ alternative to `fz_lookup_metadata()` that returns a `std::string` + or calls `fz_throw()` if not found. + */ + FZ_FUNCTION std::string fz_lookup_metadata2(fz_context* ctx, fz_document* doc, const char* key); + + /** + C++ alternative to `pdf_lookup_metadata()` that returns a `std::string` + or calls `fz_throw()` if not found. + */ + FZ_FUNCTION std::string pdf_lookup_metadata2(fz_context* ctx, pdf_document* doc, const char* key); + + /** + C++ alternative to `fz_md5_pixmap()` that returns the digest by value. + */ + FZ_FUNCTION std::vector<unsigned char> fz_md5_pixmap2(fz_context* ctx, fz_pixmap* pixmap); + + /** + C++ alternative to fz_md5_final() that returns the digest by value. + */ + FZ_FUNCTION std::vector<unsigned char> fz_md5_final2(fz_md5* md5); + + /** */ + FZ_FUNCTION long long fz_pixmap_samples_int(fz_context* ctx, fz_pixmap* pixmap); + + /** + Provides simple (but slow) access to pixmap data from Python and C#. + */ + FZ_FUNCTION int fz_samples_get(fz_pixmap* pixmap, int offset); + + /** + Provides simple (but slow) write access to pixmap data from Python and + C#. + */ + FZ_FUNCTION void fz_samples_set(fz_pixmap* pixmap, int offset, int value); + + /** + C++ alternative to fz_highlight_selection() that returns quads in a + std::vector. + */ + FZ_FUNCTION std::vector<fz_quad> fz_highlight_selection2(fz_context* ctx, fz_stext_page* page, fz_point a, fz_point b, int max_quads); + + struct fz_search_page2_hit + {{ + fz_quad quad; + int mark; + }}; + + /** + C++ alternative to fz_search_page() that returns information in a std::vector. + */ + FZ_FUNCTION std::vector<fz_search_page2_hit> fz_search_page2(fz_context* ctx, fz_document* doc, int number, const char* needle, int hit_max); + + /** + C++ alternative to fz_string_from_text_language() that returns information in a std::string. + */ + FZ_FUNCTION std::string fz_string_from_text_language2(fz_text_language lang); + + /** + C++ alternative to fz_get_glyph_name() that returns information in a std::string. + */ + FZ_FUNCTION std::string fz_get_glyph_name2(fz_context* ctx, fz_font* font, int glyph); + + /** + Extra struct containing fz_install_load_system_font_funcs()'s args, + which we wrap with virtual_fnptrs set to allow use from Python/C# via + Swig Directors. + */ + typedef struct fz_install_load_system_font_funcs_args + {{ + fz_load_system_font_fn* f; + fz_load_system_cjk_font_fn* f_cjk; + fz_load_system_fallback_font_fn* f_fallback; + }} fz_install_load_system_font_funcs_args; + + /** + Alternative to fz_install_load_system_font_funcs() that takes args in a + struct, to allow use from Python/C# via Swig Directors. + */ + FZ_FUNCTION void fz_install_load_system_font_funcs2(fz_context* ctx, fz_install_load_system_font_funcs_args* args); + + /** Internal singleton state to allow Swig Director class to find + fz_install_load_system_font_funcs_args class wrapper instance. */ + FZ_DATA extern void* fz_install_load_system_font_funcs2_state; + + /** Helper for calling `fz_document_handler::open` function pointer via + Swig from Python/C#. */ + FZ_FUNCTION fz_document* fz_document_handler_open(fz_context* ctx, const fz_document_handler *handler, fz_stream* stream, fz_stream* accel, fz_archive* dir, void* recognize_state); + + /** Helper for calling a `fz_document_handler::recognize` function + pointer via Swig from Python/C#. */ + FZ_FUNCTION int fz_document_handler_recognize(fz_context* ctx, const fz_document_handler *handler, const char *magic); + + /** Swig-friendly wrapper for pdf_choice_widget_options(), returns the + options directly in a vector. */ + FZ_FUNCTION std::vector<std::string> pdf_choice_widget_options2(fz_context* ctx, pdf_annot* tw, int exportval); + + /** Swig-friendly wrapper for fz_new_image_from_compressed_buffer(), + uses specified `decode` and `colorkey` if they are not null (in which + case we assert that they have size `2*fz_colorspace_n(colorspace)`). */ + FZ_FUNCTION fz_image* fz_new_image_from_compressed_buffer2( + fz_context* ctx, + int w, + int h, + int bpc, + fz_colorspace* colorspace, + int xres, + int yres, + int interpolate, + int imagemask, + const std::vector<float>& decode, + const std::vector<int>& colorkey, + fz_compressed_buffer* buffer, + fz_image* mask + ); + + /** Swig-friendly wrapper for pdf_rearrange_pages(). */ + void pdf_rearrange_pages2( + fz_context* ctx, + pdf_document* doc, + const std::vector<int>& pages, + pdf_clean_options_structure structure + ); + + /** Swig-friendly wrapper for pdf_subset_fonts(). */ + void pdf_subset_fonts2(fz_context *ctx, pdf_document *doc, const std::vector<int>& pages); + + /** Swig-friendly and typesafe way to do fz_snprintf(fmt, value). `fmt` + must end with one of 'efg' otherwise we throw an exception. */ + std::string fz_format_double(fz_context* ctx, const char* fmt, double value); + + struct fz_font_ucs_gid + {{ + unsigned long ucs; + unsigned int gid; + }}; + + /** SWIG-friendly wrapper for fz_enumerate_font_cmap(). */ + std::vector<fz_font_ucs_gid> fz_enumerate_font_cmap2(fz_context* ctx, fz_font* font); + + /** SWIG-friendly wrapper for pdf_set_annot_callout_line(). */ + void pdf_set_annot_callout_line2(fz_context *ctx, pdf_annot *annot, std::vector<fz_point>& callout); + + /** SWIG-friendly wrapper for fz_decode_barcode_from_display_list(), + avoiding leak of the returned string. */ + std::string fz_decode_barcode_from_display_list2(fz_context *ctx, fz_barcode_type *type, fz_display_list *list, fz_rect subarea, int rotate); + + /** SWIG-friendly wrapper for fz_decode_barcode_from_pixmap(), avoiding + leak of the returned string. */ + std::string fz_decode_barcode_from_pixmap2(fz_context *ctx, fz_barcode_type *type, fz_pixmap *pix, int rotate); + + /** SWIG-friendly wrapper for fz_decode_barcode_from_page(), avoiding + leak of the returned string. */ + std::string fz_decode_barcode_from_page2(fz_context *ctx, fz_barcode_type *type, fz_page *page, fz_rect subarea, int rotate); + ''') + +g_extra_definitions = textwrap.dedent(f''' + + FZ_FUNCTION std::string fz_lookup_metadata2( fz_context* ctx, fz_document* doc, const char* key) + {{ + /* Find length first. */ + int e = fz_lookup_metadata(ctx, doc, key, NULL /*buf*/, 0 /*size*/); + if (e < 0) + {{ + fz_throw(ctx, FZ_ERROR_GENERIC, "key not found: %s", key); + }} + assert(e != 0); + char* buf = (char*) fz_malloc(ctx, e); + int e2 = fz_lookup_metadata(ctx, doc, key, buf, e); + assert(e2 = e); + std::string ret = buf; + free(buf); + return ret; + }} + + FZ_FUNCTION std::string pdf_lookup_metadata2( fz_context* ctx, pdf_document* doc, const char* key) + {{ + /* Find length first. */ + int e = pdf_lookup_metadata(ctx, doc, key, NULL /*buf*/, 0 /*size*/); + if (e < 0) + {{ + fz_throw(ctx, FZ_ERROR_GENERIC, "key not found: %s", key); + }} + assert(e != 0); + char* buf = (char*) fz_malloc(ctx, e); + int e2 = pdf_lookup_metadata(ctx, doc, key, buf, e); + assert(e2 = e); + std::string ret = buf; + free(buf); + return ret; + }} + + FZ_FUNCTION std::vector<unsigned char> fz_md5_pixmap2(fz_context* ctx, fz_pixmap* pixmap) + {{ + std::vector<unsigned char> ret(16); + fz_md5_pixmap( ctx, pixmap, &ret[0]); + return ret; + }} + + FZ_FUNCTION long long fz_pixmap_samples_int(fz_context* ctx, fz_pixmap* pixmap) + {{ + long long ret = (intptr_t) pixmap->samples; + return ret; + }} + + FZ_FUNCTION int fz_samples_get(fz_pixmap* pixmap, int offset) + {{ + return pixmap->samples[offset]; + }} + + FZ_FUNCTION void fz_samples_set(fz_pixmap* pixmap, int offset, int value) + {{ + pixmap->samples[offset] = value; + }} + + FZ_FUNCTION std::vector<unsigned char> fz_md5_final2(fz_md5* md5) + {{ + std::vector<unsigned char> ret(16); + fz_md5_final( md5, &ret[0]); + return ret; + }} + + FZ_FUNCTION std::vector<fz_quad> fz_highlight_selection2(fz_context* ctx, fz_stext_page* page, fz_point a, fz_point b, int max_quads) + {{ + {{ + std::vector<fz_quad> ret(max_quads); + int n; + fz_try(ctx) + {{ + n = fz_highlight_selection(ctx, page, a, b, &ret[0], max_quads); + }} + fz_catch(ctx) + {{ + n = -1; + }} + if (n >= 0) + {{ + ret.resize(n); + return ret; + }} + }} + /* We are careful to only call `fz_throw()` after `ret`'s + destructor has been called. */ + fz_throw(ctx, FZ_ERROR_GENERIC, "fz_highlight_selection() failed"); + }} + + FZ_FUNCTION std::vector<fz_search_page2_hit> fz_search_page2(fz_context* ctx, fz_document* doc, int number, const char* needle, int hit_max) + {{ + std::vector<fz_quad> quads(hit_max); + std::vector<int> marks(hit_max); + int n = fz_search_page_number(ctx, doc, number, needle, &marks[0], &quads[0], hit_max); + std::vector<fz_search_page2_hit> ret(n); + for (int i=0; i<n; ++i) + {{ + ret[i].quad = quads[i]; + ret[i].mark = marks[i]; + }} + return ret; + }} + + FZ_FUNCTION std::string fz_string_from_text_language2(fz_text_language lang) + {{ + char str[8]; + fz_string_from_text_language(str, lang); + return std::string(str); + }} + + FZ_FUNCTION std::string fz_get_glyph_name2(fz_context* ctx, fz_font* font, int glyph) + {{ + char name[32]; + fz_get_glyph_name(ctx, font, glyph, name, sizeof(name)); + return std::string(name); + }} + + void fz_install_load_system_font_funcs2(fz_context* ctx, fz_install_load_system_font_funcs_args* args) + {{ + fz_install_load_system_font_funcs(ctx, args->f, args->f_cjk, args->f_fallback); + }} + + void* fz_install_load_system_font_funcs2_state = nullptr; + + FZ_FUNCTION fz_document* fz_document_handler_open(fz_context* ctx, const fz_document_handler *handler, fz_stream* stream, fz_stream* accel, fz_archive* dir, void* recognize_state) + {{ + return handler->open(ctx, handler, stream, accel, dir, recognize_state); + }} + + FZ_FUNCTION int fz_document_handler_recognize(fz_context* ctx, const fz_document_handler *handler, const char *magic) + {{ + return handler->recognize(ctx, handler, magic); + }} + + FZ_FUNCTION std::vector<std::string> pdf_choice_widget_options2(fz_context* ctx, pdf_annot* tw, int exportval) + {{ + int n = pdf_choice_widget_options(ctx, tw, exportval, nullptr); + std::vector<const char*> opts(n); + int n2 = pdf_choice_widget_options(ctx, tw, exportval, &opts[0]); + assert(n2 == n); + std::vector<std::string> ret(n); + for (int i=0; i<n; ++i) + {{ + ret[i] = opts[i]; + }} + return ret; + }} + + FZ_FUNCTION fz_image* fz_new_image_from_compressed_buffer2( + fz_context* ctx, + int w, + int h, + int bpc, + fz_colorspace* colorspace, + int xres, + int yres, + int interpolate, + int imagemask, + const std::vector<float>& decode, + const std::vector<int>& colorkey, + fz_compressed_buffer* buffer, + fz_image* mask + ) + {{ + int n = fz_colorspace_n(ctx, colorspace); + assert(decode.empty() || decode.size() == 2 * n); + assert(colorkey.empty() || colorkey.size() == 2 * n); + const float* decode2 = decode.empty() ? nullptr : &decode[0]; + const int* colorkey2 = colorkey.empty() ? nullptr : &colorkey[0]; + fz_image* ret = fz_new_image_from_compressed_buffer( + ctx, + w, + h, + bpc, + colorspace, + xres, + yres, + interpolate, + imagemask, + decode2, + colorkey2, + fz_keep_compressed_buffer(ctx, buffer), + mask + ); + return ret; + }} + + void pdf_rearrange_pages2( + fz_context* ctx, + pdf_document* doc, + const std::vector<int>& pages, + pdf_clean_options_structure structure + ) + {{ + return pdf_rearrange_pages(ctx, doc, pages.size(), &pages[0], structure); + }} + + void pdf_subset_fonts2(fz_context *ctx, pdf_document *doc, const std::vector<int>& pages) + {{ + return pdf_subset_fonts(ctx, doc, pages.size(), &pages[0]); + }} + + static void s_format_check(fz_context* ctx, const char* fmt, const char* specifiers) + {{ + int length = strlen(fmt); + if (!length || !strchr(specifiers, fmt[length-1])) + {{ + fz_throw(ctx, FZ_ERROR_ARGUMENT, "Incorrect fmt '%s' should end with one of '%s'.", fmt, specifiers); + }} + }} + + std::string fz_format_double(fz_context* ctx, const char* fmt, double value) + {{ + char buffer[256]; + s_format_check(ctx, fmt, "efg"); + fz_snprintf(buffer, sizeof(buffer), fmt, value); + return buffer; + }} + + static void fz_enumerate_font_cmap2_cb(fz_context* ctx, void* opaque, unsigned long ucs, unsigned int gid) + {{ + std::vector<fz_font_ucs_gid>& ret = *(std::vector<fz_font_ucs_gid>*) opaque; + fz_font_ucs_gid item = {{ucs, gid}}; + ret.push_back(item); + }} + + std::vector<fz_font_ucs_gid> fz_enumerate_font_cmap2(fz_context* ctx, fz_font* font) + {{ + std::vector<fz_font_ucs_gid> ret; + fz_enumerate_font_cmap(ctx, font, fz_enumerate_font_cmap2_cb, &ret); + return ret; + }} + + void pdf_set_annot_callout_line2(fz_context *ctx, pdf_annot *annot, std::vector<fz_point>& callout) + {{ + pdf_set_annot_callout_line(ctx, annot, &callout[0], callout.size()); + }} + + std::string fz_decode_barcode_from_display_list2(fz_context *ctx, fz_barcode_type *type, fz_display_list *list, fz_rect subarea, int rotate) + {{ + char* ret = fz_decode_barcode_from_display_list(ctx, type, list, subarea, rotate); + std::string ret2 = ret; + fz_free(ctx, ret); + return ret2; + }} + + std::string fz_decode_barcode_from_pixmap2(fz_context *ctx, fz_barcode_type *type, fz_pixmap *pix, int rotate) + {{ + char* ret = fz_decode_barcode_from_pixmap(ctx, type, pix, rotate); + std::string ret2 = ret; + fz_free(ctx, ret); + return ret2; + }} + + std::string fz_decode_barcode_from_page2(fz_context *ctx, fz_barcode_type *type, fz_page *page, fz_rect subarea, int rotate) + {{ + char* ret = fz_decode_barcode_from_page(ctx, type, page, subarea, rotate); + std::string ret2 = ret; + fz_free(ctx, ret); + return ret2; + }} + ''') + +def make_extra( out_extra_h, out_extra_cpp): + ''' + We write extra abstractions here. + + These are written in C++ but are at the same level of abstraction as MuPDF + C functions, for example they take `fz_context` args. This is done so that + we automatically generate wrappers as class methods as well as global + functions. + ''' + out_extra_h.write( g_extra_declarations) + + out_extra_cpp.write( textwrap.dedent(''' + #include "mupdf/extra.h" + + ''')) + out_extra_cpp.write( g_extra_definitions) + + +def make_internal_functions( namespace, out_h, out_cpp, refcheck_if, trace_if): + ''' + Writes internal support functions. + + out_h: + Stream to which we write C++ header text. + out_cpp: + Stream to which we write C++ text. + ''' + out_h.write( + textwrap.dedent( + f''' + #define internal_assert(expression) (expression) ? (void) 0 : internal_assert_fail(__FILE__, __LINE__, __FUNCTION__, #expression) + FZ_FUNCTION void internal_assert_fail(const char* file, int line, const char* fn, const char* expression); + + /** Internal use only. Looks at environmental variable <name>; returns 0 if unset else int value. */ + FZ_FUNCTION int {rename.internal('env_flag')}(const char* name); + + /** Internal use only. Looks at environmental variable <name>; returns 0 if unset else int value. */ + FZ_FUNCTION int {rename.internal('env_flag_check_unset')}( const char* if_, const char* name); + + /** Internal use only. Returns `fz_context*` for use by current thread. */ + FZ_FUNCTION fz_context* {rename.internal('context_get')}(); + ''' + )) + + out_cpp.write( + textwrap.dedent( + ''' + #include "mupdf/exceptions.h" + #include "mupdf/internal.h" + + #include <iostream> + #include <thread> + #include <mutex> + + #include <string.h> + + ''')) + + make_namespace_open( namespace, out_cpp) + + state_t = rename.internal( 'state') + thread_state_t = rename.internal( 'thread_state') + + cpp_text = textwrap.dedent( + f''' + FZ_FUNCTION void internal_assert_fail(const char* file, int line, const char* fn, const char* expression) + {{ + std::cerr << file << ":" << line << ":" << fn << "(): " + << "MuPDF C++ internal assert failure: " << expression + << "\\n" << std::flush; + abort(); + }} + + FZ_FUNCTION int {rename.internal('env_flag')}(const char* name) + {{ + const char* s = getenv( name); + if (!s) return 0; + return atoi( s); + }} + + FZ_FUNCTION int {rename.internal('env_flag_check_unset')}(const char* if_, const char* name) + {{ + const char* s = getenv( name); + if (s) std::cerr << __FILE__ << ":" << __LINE__ << ":" << __FUNCTION__ << "():" + << " Warning: ignoring environmental variable because" + << " '" << if_ << "' is false: " << name << "\\n"; + return false; + }} + + {trace_if} + static const int s_trace = mupdf::internal_env_flag("MUPDF_trace"); + #else + static const int s_trace = mupdf::internal_env_flag_check_unset("{trace_if}", "MUPDF_trace"); + #endif + + static bool s_state_valid = false; + + struct {rename.internal("state")} + {{ + /* Constructor. */ + {rename.internal("state")}() + {{ + m_locks.user = this; + m_locks.lock = lock; + m_locks.unlock = unlock; + m_ctx = nullptr; + bool multithreaded = true; + const char* s = getenv( "MUPDF_mt_ctx"); + if ( s && !strcmp( s, "0")) multithreaded = false; + reinit( multithreaded); + s_state_valid = true; + }} + + void reinit( bool multithreaded) + {{ + if (s_trace) + {{ + std::cerr << __FILE__ << ":" << __LINE__ << ":" << __FUNCTION__ << "(): " + << " calling fz_drop_context()\\n"; + }} + fz_drop_context( m_ctx); + m_multithreaded = multithreaded; + if (s_trace) + {{ + std::cerr << __FILE__ << ":" << __LINE__ << ":" << __FUNCTION__ << "(): " + << " calling fz_new_context()\\n"; + }} + m_ctx = fz_new_context(NULL /*alloc*/, (multithreaded) ? &m_locks : nullptr, FZ_STORE_DEFAULT); + if (s_trace) + {{ + std::cerr << __FILE__ << ":" << __LINE__ << ":" << __FUNCTION__ << "(): " + << "fz_new_context() => " << m_ctx << "\\n"; + }} + if (s_trace) + {{ + std::cerr << __FILE__ << ":" << __LINE__ << ":" << __FUNCTION__ << "(): " + << " calling fz_register_document_handlers()\\n"; + }} + internal_assert("m_ctx = fz_new_context()" && m_ctx); + fz_register_document_handlers(m_ctx); + }} + static void lock(void *user, int lock) + {{ + {rename.internal("state")}* self = ({rename.internal("state")}*) user; + internal_assert( self->m_multithreaded); + self->m_mutexes[lock].lock(); + }} + static void unlock(void *user, int lock) + {{ + {rename.internal("state")}* self = ({rename.internal("state")}*) user; + internal_assert( self->m_multithreaded); + self->m_mutexes[lock].unlock(); + }} + ~{rename.internal("state")}() + {{ + if (s_trace) + {{ + std::cerr << __FILE__ << ":" << __LINE__ << ":" << __FUNCTION__ << "(): " + << " calling fz_drop_context()\\n"; + }} + fz_drop_context(m_ctx); + m_ctx = nullptr; + s_state_valid = false; + }} + + bool m_multithreaded; + fz_context* m_ctx; + std::mutex m_mutex; /* Serialise access to m_ctx. fixme: not actually necessary. */ + + /* Provide thread support to mupdf. */ + std::mutex m_mutexes[FZ_LOCK_MAX]; + fz_locks_context m_locks; + }}; + + static {rename.internal("state")} s_state; + + struct {rename.internal("thread_state")} + {{ + {rename.internal("thread_state")}() + : + m_ctx( nullptr), + m_constructed( true) + {{}} + fz_context* get_context() + {{ + internal_assert( s_state.m_multithreaded); + + /* The following code checks that we are not being called after + we have been destructed. This can happen if global mupdf + wrapper class instances are defined - thread-local objects + are destructed /before/ globals. */ + if (!m_constructed) + {{ + std::cerr << __FILE__ << ":" << __LINE__ << ":" << __FUNCTION__ << ":\\n" + << "*** Error - undefined behaviour.\\n" + << "***\\n" + << "*** Attempt to get thread-local fz_context after destruction\\n" + << "*** of thread-local fz_context support instance.\\n" + << "***\\n" + << "*** This is undefined behaviour.\\n" + << "***\\n" + << "*** This can happen if mupdf wrapper class instances are\\n" + << "*** created as globals, because in C++ global object\\n" + << "*** destructors are run after thread_local destructors.\\n" + << "***\\n" + ; + }} + internal_assert( m_constructed); + if (!m_ctx) + {{ + /* Make a context for this thread by cloning the global + context. */ + /* fixme: we don't actually need to take a lock here. */ + std::lock_guard<std::mutex> lock( s_state.m_mutex); + if (s_trace) + {{ + std::cerr << __FILE__ << ":" << __LINE__ << ":" << __FUNCTION__ << "(): " + << " calling fz_clone_context()\\n"; + }} + internal_assert(s_state_valid); + m_ctx = fz_clone_context(s_state.m_ctx); + if (s_trace) + {{ + std::cerr << __FILE__ << ":" << __LINE__ << ":" << __FUNCTION__ << "(): " + << "fz_clone_context(" << s_state.m_ctx << ") => " << m_ctx << "\\n"; + }} + internal_assert("m_ctx = fz_clone_context()" && m_ctx); + }} + return m_ctx; + }} + ~{rename.internal("thread_state")}() + {{ + if (m_ctx) + {{ + internal_assert( s_state.m_multithreaded); + if (s_trace) + {{ + std::cerr << __FILE__ << ":" << __LINE__ << ":" << __FUNCTION__ << "(): " + << " calling fz_drop_context()\\n"; + }} + fz_drop_context( m_ctx); + }} + + /* These two statements are an attempt to get useful + diagnostics in cases of undefined behaviour caused by the + use of global wrapper class instances, whose destructors + will be called /after/ destruction of this thread-local + internal_thread_state instance. See check of m_constructed in + get_context(). + + This probably only works in non-optimised builds - + optimisation will simply elide both these statements. */ + m_ctx = nullptr; + m_constructed = false; + }} + fz_context* m_ctx; + bool m_constructed; + }}; + + static thread_local {rename.internal("thread_state")} s_thread_state; + + FZ_FUNCTION fz_context* {rename.internal("context_get")}() + {{ + if (s_state.m_multithreaded) + {{ + return s_thread_state.get_context(); + }} + else + {{ + /* This gives a small improvement in performance for + single-threaded use, e.g. from 552.4s to 548.1s. */ + internal_assert(s_state_valid); + fz_context* ret = s_state.m_ctx; + internal_assert(ret); + return ret; + }} + }} + + FZ_FUNCTION void reinit_singlethreaded() + {{ + if (0) + {{ + std::cerr << __FILE__ << ":" << __LINE__ << ":" << __FUNCTION__ << "(): Reinitialising as single-threaded.\\n"; + }} + s_state.reinit( false /*multithreaded*/); + }} + ''') + out_cpp.write( cpp_text) + + make_namespace_close( namespace, out_cpp) + + # Generate code that exposes C++ operator new/delete to Memento. + # + # Disabled because our generated code makes very few direct calls + # to operator new, and Memento ends up catching lots of (presumably + # false-positive) leaks in the Python interpreter, so isn't very useful. + # + if 0: + out_cpp.write( textwrap.dedent( + ''' + #ifdef MEMENTO + + void* operator new( size_t size) + { + return Memento_cpp_new( size); + } + + void operator delete( void* pointer) + { + Memento_cpp_delete( pointer); + } + + void* operator new[]( size_t size) + { + return Memento_cpp_new_array( size); + } + + void operator delete[]( void* pointer) + { + Memento_cpp_delete_array( pointer); + } + + #endif + ''' + )) + + +def make_function_wrappers( + tu, + namespace, + out_exceptions_h, + out_exceptions_cpp, + out_functions_h, + out_functions_cpp, + out_internal_h, + out_internal_cpp, + out_functions_h2, + out_functions_cpp2, + generated, + refcheck_if, + trace_if, + ): + ''' + Generates C++ source code containing wrappers for all fz_*() functions. + + We also create a function throw_exception(fz_context* ctx) that throws a + C++ exception appropriate for the error in ctx. + + If a function has first arg fz_context*, extra code is generated that + converts fz_try..fz_catch exceptions into C++ exceptions by calling + throw_exception(). + + We remove any fz_context* argument and the implementation calls + internal_get_context() to get a suitable thread-specific fz_context* to + use. + + We generate a class for each exception type. + + Returned source is just the raw functions text, e.g. it does not contain + required #include's. + + Args: + tu: + Clang translation unit. + out_exceptions_h: + Stream to which we write exception class definitions. + out_exceptions_cpp: + Stream to which we write exception class implementation. + out_functions_h: + Stream to which we write function declarations. + out_functions_cpp: + Stream to which we write function definitions. + generated: + A Generated instance. + ''' + # Look for FZ_ERROR_* enums. We generate an exception class for each of + # these. + # + error_name_prefix = 'FZ_ERROR_' + fz_error_names = [] + fz_error_names_maxlen = 0 # Used for padding so generated code aligns. + + for cursor in parse.get_children(tu.cursor): + if cursor.kind == state.clang.cindex.CursorKind.ENUM_DECL: + #log( 'enum: {cursor.spelling=}) + for child in parse.get_members( cursor): + #log( 'child:{ child.spelling=}) + if child.spelling.startswith( error_name_prefix): + name = child.spelling[ len(error_name_prefix):] + fz_error_names.append( name) + if len( name) > fz_error_names_maxlen: + fz_error_names_maxlen = len( name) + + def errors(include_error_base=False): + ''' + Yields (enum, typename, padding) for each error. + E.g.: + enum=FZ_ERROR_SYSTEM + typename=mupdf_error_memory + padding=' ' + ''' + names = fz_error_names + if include_error_base: + names = ['BASE'] + names + for name in names: + enum = f'{error_name_prefix}{name}' + typename = rename.error_class( enum) + padding = (fz_error_names_maxlen - len(name)) * ' ' + yield enum, typename, padding + + # Declare base exception class and define its methods. + # + base_name = rename.error_class('FZ_ERROR_BASE') + + out_exceptions_h.write( textwrap.dedent( + f''' + /** Base class for exceptions. */ + struct {base_name} : std::exception + {{ + int m_code; + std::string m_text; + mutable std::string m_what; + FZ_FUNCTION const char* what() const throw(); + FZ_FUNCTION {base_name}(int code, const char* text); + }}; + ''')) + + out_exceptions_cpp.write( textwrap.dedent( + f''' + FZ_FUNCTION {base_name}::{base_name}(int code, const char* text) + : + m_code(code), + m_text(text) + {{ + {trace_if} + if (s_trace_exceptions) + {{ + std::cerr << __FILE__ << ":" << __LINE__ << ":" << __FUNCTION__ << "(): {base_name}: " << m_text << "\\n"; + }} + #endif + }}; + + FZ_FUNCTION const char* {base_name}::what() const throw() + {{ + m_what = "code=" + std::to_string(m_code) + ": " + m_text; + return m_what.c_str(); + }}; + + ''')) + + # Generate SWIG Python code to allow conversion of our error class + # exceptions into equivalent Python exceptions. + error_classes_n = 0 + for enum, typename, padding in errors(): + error_classes_n += 1 + + error_classes_n += 1 # Extra space for FzErrorBase. + generated.swig_python_exceptions.write( textwrap.dedent( f''' + + void internal_set_error_classes(PyObject* classes); + + %{{ + /* A Python list of Error classes, [FzErrorNone, FzErrorMemory, FzErrorGeneric, ...]. */ + static PyObject* s_error_classes[{error_classes_n}] = {{}}; + + /* Called on startup by mupdf.py, with a list of error classes + to be copied into s_error_classes. This will allow us to create + instances of these error classes in SWIG's `%exception ...`, so + Python code will see exceptions as instances of Python error + classes. */ + void internal_set_error_classes(PyObject* classes) + {{ + assert(PyList_Check(classes)); + int n = PyList_Size(classes); + assert(n == {error_classes_n}); + for (int i=0; i<n; ++i) + {{ + PyObject* class_ = PyList_GetItem(classes, i); + s_error_classes[i] = class_; + }} + }} + + /* Sets Python exception to a new mupdf.<name> object constructed + with `text`. */ + void set_exception(PyObject* class_, int code, const std::string& text) + {{ + PyObject* args = Py_BuildValue("(s)", text.c_str()); + PyObject* instance = PyObject_CallObject(class_, args); + PyErr_SetObject(class_, instance); + Py_XDECREF(instance); + Py_XDECREF(args); + }} + + /* Exception handler for swig-generated code. Uses internal + `throw;` to recover the current C++ exception then uses + `set_exception()` to set the current Python exception. Caller + should do `SWIG_fail;` after we return. */ + void handle_exception() + {{ + try + {{ + throw; + }} + ''' + )) + + # Declare exception class for each FZ_ERROR_*. Also append catch blocks for + # each of these exception classes to `handle_exception()`. + # + for i, (enum, typename, padding) in enumerate(errors()): + out_exceptions_h.write( textwrap.dedent( + f''' + /** For `{enum}`. */ + struct {typename} : {base_name} + {{ + FZ_FUNCTION {typename}(const char* message); + }}; + + ''')) + + generated.swig_python_exceptions.write( textwrap.dedent( f''' + /**/ + catch (mupdf::{typename}& e) + {{ + if (g_mupdf_trace_exceptions) + {{ + std::cerr + << __FILE__ << ':' << __LINE__ << ':' + #ifndef _WIN32 + << __PRETTY_FUNCTION__ << ':' + #endif + << " Converting C++ std::exception mupdf::{typename} ({i=}) into Python exception:\\n" + << " e.m_code: " << e.m_code << "\\n" + << " e.m_text: " << e.m_text << "\\n" + << " e.what(): " << e.what() << "\\n" + << " typeid(e).name(): " << typeid(e).name() << "\\n" + << "\\n"; + }} + set_exception(s_error_classes[{i}], e.m_code, e.m_text); + + }}''')) + + # Append less specific exception handling. + generated.swig_python_exceptions.write( textwrap.dedent( f''' + catch (mupdf::FzErrorBase& e) + {{ + if (g_mupdf_trace_exceptions) + {{ + std::cerr + << __FILE__ << ':' << __LINE__ << ':' + #ifndef _WIN32 + << __PRETTY_FUNCTION__ << ':' + #endif + << " Converting C++ std::exception mupdf::FzErrorBase ({error_classes_n-1=}) into Python exception:\\n" + << " e.m_code: " << e.m_code << "\\n" + << " e.m_text: " << e.m_text << "\\n" + << " e.what(): " << e.what() << "\\n" + << " typeid(e).name(): " << typeid(e).name() << "\\n" + << "\\n"; + }} + PyObject* class_ = s_error_classes[{error_classes_n-1}]; + PyObject* args = Py_BuildValue("is", e.m_code, e.m_text.c_str()); + PyObject* instance = PyObject_CallObject(class_, args); + PyErr_SetObject(class_, instance); + Py_XDECREF(instance); + Py_XDECREF(args); + }} + catch (std::exception& e) + {{ + if (g_mupdf_trace_exceptions) + {{ + std::cerr + << __FILE__ << ':' << __LINE__ << ':' + #ifndef _WIN32 + << __PRETTY_FUNCTION__ << ':' + #endif + << " Converting C++ std::exception into Python exception: " + << e.what() + << " typeid(e).name(): " << typeid(e).name() << "\\n" + << "\\n"; + }} + SWIG_Error(SWIG_RuntimeError, e.what()); + + }} + catch (...) + {{ + if (g_mupdf_trace_exceptions) + {{ + std::cerr + << __FILE__ << ':' << __LINE__ << ':' + #ifndef _WIN32 + << __PRETTY_FUNCTION__ << ':' + #endif + << " Converting unknown C++ exception into Python exception." + << "\\n"; + }} + SWIG_Error(SWIG_RuntimeError, "Unknown exception"); + }} + }} + + %}} + + %exception + {{ + try + {{ + $action + }} + catch (...) + {{ + handle_exception(); + SWIG_fail; + }} + }} + ''')) + + generated.swig_python_set_error_classes.write( f'# Define __str()__ for each error/exception class, to use self.what().\n') + for enum, typename, padding in errors(include_error_base=1): + generated.swig_python_set_error_classes.write( f'{typename}.__str__ = lambda self: self.what()\n') + + generated.swig_python_set_error_classes.write( textwrap.dedent( f''' + # This must be after the declaration of mupdf::FzError* + # classes in mupdf/exceptions.h and declaration of + # `internal_set_error_classes()`, otherwise generated code is + # before the declaration of the Python class or similar. */ + internal_set_error_classes([ + ''')) + for enum, typename, padding in errors(): + generated.swig_python_set_error_classes.write(f' {typename},\n') + generated.swig_python_set_error_classes.write( textwrap.dedent( f''' + FzErrorBase, + ]) + ''')) + + # Define constructor for each exception class. + # + for enum, typename, padding in errors(): + out_exceptions_cpp.write( textwrap.dedent( + f''' + FZ_FUNCTION {typename}::{typename}(const char* text) + : {base_name}({enum}, text) + {{ + {trace_if} + if (s_trace_exceptions) + {{ + std::cerr << __FILE__ << ":" << __LINE__ << ":" << __FUNCTION__ << "(): {typename} constructor, text: " << m_text << "\\n"; + }} + #endif + }} + + ''')) + + # Generate function that throws an appropriate exception from a fz_context. + # + throw_exception = rename.internal( 'throw_exception') + out_exceptions_h.write( textwrap.dedent( + f''' + /** Throw exception appropriate for error in `ctx`. */ + FZ_FUNCTION void {throw_exception}(fz_context* ctx); + + ''')) + out_exceptions_cpp.write( textwrap.dedent( + f''' + FZ_FUNCTION void {throw_exception}(fz_context* ctx) + {{ + int code; + const char* text = fz_convert_error(ctx, &code); + {trace_if} + if (s_trace_exceptions) + {{ + std::cerr << __FILE__ << ":" << __LINE__ << ":" << __FUNCTION__ << "(): code=" << code << "\\n"; + std::cerr << __FILE__ << ":" << __LINE__ << ":" << __FUNCTION__ << "(): text=" << text << "\\n"; + }} + #endif + ''')) + for enum, typename, padding in errors(): + out_exceptions_cpp.write( f' if (code == {enum}) {padding}throw {typename}{padding}(text);\n') + out_exceptions_cpp.write( f' throw {base_name}(code, text);\n') + out_exceptions_cpp.write( f'}}\n') + out_exceptions_cpp.write( '\n') + + make_internal_functions( namespace, out_internal_h, out_internal_cpp, refcheck_if, trace_if) + + # Generate wrappers for each function that we find. + # + functions = [] + for fnname, cursor in state.state_.find_functions_starting_with( tu, ('fz_', 'pdf_'), method=False): + assert fnname not in state.omit_fns + #jlib.log( '{fnname=} {cursor.spelling=} {cursor.type.spelling=}') + if ( cursor.type == state.clang.cindex.TypeKind.FUNCTIONPROTO + and cursor.type.is_function_variadic() + ): + # We don't attempt to wrap variadic functions - would need to find + # the equivalent function that takes a va_list. + if 0: + jlib.log( 'Variadic fn: {cursor.type.spelling=}') + if fnname != 'fz_warn': + continue + if fnname == 'fz_push_try': + # This is partof implementation of fz_try/catch so doesn't make + # sense to provide a wrapper. Also it is OS-dependent so including + # it makes our generated code OS-specific. + continue + + functions.append( (fnname, cursor)) + + jlib.log1( '{len(functions)=}') + + # Sort by function-name to make output easier to read. + functions.sort() + for fnname, cursor in functions: + if state.state_.show_details( fnname): + jlib.log( 'Looking at {fnname}') + fnname_wrapper = rename.ll_fn( fnname) + function_wrapper( + tu, + cursor, + fnname, + fnname_wrapper, + out_functions_h, + out_functions_cpp, + generated, + refcheck_if, + trace_if, + ) + if not fnname.startswith( ( 'fz_keep_', 'fz_drop_', 'pdf_keep_', 'pdf_drop_')): + function_wrapper_class_aware( + tu, + register_fn_use=None, + struct_name=None, + class_name=None, + fn_cursor=cursor, + refcheck_if=refcheck_if, + trace_if=trace_if, + fnname=fnname, + out_h=out_functions_h2, + out_cpp=out_functions_cpp2, + generated=generated, + ) + + python.cppyy_add_outparams_wrapper( tu, fnname, cursor, state.state_, generated) + + if fnname == "pdf_load_field_name": #(fz_context *ctx, pdf_obj *field); + # Output wrapper that returns std::string instead of buffer that + # caller needs to free. + out_functions_h.write( + textwrap.dedent( + f''' + /** Alternative to `{rename.ll_fn('pdf_load_field_name')}()` that returns a std::string. */ + FZ_FUNCTION std::string {rename.ll_fn('pdf_load_field_name2')}(pdf_obj* field); + + ''')) + out_functions_cpp.write( + textwrap.dedent( + f''' + FZ_FUNCTION std::string {rename.ll_fn('pdf_load_field_name2')}(pdf_obj* field) + {{ + char* buffer = {rename.ll_fn('pdf_load_field_name')}( field); + std::string ret( buffer); + {rename.ll_fn('fz_free')}( buffer); + return ret; + }} + ''')) + out_functions_h2.write( + textwrap.indent( + textwrap.dedent( + f''' + /** Alternative to `{rename.fn('pdf_load_field_name')}()` that returns a std::string. */ + FZ_FUNCTION std::string {rename.fn('pdf_load_field_name2')}({rename.class_('pdf_obj')}& field); + '''), + ' ', + ) + ) + out_functions_cpp2.write( + textwrap.dedent( + f''' + FZ_FUNCTION std::string {rename.fn('pdf_load_field_name2')}({rename.class_('pdf_obj')}& field) + {{ + return {rename.ll_fn('pdf_load_field_name2')}( field.m_internal); + }} + ''')) + + # Output custom wrappers for variadic pdf_dict_getl(). + # + + decl = f'''FZ_FUNCTION pdf_obj* {rename.ll_fn('pdf_dict_getlv')}( pdf_obj* dict, va_list keys)''' + out_functions_h.write( textwrap.dedent( f''' + /* Low-level wrapper for `pdf_dict_getl()`. `keys` must be null-terminated list of `pdf_obj*`'s. */ + {decl}; + ''')) + out_functions_cpp.write( textwrap.dedent( f''' + {decl} + {{ + pdf_obj *key; + while (dict != NULL && (key = va_arg(keys, pdf_obj *)) != NULL) + {{ + dict = {rename.ll_fn('pdf_dict_get')}( dict, key); + }} + return dict; + }} + ''')) + + decl = f'''FZ_FUNCTION pdf_obj* {rename.ll_fn('pdf_dict_getl')}( pdf_obj* dict, ...)''' + out_functions_h.write( textwrap.dedent( f''' + /* Low-level wrapper for `pdf_dict_getl()`. `...` must be null-terminated list of `pdf_obj*`'s. */ + {decl}; + ''')) + out_functions_cpp.write( textwrap.dedent( f''' + {decl} + {{ + va_list keys; + va_start(keys, dict); + try + {{ + dict = {rename.ll_fn('pdf_dict_getlv')}( dict, keys); + }} + catch( std::exception&) + {{ + va_end(keys); + throw; + }} + va_end(keys); + return dict; + }} + ''')) + + decl = f'''FZ_FUNCTION {rename.class_('pdf_obj')} {rename.fn('pdf_dict_getlv')}( {rename.class_('pdf_obj')}& dict, va_list keys)''' + out_functions_h2.write( + textwrap.indent( + textwrap.dedent( f''' + /* Class-aware wrapper for `pdf_dict_getl()`. `keys` must be null-terminated list of + `pdf_obj*`'s, not `{rename.class_('pdf_obj')}*`'s, so that conventional + use with `PDF_NAME()` works. */ + {decl}; + '''), + ' ', + ) + ) + out_functions_cpp2.write( textwrap.dedent( f''' + {decl} + {{ + pdf_obj* ret = {rename.ll_fn('pdf_dict_getlv')}( dict.m_internal, keys); + return {rename.class_('pdf_obj')}( {rename.ll_fn('pdf_keep_obj')}( ret)); + }} + ''')) + + decl = f'''FZ_FUNCTION {rename.class_('pdf_obj')} {rename.fn('pdf_dict_getl')}( {rename.class_('pdf_obj')}* dict, ...)''' + out_functions_h2.write( + textwrap.indent( + textwrap.dedent( f''' + /* Class-aware wrapper for `pdf_dict_getl()`. `...` must be null-terminated list of + `pdf_obj*`'s, not `{rename.class_('pdf_obj')}*`'s, so that conventional + use with `PDF_NAME()` works. [We use pointer `dict` arg because variadic + args do not with with reference args.] */ + {decl}; + '''), + ' ', + ), + ) + out_functions_cpp2.write( textwrap.dedent( f''' + {decl} + {{ + va_list keys; + va_start(keys, dict); + try + {{ + {rename.class_('pdf_obj')} ret = {rename.fn('pdf_dict_getlv')}( *dict, keys); + va_end( keys); + return ret; + }} + catch (std::exception&) + {{ + va_end( keys); + throw; + }} + }} + ''')) + + +def class_add_iterator( tu, struct_cursor, struct_name, classname, extras, refcheck_if, trace_if): + ''' + Add begin() and end() methods so that this generated class is iterable + from C++ with: + + for (auto i: foo) {...} + + We modify <extras> to create an iterator class and add begin() and end() + methods that each return an instance of the iterator class. + ''' + it_begin, it_end = extras.iterator_next + + # Figure out type of what the iterator returns by looking at type of + # <it_begin>. + if it_begin: + c = parse.find_name( struct_cursor, it_begin) + assert c.type.kind == state.clang.cindex.TypeKind.POINTER + it_internal_type = state.get_name_canonical( c.type.get_pointee()).spelling + it_internal_type = util.clip( it_internal_type, 'struct ') + it_type = rename.class_( it_internal_type) + else: + # The container is also the first item in the linked list. + it_internal_type = struct_name + it_type = classname + + # We add to extras.methods_extra(). + # + check_refs = 1 if parse.has_refs( tu, struct_cursor.type) else 0 + extras.methods_extra.append( + classes.ExtraMethod( f'{classname}Iterator', 'begin()', + f''' + {{ + auto ret = {classname}Iterator({'m_internal->'+it_begin if it_begin else '*this'}); + {refcheck_if} + #if {check_refs} + if (s_check_refs) + {{ + s_{classname}_refs_check.check( this, __FILE__, __LINE__, __FUNCTION__); + }} + #endif + #endif + return ret; + }} + ''', + f'/* Used for iteration over linked list of {it_type} items starting at {it_internal_type}::{it_begin}. */', + ), + ) + extras.methods_extra.append( + classes.ExtraMethod( f'{classname}Iterator', 'end()', + f''' + {{ + auto ret = {classname}Iterator({it_type}()); + {refcheck_if} + #if {check_refs} + if (s_check_refs) + {{ + s_{classname}_refs_check.check( this, __FILE__, __LINE__, __FUNCTION__); + }} + #endif + #endif + return ret; + }} + ''', + f'/* Used for iteration over linked list of {it_type} items starting at {it_internal_type}::{it_begin}. */', + ), + ) + + extras.class_bottom += f'\n typedef {classname}Iterator iterator;\n' + + extras.class_pre += f'\nstruct {classname}Iterator;\n' + + extras.class_post += f''' + struct {classname}Iterator + {{ + FZ_FUNCTION {classname}Iterator(const {it_type}& item); + FZ_FUNCTION {classname}Iterator& operator++(); + FZ_FUNCTION bool operator==( const {classname}Iterator& rhs); + FZ_FUNCTION bool operator!=( const {classname}Iterator& rhs); + FZ_FUNCTION {it_type} operator*(); + FZ_FUNCTION {it_type}* operator->(); + private: + {it_type} m_item; + }}; + ''' + keep_text = '' + if extras.copyable and extras.copyable != 'default': + # Our operator++ needs to create it_type from m_item.m_internal->next, + # so we need to call fz_keep_<it_type>(). + # + # [Perhaps life would be simpler if our generated constructors always + # called fz_keep_*() as necessary? In some circumstances this would + # require us to call fz_drop_*() when constructing an instance, but + # that might be simpler?] + # + base_name = util.clip( struct_name, ('fz_', 'pdf_')) + if struct_name.startswith( 'fz_'): + keep_name = f'fz_keep_{base_name}' + elif struct_name.startswith( 'pdf_'): + keep_name = f'pdf_keep_{base_name}' + keep_name = rename.ll_fn(keep_name) + keep_text = f'{keep_name}(m_item.m_internal->next);' + + extras.extra_cpp += f''' + FZ_FUNCTION {classname}Iterator::{classname}Iterator(const {it_type}& item) + : m_item( item) + {{ + }} + FZ_FUNCTION {classname}Iterator& {classname}Iterator::operator++() + {{ + {keep_text} + m_item = {it_type}(m_item.m_internal->next); + return *this; + }} + FZ_FUNCTION bool {classname}Iterator::operator==( const {classname}Iterator& rhs) + {{ + return m_item.m_internal == rhs.m_item.m_internal; + }} + FZ_FUNCTION bool {classname}Iterator::operator!=( const {classname}Iterator& rhs) + {{ + return m_item.m_internal != rhs.m_item.m_internal; + }} + FZ_FUNCTION {it_type} {classname}Iterator::operator*() + {{ + return m_item; + }} + FZ_FUNCTION {it_type}* {classname}Iterator::operator->() + {{ + return &m_item; + }} + + ''' + + +def class_find_constructor_fns( tu, classname, struct_name, base_name, extras): + ''' + Returns list of functions that could be used as constructors of the + specified wrapper class. + + For example we look for functions that return a pointer to <struct_name> or + return a POD <struct_name> by value. + + tu: + . + classname: + Name of our wrapper class. + struct_name: + Name of underlying mupdf struct. + base_name: + Name of struct without 'fz_' prefix. + extras: + . + ''' + assert struct_name == f'fz_{base_name}' or struct_name == f'pdf_{base_name}' + verbose = state.state_.show_details( struct_name) + constructor_fns = [] + if '-' not in extras.constructor_prefixes: + # Add default constructor fn prefix. + if struct_name.startswith( 'fz_'): + extras.constructor_prefixes.insert( 0, f'fz_new_') + extras.constructor_prefixes.insert( 0, f'pdf_new_') + elif struct_name.startswith( 'pdf_'): + extras.constructor_prefixes.insert( 0, f'pdf_new_') + for fnprefix in extras.constructor_prefixes: + if verbose: + jlib.log('{struct_name=} {fnprefix=}') + for fnname, cursor in state.state_.find_functions_starting_with( tu, fnprefix, method=True): + # Check whether this has identical signature to any fn we've + # already found. + if verbose: + jlib.log( '{struct_name=} {fnname=}') + duplicate_type = None + duplicate_name = False + for f, c, is_duplicate in constructor_fns: + if verbose: + jlib.log( '{struct_name=} {cursor.spelling=} {c.type.spelling=}') + if f == fnname: + if verbose: + jlib.log('setting duplicate_name to true') + duplicate_name = True + break + if c.type == cursor.type: + if verbose: + jlib.log( '{struct_name} wrapper: ignoring candidate constructor {fnname}() because prototype is indistinguishable from {f=}()') + duplicate_type = f + break + if duplicate_name: + continue + ok = False + + arg, n = parse.get_first_arg( tu, cursor) + if arg and n == 1 and parse.is_pointer_to( arg.cursor.type, struct_name): + # This avoids generation of bogus copy constructor wrapping + # function fz_new_pixmap_from_alpha_channel() introduced + # 2021-05-07. + # + if verbose: + jlib.log('ignoring possible constructor because looks like copy constructor: {fnname}') + elif fnname in extras.constructor_excludes: + if verbose: + jlib.log('{fnname=} is in {extras.constructor_excludes=}') + elif extras.pod and extras.pod != 'none' and state.get_name_canonical( cursor.result_type).spelling == f'{struct_name}': + # Returns POD struct by value. + ok = True + elif not extras.pod and parse.is_pointer_to( cursor.result_type, f'{struct_name}'): + # Returns pointer to struct. + ok = True + + if ok: + if duplicate_type and extras.copyable: + if verbose: + jlib.log1( 'adding static method wrapper for {fnname}') + extras.method_wrappers_static.append( fnname) + else: + if duplicate_type: + if verbose: + jlib.log( 'not able to provide static factory fn {struct_name}::{fnname} because wrapper class is not copyable.') + if verbose: + jlib.log( 'adding constructor wrapper for {fnname}') + constructor_fns.append( (fnname, cursor, duplicate_type)) + else: + if verbose: + jlib.log( 'ignoring possible constructor for {classname=} because does not return required type: {fnname=} -> {cursor.result_type.spelling=}') + + constructor_fns.sort() + return constructor_fns + + +def class_find_destructor_fns( tu, struct_name, base_name): + ''' + Returns list of functions that could be used by destructor - must be called + 'fz_drop_<typename>', must take a <struct>* arg, may take a fz_context* + arg. + ''' + if struct_name.startswith( 'fz_'): + destructor_prefix = f'fz_drop_{base_name}' + elif struct_name.startswith( 'pdf_'): + destructor_prefix = f'pdf_drop_{base_name}' + destructor_fns = [] + for fnname, cursor in state.state_.find_functions_starting_with( tu, destructor_prefix, method=True): + arg_struct = False + arg_context = False + args_num = 0 + for arg in parse.get_args( tu, cursor): + if not arg_struct and parse.is_pointer_to( arg.cursor.type, struct_name): + arg_struct = True + elif not arg_context and parse.is_pointer_to( arg.cursor.type, 'fz_context'): + arg_context = True + args_num += 1 + if arg_struct: + if args_num == 1 or (args_num == 2 and arg_context): + # No params other than <struct>* and fz_context* so this is + # candidate destructor. + #log( 'adding candidate destructor: {fnname}') + destructor_fns.append( (fnname, cursor)) + + destructor_fns.sort() + return destructor_fns + + +def num_instances(refcheck_if, delta, name): + ''' + Returns C++ code to embed in a wrapper class constructor/destructor function + to update the class static `s_num_instances` variable. + ''' + ret = '' + ret += f' {refcheck_if}\n' + if delta == +1: + ret += ' ++s_num_instances;\n' + elif delta == -1: + ret += ' --s_num_instances;\n' + else: + assert 0 + ret += ' #endif\n' + return ret + + +def class_constructor_default( + tu, + struct_cursor, + classname, + extras, + out_h, + out_cpp, + refcheck_if, + trace_if, + ): + ''' + Generates constructor that sets each member to default value. + ''' + if extras.pod: + comment = f'Default constructor, sets each member to default value.' + else: + comment = f'Default constructor, sets `m_internal` to null.' + out_h.write( '\n') + out_h.write( f' /** {comment} */\n') + out_h.write( f' FZ_FUNCTION {classname}();\n') + + out_cpp.write( f'/** {comment} */\n') + out_cpp.write( f'FZ_FUNCTION {classname}::{classname}()\n') + if not extras.pod: + out_cpp.write( f': m_internal(nullptr)\n') + out_cpp.write( f'{{\n') + if extras.pod == 'none': + pass + elif extras.pod: + for c in parse.get_members(struct_cursor): + if extras.pod == 'inline': + c_name = f'this->{c.spelling}' + else: + c_name = f'this->m_internal.{c.spelling}' + if c.type.kind == state.clang.cindex.TypeKind.CONSTANTARRAY: + out_cpp.write( f' memset(&{c_name}, 0, sizeof({c_name}));\n') + else: + out_cpp.write( f' {c_name} = {{}};\n') + else: + if parse.has_refs( tu, struct_cursor.type): + out_cpp.write(f' {refcheck_if}\n') + out_cpp.write( ' if (s_check_refs)\n') + out_cpp.write( ' {\n') + out_cpp.write(f' s_{classname}_refs_check.add( this, __FILE__, __LINE__, __FUNCTION__);\n') + out_cpp.write( ' }\n') + out_cpp.write( ' #endif\n') + + out_cpp.write(num_instances(refcheck_if, +1, classname)) + + out_cpp.write( f'}};\n') + + +def class_copy_constructor( + tu, + functions, + struct_name, + struct_cursor, + base_name, + classname, + constructor_fns, + out_h, + out_cpp, + refcheck_if, + trace_if, + ): + ''' + Generate a copy constructor and operator= by finding a suitable fz_keep_*() + function. + + We raise an exception if we can't find one. + ''' + if struct_name.startswith( 'fz_'): + keep_name = f'fz_keep_{base_name}' + drop_name = f'fz_drop_{base_name}' + elif struct_name.startswith( 'pdf_'): + keep_name = f'pdf_keep_{base_name}' + drop_name = f'pdf_drop_{base_name}' + for name in keep_name, drop_name: + cursor = state.state_.find_function( tu, name, method=True) + if not cursor: + classextra = classes.classextras.get( tu, struct_name) + if classextra.copyable: + if 1 or state.state_.show_details( struct_name): + jlib.log( 'changing to non-copyable because no function {name}(): {struct_name}') + classextra.copyable = False + return + if name == keep_name: + pvoid = parse.is_pointer_to( cursor.result_type, 'void') + assert ( pvoid + or parse.is_pointer_to( cursor.result_type, struct_name) + ), ( + f'Function {name}(): result_type not void* or pointer to {struct_name}: {cursor.result_type.spelling}' + ) + arg, n = parse.get_first_arg( tu, cursor) + assert n == 1, f'should take exactly one arg: {cursor.spelling}()' + assert parse.is_pointer_to( arg.cursor.type, struct_name), ( + f'arg0 is not pointer to {struct_name}: {cursor.spelling}(): {arg.cursor.spelling} {arg.name}') + + for fnname, cursor, duplicate_type in constructor_fns: + fnname2 = rename.ll_fn(fnname) + if fnname2 == keep_name: + jlib.log( 'not generating copy constructor with {keep_name=} because already used by a constructor.') + break + else: + functions( keep_name) + comment = f'Copy constructor using `{keep_name}()`.' + out_h.write( '\n') + out_h.write( f' /** {comment} */\n') + out_h.write( f' FZ_FUNCTION {classname}(const {classname}& rhs);\n') + out_h.write( '\n') + + cast = '' + if pvoid: + # Need to cast the void* to the correct type. + cast = f'(::{struct_name}*) ' + + out_cpp.write( f'/** {comment} */\n') + out_cpp.write( f'FZ_FUNCTION {classname}::{classname}(const {classname}& rhs)\n') + out_cpp.write( f': m_internal({cast}{rename.ll_fn(keep_name)}(rhs.m_internal))\n') + out_cpp.write( '{\n') + + # Write trace code. + out_cpp.write( f' {trace_if}\n') + out_cpp.write( f' if (s_trace_keepdrop) {{\n') + out_cpp.write( f' std::cerr << __FILE__ << ":" << __LINE__ << ":" << __FUNCTION__ << "():"\n') + out_cpp.write( f' << " have called {rename.ll_fn(keep_name)}(rhs.m_internal)\\n"\n') + out_cpp.write( f' ;\n') + out_cpp.write( f' }}\n') + out_cpp.write( f' #endif\n') + + if parse.has_refs( tu, struct_cursor.type): + out_cpp.write(f' {refcheck_if}\n') + out_cpp.write( ' if (s_check_refs)\n') + out_cpp.write( ' {\n') + out_cpp.write(f' s_{classname}_refs_check.add( this, __FILE__, __LINE__, __FUNCTION__);\n') + out_cpp.write( ' }\n') + out_cpp.write( ' #endif\n') + out_cpp.write(num_instances(refcheck_if, +1, classname)) + out_cpp.write( '}\n') + out_cpp.write( '\n') + + # Make operator=(). + # + comment = f'operator= using `{keep_name}()` and `{drop_name}()`.' + out_h.write( f' /** {comment} */\n') + out_h.write( f' FZ_FUNCTION {classname}& operator=(const {classname}& rhs);\n') + + out_cpp.write( f'/* {comment} */\n') + out_cpp.write( f'FZ_FUNCTION {classname}& {classname}::operator=(const {classname}& rhs)\n') + out_cpp.write( '{\n') + out_cpp.write( f' {trace_if}\n') + out_cpp.write( f' if (s_trace_keepdrop) {{\n') + out_cpp.write( f' std::cerr << __FILE__ << ":" << __LINE__ << ":" << __FUNCTION__ << "():"\n') + out_cpp.write( f' << " calling {rename.ll_fn(drop_name)}(this->m_internal)"\n') + out_cpp.write( f' << " and {rename.ll_fn(keep_name)}(rhs.m_internal)\\n"\n') + out_cpp.write( f' ;\n') + out_cpp.write( f' }}\n') + out_cpp.write( f' #endif\n') + out_cpp.write( f' {rename.ll_fn(drop_name)}(this->m_internal);\n') + out_cpp.write( f' {rename.ll_fn(keep_name)}(rhs.m_internal);\n') + if parse.has_refs( tu, struct_cursor.type): + out_cpp.write(f' {refcheck_if}\n') + out_cpp.write( ' if (s_check_refs)\n') + out_cpp.write( ' {\n') + out_cpp.write(f' s_{classname}_refs_check.remove( this, __FILE__, __LINE__, __FUNCTION__);\n') + out_cpp.write( ' }\n') + out_cpp.write( ' #endif\n') + out_cpp.write( f' this->m_internal = {cast}rhs.m_internal;\n') + if parse.has_refs( tu, struct_cursor.type): + out_cpp.write(f' {refcheck_if}\n') + out_cpp.write( ' if (s_check_refs)\n') + out_cpp.write( ' {\n') + out_cpp.write(f' s_{classname}_refs_check.add( this, __FILE__, __LINE__, __FUNCTION__);\n') + out_cpp.write( ' }\n') + out_cpp.write( ' #endif\n') + out_cpp.write( f' return *this;\n') + out_cpp.write( '}\n') + out_cpp.write( '\n') + +def function_name_implies_kept_references( fnname): + ''' + Returns true if <fnname> implies the function would return kept + reference(s). + ''' + if fnname in ( + 'pdf_page_write', + 'fz_decomp_image_from_stream', + 'fz_get_pixmap_from_image', + ): + return True + for i in ( + 'add', + 'convert', + 'copy', + 'create', + 'deep_copy', + 'find', + 'graft', + 'keep', + 'load', + 'new', + 'open', + 'parse', + 'read', + ): + if fnname.startswith(f'fz_{i}_') or fnname.startswith(f'pdf_{i}_'): + if state.state_.show_details(fnname): + jlib.log('Assuming that {fnname=} returns a kept reference.') + return True + return False + + +def function_wrapper_class_aware_body( + tu, + fnname, + out_cpp, + struct_name, + class_name, + class_static, + class_constructor, + extras, + struct_cursor, + fn_cursor, + return_cursor, + wrap_return, + refcheck_if, + trace_if, + ): + ''' + Writes function or method body to <out_cpp> that calls a generated C++ wrapper + function. + + fnname: + . + out_cpp: + . + struct_name: + If false, we write a class-aware wrapping function body. Otherwise name + of struct such as 'fz_rect' and we write method body for the struct's + wrapper class. + class_name: + class_static: + If true, this is a static class method. + class_constructor: + If true, this is a constructor. + extras: + . + struct_cursor: + . + fn_cursor: + Cursor for the underlying MuPDF function. + return_cursor: + If not None, the cursor for definition of returned type. + wrap_return: + If 'pointer', the underlying function returns a pointer to a struct + that we wrap. + + If 'value' the underlying function returns, by value, a + struct that we wrap, so we need to construct our wrapper from the + address of this value. + + Otherwise we don't wrap the returned value. + ''' + verbose = state.state_.show_details( fnname) + out_cpp.write( f'{{\n') + return_void = (fn_cursor.result_type.spelling == 'void') + + # Write trace code. + out_cpp.write( f' {trace_if}\n') + out_cpp.write( f' if (s_trace) {{\n') + out_cpp.write( f' std::cerr << __FILE__ << ":" << __LINE__ << ":" << __FUNCTION__ << "():"\n') + out_cpp.write( f' << " calling mupdf::{rename.ll_fn(fnname)}()\\n";\n') + out_cpp.write( f' }}\n') + out_cpp.write( f' #endif\n') + + if fn_cursor.type.is_function_variadic(): + assert fnname == 'fz_warn', f'{fnname=}' + out_cpp.write( f' va_list ap;\n') + out_cpp.write( f' va_start( ap, fmt);\n') + out_cpp.write( f' {rename.ll_fn("fz_vwarn")}( fmt, ap);\n') + out_cpp.write( f' va_end( ap);\n') + + elif class_constructor or not struct_name: + # This code can generate a class method, but we choose to not use this, + # instead method body simply calls the class-aware function (see below). + def get_keep_drop(arg): + name = util.clip( arg.alt.type.spelling, 'struct ') + if name.startswith('fz_'): + prefix = 'fz' + name = name[3:] + elif name.startswith('pdf_'): + prefix = 'pdf' + name = name[4:] + else: + assert 0 + return rename.ll_fn(f'{prefix}_keep_{name}'), rename.ll_fn(f'{prefix}_drop_{name}') + + # Handle wrapper-class out-params - need to drop .m_internal and set to + # null. + # + # fixme: maybe instead simply call <arg.name>'s destructor directly? + # + for arg in parse.get_args( tu, fn_cursor): + if arg.alt and arg.out_param: + if parse.has_refs(tu, arg.alt.type): + keep_fn, drop_fn = get_keep_drop(arg) + out_cpp.write( f' /* Out-param {arg.name}.m_internal will be overwritten. */\n') + out_cpp.write( f' {drop_fn}({arg.name}.m_internal);\n') + out_cpp.write( f' {arg.name}.m_internal = nullptr;\n') + + # Write function call. + if class_constructor: + if extras.pod: + if extras.pod == 'inline': + out_cpp.write( f' *(::{struct_name}*) &this->{parse.get_field0(struct_cursor.type).spelling} = ') + else: + out_cpp.write( f' this->m_internal = ') + if fn_cursor.result_type.kind == state.clang.cindex.TypeKind.POINTER: + out_cpp.write( f'*') + else: + out_cpp.write( f' this->m_internal = ') + if fn_cursor.result_type.kind == state.clang.cindex.TypeKind.POINTER: + pass + else: + assert 0, 'cannot handle underlying fn returning by value when not pod.' + out_cpp.write( f'{rename.ll_fn(fnname)}(') + elif wrap_return == 'value': + out_cpp.write( f' {_make_top_level(return_cursor.spelling)} temp = mupdf::{rename.ll_fn(fnname)}(') + elif wrap_return == 'pointer': + out_cpp.write( f' {_make_top_level(return_cursor.spelling)}* temp = mupdf::{rename.ll_fn(fnname)}(') + elif wrap_return == 'const pointer': + out_cpp.write( f' const {_make_top_level(return_cursor.spelling)}* temp = mupdf::{rename.ll_fn(fnname)}(') + elif return_void: + out_cpp.write( f' mupdf::{rename.ll_fn(fnname)}(') + else: + out_cpp.write( f' auto ret = mupdf::{rename.ll_fn(fnname)}(') + + have_used_this = False + sep = '' + for arg in parse.get_args( tu, fn_cursor): + arg_classname = class_name + if class_static or class_constructor: + arg_classname = None + out_cpp.write( sep) + have_used_this = write_call_arg( + tu, + arg, + arg_classname, + have_used_this, + out_cpp, + state.state_.show_details(fnname), + ) + sep = ', ' + out_cpp.write( f');\n') + + if state.state_.show_details(fnname): + jlib.log('{=wrap_return}') + refcounted_return = False + if wrap_return in ('pointer', 'const pointer') and parse.has_refs( tu, return_cursor.type): + refcounted_return = True + refcounted_return_struct_cursor = return_cursor + elif class_constructor and parse.has_refs( tu, struct_cursor.type): + refcounted_return = True + refcounted_return_struct_cursor = struct_cursor + + if refcounted_return: + # This MuPDF function returns pointer to a struct which uses reference + # counting. If the function returns a borrowed reference, we need + # to increment its reference count before passing it to our wrapper + # class's constructor. + # + #jlib.log('Function returns pointer to {return_cursor=}') + return_struct_name = util.clip( refcounted_return_struct_cursor.spelling, 'struct ') + if return_struct_name.startswith('fz_'): + prefix = 'fz_' + elif return_struct_name.startswith('pdf_'): + prefix = 'pdf_' + else: + prefix = None + if state.state_.show_details(fnname): + jlib.log('{=prefix}') + if prefix: + if function_name_implies_kept_references( fnname): + pass + #out_cpp.write( f' /* We assume that {fnname} returns a kept reference. */\n') + else: + if state.state_.show_details(fnname): + jlib.log('{=classname fnname constructor} Assuming that {fnname=} returns a borrowed reference.') + # This function returns a borrowed reference. + suffix = return_struct_name[ len(prefix):] + keep_fn = f'{prefix}keep_{suffix}' + #jlib.log('Function assumed to return borrowed reference: {fnname=} => {return_struct_name=} {keep_fn=}') + #out_cpp.write( f' /* We assume that {fnname} returns a borrowed reference. */\n') + if class_constructor: + out_cpp.write( f' {rename.ll_fn(keep_fn)}(this->m_internal);\n') + else: + out_cpp.write( f' {rename.ll_fn(keep_fn)}(temp);\n') + + if wrap_return == 'value': + out_cpp.write( f' auto ret = {rename.class_(return_cursor.spelling)}(&temp);\n') + elif wrap_return in ('pointer', 'const pointer'): + out_cpp.write( f' auto ret = {rename.class_(return_cursor.spelling)}(temp);\n') + + # Handle wrapper-class out-params - need to keep arg.m_internal if + # fnname implies it will be a borrowed reference. + for arg in parse.get_args( tu, fn_cursor): + if arg.alt and arg.out_param: + if parse.has_refs(tu, arg.alt.type): + if function_name_implies_kept_references( fnname): + out_cpp.write( f' /* We assume that out-param {arg.name}.m_internal is a kept reference. */\n') + else: + keep_fn, drop_fn = get_keep_drop(arg) + out_cpp.write( f' /* We assume that out-param {arg.name}.m_internal is a borrowed reference. */\n') + out_cpp.write( f' {keep_fn}({arg.name}.m_internal);\n') + else: + # Class method simply calls the class-aware function, which will have + # been generated elsewhere. + out_cpp.write( ' ') + if not return_void: + out_cpp.write( 'auto ret = ') + + out_cpp.write( f'mupdf::{rename.fn(fnname)}(') + sep = '' + for i, arg in enumerate( parse.get_args( tu, fn_cursor)): + out_cpp.write( sep) + if i==0 and not class_static: + out_cpp.write( '*this') + else: + out_cpp.write( f'{arg.name}') + sep = ', ' + + out_cpp.write( ');\n') + + if struct_name and not class_static: + if parse.has_refs( tu, struct_cursor.type): + # Write code that does runtime checking of reference counts. + out_cpp.write( f' {refcheck_if}\n') + out_cpp.write( f' if (s_check_refs)\n') + out_cpp.write( f' {{\n') + if class_constructor: + out_cpp.write( f' s_{class_name}_refs_check.add( this, __FILE__, __LINE__, __FUNCTION__);\n') + else: + out_cpp.write( f' s_{class_name}_refs_check.check( this, __FILE__, __LINE__, __FUNCTION__);\n') + out_cpp.write( f' }}\n') + out_cpp.write( f' #endif\n') + + if class_constructor: + out_cpp.write(num_instances(refcheck_if, +1, class_name)) + + if not return_void and not class_constructor: + out_cpp.write( f' return ret;\n') + + out_cpp.write( f'}}\n') + out_cpp.write( f'\n') + + +def function_wrapper_class_aware( + tu, + register_fn_use, + fnname, + out_h, + out_cpp, + struct_name, + class_name, + fn_cursor, + refcheck_if, + trace_if, + class_static=False, + class_constructor=False, + extras=None, + struct_cursor=None, + duplicate_type=None, + generated=None, + debug=None, + ): + ''' + Writes a function or class method that calls <fnname>. + + Also appends python and C# code to generated.swig_python and + generated.swig_csharp if <generated> is not None. + + tu + . + register_fn_use + Callback to keep track of what fz_*() fns have been used. + fnname + Name of fz_*() fn to wrap, e.g. fz_concat. + out_h + out_cpp + Where to write generated code. + struct_name + If false, we generate class-aware wrapping function. Otherwise name + of struct such as 'fz_rect' and we create a method in the struct's + wrapper class. + class_name + Ignored if struct_name is false. + + Name of wrapper class, e.g. 'Rect'. + class_static + Ignored if struct_name is false. + + If true, we generate a static method. + + Otherwise we generate a normal class method, where first arg that + is type <struct_name> is omitted from the generated method's + prototype; in the implementation we use <this>. + class_constructor + If true, we write a constructor. + extras + None or ClassExtras instance. + Only used if <constructor> is true. + struct_cursor + None or cursor for the struct definition. + Only used if <constructor> is true. + duplicate_type: + If true, we have already generated a method with the same args, so + this generated method will be commented-out. + generated: + If not None and there are one or more out-params, we write + python code to generated.swig_python that overrides the default + SWIG-generated method to call our *_outparams_fn() alternative. + debug + Show extra diagnostics. + ''' + verbose = state.state_.show_details( fnname) + if fn_cursor and fn_cursor.type.is_function_variadic() and fnname != 'fz_warn': + jlib.log( 'Not writing class-aware wrapper because variadic: {fnname=}', 1) + return + if verbose: + jlib.log( 'Writing class-aware wrapper for {fnname=}') + if struct_name: + assert fnname not in state.omit_methods, jlib.log_text( '{=fnname}') + if debug: + jlib.log( '{class_name=} {fnname=}') + assert fnname.startswith( ('fz_', 'pdf_')) + if not fn_cursor: + fn_cursor = state.state_.find_function( tu, fnname, method=True) + if not fn_cursor: + jlib.log( '*** ignoring {fnname=}') + return + + if fnname.endswith('_drop'): + # E.g. fz_concat_push_drop() is not safe (or necessary) for us because + # we need to manage reference counts ourselves. + #jlib.log('Ignoring because ends with "_drop": {fnname}') + return + + if struct_name: + methodname = rename.method( struct_name, fnname) + else: + methodname = rename.fn( fnname) + + if verbose: + jlib.log( 'Writing class-aware wrapper for {fnname=}') + # Construct prototype fnname(args). + # + if class_constructor: + assert struct_name + decl_h = f'{class_name}(' + decl_cpp = f'{class_name}(' + else: + decl_h = f'{methodname}(' + decl_cpp = f'{methodname}(' + have_used_this = False + num_out_params = 0 + num_class_wrapper_params = 0 + comma = '' + this_is_const = False + debug = state.state_.show_details( fnname) + + for arg in parse.get_args( tu, fn_cursor): + if debug: + jlib.log( 'Looking at {struct_name=} {fnname=} {fnname_wrapper} {arg=}', 1) + decl_h += comma + decl_cpp += comma + if arg.out_param: + num_out_params += 1 + if arg.alt: + # This parameter is a pointer to a struct that we wrap. + num_class_wrapper_params += 1 + arg_extras = classes.classextras.get( tu, arg.alt.type.spelling) + assert arg_extras, jlib.log_text( '{=structname fnname arg.alt.type.spelling}') + const = '' + if not arg.out_param and (not arg_extras.pod or arg.cursor.type.kind != state.clang.cindex.TypeKind.POINTER): + const = 'const ' + + if (1 + and struct_name + and not class_static + and not class_constructor + and rename.class_(util.clip( arg.alt.type.spelling, 'struct ')) == class_name + and not have_used_this + ): + assert not arg.out_param + # Omit this arg from the method's prototype - we'll use <this> + # when calling the underlying fz_ function. + have_used_this = True + if not arg_extras.pod: + this_is_const = const + continue + + if arg_extras.pod == 'none': + jlib.log( 'Not wrapping because {arg=} wrapper has {extras.pod=}', 1) + return + text = f'{const}{rename.class_(arg.alt.type.spelling)}& {arg.name}' + decl_h += text + decl_cpp += text + else: + jlib.logx( '{arg.spelling=}') + decl_text = declaration_text( arg.cursor.type, arg.name) + decl_h += decl_text + decl_cpp += decl_text + comma = ', ' + + if fn_cursor.type.is_function_variadic(): + decl_h += f'{comma}...' + decl_cpp += f'{comma}...' + + decl_h += ')' + decl_cpp += ')' + if this_is_const: + decl_h += ' const' + decl_cpp += ' const' + + if verbose: + jlib.log( '{=struct_name class_constructor}') + if class_constructor: + comment = f'Constructor using `{fnname}()`.' + else: + comment = make_wrapper_comment( + tu, + fn_cursor, + fnname, + methodname, + indent=' ', + is_method=bool(struct_name), + is_low_level=False, + ) + + if struct_name and not class_static and not class_constructor: + assert have_used_this, f'error: wrapper for {struct_name}: {fnname}() is not useful - does not have a {struct_name} arg.' + + if struct_name and not duplicate_type: + register_fn_use( fnname) + + # If this is true, we explicitly construct a temporary from what the + # wrapped function returns. + # + wrap_return = None + + warning_not_copyable = False + warning_no_raw_constructor = False + + # Figure out return type for our generated function/method. + # + if verbose: + jlib.log( 'Looking at return type...') + return_cursor = None + return_type = None + return_extras = None + if class_constructor: + assert struct_name + fn_h = f'{decl_h}' + fn_cpp = f'{class_name}::{decl_cpp}' + else: + fn_h = declaration_text( fn_cursor.result_type, decl_h) + if verbose: + jlib.log( '{fn_cursor.result_type=}') + if struct_name: + fn_cpp = declaration_text( fn_cursor.result_type, f'{class_name}::{decl_cpp}') + else: + fn_cpp = declaration_text( fn_cursor.result_type, f'{decl_cpp}') + + # See whether we can convert return type to an instance of a wrapper + # class. + # + if verbose: + jlib.log( '{fn_cursor.result_type.kind=}') + if fn_cursor.result_type.kind == state.clang.cindex.TypeKind.POINTER: + # Function returns a pointer. + t = state.get_name_canonical( fn_cursor.result_type.get_pointee()) + if verbose: + jlib.log( '{t.spelling=}') + return_cursor = parse.find_struct( tu, t.spelling, require_definition=False) + if verbose: + jlib.log( '{=t.spelling return_cursor}') + if return_cursor: + # Function returns a pointer to a struct. + return_extras = classes.classextras.get( tu, return_cursor.spelling) + if return_extras: + # Function returns a pointer to a struct for which we + # generate a class wrapper, so change return type to be an + # instance of the class wrapper. + return_type = rename.class_(return_cursor.spelling) + if verbose: + jlib.log( '{=return_type}') + if 0 and (state.state_.show_details(return_cursor.type.spelling) or state.state_.show_details(struct_name)): + jlib.log('{return_cursor.type.spelling=}' + ' {return_cursor.spelling=}' + ' {struct_name=} {return_extras.copyable=}' + ' {return_extras.constructor_raw=}' + ) + fn_h = f'{return_type} {decl_h}' + if struct_name: + fn_cpp = f'{return_type} {class_name}::{decl_cpp}' + else: + fn_cpp = f'{return_type} {decl_cpp}' + if t.is_const_qualified(): + wrap_return = 'const pointer' + else: + wrap_return = 'pointer' + else: + return_pointee = fn_cursor.result_type.get_pointee() + if 'char' in return_pointee.spelling: + if function_name_implies_kept_references(fnname): + # For now we just output a diagnostic, but eventually + # we might make C++ wrappers return a std::string here, + # free()-ing the char* before returning. + jlib.log1( 'Function name implies kept reference and returns char*:' + ' {fnname}(): {fn_cursor.result_type.spelling=}' + ' -> {return_pointee.spelling=}.' + ) + + if verbose: + jlib.log( '{=warning_not_copyable warning_no_raw_constructor}') + else: + # The fz_*() function returns by value. See whether we can convert + # its return type to an instance of a wrapper class. + # + # If so, we will use constructor that takes pointer to the fz_ + # struct. C++ doesn't allow us to use address of temporary, so we + # generate code like this: + # + # fz_quad_s ret = mupdf_snap_selection(...); + # return Quad(&ret); + # + t = state.get_name_canonical( fn_cursor.result_type) + + # 2023-02-09: parse.find_struct() will actually find any definition, + # and we now prefix Fitz headers with a typedef of size_t on Linux, + # so we need to avoid calling parse.find_struct() unless `t` is for + # a MuPDF type. + # + if t.spelling.startswith( ('fz_', 'pdf_')): + return_cursor = parse.find_struct( tu, t.spelling) + if return_cursor: + tt = state.get_name_canonical( return_cursor.type) + if tt.kind == state.clang.cindex.TypeKind.ENUM: + # For now, we return this type directly with no wrapping. + pass + else: + return_extras = classes.classextras.get( tu, return_cursor.spelling) + return_type = rename.class_(return_cursor.type.spelling) + fn_h = f'{return_type} {decl_h}' + if struct_name: + fn_cpp = f'{return_type} {class_name}::{decl_cpp}' + else: + fn_cpp = f'{return_type} {decl_cpp}' + wrap_return = 'value' + + if return_extras: + if not return_extras.copyable: + out_h.write( + textwrap.indent( + textwrap.dedent( f''' + /* Class-aware wrapper for `{fnname}()` + is not available because returned wrapper class for `{return_cursor.spelling}` + is non-copyable. */ + ''' + ), + ' ', + ) + ) + if verbose: + jlib.log( 'Not creating class-aware wrapper because returned wrapper class is non-copyable: {fnname=}.') + return + if not return_extras.constructor_raw: + out_h.write( + textwrap.indent( + textwrap.dedent( f''' + /* Class-aware wrapper for `{fnname}()` + is not available because returned wrapper class for `{return_cursor.spelling}` + does not have raw constructor. */ + ''' + ), + ' ', + ) + ) + if verbose: + jlib.log( 'Not creating class-aware wrapper because returned wrapper class does not have raw constructor: {fnname=}.') + return + + out_h.write( '\n') + out_h.write( f' /** {comment} */\n') + + # Copy any comment (indented) into class definition above method + # declaration. + if fn_cursor.raw_comment: + raw_comment = fn_cursor.raw_comment.replace('\r', '') + for line in raw_comment.split( '\n'): + out_h.write( f' {line}\n') + + if duplicate_type: + out_h.write( f' /* Disabled because same args as {duplicate_type}.\n') + + out_h.write( f' FZ_FUNCTION {"static " if class_static else ""}{fn_h};\n') + + if duplicate_type: + out_h.write( f' */\n') + + if not struct_name: + # Use extra spacing between non-class functions. Class methods are + # grouped together. + out_cpp.write( f'\n') + + out_cpp.write( f'/* {comment} */\n') + if duplicate_type: + out_cpp.write( f'/* Disabled because same args as {duplicate_type}.\n') + + out_cpp.write( f'FZ_FUNCTION {fn_cpp}\n') + + function_wrapper_class_aware_body( + tu, + fnname, + out_cpp, + struct_name, + class_name, + class_static, + class_constructor, + extras, + struct_cursor, + fn_cursor, + return_cursor, + wrap_return, + refcheck_if, + trace_if, + ) + + if struct_name: + if duplicate_type: + out_cpp.write( f'*/\n') + + # fixme: the test of `struct_name` means that we don't generate outparam override for + # class-aware fns which don't have any struct/class args, e.g. fz_lookup_cjk_font(). + # + + if generated and num_out_params: + make_python_class_method_outparam_override( + tu, + fn_cursor, + fnname, + generated, + struct_name, + class_name, + return_type, + ) + + +def class_custom_method( + tu, + register_fn_use, + struct_cursor, + classname, + extramethod, + out_h, + out_cpp, + refcheck_if, + trace_if, + ): + ''' + Writes custom method as specified by <extramethod>. + + tu + . + register_fn_use + Callable taking single <fnname> arg. + struct_cursor + Cursor for definition of MuPDF struct. + classname + Name of wrapper class for <struct_cursor>. + extramethod + An ExtraMethod or ExtraConstructor instance. + out_h + out_cpp + Where to write generated code. + ''' + assert isinstance( extramethod, ( classes.ExtraMethod, classes.ExtraConstructor)), f'{type(extramethod)}' + is_constructor = False + is_destructor = False + is_begin_end = False + + if extramethod.return_: + name_args = extramethod.name_args + return_space = f'{extramethod.return_} ' + comment = 'Custom method.' + if name_args.startswith( 'begin(') or name_args.startswith( 'end('): + is_begin_end = True + elif extramethod.name_args == '~()': + # Destructor. + name_args = f'~{classname}{extramethod.name_args[1:]}' + return_space = '' + comment = 'Custom destructor.' + is_destructor = True + elif extramethod.name_args.startswith('operator '): + name_args = extramethod.name_args + comment = 'Custom operator.' + return_space = '' + else: + # Constructor. + assert extramethod.name_args.startswith( '('), f'bad constructor/destructor in {classname=}: {extramethod.name_args=}' + name_args = f'{classname}{extramethod.name_args}' + return_space = '' + comment = 'Custom constructor.' + is_constructor = True + + out_h.write( f'\n') + if extramethod.comment: + for i, line in enumerate( extramethod.comment.strip().split('\n')): + line = line.replace( '/* ', '/** ') + out_h.write( f' {line}\n') + else: + out_h.write( f' /** {comment} */\n') + out_h.write( f' FZ_FUNCTION {return_space}{name_args};\n') + + out_cpp.write( f'/** {comment} */\n') + # Remove any default arg values from <name_args>. + name_args_no_defaults = re.sub('= *[^(][^),]*', '', name_args) + if name_args_no_defaults != name_args: + jlib.log('have changed {name_args=} to {name_args_no_defaults=}', 1) + out_cpp.write( f'FZ_FUNCTION {return_space}{classname}::{name_args_no_defaults}') + + body = textwrap.dedent(extramethod.body) + + end = body.rfind('}') + assert end >= 0 + out_cpp.write( body[:end]) + + if is_constructor and parse.has_refs( tu, struct_cursor.type): + # Insert ref checking code into end of custom constructor body. + out_cpp.write( f' {refcheck_if}\n') + out_cpp.write( f' if (s_check_refs)\n') + out_cpp.write( f' {{\n') + out_cpp.write( f' s_{classname}_refs_check.add( this, __FILE__, __LINE__, __FUNCTION__);\n') + out_cpp.write( f' }}\n') + out_cpp.write( f' #endif\n') + if is_constructor: + out_cpp.write( num_instances(refcheck_if, +1, classname)) + if is_destructor: + out_cpp.write( num_instances(refcheck_if, -1, classname)) + + out_cpp.write( body[end:]) + + out_cpp.write( f'\n') + + if 1: # lgtm [py/constant-conditional-expression] + # Register calls of all fz_* functions. Not necessarily helpful - we + # might only be interested in calls of fz_* functions that are directly + # available to uses of class. + # + for fnname in re.findall( '(mupdf::[a-zA-Z0-9_]+) *[(]', extramethod.body): + fnname = util.clip( fnname, 'mupdf::') + if not fnname.startswith( 'pdf_'): + fnname = 'fz_' + fnname + #log( 'registering use of {fnname} in extramethod {classname}::{name_args}') + register_fn_use( fnname) + + return is_constructor, is_destructor, is_begin_end + + +def class_raw_constructor( + tu, + register_fn_use, + classname, + struct_cursor, + struct_name, + base_name, + extras, + constructor_fns, + out_h, + out_cpp, + refcheck_if, + trace_if, + ): + ''' + Create a raw constructor - a constructor taking a pointer to underlying + struct. This raw constructor assumes that it already owns the pointer so it + does not call fz_keep_*(); the class's destructor will call fz_drop_*(). + ''' + #jlib.log( 'Creating raw constructor {classname=} {struct_name=} {extras.pod=} {extras.constructor_raw=} {fnname=}') + comment = f'/** Constructor using raw copy of pre-existing `::{struct_name}`. */' + if extras.pod: + constructor_decl = f'{classname}(const ::{struct_name}* internal)' + else: + constructor_decl = f'{classname}(::{struct_name}* internal)' + out_h.write( '\n') + out_h.write( f' {comment}\n') + explicit = '' + if parse.has_refs( tu, struct_cursor.type): + # Don't allow implicit construction from low-level struct, because our + # destructor will drop it without a prior balancing keep. + explicit = f'explicit ' + out_h.write( + f' /* This constructor is marked as `explicit` because wrapper classes do not\n' + f' call `keep`in constructors, but do call `drop` in destructors. So\n' + f' automatic construction from a {struct_name}* will generally cause an\n' + f' unbalanced `drop` resulting in errors such as SEGV. */\n' + ) + if extras.constructor_raw == 'default': + out_h.write( f' FZ_FUNCTION {explicit}{classname}(::{struct_name}* internal=NULL);\n') + else: + out_h.write( f' FZ_FUNCTION {explicit}{constructor_decl};\n') + + if extras.constructor_raw != 'declaration_only': + out_cpp.write( f'FZ_FUNCTION {classname}::{constructor_decl}\n') + if extras.pod == 'inline': + pass + elif extras.pod: + out_cpp.write( ': m_internal(*internal)\n') + else: + out_cpp.write( ': m_internal(internal)\n') + out_cpp.write( '{\n') + if extras.pod == 'inline': + assert struct_cursor, f'cannot form raw constructor for inline pod {classname} without cursor for underlying {struct_name}' + out_cpp.write( f' assert( internal);\n') + for c in parse.get_members(struct_cursor): + if c.type.kind == state.clang.cindex.TypeKind.CONSTANTARRAY: + out_cpp.write( f' memcpy(this->{c.spelling}, internal->{c.spelling}, sizeof(this->{c.spelling}));\n') + else: + out_cpp.write( f' this->{c.spelling} = internal->{c.spelling};\n') + if parse.has_refs( tu, struct_cursor.type): + out_cpp.write( f' {refcheck_if}\n') + out_cpp.write( f' if (s_check_refs)\n') + out_cpp.write( f' {{\n') + out_cpp.write( f' s_{classname}_refs_check.add( this, __FILE__, __LINE__, __FUNCTION__);\n') + out_cpp.write( f' }}\n') + out_cpp.write( f' #endif\n') + + out_cpp.write(num_instances(refcheck_if, +1, classname)) + + out_cpp.write( '}\n') + out_cpp.write( '\n') + + if extras.pod == 'inline': + # Write second constructor that takes underlying struct by value. + # + assert not parse.has_refs( tu, struct_cursor.type) + constructor_decl = f'{classname}(const ::{struct_name} internal)' + out_h.write( '\n') + out_h.write( f' {comment}\n') + out_h.write( f' FZ_FUNCTION {constructor_decl};\n') + + if extras.constructor_raw != 'declaration_only': + out_cpp.write( f'FZ_FUNCTION {classname}::{constructor_decl}\n') + out_cpp.write( '{\n') + for c in parse.get_members(struct_cursor): + if c.type.kind == state.clang.cindex.TypeKind.CONSTANTARRAY: + out_cpp.write( f' memcpy(this->{c.spelling}, &internal.{c.spelling}, sizeof(this->{c.spelling}));\n') + else: + out_cpp.write( f' this->{c.spelling} = internal.{c.spelling};\n') + + out_cpp.write(num_instances(refcheck_if, +1, classname)) + out_cpp.write( '}\n') + out_cpp.write( '\n') + + # Write accessor for inline state.state_. + # + for const in False, True: + space_const = ' const' if const else '' + const_space = 'const ' if const else '' + out_h.write( '\n') + out_h.write( f' /** Access as underlying struct. */\n') + out_h.write( f' FZ_FUNCTION {const_space}::{struct_name}* internal(){space_const};\n') + out_cpp.write( f'{comment}\n') + out_cpp.write( f'FZ_FUNCTION {const_space}::{struct_name}* {classname}::internal(){space_const}\n') + out_cpp.write( '{\n') + field0 = parse.get_field0(struct_cursor.canonical).spelling + out_cpp.write( f' auto ret = ({const_space}::{struct_name}*) &this->{field0};\n') + if parse.has_refs( tu, struct_cursor.type): + out_cpp.write( f' {refcheck_if}\n') + out_cpp.write( f' if (s_check_refs)\n') + out_cpp.write( f' {{\n') + out_cpp.write( f' s_{classname}_refs_check.add( this, __FILE__, __LINE__, __FUNCTION__);\n') + out_cpp.write( f' }}\n') + out_cpp.write( f' #endif\n') + + out_cpp.write( ' return ret;\n') + out_cpp.write( '}\n') + out_cpp.write( '\n') + + + +def class_accessors( + tu, + register_fn_use, + classname, + struct_cursor, + struct_name, + extras, + out_h, + out_cpp, + ): + ''' + Writes accessor functions for member data. + ''' + if not extras.pod: + jlib.logx( 'creating accessor for non-pod class {classname=} wrapping {struct_name}') + + n = 0 + + for cursor in parse.get_members(struct_cursor): + n += 1 + #jlib.log( 'accessors: {cursor.spelling=} {cursor.type.spelling=}') + + # We set this to fz_keep_<type>() function to call, if we return a + # wrapper class constructed from raw pointer to underlying fz_* struct. + keep_function = None + + # Set <decl> to be prototype with %s where the name is, e.g. 'int + # %s()'; later on we use python's % operator to replace the '%s' + # with the name we want. + # + if cursor.type.kind == state.clang.cindex.TypeKind.POINTER: + decl = 'const ' + declaration_text( cursor.type, '%s()') + pointee_type = state.get_name_canonical( cursor.type.get_pointee()).spelling + pointee_type = util.clip( pointee_type, 'const ') + pointee_type = util.clip( pointee_type, 'struct ') + #if 'fz_' in pointee_type: + # jlib.log( '{pointee_type=}') + # We don't attempt to make accessors to function pointers. + if state.get_name_canonical( cursor.type.get_pointee()).kind == state.clang.cindex.TypeKind.FUNCTIONPROTO: + jlib.logx( 'ignoring {cursor.spelling=} because pointer to FUNCTIONPROTO') + continue + elif pointee_type.startswith( ('fz_', 'pdf_')): + extras2 = parse.get_fz_extras( tu, pointee_type) + if extras2: + # Make this accessor return an instance of the wrapping + # class by value. + # + classname2 = rename.class_( pointee_type) + decl = f'{classname2} %s()' + + # If there's a fz_keep_() function, we must call it on the + # internal data before returning the wrapper class. + pointee_type_base = util.clip( pointee_type, ('fz_', 'pdf_')) + keep_function = f'{parse.prefix(pointee_type)}keep_{pointee_type_base}' + if state.state_.find_function( tu, keep_function, method=False): + jlib.logx( 'using {keep_function=}') + else: + jlib.log( 'cannot find {keep_function=}') + keep_function = None + elif cursor.type.kind == state.clang.cindex.TypeKind.FUNCTIONPROTO: + jlib.log( 'ignoring {cursor.spelling=} because FUNCTIONPROTO') + continue + else: + if 0 and extras.pod: # lgtm [py/unreachable-statement] + # Return reference so caller can modify. Unfortunately SWIG + # converts non-const references to pointers, so generated + # python isn't useful. + fn_args = '& %s()' + else: + fn_args = '%s()' + if cursor.type.get_array_size() >= 0: + if 0: # lgtm [py/unreachable-statement] + # Return reference to the array; we need to put fn name + # and args inside (...) to allow the declaration syntax + # to work - we end up with things like: + # + # char (& media_class())[64]; + # + # Unfortunately SWIG doesn't seem to be able to cope + # with this. + decl = declaration_text( cursor.type, '(%s)' % fn_args) + else: + # Return pointer to the first element of the array, so + # that SWIG can cope. + fn_args = '* %s()' + type_ = cursor.type.get_array_element_type() + decl = declaration_text( type_, fn_args) + else: + if ( cursor.type.kind == state.clang.cindex.TypeKind.TYPEDEF + and cursor.type.get_typedef_name() in ('uint8_t', 'int8_t') + ): + # Don't let accessor return uint8_t because SWIG thinks it + # is a char*, leading to memory errors. Instead return int. + # + jlib.logx('Changing from {cursor.type.get_typedef_name()=} {cursor.type=} to int') + decl = f'int {fn_args}' + else: + decl = declaration_text( cursor.type, fn_args) + + # todo: if return type is uint8_t or int8_t, maybe return as <int> + # so SWIG doesn't think it is a string? This would fix errors with + # fz_image::n and fz_image::bpc. + out_h.write( f' FZ_FUNCTION {decl % cursor.spelling};\n') + out_cpp.write( 'FZ_FUNCTION %s\n' % (decl % ( f'{classname}::{cursor.spelling}'))) + out_cpp.write( '{\n') + if keep_function: + out_cpp.write( f' {rename.ll_fn(keep_function)}(m_internal->{cursor.spelling});\n') + out_cpp.write( f' return ({classname2}) m_internal->{cursor.spelling};\n') + else: + if extras.pod: + out_cpp.write( f' return m_internal.{cursor.spelling};\n') + else: + out_cpp.write( f' return m_internal->{cursor.spelling};\n') + out_cpp.write( '}\n') + out_cpp.write( '\n') + assert n, f'No fields found for {struct_cursor.spelling}.' + + + +def class_destructor( + tu, + register_fn_use, + classname, + extras, + struct_cursor, + destructor_fns, + out_h, + out_cpp, + refcheck_if, + trace_if, + ): + if len(destructor_fns) > 1: + # Use function with shortest name. + if 0: # lgtm [py/unreachable-statement] + jlib.log( 'Multiple possible destructor fns for {classname=}') + for fnname, cursor in destructor_fns: + jlib.log( ' {fnname=} {cursor.spelling=}') + shortest = None + for i in destructor_fns: + if shortest is None or len(i[0]) < len(shortest[0]): + shortest = i + #jlib.log( 'Using: {shortest[0]=}') + destructor_fns = [shortest] + if len(destructor_fns): + fnname, cursor = destructor_fns[0] + register_fn_use( cursor.spelling) + out_h.write( f' /** Destructor using {cursor.spelling}(). */\n') + out_h.write( f' FZ_FUNCTION ~{classname}();\n') + + out_cpp.write( f'FZ_FUNCTION {classname}::~{classname}()\n') + out_cpp.write( '{\n') + out_cpp.write( f' {rename.ll_fn(fnname)}(m_internal);\n') + if parse.has_refs( tu, struct_cursor.type): + out_cpp.write( f' {refcheck_if}\n') + out_cpp.write( f' if (s_check_refs)\n') + out_cpp.write( ' {\n') + out_cpp.write( f' s_{classname}_refs_check.remove( this, __FILE__, __LINE__, __FUNCTION__);\n') + out_cpp.write( ' }\n') + out_cpp.write( f' #endif\n') + + out_cpp.write(num_instances(refcheck_if, -1, classname)) + + out_cpp.write( '}\n') + out_cpp.write( '\n') + else: + out_h.write(f' {refcheck_if}\n') + out_h.write(f' /** Destructor only decrements s_num_instances. */\n') + out_h.write(f' FZ_FUNCTION ~{classname}();\n') + out_h.write( ' #else\n') + out_h.write( ' /** We use default destructor. */\n') + out_h.write( ' #endif\n') + + out_cpp.write( f'{refcheck_if}\n') + out_cpp.write( f'FZ_FUNCTION {classname}::~{classname}()\n') + out_cpp.write( '{\n') + out_cpp.write(num_instances(refcheck_if, -1, classname)) + out_cpp.write( '}\n') + out_cpp.write( '#endif\n') + out_cpp.write( '\n') + + + +def pod_class_members( + tu, + classname, + struct_cursor, + struct_name, + extras, + out_h, + out_cpp, + ): + ''' + Writes code for wrapper class's to_string() member function. + ''' + out_h.write( f'\n') + out_h.write( f' /** Returns string containing our members, labelled and inside (...), using operator<<. */\n') + out_h.write( f' FZ_FUNCTION std::string to_string();\n') + + out_h.write( f'\n') + out_h.write( f' /** Comparison method. */\n') + out_h.write( f' FZ_FUNCTION bool operator==(const {classname}& rhs);\n') + + out_h.write( f'\n') + out_h.write( f' /** Comparison method. */\n') + out_h.write( f' FZ_FUNCTION bool operator!=(const {classname}& rhs);\n') + + out_cpp.write( f'FZ_FUNCTION std::string {classname}::to_string()\n') + out_cpp.write( f'{{\n') + out_cpp.write( f' std::ostringstream buffer;\n') + out_cpp.write( f' buffer << *this;\n') + out_cpp.write( f' return buffer.str();\n') + out_cpp.write( f'}}\n') + out_cpp.write( f'\n') + + out_cpp.write( f'FZ_FUNCTION bool {classname}::operator==(const {classname}& rhs)\n') + out_cpp.write( f'{{\n') + out_cpp.write( f' return ::operator==( *this, rhs);\n') + out_cpp.write( f'}}\n') + out_cpp.write( f'\n') + + out_cpp.write( f'FZ_FUNCTION bool {classname}::operator!=(const {classname}& rhs)\n') + out_cpp.write( f'{{\n') + out_cpp.write( f' return ::operator!=( *this, rhs);\n') + out_cpp.write( f'}}\n') + out_cpp.write( f'\n') + + +def struct_to_string_fns( + tu, + struct_cursor, + struct_name, + extras, + out_h, + out_cpp, + ): + ''' + Writes functions for text representation of struct/wrapper-class members. + ''' + out_h.write( f'\n') + out_h.write( f'/** Returns string containing a {struct_name}\'s members, labelled and inside (...), using operator<<. */\n') + out_h.write( f'FZ_FUNCTION std::string to_string_{struct_name}(const ::{struct_name}& s);\n') + + out_h.write( f'\n') + out_h.write( f'/** Returns string containing a {struct_name}\'s members, labelled and inside (...), using operator<<.\n') + out_h.write( f'(Convenience overload). */\n') + out_h.write( f'FZ_FUNCTION std::string to_string(const ::{struct_name}& s);\n') + + out_cpp.write( f'\n') + out_cpp.write( f'FZ_FUNCTION std::string to_string_{struct_name}(const ::{struct_name}& s)\n') + out_cpp.write( f'{{\n') + out_cpp.write( f' std::ostringstream buffer;\n') + out_cpp.write( f' buffer << s;\n') + out_cpp.write( f' return buffer.str();\n') + out_cpp.write( f'}}\n') + + out_cpp.write( f'\n') + out_cpp.write( f'FZ_FUNCTION std::string to_string(const ::{struct_name}& s)\n') + out_cpp.write( f'{{\n') + out_cpp.write( f' return to_string_{struct_name}(s);\n') + out_cpp.write( f'}}\n') + + +def pod_struct_fns( + tu, + namespace, + struct_cursor, + struct_name, + extras, + out_h, + out_cpp, + ): + ''' + Writes extra fns for POD structs - operator<<(), operator==(), operator!=(). + ''' + # Write operator<< functions for streaming text representation of C struct + # members. We should be at top-level in out_h and out_cpp, i.e. not inside + # 'namespace mupdf {...}'. + out_h.write( f'\n') + out_h.write( f'/** {struct_name}: writes members, labelled and inside (...), to a stream. */\n') + out_h.write( f'FZ_FUNCTION std::ostream& operator<< (std::ostream& out, const ::{struct_name}& rhs);\n') + + out_cpp.write( f'\n') + out_cpp.write( f'FZ_FUNCTION std::ostream& operator<< (std::ostream& out, const ::{struct_name}& rhs)\n') + out_cpp.write( f'{{\n') + i = 0 + out_cpp.write( f' out\n') + out_cpp.write( f' << "("\n'); + for cursor in parse.get_members(struct_cursor): + out_cpp.write( f' << '); + if i: + out_cpp.write( f'" {cursor.spelling}="') + else: + out_cpp.write( f' "{cursor.spelling}="') + out_cpp.write( f' << rhs.{cursor.spelling}\n') + i += 1 + out_cpp.write( f' << ")"\n'); + out_cpp.write( f' ;\n') + out_cpp.write( f' return out;\n') + out_cpp.write( f'}}\n') + + # Write comparison fns. + out_h.write( f'\n') + out_h.write( f'/** {struct_name}: comparison function. */\n') + out_h.write( f'FZ_FUNCTION bool operator==( const ::{struct_name}& lhs, const ::{struct_name}& rhs);\n') + out_h.write( f'\n') + out_h.write( f'/** {struct_name}: comparison function. */\n') + out_h.write( f'FZ_FUNCTION bool operator!=( const ::{struct_name}& lhs, const ::{struct_name}& rhs);\n') + + out_cpp.write( f'\n') + out_cpp.write( f'FZ_FUNCTION bool operator==( const ::{struct_name}& lhs, const ::{struct_name}& rhs)\n') + out_cpp.write( f'{{\n') + for cursor in parse.get_members(struct_cursor): + out_cpp.write( f' if (lhs.{cursor.spelling} != rhs.{cursor.spelling}) return false;\n') + out_cpp.write( f' return true;\n') + out_cpp.write( f'}}\n') + out_cpp.write( f'FZ_FUNCTION bool operator!=( const ::{struct_name}& lhs, const ::{struct_name}& rhs)\n') + out_cpp.write( f'{{\n') + out_cpp.write( f' return !(lhs == rhs);\n') + out_cpp.write( f'}}\n') + + +def pod_class_fns( + tu, + classname, + struct_cursor, + struct_name, + extras, + out_h, + out_cpp, + ): + ''' + Writes extra fns for wrappers for POD structs - operator<<(), operator==(), + operator!=(). + ''' + # Write functions for text representation of wrapper-class members. These + # functions make use of the corresponding struct functions created by + # struct_to_string_fns(). + # + assert extras.pod != 'none' + classname = f'mupdf::{classname}' + out_h.write( f'\n') + out_h.write( f'/** {classname}: writes underlying {struct_name}\'s members, labelled and inside (...), to a stream. */\n') + out_h.write( f'FZ_FUNCTION std::ostream& operator<< (std::ostream& out, const {classname}& rhs);\n') + + out_cpp.write( f'\n') + out_cpp.write( f'FZ_FUNCTION std::ostream& operator<< (std::ostream& out, const {classname}& rhs)\n') + out_cpp.write( f'{{\n') + if extras.pod == 'inline': + out_cpp.write( f' return out << *rhs.internal();\n') + elif extras.pod: + out_cpp.write( f' return out << rhs.m_internal;\n') + else: + out_cpp.write( f' return out << " " << *rhs.m_internal;\n') + out_cpp.write( f'}}\n') + + # Write comparison fns, using comparison of underlying MuPDF struct. + out_h.write( f'\n') + out_h.write( f'/** {classname}: comparison function. */\n') + out_h.write( f'FZ_FUNCTION bool operator==( const {classname}& lhs, const {classname}& rhs);\n') + out_h.write( f'\n') + out_h.write( f'/** {classname}: comparison function. */\n') + out_h.write( f'FZ_FUNCTION bool operator!=( const {classname}& lhs, const {classname}& rhs);\n') + + out_cpp.write( f'\n') + out_cpp.write( f'FZ_FUNCTION bool operator==( const {classname}& lhs, const {classname}& rhs)\n') + out_cpp.write( f'{{\n') + if extras.pod == 'inline': + out_cpp.write( f' return *lhs.internal() == *rhs.internal();\n') + else: + out_cpp.write( f' return lhs.m_internal == rhs.m_internal;\n') + out_cpp.write( f'}}\n') + + out_cpp.write( f'\n') + out_cpp.write( f'FZ_FUNCTION bool operator!=( const {classname}& lhs, const {classname}& rhs)\n') + out_cpp.write( f'{{\n') + if extras.pod == 'inline': + out_cpp.write( f' return *lhs.internal() != *rhs.internal();\n') + else: + out_cpp.write( f' return lhs.m_internal != rhs.m_internal;\n') + out_cpp.write( f'}}\n') + + +def get_struct_fnptrs( cursor_struct, shallow_typedef_expansion=False, verbose=False): + ''' + Yields (cursor, fnptr_type) for function-pointer members of struct defined + at cusor, where <cursor> is the cursor of the member and <fntr_type> is the + type. + + cursor_struct: + Cursor for definition of struct; this can be a typedef. + shallow_typedef_expansion: + If true, the returned <fnptr_type> has any top-level typedefs resolved + so will be a clang.cindex.TypeKind.FUNCTIONPROTO, but typedefs within + the function args are not resolved, e.g. they can be size_t. This can + be useful when generating code that will be compiled on different + platforms with differing definitions of size_t. + ''' + if verbose: + jlib.log('Looking for fnptrs in {cursor_struct.spelling=}') + for cursor in parse.get_members(cursor_struct): + t = cursor.type + if verbose: + jlib.log('{t.kind=} {cursor.spelling=}') + if t.kind == state.clang.cindex.TypeKind.POINTER: + t = cursor.type.get_pointee() + if t.kind in (state.clang.cindex.TypeKind.TYPEDEF, state.clang.cindex.TypeKind.ELABORATED): + t_cursor = t.get_declaration() + t = t_cursor.underlying_typedef_type + if t.kind == state.clang.cindex.TypeKind.FUNCTIONPROTO: + if shallow_typedef_expansion: + if verbose: + jlib.log('Not calling state.get_name_canonical() for {t.spelling=}. {cursor.spelling=}.') + else: + tt = state.get_name_canonical( t) + if verbose: + jlib.log('{tt.spelling=}') + if (0 + or 'struct (unnamed at ' in tt.spelling + or 'unnamed struct at ' in tt.spelling + ): + + # This is clang giving an unhelpful name to an + # anonymous struct. + if verbose: + jlib.log( 'Avoiding clang anonymous struct placeholder: {tt.spelling=}') + else: + t = tt + if verbose: + jlib.log('Yielding: {cursor.spelling=} {t.spelling=}') + yield cursor, t + + +def class_wrapper_virtual_fnptrs( + tu, + struct_cursor, + struct_name, + classname, + extras, + out_h, + out_cpp, + out_h_end, + generated, + refcheck_if, + trace_if, + ): + ''' + Generate extra wrapper class if struct contains function pointers, for + use as a SWIG Director class so that the function pointers can be made to + effectively point to Python or C# code. + ''' + if not extras.virtual_fnptrs: + return + verbose = state.state_.show_details( struct_name) + generated.virtual_fnptrs.append( f'{classname}2') + + self_ = extras.virtual_fnptrs.pop( 'self_') + self_n = extras.virtual_fnptrs.pop( 'self_n', 1) + alloc = extras.virtual_fnptrs.pop( 'alloc') + free = extras.virtual_fnptrs.pop( 'free', None) + comment = extras.virtual_fnptrs.pop( 'comment', None) + assert not extras.virtual_fnptrs, f'Unused items in virtual_fnptrs: {extras.virtual_fnptrs}' + + # Class definition beginning. + # + out_h.write( '\n') + out_h.write( f'/** Wrapper class for struct {struct_name} with virtual fns for each fnptr; this is for use as a SWIG Director class. */\n') + if comment: + out_h.write(comment) + out_h.write( f'struct {classname}2 : {classname}\n') + out_h.write( '{\n') + + out_cpp.write( f'/* Implementation of methods for `{classname}2`, virtual_fnptrs wrapper for `{struct_name}`). */\n') + out_cpp.write( '\n') + + def get_fnptrs( shallow_typedef_expansion=False): + for i in get_struct_fnptrs( struct_cursor, shallow_typedef_expansion, verbose=verbose): + yield i + + # Constructor + # + out_h.write( '\n') + out_h.write( ' /** == Constructor. */\n') + out_h.write(f' FZ_FUNCTION {classname}2();\n') + out_cpp.write('\n') + out_cpp.write(f'FZ_FUNCTION {classname}2::{classname}2()\n') + out_cpp.write( '{\n') + alloc = [''] + alloc.split('\n') + alloc = '\n '.join(alloc) + out_cpp.write(f'{alloc}\n') + out_cpp.write(f' {trace_if}\n') + out_cpp.write(f' if (s_trace_director)\n') + out_cpp.write( ' {\n') + out_cpp.write(f' std::cerr << __FILE__ << ":" << __LINE__ << ":" << __FUNCTION__ << ": {classname}2::{classname}2(): this=" << this << "\\n";\n') + if not extras.pod: + out_cpp.write(f' std::cerr << __FILE__ << ":" << __LINE__ << ":" << __FUNCTION__ << ": {classname}2::{classname}2(): m_internal=" << m_internal << "\\n";\n') + out_cpp.write(f' {classname}2* self = {self_("m_internal")};\n') + out_cpp.write(f' std::cerr << __FILE__ << ":" << __LINE__ << ":" << __FUNCTION__ << ": {classname}2::{classname}2(): self=" << self << "\\n";\n') + out_cpp.write(' }\n') + out_cpp.write(' #endif\n') + out_cpp.write( '}\n') + + # Destructor. This needs to be virtual with an empty implementation, + # because instances will generally be derived classes. + out_h.write( '\n') + out_h.write( ' /** == Destructor. */\n') + out_h.write(f' FZ_FUNCTION virtual ~{classname}2();\n') + out_cpp.write('\n') + out_cpp.write(f'FZ_FUNCTION {classname}2::~{classname}2()\n') + out_cpp.write( '{\n') + out_cpp.write(f' {trace_if}\n') + out_cpp.write(f' if (s_trace_director)\n') + out_cpp.write( ' {\n') + out_cpp.write(f' std::cerr << __FILE__ << ":" << __LINE__ << ":" << __FUNCTION__ << ": ~{classname}2(): this=" << this << "\\n";\n') + if not extras.pod: + out_cpp.write( f' std::cerr << __FILE__ << ":" << __LINE__ << ":" << __FUNCTION__ << ": ~{classname}2(): m_internal=" << m_internal << "\\n";\n') + out_cpp.write( ' }\n') + out_cpp.write(f' #endif\n') + if free: + out_cpp.write(f' {free}\n') + out_cpp.write( '}\n') + + def write(text): + out_h.write(text) + out_cpp.write(text) + + # Define static callback for each fnptr. It's important that these + # functions do not resolve function parameter typedefs such as size_t to + # the underlying types such as long int, because: + # + # * Our generated code can be compiled on different machines where types + # such as size_t can be typedef-ed differently. + # + # * Elsewhere, code that we generate will assign our static callback + # functions to MuPDF's function pointers (which use things like size_t). + # + # * These assignments will fail if the types don't match exactly. + # + # For example fz_output has a member: + # fz_output_write_fn *write; + # + # This typedef is: + # void (fz_output_write_fn)(fz_context *ctx, void *state, const void *data, size_t n); + # + # We generate a static function called Output2_s_write() and we will be + # setting a fz_output's write member to point to Output2_s_write(), which + # only works if the types match exactly. + # + # So we need to resolve the outer 'typedef fz_output_write_fn', but not + # the inner 'size_t' typedef for the <n> arg. This is slightly tricky with + # clang-python - it provide a Type.get_canonical() method that resolves all + # typedefs, but to resolve just one level of typedefs requires a bit more + # work. See get_struct_fnptrs() for details. + # + # [Usually our generated code deliberately resolves typedefs such as size_t + # to long int etc, because SWIG-generated code for size_t etc does not + # always work properly due to SWIG having its own definitions of things + # like size_t in Python/C#. But in this case the generated static function + # is not seen by SWIG so it's ok to make it use size_t etc.] + # + for cursor, fnptr_type in get_fnptrs( shallow_typedef_expansion=True): + + # Write static callback. + return_type = _make_top_level(fnptr_type.get_result().spelling) + out_cpp.write(f'/* Static callback, calls self->{cursor.spelling}(). */\n') + out_cpp.write(f'static {return_type} {classname}2_s_{cursor.spelling}') + out_cpp.write('(') + sep = '' + for i, arg_type in enumerate( fnptr_type.argument_types()): + name = f'arg_{i}' + out_cpp.write(sep) + out_cpp.write( declaration_text( arg_type, name, expand_typedef=False)) + sep = ', ' + out_cpp.write(')') + out_cpp.write('\n') + out_cpp.write('{\n') + self_expression = self_() if self_n is None else self_( f'arg_{self_n}') + out_cpp.write(f' {classname}2* self = {self_expression};\n') + out_cpp.write(f' {trace_if}\n') + out_cpp.write(f' if (s_trace_director)\n') + out_cpp.write( ' {\n') + out_cpp.write(f' std::cerr << __FILE__ << ":" << __LINE__ << ":" << __FUNCTION__ << ": {classname}2_s_{cursor.spelling}(): arg_0=" << arg_0 << " arg_1=" << arg_1 << " self=" << self << "\\n";\n') + out_cpp.write( ' }\n') + out_cpp.write( ' #endif\n') + out_cpp.write( ' {\n') + out_cpp.write( ' char error_message[256] = "";\n') + out_cpp.write( ' try\n') + out_cpp.write( ' {\n') + out_cpp.write(f' return self->{cursor.spelling}(') + sep = '' + for i, arg_type in enumerate( fnptr_type.argument_types()): + if i == self_n: + # This is the void* from which we found `self` so ignore + # here. Note that we still pass the fz_context to the virtual + # fn. + continue + name = f'arg_{i}' + out_cpp.write( f'{sep}{name}') + sep = ', ' + out_cpp.write(');\n') + out_cpp.write(' }\n') + + # todo: catch our different exception types and map to FZ_ERROR_*. + out_cpp.write( ' catch (std::exception& e)\n') + out_cpp.write( ' {\n') + out_cpp.write(f' {trace_if}\n') + out_cpp.write( ' if (s_trace_director)\n') + out_cpp.write( ' {\n') + out_cpp.write(f' std::cerr << __FILE__ << ":" << __LINE__ << ":" << __FUNCTION__ << ": {classname}2_s_{cursor.spelling}(): converting std::exception to fz_throw(): " << e.what() << "\\n";\n') + out_cpp.write( ' }\n') + out_cpp.write( ' #endif\n') + out_cpp.write( ' fz_strlcpy(error_message, e.what(), sizeof(error_message));\n') + out_cpp.write( ' }\n') + out_cpp.write( ' /* We defer fz_throw() to here, to ensure that `std::exception& e` has been destructed. */\n') + out_cpp.write( ' fz_throw(arg_0, FZ_ERROR_GENERIC, "%s", error_message);\n') + if return_type != 'void': + out_cpp.write(f' /* Keep compiler happy. */\n') + out_cpp.write(f' {return_type} ret;\n') + out_cpp.write(f' return ret;\n') + out_cpp.write( ' }\n') + out_cpp.write('}\n') + + # Define use_virtual_<name>( bool use) method for each fnptr. + # + out_h.write(f'\n') + # Using a Doxygen-style `/**` comment prefix here can break swig with + # `Error: Syntax error in input(3).` if there are no following method + # declarations. + out_h.write(f' /** These methods set the function pointers in *m_internal\n') + out_h.write(f' to point to internal callbacks that call our virtual methods. */\n') + for cursor, fnptr_type in get_fnptrs(): + out_h.write(f' FZ_FUNCTION void use_virtual_{cursor.spelling}( bool use=true);\n') + out_cpp.write(f'FZ_FUNCTION void {classname}2::use_virtual_{cursor.spelling}( bool use)\n') + out_cpp.write( '{\n') + + out_cpp.write(f' {trace_if}\n') + out_cpp.write(f' if (s_trace_director)\n') + out_cpp.write( ' {\n') + out_cpp.write(f' std::cerr << __FILE__ << ":" << __LINE__ << ":" << __FUNCTION__ << ": {classname}2::use_virtual_{cursor.spelling}(): this=" << this << " use=" << use << "\\n";\n') + out_cpp.write( ' }\n') + out_cpp.write( ' #endif\n') + + if extras.pod == 'inline': + # Fnptr (in {classname}2) and virtual function (in {classname}) + # have same name, so we need qualify the fnptr with {classname} to + # ensure we distinguish between the two. + out_cpp.write(f' {classname}::{cursor.spelling} = (use) ? {classname}2_s_{cursor.spelling} : nullptr;\n') + elif extras.pod: + out_cpp.write(f' m_internal.{cursor.spelling} = (use) ? {classname}2_s_{cursor.spelling} : nullptr;\n') + else: + out_cpp.write(f' m_internal->{cursor.spelling} = (use) ? {classname}2_s_{cursor.spelling} : nullptr;\n') + out_cpp.write( '}\n') + + # Write virtual fn default implementations. + # + out_h.write(f'\n') + + # Using a Doxygen-style `/**` comment prefix here can break swig with + # `Error: Syntax error in input(3).` if there are no following method + # declarations. + out_h.write(f' /** Default virtual method implementations; these all throw an exception. */\n') + for cursor, fnptr_type in get_fnptrs(): + + out_h.write(f' FZ_FUNCTION virtual {_make_top_level(fnptr_type.get_result().spelling)} {cursor.spelling}(') + out_cpp.write(f'/* Default implementation of virtual method. */\n') + out_cpp.write(f'FZ_FUNCTION {_make_top_level(fnptr_type.get_result().spelling)} {classname}2::{cursor.spelling}(') + sep = '' + for i, arg_type in enumerate( fnptr_type.argument_types()): + if i == self_n: + # This is the void* from which we found `self` so ignore + # here. Note that we still pass the fz_context to the virtual + # fn. + continue + name = f'arg_{i}' + write(f'{sep}') + decl_text = declaration_text(arg_type, name, verbose=0) + write(decl_text) + sep = ', ' + out_h.write( ');\n') + out_cpp.write( ')\n') + out_cpp.write( '{\n') + out_cpp.write(f' std::cerr << "Unexpected call of unimplemented virtual_fnptrs fn {classname}2::{cursor.spelling}(): this=" << this << ".\\n";\n') + out_cpp.write(f' throw std::runtime_error( "Unexpected call of unimplemented virtual_fnptrs fn {classname}2::{cursor.spelling}()");\n') + out_cpp.write( '}\n') + + out_h.write( '};\n') + + +def class_wrapper( + tu, + register_fn_use, + struct_cursor, + struct_name, + classname, + extras, + out_h, + out_cpp, + out_h_end, + out_cpp2, + out_h2, + generated, + refcheck_if, + trace_if, + ): + ''' + Creates source for a class called <classname> that wraps <struct_name>, + with methods that call selected fz_*() functions. Writes to out_h and + out_cpp. + + Created source is just the per-class code, e.g. it does not contain + #include's. + + Args: + tu: + Clang translation unit. + struct_cursor: + Cursor for struct to wrap. + struct_name: + Name of struct to wrap. + classname: + Name of wrapper class to create. + out_h: + Stream to which we write class definition. + out_cpp: + Stream to which we write method implementations. + out_h_end: + Stream for text that should be put at the end of the generated + header text. + generated: + We write extra python and C# code to generated.out_swig_python and + generated.out_swig_csharp for use in the swig .i file. + + Returns (is_container, has_to_string). <is_container> is true if generated + class has custom begin() and end() methods; <has_to_string> is true if we + have created a to_string() method. + ''' + assert extras, f'extras is None for {struct_name}' + if extras.iterator_next: + class_add_iterator( tu, struct_cursor, struct_name, classname, extras, refcheck_if, trace_if) + + if extras.class_pre: + out_h.write( textwrap.dedent( extras.class_pre)) + + base_name = util.clip( struct_name, ('fz_', 'pdf_')) + + constructor_fns = class_find_constructor_fns( tu, classname, struct_name, base_name, extras) + for fnname in extras.constructors_wrappers: + cursor = state.state_.find_function( tu, fnname, method=True) + assert cursor, f'No cursor for constructor wrapper fnname={fnname}' + constructor_fns.append( (fnname, cursor, None)) + + destructor_fns = class_find_destructor_fns( tu, struct_name, base_name) + + # Class definition beginning. + # + out_h.write( '\n') + if extras.copyable: + out_h.write( f'/** Wrapper class for struct `{struct_name}`. */\n') + else: + out_h.write( f'/** Wrapper class for struct `{struct_name}`. Not copyable or assignable. */\n') + if struct_cursor.raw_comment: + raw_comment = struct_cursor.raw_comment.replace('\r', '') + out_h.write(raw_comment) + if not raw_comment.endswith( '\n'): + out_h.write( '\n') + out_h.write( f'struct {classname}\n{{') + + out_cpp.write( '\n') + out_cpp.write( f'/* Implementation of methods for {classname} (wrapper for {struct_name}). */\n') + out_cpp.write( '\n') + refs = parse.has_refs( tu, struct_cursor.type) + if refs: + refs_name, refs_size = refs + out_cpp.write( f'{refcheck_if}\n') + if isinstance(refs_name, int): + # <refs_name> is offset of .refs in the struct. + allow_int_this = ', true /*allow_int_this*/' if struct_name == 'pdf_obj' else '' + out_cpp.write( f'static RefsCheck<::{struct_name}, {classname}{allow_int_this}> s_{classname}_refs_check({refs_name}, {refs_size});\n') + else: + # <refs_name> is name of .refs in the struct. + out_cpp.write( f'static RefsCheck<::{struct_name}, {classname}> s_{classname}_refs_check(offsetof(::{struct_name}, {refs_name}), {refs_size});\n') + out_cpp.write( f'#endif\n') + out_cpp.write( '\n') + + # Trailing text in header, e.g. typedef for iterator. + # + if extras.class_top: + # Strip leading blank line to avoid slightly odd-looking layout. + text = util.clip( extras.class_top, '\n') + text = textwrap.dedent( text) + text = textwrap.indent( text, ' ') + out_h.write( '\n') + out_h.write( text) + + # Constructors + # + num_constructors = 0 + have_created_default_constructor = False + + if constructor_fns: + out_h.write( '\n') + out_h.write( ' /** == Constructors. */\n') + num_constructors += len(constructor_fns) + for fnname, cursor, duplicate_type in constructor_fns: + # clang-6 appears not to be able to handle fn args that are themselves + # function pointers, so for now we allow function_wrapper() to fail, + # so we need to use temporary buffers, otherwise out_functions_h and + # out_functions_cpp can get partial text written. + # + assert cursor, f'No cursor for constructor function. fnname={fnname} duplicate_type={duplicate_type}' + temp_out_h = io.StringIO() + temp_out_cpp = io.StringIO() + if state.state_.show_details(fnname): + jlib.log('Creating constructor for {=classname fnname}') + if parse.get_first_arg( tu, cursor) == (None, 0): + have_created_default_constructor = True + try: + function_wrapper_class_aware( + tu, + register_fn_use, + fnname, + temp_out_h, + temp_out_cpp, + struct_name, + classname, + cursor, + refcheck_if, + trace_if, + class_static=False, + class_constructor=True, + extras=extras, + struct_cursor=struct_cursor, + duplicate_type=duplicate_type, + ) + except Clang6FnArgsBug as e: + jlib.log( 'Unable to wrap function {fnname} because: {e}') + else: + out_h.write( temp_out_h.getvalue()) + out_cpp.write( temp_out_cpp.getvalue()) + + # Custom constructors. + # + for extra_constructor in extras.constructors_extra: + if extra_constructor.name_args == '()': + have_created_default_constructor = True + class_custom_method( + tu, + register_fn_use, + struct_cursor, + classname, + extra_constructor, + out_h, + out_cpp, + refcheck_if, + trace_if, + ) + num_constructors += 1 + + # Look for function that can be used by copy constructor and operator=. + # + if refs: + assert extras.copyable != 'default', f'Non-POD class for {struct_name=} has refs, so must not use copyable="default"' + + if not extras.pod and extras.copyable and extras.copyable != 'default': + class_copy_constructor( + tu, + register_fn_use, + struct_name, + struct_cursor, + base_name, + classname, + constructor_fns, + out_h, + out_cpp, + refcheck_if, + trace_if, + ) + elif extras.copyable: + out_h.write( '\n') + out_h.write( ' /** We use default copy constructor and operator=. */\n') + + if extras.constructor_default: + if have_created_default_constructor: + if 0: + jlib.log( 'Not creating default constructor because default custom constructor. {struct_name=}') + elif extras.constructor_raw == 'default': + if 0: + jlib.log( 'Not creating default constructor because default raw constructor. {struct_name=}') + else: + class_constructor_default( + tu, + struct_cursor, + classname, + extras, + out_h, + out_cpp, + refcheck_if, + trace_if, + ) + num_constructors += 1 + + # Auto-add all methods that take <struct_name> as first param, but + # skip methods that are already wrapped in extras.method_wrappers or + # extras.methods_extra etc. + # + for fnname in parse.find_wrappable_function_with_arg0_type( tu, struct_name): + if state.state_.show_details(fnname): + jlib.log('{struct_name=}: looking at potential method wrapping {fnname=}') + if fnname in extras.method_wrappers: + #log( 'auto-detected fn already in {struct_name} method_wrappers: {fnname}') + # Omit this function, because there is an extra method with the + # same name. (We could probably include both as they will generally + # have different args so overloading will distinguish them, but + # extra methods are usually defined to be used in preference.) + pass + elif fnname.startswith( 'fz_new_draw_device'): + # fz_new_draw_device*() functions take first arg fz_matrix, but + # aren't really appropriate for the fz_matrix wrapper class. + # + pass + elif isinstance( fnname, list): + assert 0 + else: + for extramethod in extras.methods_extra: + if not extramethod.overload: + if extramethod.name_args.startswith( f'{rename.method( struct_name, fnname)}('): + jlib.log1( 'Omitting default method because same name as extramethod: {extramethod.name_args}') + break + else: + #log( 'adding to extras.method_wrappers: {fnname}') + extras.method_wrappers.append( fnname) + + # Extra static methods. + # + if extras.method_wrappers_static: + out_h.write( '\n') + out_h.write( ' /* == Static methods. */\n') + for fnname in extras.method_wrappers_static: + function_wrapper_class_aware( + tu, + register_fn_use, + fnname, + out_h, + out_cpp, + struct_name, + classname, + fn_cursor=None, + refcheck_if=refcheck_if, + trace_if=trace_if, + class_static=True, + struct_cursor=struct_cursor, + generated=generated, + ) + + # Extra methods that wrap fz_*() fns. + # + if extras.method_wrappers or extras.methods_extra: + out_h.write( '\n') + out_h.write( ' /* == Methods. */') + out_h.write( '\n') + extras.method_wrappers.sort() + for fnname in extras.method_wrappers: + function_wrapper_class_aware( + tu, + register_fn_use, + fnname, + out_h, + out_cpp, + struct_name, + classname, + None, #fn_cursor + refcheck_if, + trace_if, + struct_cursor=struct_cursor, + generated=generated, + debug=state.state_.show_details(fnname), + ) + + # Custom methods. + # + is_container = 0 + custom_destructor = False + for extramethod in extras.methods_extra: + is_constructor, is_destructor, is_begin_end = class_custom_method( + tu, + register_fn_use, + struct_cursor, + classname, + extramethod, + out_h, + out_cpp, + refcheck_if, + trace_if, + ) + if is_constructor: + num_constructors += 1 + if is_destructor: + custom_destructor = True + if is_begin_end: + is_container += 1 + + assert is_container==0 or is_container==2, f'struct_name={struct_name} is_container={is_container}' # Should be begin()+end() or neither. + if is_container: + pass + #jlib.log( 'Generated class has begin() and end(): {classname=}') + + if num_constructors == 0 or extras.constructor_raw: + if state.state_.show_details(struct_name): + jlib.log('calling class_raw_constructor(). {struct_name=}') + class_raw_constructor( + tu, + register_fn_use, + classname, + struct_cursor, + struct_name, + base_name, + extras, + constructor_fns, + out_h, + out_cpp, + refcheck_if, + trace_if, + ) + + # Accessor methods to POD data. + # + if extras.accessors and extras.pod == 'inline': + jlib.log( 'ignoring {extras.accessors=} for {struct_name=} because {extras.pod=}.') + elif extras.accessors: + out_h.write( f'\n') + out_h.write( f' /* == Accessors to members of ::{struct_name} m_internal. */\n') + out_h.write( '\n') + class_accessors( + tu, + register_fn_use, + classname, + struct_cursor, + struct_name, + extras, + out_h, + out_cpp, + ) + + # Destructor. + # + if not custom_destructor: + out_h.write( f'\n') + class_destructor( + tu, + register_fn_use, + classname, + extras, + struct_cursor, + destructor_fns, + out_h, + out_cpp, + refcheck_if, + trace_if, + ) + + # If class has '{structname}* m_internal;', provide access to m_iternal as + # an integer, for use by python etc, and provide `operator bool()`. + if not extras.pod: + class_custom_method( + tu, + register_fn_use, + struct_cursor, + classname, + classes.ExtraMethod( + 'long long', + 'm_internal_value()', + ''' + { + return (uintptr_t) m_internal; + } + ''', + '/** Return numerical value of .m_internal; helps with Python debugging. */', + ), + out_h, + out_cpp, + refcheck_if, + trace_if, + ) + class_custom_method( + tu, + register_fn_use, + struct_cursor, + classname, + classes.ExtraMethod( + '', + 'operator bool()', + f''' + {{ + {trace_if} + if (s_trace) + {{ + std::cerr << __FILE__ << ":" << __LINE__ << ":" + << " {classname}::operator bool() called," + << " m_internal=" << m_internal << "." + << "\\n"; + }} + #endif + return m_internal ? true : false; + }} + ''', + '/** Return true iff `m_internal` is not null. */', + ), + out_h, + out_cpp, + refcheck_if, + trace_if, + ) + + # Class members. + # + out_h.write( '\n') + out_h.write( ' /* == Member data. */\n') + out_h.write( '\n') + if extras.pod == 'none': + pass + elif extras.pod == 'inline': + out_h.write( f' /* These members are the same as the members of ::{struct_name}. */\n') + for c in parse.get_members(struct_cursor): + out_h.write( f' {declaration_text(c.type, c.spelling)};\n') + elif extras.pod: + out_h.write( f' ::{struct_cursor.spelling} m_internal; /** Wrapped data is held by value. */\n') + else: + # Putting this double-asterix comment on same line as m_internal breaks + # swig-4.02 with "Error: Syntax error in input(3).". + out_h.write( f' /** Pointer to wrapped data. */\n') + out_h.write( f' ::{struct_name}* m_internal;\n') + + # Declare static `num_instances` variable. + out_h.write( '\n') + out_h.write(f' /* Ideally this would be in `{refcheck_if}...#endif`, but Swig will\n') + out_h.write(f' generate code regardless so we always need to have this available. */\n') + out_h.write(f' FZ_DATA static int s_num_instances;\n') + + out_cpp.write(f'/* Ideally this would be in `{refcheck_if}...#endif`, but Swig will\n') + out_cpp.write(f'generate code regardless so we always need to have this available. */\n') + out_cpp.write(f'int {classname}::s_num_instances = 0;\n') + out_cpp.write(f'\n') + + # Make operator<< (std::ostream&, ...) for POD classes. + # + has_to_string = False + if extras.pod and extras.pod != 'none': + has_to_string = True + pod_class_members( + tu, + classname, + struct_cursor, + struct_name, + extras, + out_h, + out_cpp, + ) + + # Trailing text in header, e.g. typedef for iterator. + # + if extras.class_bottom: + out_h.write( textwrap.indent( textwrap.dedent( extras.class_bottom), ' ')) + + # Private copy constructor if not copyable. + # + if not extras.copyable: + out_h.write( '\n') + out_h.write( ' private:\n') + out_h.write( '\n') + out_h.write( ' /** This class is not copyable or assignable. */\n') + out_h.write( f' {classname}(const {classname}& rhs);\n') + out_h.write( f' {classname}& operator=(const {classname}& rhs);\n') + + # Class definition end. + # + out_h.write( '};\n') + + if extras.class_post: + out_h_end.write( textwrap.dedent( extras.class_post)) + + if extras.extra_cpp: + out_cpp.write( f'/* .extra_cpp for {struct_name}. */\n') + out_cpp.write( textwrap.dedent( extras.extra_cpp)) + + class_wrapper_virtual_fnptrs( + tu, + struct_cursor, + struct_name, + classname, + extras, + out_h, + out_cpp, + out_h_end, + generated, + refcheck_if, + trace_if, + ) + + return is_container, has_to_string + + +def header_guard( name, out): + ''' + Writes header guard for <name> to stream <out>. + ''' + m = '' + for c in name: + m += c.upper() if c.isalnum() else '_' + out.write( f'#ifndef {m}\n') + out.write( f'#define {m}\n') + out.write( '\n') + + +def tabify( filename, text): + ''' + Returns <text> with leading multiples of 4 spaces converted to tab + characters. + ''' + ret = '' + linenum = 0 + for line in text.split( '\n'): + linenum += 1 + i = 0 + while 1: + if i == len(line): + break + if line[i] != ' ': + break + i += 1 + if i % 4: + if line[ int(i/4)*4:].startswith( ' *'): + # This can happen in comments. + pass + else: + jlib.log( '*** {filename}:{linenum}: {i=} {line!r=} indentation is not a multiple of 4') + num_tabs = int(i / 4) + ret += num_tabs * '\t' + line[ num_tabs*4:] + '\n' + + # We use [:-1] here because split() always returns extra last item '', so + # we will have appended an extra '\n'. + # + return ret[:-1] + + +def refcount_check_code( out, refcheck_if): + ''' + Writes reference count checking code to <out>. + ''' + out.write( textwrap.dedent( + f''' + /* Support for checking that reference counts of underlying + MuPDF structs are not smaller than the number of wrapper class + instances. Enable at runtime by setting environmental variable + MUPDF_check_refs to "1". */ + + static const bool s_check_refs = internal_env_flag("MUPDF_check_refs"); + + /* For each MuPDF struct that has an 'int refs' member, we create + a static instance of this class template with T set to our wrapper + class, for example: + + static RefsCheck<fz_document, FzDocument> s_FzDocument_refs_check; + + Then if s_check_refs is true, each constructor function calls + .add(), the destructor calls .remove() and other class functions + call .check(). This ensures that we check reference counting after + each class operation. + + If <allow_int_this> is true, we allow _this->m_internal to be + an invalid pointer less than 4096, in which case we don't try + to check refs. This is used for pdf_obj because in Python the + enums PDF_ENUM_NAME_* are converted to mupdf.PdfObj's contain + .m_internal's which are the enum values cast to (for_pdf_obj*), so + that they can be used directly. + + If m_size is -1, we don't attempt any checking; this is for fz_xml + which is reference counted but does not have a simple .refs member. + */ + {refcheck_if} + template<typename Struct, typename ClassWrapper, bool allow_int_this=false> + struct RefsCheck + {{ + std::mutex m_mutex; + int m_offset; + int m_size; + std::map<Struct*, int> m_this_to_num; + + RefsCheck(int offset, int size) + : m_offset(offset), m_size(size) + {{ + assert(offset >= 0 && offset < 1000); + assert(m_size == 32 || m_size == 16 || m_size == 8 || m_size == -1); + }} + + void change( const ClassWrapper* this_, const char* file, int line, const char* fn, int delta) + {{ + assert( s_check_refs); + if (m_size == -1) + {{ + /* No well-defined .refs member for us to check, e.g. fz_xml. */ + return; + }} + if (!this_->m_internal) return; + if (allow_int_this) + {{ + #if 0 // Historic diagnostics, might still be useful. + std::cerr << __FILE__ << ":" << __LINE__ + << " " << file << ":" << line << ":" << fn << ":" + << " this_->m_internal=" << this_->m_internal + << "\\n"; + #endif + if ((intptr_t) this_->m_internal < 4096) + {{ + #if 0 // Historic diagnostics, might still be useful. + std::cerr << __FILE__ << ":" << __LINE__ + << " " << file << ":" << line << ":" << fn << ":" + << " Ignoring this_->m_internal=" << this_->m_internal + << "\\n"; + #endif + return; + }} + }} + std::lock_guard< std::mutex> lock( m_mutex); + /* Our lock doesn't make our access to + this_->m_internal->refs thead-safe - other threads + could be modifying it via fz_keep_<Struct>() or + fz_drop_<Struct>(). But hopefully our read will be atomic + in practise anyway? */ + void* refs_ptr = (char*) this_->m_internal + m_offset; + int refs; + if (m_size == 32) refs = *(int32_t*) refs_ptr; + if (m_size == 16) refs = *(int16_t*) refs_ptr; + if (m_size == 8) refs = *(int8_t* ) refs_ptr; + + int& n = m_this_to_num[ this_->m_internal]; + int n_prev = n; + assert( n >= 0); + n += delta; + #if 0 // Historic diagnostics, might still be useful. + std::cerr << file << ":" << line << ":" << fn << "():" + // << " " << typeid(ClassWrapper).name() << ":" + << " this_=" << this_ + << " this_->m_internal=" << this_->m_internal + << " refs=" << refs + << " n: " << n_prev << " => " << n + << "\\n"; + #endif + if ( n < 0) + {{ + #if 0 // Historic diagnostics, might still be useful. + std::cerr << file << ":" << line << ":" << fn << "():" + // << " " << typeid(ClassWrapper).name() << ":" + << " this_=" << this_ + << " this_->m_internal=" << this_->m_internal + << " bad n: " << n_prev << " => " << n + << "\\n"; + #endif + abort(); + }} + if ( n && refs < n) + {{ + #if 0 // Historic diagnostics, might still be useful. + std::cerr << file << ":" << line << ":" << fn << "():" + // << " " << typeid(ClassWrapper).name() << ":" + << " this_=" << this_ + << " this_->m_internal=" << this_->m_internal + << " refs=" << refs + << " n: " << n_prev << " => " << n + << " refs mismatch (refs<n):" + << "\\n"; + #endif + abort(); + }} + if (n && ::abs( refs - n) > 1000) + {{ + /* This traps case where n > 0 but underlying struct is + freed and .ref is set to bogus value by fz_free() or + similar. */ + #if 0 // Historic diagnostics, might still be useful. + std::cerr << file << ":" << line << ":" << fn << "(): " << ": " << typeid(ClassWrapper).name() + << " bad change to refs." + << " this_=" << this_ + << " refs=" << refs + << " n: " << n_prev << " => " << n + << "\\n"; + #endif + abort(); + }} + if (n == 0) m_this_to_num.erase( this_->m_internal); + }} + void add( const ClassWrapper* this_, const char* file, int line, const char* fn) + {{ + change( this_, file, line, fn, +1); + }} + void remove( const ClassWrapper* this_, const char* file, int line, const char* fn) + {{ + change( this_, file, line, fn, -1); + }} + void check( const ClassWrapper* this_, const char* file, int line, const char* fn) + {{ + change( this_, file, line, fn, 0); + }} + }}; + #endif + + ''' + )) + +def cpp_source( + dir_mupdf, + namespace, + base, + header_git, + generated, + check_regress, + clang_info_version, + refcheck_if, + trace_if, + debug, + ): + ''' + Generates all .h and .cpp files. + + Args: + + dir_mupdf: + Location of mupdf checkout. + namespace: + C++ namespace to use. + base: + Directory in which all generated files are placed. + header_git: + If true we include git info in the file comment that is written + into all generated files. + generated: + A Generated instance. + check_regress: + If true, we raise exception if generated content differs from what + is in existing files. + refcheck_if: + `#if ... ' text for enabling reference-checking code. For example + `#if 1` to always enable, `#ifndef NDEBUG` to only enable in debug + builds, `#if 0` to always disable. + refcheck_if: + `#if ... ' text for enabling optional runtime diagnostic, for + example by setting `MuPDF_trace=1` runtime. For example `#if 1` to + always enable, `#ifndef NDEBUG` to only enable in debug builds, + `#if 0` to always disable. + debug: + True if debug build. + + Updates <generated> and returns <tu> from clang.. + ''' + assert isinstance(generated, Generated) + assert not dir_mupdf.endswith( '/') + assert not base.endswith( '/') + + # Do initial setting up of generated files before parse, because we include extra.h in our parse input. + + doit = True + if doit: + class File: + def __init__( self, filename, tabify=True): + self.filename = filename + self.tabify = tabify + self.file = io.StringIO() + self.line_begin = True + self.regressions = True + self.closed = False + def write( self, text, fileline=False): + # Do not allow writes after .close(). + assert not self.closed, f'File.write() called after .close(). {self.filename=}' + if fileline: + # Generate #line <line> "<filename>" for our caller's + # location. This makes any compiler warnings refer to their + # python code rather than the generated C++ code. + tb = traceback.extract_stack( None) + filename, line, function, source = tb[0] + if self.line_begin: + self.file.write( f'#line {line} "{filename}"\n') + self.file.write( text) + self.line_begin = text.endswith( '\n') + def close( self): + if self.closed: + # Allow multiple calls to .close(). + return + self.closed = True + if self.filename: + # Overwrite if contents differ. + text = self.get() + if self.tabify: + text = tabify( self.filename, text) + cr = check_regress + jlib.log('calling util.update_file_regress() check_regress={cr}: {self.filename=}', 1) + e = util.update_file_regress( text, self.filename, check_regression=cr) + jlib.log('util.update_file_regress() returned => {e}', 1) + if e: + jlib.log('util.update_file_regress() => {e=}', 1) + self.regressions = True + jlib.log(f'File updated: {os.path.relpath(self.filename)}') + else: + jlib.log(f'File unchanged: {os.path.relpath(self.filename)}') + def get( self): + return self.file.getvalue() + else: + class File: + def __init__( self, filename): + pass + def write( self, text, fileline=False): + pass + def close( self): + pass + + class Outputs: + ''' + A set of output files. + + For convenience, after outputs.add( 'foo', 'foo.c'), outputs.foo is a + python stream that writes to 'foo.c'. + ''' + def __init__( self): + self.items = [] + + def add( self, name, filename): + ''' + Sets self.<name> to file opened for writing on <filename>. + ''' + file = File( filename) + self.items.append( (name, filename, file)) + setattr( self, name, file) + + def get( self): + ''' + Returns list of (name, filename, file) tuples. + ''' + return self.items + + def close( self): + for name, filename, file in self.items: + file.close() + + out_cpps = Outputs() + out_hs = Outputs() + for name in ( + 'classes', + 'classes2', + 'exceptions', + 'functions', + 'internal', + 'extra', + ): + out_hs.add( name, f'{base}/include/mupdf/{name}.h') + out_cpps.add( name, f'{base}/implementation/{name}.cpp') + + # Make text of header comment for all generated file. + # + header_text = textwrap.dedent( + f''' + /** + This file was auto-generated by mupdfwrap.py. + ''') + + if header_git: + git_id = jlib.get_git_id( dir_mupdf, allow_none=True) + if git_id: + git_id = git_id.split('\n', 1) + header_text += textwrap.dedent( + f''' + mupdf checkout: + {git_id[0]}' + ''') + + header_text += '*/\n' + header_text += '\n' + header_text = header_text[1:] # Strip leading \n. + for _, _, file in out_cpps.get() + out_hs.get(): + file.write( header_text) + + os.makedirs( f'{base}/include/mupdf', exist_ok=True) + os.makedirs( f'{base}/implementation', exist_ok=True) + + num_regressions = 0 + # Create extra File that writes to internal buffer rather than an actual + # file, which we will append to out_h. + # + out_h_classes_end = File( None) + + # Write multiple-inclusion guards into headers: + # + for name, filename, file in out_hs.get(): + prefix = f'{base}/include/' + assert filename.startswith( prefix) + name = filename[ len(prefix):] + header_guard( name, file) + + # We need to write to out_hs.extra here before we do the parse + # because out_hs.extra will be part of the input text passed to the + # clang parser. + # + make_extra(out_hs.extra, out_cpps.extra) + out_hs.extra.write( textwrap.dedent(''' + #endif + ''')) + out_hs.extra.close() + out_cpps.extra.close() + + # Now parse. + # + try: + index = state.clang.cindex.Index.create() + except Exception as e: + raise Exception(f'libclang does not appear to be installed') from e + + header = f'{dir_mupdf}/include/mupdf/fitz.h' + assert os.path.isfile( header), f'header={header}' + + # Get clang to parse mupdf/fitz.h and mupdf/pdf.h and mupdf/extra.h. + # + # It might be possible to use index.parse()'s <unsaved_files> arg to + # specify these multiple files, but i couldn't get that to work. + # + # So instead we write some #include's to a temporary file and ask clang to + # parse it. + # + temp_h = f'_mupdfwrap_temp.cpp' + try: + with open( temp_h, 'w') as f: + if state.state_.linux or state.state_.macos: + jlib.log1('Prefixing Fitz headers with `typedef unsigned long size_t;`' + ' because size_t not available to clang on Linux/MacOS.') + # On Linux, size_t is defined internally in gcc (e.g. not even + # in /usr/include/stdint.h) and so not visible to clang. + # + # If we don't define it, clang complains about C99 not + # supporting implicit int and appears to variously expand + # size_t as different function pointers, e.g. `int (int *)` and + # `int (*)(int *)`. + # + f.write( textwrap.dedent(''' + /* + Workaround on Linux/MacOS. size_t is defined internally in + gcc (e.g. not even in /usr/include/stdint.h) and so not visible to clang. + */ + typedef unsigned long size_t; + ''')) + if state.state_.macos: + f.write( textwrap.dedent(''' + /* + Workaround on MacOS: we need to define fixed-size int types + and FILE and va_list, similarly as with size_t above. + */ + typedef signed char int8_t; + typedef short int16_t; + typedef int int32_t; + typedef long long int64_t; + typedef unsigned char uint8_t; + typedef unsigned short uint16_t; + typedef unsigned int uint32_t; + typedef unsigned long long uint64_t; + typedef struct FILE FILE; + typedef struct va_list va_list; + ''')) + f.write( textwrap.dedent(''' + #include "mupdf/extra.h" + + #include "mupdf/fitz.h" + #include "mupdf/pdf.h" + ''')) + + # libclang often doesn't have access to system headers so we define + # MUPDF_WRAP_LIBCLANG so that extra.h can use dummy definition of + # std::vector. + # + args = [ + '-I', f'{dir_mupdf}/include', + '-I', f'{dir_mupdf}/platform/c++/include', + '-D', 'MUPDF_WRAP_LIBCLANG', + '-D', 'FZ_FUNCTION=', + ] + tu = index.parse( + temp_h, + args = args, + options = 0 + | state.clang.cindex.TranslationUnit.PARSE_INCOMPLETE + | state.clang.cindex.TranslationUnit.PARSE_SKIP_FUNCTION_BODIES + , + ) + + # Show warnings/errors from the parse. Failure to include stddef.h + # appears to be harmless on Linux, but other failures seem to cause + # more problems. + # + def show_clang_diagnostic( diagnostic, depth=0): + for diagnostic2 in diagnostic.children: + show_clang_diagnostic( diagnostic2, depth + 1) + jlib.log1( '{" "*4*depth}{diagnostic}') + if tu.diagnostics: + jlib.log1( 'tu.diagnostics():') + for diagnostic in tu.diagnostics: + show_clang_diagnostic(diagnostic, 1) + + finally: + if os.path.isfile( temp_h): + os.remove( temp_h) + + # Write required #includes into .h files: + # + out_hs.exceptions.write( textwrap.dedent( + ''' + #include <stdexcept> + #include <string> + + #include "mupdf/fitz.h" + + ''')) + + out_hs.internal.write( textwrap.dedent( + ''' + #include <iostream> + + ''')) + + out_hs.functions.write( textwrap.dedent( + ''' + #include "mupdf/extra.h" + + #include "mupdf/fitz.h" + #include "mupdf/pdf.h" + + #include <iostream> + #include <string> + #include <vector> + + ''')) + + out_hs.classes.write( textwrap.dedent( + ''' + #include "mupdf/fitz.h" + #include "mupdf/functions.h" + #include "mupdf/pdf.h" + + #include <map> + #include <string> + #include <vector> + + ''')) + + out_hs.classes2.write( textwrap.dedent( + ''' + #include "classes.h" + + ''')) + + # Write required #includes into .cpp files: + # + out_cpps.exceptions.write( textwrap.dedent( + f''' + #include "mupdf/exceptions.h" + #include "mupdf/fitz.h" + #include "mupdf/internal.h" + + #include <iostream> + + #include <string.h> + + {trace_if} + static const bool s_trace_exceptions = mupdf::internal_env_flag("MUPDF_trace_exceptions"); + #else + static const bool s_trace_exceptions_dummy = mupdf::internal_env_flag_check_unset("{trace_if}", "MUPDF_trace_exceptions"); + #endif + ''')) + + out_cpps.functions.write( textwrap.dedent( + ''' + #include "mupdf/exceptions.h" + #include "mupdf/functions.h" + #include "mupdf/internal.h" + #include "mupdf/extra.h" + + #include <assert.h> + #include <sstream> + + #include <string.h> + + ''')) + + out_cpps.classes.write( + textwrap.dedent( + f''' + #include "mupdf/classes.h" + #include "mupdf/classes2.h" + #include "mupdf/exceptions.h" + #include "mupdf/internal.h" + + #include "mupdf/fitz/geometry.h" + + #include <algorithm> + #include <map> + #include <mutex> + #include <sstream> + #include <string.h> + #include <thread> + + #include <string.h> + + {trace_if} + static const int s_trace = mupdf::internal_env_flag("MUPDF_trace"); + static const bool s_trace_keepdrop = mupdf::internal_env_flag("MUPDF_trace_keepdrop"); + static const bool s_trace_director = mupdf::internal_env_flag("MUPDF_trace_director"); + #else + static const int s_trace = mupdf::internal_env_flag_check_unset("{trace_if}", "MUPDF_trace"); + static const bool s_trace_keepdrop = mupdf::internal_env_flag_check_unset("{trace_if}", "MUPDF_trace_keepdrop"); + static const bool s_trace_director = mupdf::internal_env_flag_check_unset("{trace_if}", "MUPDF_trace_director"); + #endif + ''')) + + out_cpps.classes2.write( + textwrap.dedent( + f''' + #include "mupdf/classes2.h" + #include "mupdf/exceptions.h" + #include "mupdf/internal.h" + + #include "mupdf/fitz/geometry.h" + + #include <map> + #include <mutex> + #include <sstream> + #include <string.h> + #include <thread> + + #include <string.h> + + {trace_if} + static const int s_trace = mupdf::internal_env_flag("MUPDF_trace"); + #else + static const int s_trace = mupdf::internal_env_flag_check_unset("{trace_if}", "MUPDF_trace"); + #endif + ''')) + + namespace = 'mupdf' + for _, _, file in out_cpps.get() + out_hs.get(): + if file in (out_cpps.internal, out_cpps.extra, out_hs.extra): + continue + make_namespace_open( namespace, file) + + # Write reference counting check code to out_cpps.classes. + refcount_check_code( out_cpps.classes, refcheck_if) + + # Write declaration and definition for metadata_keys global. + # + out_hs.functions.write( + textwrap.dedent( + ''' + /* + The keys that are defined for fz_lookup_metadata(). + */ + FZ_DATA extern const std::vector<std::string> metadata_keys; + + ''')) + out_cpps.functions.write( + textwrap.dedent( + f''' + FZ_FUNCTION const std::vector<std::string> metadata_keys = {{ + "format", + "encryption", + "info:Title", + "info:Author", + "info:Subject", + "info:Keywords", + "info:Creator", + "info:Producer", + "info:CreationDate", + "info:ModDate", + }}; + + {trace_if} + static const int s_trace = internal_env_flag("MUPDF_trace"); + static const bool s_trace_keepdrop = internal_env_flag("MUPDF_trace_keepdrop"); + static const bool s_trace_exceptions = internal_env_flag("MUPDF_trace_exceptions"); + static const bool s_check_error_stack = internal_env_flag("MUPDF_check_error_stack"); + #else + static const int s_trace = internal_env_flag_check_unset("{trace_if}", "MUPDF_trace"); + static const bool s_trace_keepdrop = internal_env_flag_check_unset("{trace_if}", "MUPDF_trace_keepdrop"); + static const bool s_trace_exceptions = internal_env_flag_check_unset("{trace_if}", "MUPDF_trace_exceptions"); + static const bool s_check_error_stack = internal_env_flag_check_unset("{trace_if}", "MUPDF_check_error_stack"); + #endif + + ''')) + + # Write source code for exceptions and wrapper functions. + # + jlib.log( 'Creating wrapper functions...') + make_function_wrappers( + tu, + namespace, + out_hs.exceptions, + out_cpps.exceptions, + out_hs.functions, + out_cpps.functions, + out_hs.internal, + out_cpps.internal, + out_hs.classes2, + out_cpps.classes2, + generated, + refcheck_if, + trace_if, + ) + + fn_usage = dict() + functions_unrecognised = set() + + for fnname, cursor in state.state_.find_functions_starting_with( tu, '', method=True): + fn_usage[ fnname] = [0, cursor] + generated.c_functions.append(fnname) + + for structname, cursor in state.state_.structs[ tu].items(): + generated.c_structs.append( structname) + + # Create windows_mupdf.def, containing explicit exports for all MuPDF + # global data and functions. We do this instead of explicitly prefixing + # everything with FZ_FUNCTION or FZ_DATA in the MuPDF header files. + # + windows_def_path = os.path.relpath(f'{base}/windows_mupdf.def') + windows_def = '' + windows_def += 'EXPORTS\n' + + for name, cursor in state.state_.find_global_data_starting_with( tu, ('fz_', 'pdf_')): + if state.state_.show_details(name): + jlib.log('global: {name=}') + generated.c_globals.append(name) + windows_def += f' {name} DATA\n' + for fnname, cursor in state.state_.find_functions_starting_with( tu, ('fz_', 'pdf_', 'FT_'), method=False): + if cursor.storage_class == state.clang.cindex.StorageClass.STATIC: + # These fns do not work in windows.def, probably because they are + # usually inline? + # + jlib.log('Not adding to windows_def because static: {fnname}()', 1) + elif os.path.abspath(cursor.extent.start.file.name) == os.path.abspath(out_hs.extra.filename): + # Items defined in out_hs.extra are C++ so we would need to use the + # mangled name if we added them to windows_def. Instead they are + # explicitly prefixed with `FZ_FUNCTION`. + # + # (We use os.path.abspath() to avoid problems with back and forward + # slashes in cursor.extent.start.file.name on Windows.) + # + jlib.log1('Not adding to {windows_def_path} because defined in {os.path.relpath(out_hs.extra.filename)}: {cursor.spelling}') + else: + windows_def += f' {fnname}\n' + # Add some internal fns that PyMuPDF requires. + for fnname in ( + 'FT_Get_First_Char', + 'FT_Get_Next_Char', + ): + windows_def += f' {fnname}\n' + + if debug: + # In debug builds these are real fns, not macros, and we need to + # make them exported. + windows_def += f' fz_lock_debug_lock\n' + windows_def += f' fz_lock_debug_unlock\n' + + jlib.fs_update( windows_def, windows_def_path) + + def register_fn_use( name): + assert name.startswith( ('fz_', 'pdf_')) + if name in fn_usage: + fn_usage[ name][0] += 1 + else: + functions_unrecognised.add( name) + + # Write source code for wrapper classes. + # + jlib.log( 'Creating wrapper classes...') + + # Find all classes that we can create. + # + classes_ = [] + for cursor in parse.get_children(tu.cursor): + if not cursor.spelling.startswith( ('fz_', 'pdf_')): + continue + if cursor.kind != state.clang.cindex.CursorKind.TYPEDEF_DECL: + continue; + type_ = state.get_name_canonical( cursor.underlying_typedef_type) + if type_.kind not in (state.clang.cindex.TypeKind.RECORD, state.clang.cindex.TypeKind.ELABORATED): + continue + if type_.kind == state.clang.cindex.TypeKind.ELABORATED: + jlib.log( 'state.clang.cindex.TypeKind.ELABORATED: {type_.spelling=}') + + if not cursor.is_definition(): + # Handle abstract type only if we have an ClassExtra for it. + extras = classes.classextras.get( tu, cursor.spelling) + if extras and extras.opaque: + pass + #log( 'Creating wrapper for opaque struct: {cursor.spelling=}') + else: + continue + + #struct_name = type_.spelling + struct_name = cursor.spelling + struct_name = util.clip( struct_name, 'struct ') + if cursor.spelling != struct_name: + jlib.log('{type_.spelling=} {struct_name=} {cursor.spelling=}') + classname = rename.class_( struct_name) + + # For some reason after updating mupdf 2020-04-13, clang-python is + # returning two locations for struct fz_buffer_s, both STRUCT_DECL. One + # is 'typedef struct fz_buffer_s fz_buffer;', the other is the full + # struct definition. + # + # No idea why this is happening. Using .canonical doesn't seem to + # affect things. + # + for cl, cu, s in classes_: + if cl == classname: + jlib.logx( 'ignoring duplicate STRUCT_DECL for {struct_name=}') + break + else: + classes_.append( (classname, cursor, struct_name)) + + classes_.sort() + + # Write forward declarations - this is required because some class + # methods take pointers to other classes. + # + out_hs.classes.write( '\n') + out_hs.classes.write( '/* Forward declarations of all classes that we define. */\n') + for classname, struct_cursor, struct_name in classes_: + out_hs.classes.write( f'struct {classname};\n') + out_hs.classes.write( '\n') + + # Create each class. + # + for classname, struct_cursor, struct_name in classes_: + #jlib.log( 'creating wrapper {classname} for {cursor.spelling}') + extras = classes.classextras.get( tu, struct_name) + assert extras, f'struct_name={struct_name}' + if extras.pod: + struct_to_string_fns( + tu, + struct_cursor, + struct_name, + extras, + out_hs.functions, + out_cpps.functions, + ) + + with jlib.LogPrefixScope( f'{struct_name}: '): + is_container, has_to_string = class_wrapper( + tu, + register_fn_use, + struct_cursor, + struct_name, + classname, + extras, + out_hs.classes, + out_cpps.classes, + out_h_classes_end, + out_cpps.classes2, + out_hs.classes2, + generated, + refcheck_if, + trace_if, + ) + if is_container: + generated.container_classnames.append( classname) + if has_to_string: + generated.to_string_structnames.append( struct_name) + + out_hs.functions.write( textwrap.dedent( ''' + /** Reinitializes the MuPDF context for single-threaded use, which + is slightly faster when calling code is single threaded. + + This should be called before any other use of MuPDF. + */ + FZ_FUNCTION void reinit_singlethreaded(); + + ''')) + + # Generate num_instances diagnostic fn. + out_hs.classes.write('\n') + out_hs.classes.write('/** Returns map from class name (for example FzDocument) to s_num_instances. */\n') + out_hs.classes.write('FZ_FUNCTION std::map<std::string, int> num_instances();\n') + out_cpps.classes.write('FZ_FUNCTION std::map<std::string, int> num_instances()\n') + out_cpps.classes.write('{\n') + out_cpps.classes.write(' std::map<std::string, int> ret;\n') + for classname, struct_cursor, struct_name in classes_: + out_cpps.classes.write(f' ret["{classname}"] = {classname}::s_num_instances;\n') + out_cpps.classes.write(' \n') + out_cpps.classes.write(' return ret;\n') + out_cpps.classes.write('}\n') + + # Write close of namespace. + out_hs.classes.write( out_h_classes_end.get()) + for _, _, file in out_cpps.get() + out_hs.get(): + if file in (out_cpps.internal, out_cpps.extra, out_hs.extra): + continue + make_namespace_close( namespace, file) + + # Write pod struct fns such as operator<<(), operator==() - these need to + # be outside the namespace. + # + for classname, struct_cursor, struct_name in classes_: + extras = classes.classextras.get( tu, struct_name) + if extras.pod: + # Make operator<<(), operator==(), operator!=() for POD struct. + # + pod_struct_fns( + tu, + namespace, + struct_cursor, + struct_name, + extras, + out_hs.functions, + out_cpps.functions, + ) + if extras.pod != 'none': + # Make operator<<(), operator==(), operator!=() for POD class + # wrappers. + # + pod_class_fns( + tu, + classname, + struct_cursor, + struct_name, + extras, + out_hs.classes, + out_cpps.classes, + ) + + + # Terminate multiple-inclusion guards in headers: + # + for name, _, file in out_hs.get(): + if name != 'extra': + file.write( '\n#endif\n') + + out_hs.close() + out_cpps.close() + + generated.h_files = [filename for _, filename, _ in out_hs.get()] + generated.cpp_files = [filename for _, filename, _ in out_cpps.get()] + if 0: # lgtm [py/unreachable-statement] + jlib.log( 'Have created:') + for filename in filenames_h + filenames_cpp: + jlib.log( ' {filename}') + + + # Output usage information. + # + + fn_usage_filename = f'{base}/fn_usage.txt' + out_fn_usage = File( fn_usage_filename, tabify=False) + functions_unused = 0 + functions_used = 0 + + for fnname in sorted( fn_usage.keys()): + n, cursor = fn_usage[ fnname] + exclude_reasons = parse.find_wrappable_function_with_arg0_type_excluded_cache.get( fnname, []) + if n: + functions_used += 1 + else: + functions_unused += 1 + if n and not exclude_reasons: + continue + + out_fn_usage.write( f'Functions not wrapped by class methods:\n') + out_fn_usage.write( '\n') + + for fnname in sorted( fn_usage.keys()): + n, cursor = fn_usage[ fnname] + exclude_reasons = parse.find_wrappable_function_with_arg0_type_excluded_cache.get( fnname, []) + if not exclude_reasons: + continue + if n: + continue + num_interesting_reasons = 0 + for t, description in exclude_reasons: + if t == parse.MethodExcludeReason_FIRST_ARG_NOT_STRUCT: + continue + if t == parse.MethodExcludeReason_VARIADIC: + continue + num_interesting_reasons += 1 + if num_interesting_reasons: + try: + out_fn_usage.write( f' {declaration_text( cursor.type, cursor.spelling)}\n') + except Clang6FnArgsBug as e: + out_fn_usage.write( f' {cursor.spelling} [full prototype not available due to known clang-6 issue]\n') + for t, description in exclude_reasons: + if t == parse.MethodExcludeReason_FIRST_ARG_NOT_STRUCT: + continue + out_fn_usage.write( f' {description}\n') + out_fn_usage.write( '\n') + + out_fn_usage.write( f'\n') + out_fn_usage.write( f'Functions used more than once:\n') + for fnname in sorted( fn_usage.keys()): + n, cursor = fn_usage[ fnname] + if n > 1: + out_fn_usage.write( f' n={n}: {declaration_text( cursor.type, cursor.spelling)}\n') + + out_fn_usage.write( f'\n') + out_fn_usage.write( f'Number of wrapped functions: {len(fn_usage)}\n') + out_fn_usage.write( f'Number of wrapped functions used by wrapper classes: {functions_used}\n') + out_fn_usage.write( f'Number of wrapped functions not used by wrapper classes: {functions_unused}\n') + + out_fn_usage.close() + + generated.c_enums = state.state_.enums[ tu] + + if num_regressions: + raise Exception( f'There were {num_regressions} regressions') + return tu + + +def test(): + ''' + Place to experiment with clang-python. + ''' + text = '' + if state.state_.linux: + text += textwrap.dedent(''' + /* + Workaround on Linux. size_t is defined internally in gcc. It isn't + even in stdint.h. + */ + typedef unsigned long size_t; + ''') + + text += textwrap.dedent(''' + #include "mupdf/fitz.h" + #include "mupdf/pdf.h" + ''') + path = 'wrap-test.c' + jlib.fs_update( text, path) + index = state.clang.cindex.Index.create() + tu = index.parse( path, '-I /usr/include -I include'.split(' ')) + path2 = 'wrap-test.c.c' + tu.save(path2) + jlib.log( 'Have saved to: {path2}') + parse.dump_ast( tu.cursor, 'ast') + for diagnostic in tu.diagnostics: + jlib.log('{diagnostic=}') + for cursor in parse.get_members( tu.cursor): + if 'cpp_test_' in cursor.spelling: + parse.dump_ast(cursor, out=jlib.log)
