summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'demos/python/gsapiwrap.py')
-rwxr-xr-xdemos/python/gsapiwrap.py699
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(&ltime);
+ return ctime(&ltime);
+ }
+ ''')
+ 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)