#! /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 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 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)