diff options
Diffstat (limited to 'demos/python/gsapiwrap.py')
-rwxr-xr-x | demos/python/gsapiwrap.py | 699 |
1 files changed, 699 insertions, 0 deletions
diff --git a/demos/python/gsapiwrap.py b/demos/python/gsapiwrap.py new file mode 100755 index 00000000..0ef0bb08 --- /dev/null +++ b/demos/python/gsapiwrap.py @@ -0,0 +1,699 @@ +#! /usr/bin/env python3 + +''' +Use Swig to build wrappers for gsapi. + +Example usage: + + Note that we use mupdf's scripts/jlib.py, and assume that there is a mupdf + checkout in the parent directory of the ghostpdl checkout - see 'import + jlib' below. + + ./toolbin/gsapiwrap.py --python -l -0 -1 -t + Build python wrapper for gsapi and run simple test. + + ./toolbin/gsapiwrap.py --csharp -l -0 -1 -t + Build C# wrapper for gsapi and run simple test. + +Args: + + -c: + Clean language-specific out-dir. + + -l: + Build libgs.so (by running make). + + -0: + Run swig to generate language-specific files. + + -1: + Generate language wrappers by compiling/linking the files generated by + -0. + + --csharp: + Generate C# wrappers (requires Mono on Linux). Should usually be first + param. + + --python + Generate Python wrappers. Should usually be first param. + + --swig <swig> + Set location of swig binary. + + -t + Run simple test of language wrappers generated by -1. + +Status: + As of 2020-05-22: + Some python wrappers seem to work ok. + + C# wrappers are not implemented for gsapi_set_poll() and + gsapi_set_stdio(). +''' + +import os +import re +import sys +import textwrap + +import jlib + + +def devpython_info(): + ''' + Use python3-config to find libpython.so and python-dev include path etc. + ''' + python_configdir = jlib.system( 'python3-config --configdir', out='return') + libpython_so = os.path.join( + python_configdir.strip(), + f'libpython{sys.version_info[0]}.{sys.version_info[1]}.so', + ) + assert os.path.isfile( libpython_so), f'cannot find libpython_so={libpython_so}' + + python_includes = jlib.system( 'python3-config --includes', out='return') + python_includes = python_includes.strip() + return python_includes, libpython_so + +def swig_version( swig='swig'): + t = jlib.system( f'{swig} -version', out='return') + m = re.search( 'SWIG Version ([0-9]+)[.]([0-9]+)[.]([0-9]+)', t) + assert m + swig_major = int( m.group(1)) + return swig_major + + +dir_ghostpdl = os.path.abspath( f'{__file__}/../../') + '/' + + +def out_dir( language): + if language == 'python': + return 'gsapiwrap/python/' + if language == 'csharp': + return 'gsapiwrap/csharp/' + assert 0 + +def out_so( language): + ''' + Returns name of .so that implements language-specific wrapper. I think + these names have to match what the language runtime requires. + + For python, Swig generates a module foo.py which does 'import _foo'. + + Similarly C# assumes a file called 'libfoo.so'. + ''' + if language == 'python': + return f'{out_dir(language)}_gsapi.so' + if language == 'csharp': + return f'{out_dir(language)}libgsapi.so' + assert 0 + +def lib_gs_info(): + return f'{dir_ghostpdl}sodebugbin/libgs.so', 'make sodebug' + return f'{dir_ghostpdl}sobin/libgs.so', 'make so' + +def lib_gs(): + ''' + Returns name of the gs shared-library. + ''' + return lib_gs_info()[0] + + +def swig_i( swig, language): + ''' + Returns text for a swig .i file for psi/iapi.h. + ''' + swig_major = swig_version( swig) + + + # We need to redeclare or wrap some functions, e.g. to add OUTPUT + # annotations. We use #define, %ignore and #undef to hide the original + # declarations in the .h file. + # + fns_redeclare = ( + 'gsapi_run_file', + 'gsapi_run_string', + 'gsapi_run_string_begin', + 'gsapi_run_string_continue', + 'gsapi_run_string_end', + 'gsapi_run_string_with_length', + 'gsapi_set_poll', + 'gsapi_set_poll_with_handle', + 'gsapi_set_stdio', + 'gsapi_set_stdio_with_handle', + 'gsapi_new_instance', + ) + + + swig_i_text = textwrap.dedent(f''' + %module(directors="1") gsapi + + %include cpointer.i + %pointer_functions(int, pint); + + // This seems to be necessary to make csharp handle OUTPUT args. + // + %include typemaps.i + + // For gsapi_init_with_args(). + %include argcargv.i + + %include cstring.i + + // Include type information in python doc strings. If we have + // swig-4, we can propogate comments from the C api instead, which + // is preferred. + // + {'%feature("autodoc", "3");' if swig_major < 4 else ''} + + %{{ + #include "psi/iapi.h" + //#include "base/gserrors.h" + + // Define wrapper functions that present a modified API that + // swig can cope with. + // + + // Swig cannot handle void** out-param. + // + static void* new_instance( void* caller_handle, int* out) + {{ + void* ret = NULL; + *out = gsapi_new_instance( &ret, caller_handle); + printf( "gsapi_new_instance() returned *out=%i ret=%p\\n", *out, ret); + fflush( stdout); + return ret; + }} + + // Swig cannot handle (const char* str, int strlen) args. + // + static int run_string_continue(void *instance, const char *str, int user_errors, int *pexit_code) {{ + + return gsapi_run_string_continue( instance, str, strlen(str), user_errors, pexit_code); + }} + %}} + + // Strip gsapi_ prefix from all generated names. + // + %rename("%(strip:[gsapi_])s") ""; + + // Tell Swig about gsapi_get_default_device_list()'s out-params, so + // it adds them to the returned object. + // + // I think the '(void) *$1' will ensure that swig code doesn't + // attempt to free() the returned string. + // + {'%cstring_output_allocate_size(char **list, int *listlen, (void) *$1);' if language == 'python' else ''} + + // Tell swig about the (argc,argv) args in gsapi_init_with_args(). + // + %apply (int ARGC, char **ARGV) {{ (int argc, char **argv) }} + + // Support for wrapping various functions that take function + // pointer args. For each, we define a wrapper function that, + // instead of having function pointer args, takes a class with + // virtual methods. This allows swig to wrap things - python/c# etc + // can create a derived class that implements these virtual methods + // in the python/c# world. + // + + // Wrap gsapi_set_stdio_with_handle(). + // + %feature("director") set_stdio_class; + + %inline {{ + struct set_stdio_class {{ + + virtual int stdin_fn( char* buf, int len) = 0; + virtual int stdout_fn( const char* buf, int len) = 0; + virtual int stderr_fn( const char* buf, int len) = 0; + + static int stdin_fn_wrap( void *caller_handle, char *buf, int len) {{ + return ((set_stdio_class*) caller_handle)->stdin_fn(buf, len); + }} + static int stdout_fn_wrap( void *caller_handle, const char *buf, int len) {{ + return ((set_stdio_class*) caller_handle)->stdout_fn(buf, len); + }} + static int stderr_fn_wrap( void *caller_handle, const char *buf, int len) {{ + return ((set_stdio_class*) caller_handle)->stderr_fn(buf, len); + }} + + virtual ~set_stdio_class() {{}} + }}; + + int set_stdio_with_class( void *instance, set_stdio_class* class_) {{ + return gsapi_set_stdio_with_handle( + instance, + set_stdio_class::stdin_fn_wrap, + set_stdio_class::stdout_fn_wrap, + set_stdio_class::stderr_fn_wrap, + (void*) class_ + ); + }} + + + }} + + // Wrap gsapi_set_poll(). + // + %feature("director") set_poll_class; + + %inline {{ + struct set_poll_class {{ + virtual int poll_fn() = 0; + + static int poll_fn_wrap( void* caller_handle) {{ + return ((set_poll_class*) caller_handle)->poll_fn(); + }} + + virtual ~set_poll_class() {{}} + }}; + + int set_poll_with_class( void* instance, set_poll_class* class_) {{ + return gsapi_set_poll_with_handle( + instance, + set_poll_class::poll_fn_wrap, + (void*) class_ + ); + }} + + }} + + // For functions that we re-declare (typically to specify OUTPUT on + // one or more args), use a macro to rename the declaration in the + // header file and tell swig to ignore these renamed declarations. + // + ''') + + for fn in fns_redeclare: + swig_i_text += f'#define {fn} {fn}0\n' + + for fn in fns_redeclare: + swig_i_text += f'%ignore {fn}0;\n' + + swig_i_text += textwrap.dedent(f''' + #include "psi/iapi.h" + //#include "base/gserrors.h" + ''') + + for fn in fns_redeclare: + swig_i_text += f'#undef {fn}\n' + + + swig_i_text += textwrap.dedent(f''' + // Tell swig about our wrappers and altered declarations. + // + + // Use swig's OUTPUT annotation for out-parameters. + // + int gsapi_run_file(void *instance, const char *file_name, int user_errors, int *OUTPUT); + int gsapi_run_string_begin(void *instance, int user_errors, int *OUTPUT); + int gsapi_run_string_end(void *instance, int user_errors, int *OUTPUT); + //int gsapi_run_string_with_length(void *instance, const char *str, unsigned int length, int user_errors, int *OUTPUT); + int gsapi_run_string(void *instance, const char *str, int user_errors, int *OUTPUT); + + // Declare functions defined above that we want swig to wrap. These + // don't have the gsapi_ prefix, so that they can internally call + // the wrapped gsapi_*() function. [We've told swig to strip the + // gsapi_ prefix on generated functions anyway, so this doesn't + // afffect the generated names.] + // + static int run_string_continue(void *instance, const char *str, int user_errors, int *OUTPUT); + static void* new_instance(void* caller_handle, int* OUTPUT); + ''') + + if language == 'python': + swig_i_text += textwrap.dedent(f''' + + // Define python code that is needed to handle functions with + // function-pointer args. + // + %pythoncode %{{ + + set_stdio_g = None + def set_stdio( instance, stdin, stdout, stderr): + class derived( set_stdio_class): + def stdin_fn( self): + return stdin() + def stdout_fn( self, text, len): + return stdout( text, len) + def stderr_fn( self, text, len): + return stderr( text) + + global set_stdio_g + set_stdio_g = derived() + return set_stdio_with_class( instance, set_stdio_g) + + set_poll_g = None + def set_poll( instance, fn): + class derived( set_poll_class): + def poll_fn( self): + return fn() + global set_poll_g + set_poll_g = derived() + return set_poll_with_class( instance, set_poll_g) + %}} + ''') + + return swig_i_text + + + +def run_swig( swig, language): + ''' + Runs swig using a generated .i file. + + The .i file modifies the gsapi API in places to allow specification of + out-parameters that swig understands - e.g. void** OUTPUT doesn't work. + ''' + os.makedirs( out_dir(language), exist_ok=True) + swig_major = swig_version( swig) + + swig_i_text = swig_i( swig, language) + swig_i_filename = f'{out_dir(language)}iapi.i' + jlib.update_file( swig_i_text, swig_i_filename) + + out_cpp = f'{out_dir(language)}gsapi.cpp' + + if language == 'python': + out_lang = f'{out_dir(language)}gsapi.py' + elif language == 'csharp': + out_lang = f'{out_dir(language)}gsapi.cs' + else: + assert 0 + + out_files = (out_cpp, out_lang) + + doxygen_arg = '' + if swig_major >= 4 and language == 'python': + doxygen_arg = '-doxygen' + + extra = '' + if language == 'csharp': + # Tell swig to put all generated csharp code into a single file. + extra = f'-outfile gsapi.cs' + + command = (textwrap.dedent(f''' + {swig} + -Wall + -c++ + -{language} + {doxygen_arg} + -module gsapi + -outdir {out_dir(language)} + -o {out_cpp} + {extra} + -includeall + -I{dir_ghostpdl} + -ignoremissing + {swig_i_filename} + ''').strip().replace( '\n', ' \\\n') + ) + + jlib.build( + (swig_i_filename,), + out_files, + command, + prefix=' ', + ) + + +def main( argv): + + swig = 'swig' + language = 'python' + + args = jlib.Args( sys.argv[1:]) + while 1: + try: + arg = args.next() + except StopIteration: + break + + if 0: + pass + + elif arg == '-c': + jlib.system( f'rm {out_dir(language)}* || true', verbose=1, prefix=' ') + + elif arg == '-l': + command = lib_gs_info()[1] + jlib.system( command, verbose=1, prefix=' ') + + elif arg == '-0': + run_swig( swig, language) + + elif arg == '-1': + + libs = [lib_gs()] + includes = [dir_ghostpdl] + file_cpp = f'{out_dir(language)}gsapi.cpp' + + if language == 'python': + python_includes, libpython_so = devpython_info() + libs.append( libpython_so) + includes.append( python_includes) + + includes_text = '' + for i in includes: + includes_text += f' -I{i}' + command = textwrap.dedent(f''' + g++ + -g + -Wall -W + -o {out_so(language)} + -fPIC + -shared + {includes_text} + {jlib.link_l_flags(libs)} + {file_cpp} + ''').strip().replace( '\n', ' \\\n') + jlib.build( + (file_cpp, lib_gs(), 'psi/iapi.h'), + (out_so(language),), + command, + prefix=' ', + ) + + elif arg == '--csharp': + language = 'csharp' + + elif arg == '--python': + language = 'python' + + elif arg == '--swig': + swig = args.next() + + elif arg == '-t': + + if language == 'python': + text = textwrap.dedent(''' + #!/usr/bin/env python3 + + import os + import sys + + import gsapi + + gsapi.gs_error_Quit = -101 + + def main(): + minst, code = gsapi.new_instance(None) + print( f'minst={minst} code={code}') + + if 1: + def stdin_local(len): + # Not sure whether this is right. + return sys.stdin.read(len) + def stdout_local(text, l): + sys.stdout.write(text[:l]) + return l + def stderr_local(text, l): + sys.stderr.write(text[:l]) + return l + gsapi.set_stdio( minst, None, stdout_local, stderr_local); + + if 1: + def poll_fn(): + return 0 + gsapi.set_poll(minst, poll_fn) + if 1: + s = 'display x11alpha x11 bbox' + gsapi.set_default_device_list( minst, s, len(s)) + + e, text = gsapi.get_default_device_list( minst) + print( f'gsapi.get_default_device_list() returned e={e} text={text!r}') + + out = 'out.pdf' + if os.path.exists( out): + os.remove( out) + assert not os.path.exists( out) + + gsargv = [''] + gsargv += f'-dNOPAUSE -dBATCH -dSAFER -sDEVICE=pdfwrite -sOutputFile={out} contrib/pcl3/ps/levels-test.ps'.split() + print( f'gsargv={gsargv}') + code = gsapi.set_arg_encoding(minst, gsapi.GS_ARG_ENCODING_UTF8) + if code == 0: + code = gsapi.init_with_args(minst, gsargv) + + code, exit_code = gsapi.run_string_begin( minst, 0) + print( f'gsapi.run_string_begin() returned code={code} exit_code={exit_code}') + assert code == 0 + assert exit_code == 0 + + gsapi.run_string + + code1 = gsapi.exit(minst) + if (code == 0 or code == gsapi.gs_error_Quit): + code = code1 + gsapi.delete_instance(minst) + assert os.path.isfile( out) + if code == 0 or code == gsapi.gs_error_Quit: + return 0 + return 1 + + if __name__ == '__main__': + code = main() + assert code == 0 + sys.exit( code) + ''') + text = text[1:] # skip leading \n. + test_py = f'{out_dir(language)}test.py' + jlib.update_file( text, test_py) + os.chmod( test_py, 0o744) + + jlib.system( + f'LD_LIBRARY_PATH={os.path.abspath( f"{lib_gs()}/..")}' + f' PYTHONPATH={out_dir(language)}' + f' {test_py}' + , + verbose = 1, + prefix=' ', + ) + + elif language == 'csharp': + # See: https://github.com/swig/swig/blob/master/Lib/csharp/typemaps.i + # + text = textwrap.dedent(''' + using System; + public class runme { + static void Main() { + int code; + SWIGTYPE_p_void instance; + Console.WriteLine("hello world"); + instance = gsapi.new_instance(null, out code); + Console.WriteLine("code is: " + code); + gsapi.add_control_path(instance, 0, "hello"); + } + } + ''') + test_cs = f'{out_dir(language)}test.cs' + jlib.update_file( text, test_cs) + files_in = f'{out_dir(language)}gsapi.cs', test_cs + file_out = f'{out_dir(language)}test.exe' + command = f'mono-csc -debug+ -out:{file_out} {" ".join(files_in)}' + jlib.build( files_in, (file_out,), command, prefix=' ') + + ld_library_path = f'{dir_ghostpdl}sobin' + jlib.system( f'LD_LIBRARY_PATH={ld_library_path} {file_out}', verbose=jlib.log, prefix=' ') + + elif arg == '--tt': + # small swig test case. + os.makedirs( 'swig-tt', exist_ok=True) + i = textwrap.dedent(f''' + %include cpointer.i + %include cstring.i + %feature("autodoc", "3"); + %cstring_output_allocate_size(char **list, int *listlen, (void) *$1); + %inline {{ + static inline int gsapi_get_default_device_list(void *instance, char **list, int *listlen) + {{ + *list = (char*) "hello world"; + *listlen = 6; + return 0; + }} + }} + ''') + jlib.update_file(i, 'swig-tt/tt.i') + jlib.system('swig -c++ -python -module tt -outdir swig-tt -o swig-tt/tt.cpp swig-tt/tt.i', verbose=1) + p = textwrap.dedent(f''' + #!/usr/bin/env python3 + import tt + print( tt.gsapi_get_default_device_list(None)) + ''')[1:] + jlib.update_file( p, 'swig-tt/test.py') + python_includes, python_so = devpython_info() + includes = f'-I {python_includes}' + link_flags = jlib.link_l_flags( [python_so]) + jlib.system( f'g++ -shared -fPIC {includes} {link_flags} -o swig-tt/_tt.so swig-tt/tt.cpp', verbose=1) + jlib.system( f'cd swig-tt; python3 test.py', verbose=1) + + elif arg == '-T': + # Very simple test that we can create c# wrapper for trivial code. + os.makedirs( 'swig-cs-test', exist_ok=True) + example_cpp = textwrap.dedent(''' + #include <time.h> + double My_variable = 3.0; + + int fact(int n) { + if (n <= 1) return 1; + else return n*fact(n-1); + } + + int my_mod(int x, int y) { + return (x%y); + } + + char *get_time() + { + time_t ltime; + time(<ime); + return ctime(<ime); + } + ''') + jlib.update_file( example_cpp, 'swig-cs-test/example.cpp') + + example_i = textwrap.dedent(''' + %module example + %{ + /* Put header files here or function declarations like below */ + extern double My_variable; + extern int fact(int n); + extern int my_mod(int x, int y); + extern char *get_time(); + %} + + extern double My_variable; + extern int fact(int n); + extern int my_mod(int x, int y); + extern char *get_time(); + ''') + jlib.update_file( example_i, 'swig-cs-test/example.i') + + runme_cs = textwrap.dedent(''' + using System; + public class runme { + static void Main() { + Console.WriteLine(example.My_variable); + Console.WriteLine(example.fact(5)); + Console.WriteLine(example.get_time()); + } + } + ''') + jlib.update_file( runme_cs, 'swig-cs-test/runme.cs') + jlib.system( 'g++ -g -fPIC -shared -o swig-cs-test/libfoo.so swig-cs-test/example.cpp', verbose=1) + jlib.system( 'swig -c++ -csharp -module example -outdir swig-cs-test -o swig-cs-test/example_wrap.cpp -outfile example.cs swig-cs-test/example.i', verbose=1) + jlib.system( 'g++ -g -fPIC -shared -L swig-cs-test -l foo swig-cs-test/example_wrap.cpp -o swig-cs-test/libexample.so', verbose=1) + jlib.system( 'cd swig-cs-test; mono-csc -out:runme.exe example.cs runme.cs', verbose=1) + jlib.system( 'cd swig-cs-test; LD_LIBRARY_PATH=`pwd` ./runme.exe', verbose=1) + jlib.system( 'ls -l swig-cs-test', verbose=1) + + + else: + raise Exception( f'unrecognised arg: {arg}') + +if __name__ == '__main__': + try: + main( sys.argv) + except Exception as e: + jlib.exception_info( out=sys.stdout) + sys.exit(1) |