diff --git a/AUTHORS.rst b/AUTHORS.rst index 2c74dbc5..eb92c0ac 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -1,6 +1,10 @@ -zugbruecke contributors -======================= +zugbruecke authors +================== -In alphabetical order: +Core developer: - Sebastian M. Ernst + +Contributors, in alphabetical order: + +- Jimmy M. Gong diff --git a/CHANGES.rst b/CHANGES.rst index f217448a..e7937c12 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,6 +1,19 @@ Changes ======= +0.0.9 (2018-03-21) +------------------ + +* FIX: Arch "win64" was broken because of wrong download URL for embedded CPython for win64/amd64, see issue #27. +* FIX: Function pointers in struct types were not handled, see issue #28. +* FIX: Memsync directives pointing to elements within structs were not handled properly, see issue #29. +* FIX: Missing DLLs of type windll and oledll now raise OSError as expected, see issue #30. +* FIX: Missing routines in DLLs now raise AttributeError as expected, see issue #31. +* FIX: Wrong or unconfigured argtypes as well as wrong number of arguments do raise appropriate errors (ValueError, ArgumentError or TypeError), see issue #32. +* Isolated argument packing and unpacking code, preparing to solve issue #25. +* Renamed "logwrite" parameter & command line option into "log_write". +* Reduced number of RPC servers to one per side (Unix and Wine). + 0.0.8 (2018-03-18) ------------------ diff --git a/README.rst b/README.rst index 44e6b9a0..e50f8f11 100644 --- a/README.rst +++ b/README.rst @@ -50,7 +50,7 @@ zugbruecke is **built on top of Wine**. A stand-alone Windows Python interpreter launched in the background is used to execute the called DLL routines. Communication between the Unix-side and the Windows/Wine-side is based on Python's build-in multiprocessing connection capability. -zugbruecke has (limited) support for pointers and struct types. +zugbruecke has (limited) support for pointers, struct types and call-back functions. zugbruecke comes with extensive logging features allowing to debug problems associated with both itself and with Wine. zugbruecke is written using **Python 3 syntax** and primarily targets the diff --git a/demo_dll/demo_dll.c b/demo_dll/demo_dll.c index 326d2449..4de08168 100644 --- a/demo_dll/demo_dll.c +++ b/demo_dll/demo_dll.c @@ -149,6 +149,26 @@ void __stdcall DEMODLL bubblesort( } +void __stdcall DEMODLL bubblesort_struct( + bubblesort_data *data + ) +{ + int i, j; + for (i = 0; i < data->n - 1; ++i) + { + for (j = 0; j < data->n - i - 1; ++j) + { + if (data->a[j] > data->a[j + 1]) + { + float tmp = data->a[j]; + data->a[j] = data->a[j + 1]; + data->a[j + 1] = tmp; + } + } + } +} + + void __stdcall DEMODLL mix_rgb_colors( int8_t color_a[3], int8_t color_b[3], @@ -219,11 +239,55 @@ vector3d __stdcall DEMODLL *vector3d_add( int16_t __stdcall DEMODLL sqrt_int( int16_t a ) +{ + return sqrt(a); +} + + +int16_t __stdcall DEMODLL square_int( + int16_t a + ) { return a * a; } +int16_t __stdcall DEMODLL add_ints( + int16_t a, + int16_t b + ) +{ + return a + b; +} + + +float __stdcall DEMODLL add_floats( + float a, + float b + ) +{ + return a + b; +} + + +int16_t __stdcall DEMODLL subtract_ints( + int16_t a, + int16_t b + ) +{ + return a - b; +} + + +int16_t __stdcall DEMODLL pow_ints( + int16_t a, + int16_t b + ) +{ + return pow(a, b); +} + + int16_t __stdcall DEMODLL get_const_int(void) { return sqrt(49); @@ -287,6 +351,24 @@ int16_t __stdcall DEMODLL sum_elements_from_callback( } +int16_t __stdcall DEMODLL sum_elements_from_callback_in_struct( + struct conveyor_belt_data *data + ) +{ + + int16_t sum = 0; + int16_t i; + + for(i = 0; i < data->len; i++) + { + sum += data->get_data(i); + } + + return sum; + +} + + // +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ // DLL infrastructure // +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ diff --git a/demo_dll/demo_dll.h b/demo_dll/demo_dll.h index eb27ed17..cac85b08 100644 --- a/demo_dll/demo_dll.h +++ b/demo_dll/demo_dll.h @@ -102,6 +102,15 @@ void __stdcall DEMODLL bubblesort( int n ); +typedef struct bubblesort_data { + float *a; + int n; +} bubblesort_data; + +void __stdcall DEMODLL bubblesort_struct( + bubblesort_data *data + ); + void __stdcall DEMODLL mix_rgb_colors( int8_t color_a[3], int8_t color_b[3], @@ -126,6 +135,30 @@ int16_t __stdcall DEMODLL sqrt_int( int16_t a ); +int16_t __stdcall DEMODLL square_int( + int16_t a + ); + +int16_t __stdcall DEMODLL add_ints( + int16_t a, + int16_t b + ); + +float __stdcall DEMODLL add_floats( + float a, + float b + ); + +int16_t __stdcall DEMODLL subtract_ints( + int16_t a, + int16_t b + ); + +int16_t __stdcall DEMODLL pow_ints( + int16_t a, + int16_t b + ); + int16_t __stdcall DEMODLL get_const_int(void); struct test @@ -162,6 +195,15 @@ int16_t __stdcall DEMODLL sum_elements_from_callback( conveyor_belt get_data ); +typedef struct conveyor_belt_data { + int16_t len; + conveyor_belt get_data; +} conveyor_belt_data; + +int16_t __stdcall DEMODLL sum_elements_from_callback_in_struct( + struct conveyor_belt_data *data + ); + // +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ // DLL infrastructure diff --git a/docs/bugs.rst b/docs/bugs.rst index 7cb156da..f2662ace 100644 --- a/docs/bugs.rst +++ b/docs/bugs.rst @@ -46,7 +46,7 @@ your current working directory or *zugbruecke*'s configuration directory (likely .. code:: javascript - {"log_level": 10, "logwrite": true} + {"log_level": 10, "log_write": true} The higher the log level, the more output you will get. Default is 0 for no logs. The on-screen log is color-coded for readability. The log can also, in addition, diff --git a/docs/configuration.rst b/docs/configuration.rst index abdf65b9..44a92cc6 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -82,8 +82,8 @@ Tells *zugbuecke* to show messages its sub-processes are writing to ``stdout``. Tells *zugbuecke* to show messages its sub-processes are writing to ``stderr``. ``True`` by default. -``logwrite`` (bool) -^^^^^^^^^^^^^^^^^^^ +``log_write`` (bool) +^^^^^^^^^^^^^^^^^^^^ Tells *zugbuecke* to write its logs to disk into the current working directory. ``False`` by default. diff --git a/docs/introduction.rst b/docs/introduction.rst index cc9f51f1..0802f7e2 100644 --- a/docs/introduction.rst +++ b/docs/introduction.rst @@ -23,7 +23,7 @@ Unices / *Unix*-like systems such as *Linux*, *MacOS* or *BSD*. launched in the background is used to execute the called DLL routines. Communication between the *Unix*-side and the *Windows*/*Wine*-side is based on *Python*'s build-in multiprocessing connection capability. -*zugbruecke* has (limited) support for pointers and struct types. +*zugbruecke* has (limited) support for pointers, struct types and call-back functions. *zugbruecke* comes with extensive logging features allowing to debug problems associated with both itself and with *Wine*. *zugbruecke* is written using *Python* 3 syntax and primarily targets the diff --git a/docs/session.rst b/docs/session.rst index f3ef4ae1..407133e5 100644 --- a/docs/session.rst +++ b/docs/session.rst @@ -99,6 +99,6 @@ Instance: ``zugbruecke.current_session`` This is the default session of *zugbruecke*. It will be started during import. Like every session, it can be :ref:`re-configured ` during run-time. If any of the usual *ctypes* members are imported from -*zugbruecke*, like for instance ``cdll``, ``CDLL``, ``windll``, ``WinDLL``, -``oledll``, ``OleDLL``, ``FormatError``, ``get_last_error``, ``GetLastError``, +*zugbruecke*, like for instance ``cdll``, ``CDLL``, ``CFUNCTYPE``, ``windll``, ``WinDLL``, +``WINFUNCTYPE``, ``oledll``, ``OleDLL``, ``FormatError``, ``get_last_error``, ``GetLastError``, ``set_last_error`` or ``WinError``, this session will be used. diff --git a/examples/test_callback.py b/examples/test_callback.py index e2d3c5f9..c83156a8 100755 --- a/examples/test_callback.py +++ b/examples/test_callback.py @@ -73,3 +73,20 @@ def get_data(index): test_sum = sum_elements_from_callback(len(DATA), get_data) print(('sum', 48, test_sum)) + + # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + + class conveyor_belt_data(ctypes.Structure): + _fields_ = [ + ('len', ctypes.c_int16), + ('get_data', conveyor_belt) + ] + + sum_elements_from_callback_in_struct = dll.sum_elements_from_callback_in_struct + sum_elements_from_callback_in_struct.argtypes = (ctypes.POINTER(conveyor_belt_data),) + sum_elements_from_callback_in_struct.restype = ctypes.c_int16 + + in_struct = conveyor_belt_data(len(DATA), get_data) + + test_struct_sum = sum_elements_from_callback_in_struct(in_struct) + print(('sum', 48, test_struct_sum)) diff --git a/setup.py b/setup.py index efc00c4e..6b489923 100644 --- a/setup.py +++ b/setup.py @@ -46,7 +46,7 @@ # Bump version HERE! -_version_ = '0.0.8' +_version_ = '0.0.9' # List all versions of Python which are supported diff --git a/src/zugbruecke/_server_.py b/src/zugbruecke/_server_.py index ea0c5d7f..c3afd4d9 100755 --- a/src/zugbruecke/_server_.py +++ b/src/zugbruecke/_server_.py @@ -49,19 +49,16 @@ '--id', type = str, nargs = 1 ) parser.add_argument( - '--port_socket_ctypes', type = int, nargs = 1 + '--port_socket_unix', type = int, nargs = 1 ) parser.add_argument( - '--port_socket_callback', type = int, nargs = 1 - ) - parser.add_argument( - '--port_socket_log_main', type = int, nargs = 1 + '--port_socket_wine', type = int, nargs = 1 ) parser.add_argument( '--log_level', type = int, nargs = 1 ) parser.add_argument( - '--logwrite', type = int, nargs = 1 + '--log_write', type = int, nargs = 1 ) args = parser.parse_args() @@ -71,13 +68,10 @@ 'platform': 'WINE', 'stdout': False, 'stderr': False, - 'logwrite': bool(args.logwrite[0]), - 'remote_log': True, + 'log_write': bool(args.log_write[0]), 'log_level': args.log_level[0], - 'log_server': False, - 'port_socket_ctypes': args.port_socket_ctypes[0], - 'port_socket_callback': args.port_socket_callback[0], - 'port_socket_log_main': args.port_socket_log_main[0] + 'port_socket_wine': args.port_socket_wine[0], + 'port_socket_unix': args.port_socket_unix[0] } # Fire up wine server session with parsed parameters diff --git a/src/zugbruecke/_wrapper_.py b/src/zugbruecke/_wrapper_.py index a489a583..c8696843 100644 --- a/src/zugbruecke/_wrapper_.py +++ b/src/zugbruecke/_wrapper_.py @@ -107,8 +107,8 @@ def _check_HRESULT(result): # EXPORT WINFUNCTYPE = current_session.ctypes_WINFUNCTYPE # EXPORT # Used as cache by CFUNCTYPE and WINFUNCTYPE -_c_functype_cache = current_session.cache_dict['func_type'][_FUNCFLAG_CDECL] # EXPORT -_win_functype_cache = current_session.cache_dict['func_type'][_FUNCFLAG_STDCALL] # EXPORT +_c_functype_cache = current_session.data.cache_dict['func_type'][_FUNCFLAG_CDECL] # EXPORT +_win_functype_cache = current_session.data.cache_dict['func_type'][_FUNCFLAG_STDCALL] # EXPORT # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ diff --git a/src/zugbruecke/core/callback_client.py b/src/zugbruecke/core/callback_client.py index ad384ba8..daa42783 100644 --- a/src/zugbruecke/core/callback_client.py +++ b/src/zugbruecke/core/callback_client.py @@ -42,7 +42,7 @@ class callback_translator_client_class: - def __init__(self, parent_routine, routine_name, routine_handler, argtypes_d, restype_d): + def __init__(self, data, routine_name, routine_handler, argtypes_d, restype_d): # Store my own name self.name = routine_name @@ -50,11 +50,11 @@ def __init__(self, parent_routine, routine_name, routine_handler, argtypes_d, re # Store handler self.handler = routine_handler - # Store handle on parent routine - self.parent_routine = parent_routine + # Store handle on data + self.data = data # Get handle on log - self.log = self.parent_routine.log + self.log = self.data.log # Store definition of argument types self.argtypes_d = argtypes_d @@ -62,11 +62,6 @@ def __init__(self, parent_routine, routine_name, routine_handler, argtypes_d, re # Store definition of return value type self.restype_d = restype_d - # Store handlers on packing/unpacking routines - self.arg_list_unpack = self.parent_routine.arg_list_unpack - self.arg_list_pack = self.parent_routine.arg_list_pack - self.return_msg_pack = self.parent_routine.return_msg_pack - def __call__(self, arg_message_list): @@ -74,7 +69,7 @@ def __call__(self, arg_message_list): self.log.out('[callback-client] Trying to call callback routine "%s" ...' % self.name) # Unpack arguments - args_list = self.arg_list_unpack(arg_message_list, self.argtypes_d) + args_list = self.data.arg_list_unpack(arg_message_list, self.argtypes_d) # Default return value return_value = None @@ -86,13 +81,13 @@ def __call__(self, arg_message_list): return_value = self.handler(*args_list) # Get new arg message list - arg_message_list = self.arg_list_pack(args_list, self.argtypes_d) + arg_message_list = self.data.arg_list_pack(args_list, self.argtypes_d) # Pack return value - return_message = self.return_msg_pack(return_value, self.restype_d) + return_message = self.data.return_msg_pack(return_value, self.restype_d) # Log status - self.log.out('[routine-client] ... done.') + self.log.out('[callback-client] ... done.') # Ship data back to Wine side return { @@ -105,7 +100,7 @@ def __call__(self, arg_message_list): except: # Log status - self.log.out('[routine-client] ... failed!') + self.log.out('[callback-client] ... failed!') # Push traceback to log self.log.err(traceback.format_exc()) diff --git a/src/zugbruecke/core/callback_server.py b/src/zugbruecke/core/callback_server.py index 1d670a6c..e0477ebc 100644 --- a/src/zugbruecke/core/callback_server.py +++ b/src/zugbruecke/core/callback_server.py @@ -42,7 +42,7 @@ class callback_translator_server_class: - def __init__(self, parent_routine, routine_name, routine_handler, argtypes_d, restype_d): + def __init__(self, data, routine_name, routine_handler, argtypes_d, restype_d): # Store my own name self.name = routine_name @@ -50,11 +50,11 @@ def __init__(self, parent_routine, routine_name, routine_handler, argtypes_d, re # Store handler self.handler = routine_handler - # Store handle on parent routine - self.parent_routine = parent_routine + # Store handle on data + self.data = data # Get handle on log - self.log = self.parent_routine.log + self.log = self.data.log # Store definition of argument types self.argtypes_d = argtypes_d @@ -62,12 +62,6 @@ def __init__(self, parent_routine, routine_name, routine_handler, argtypes_d, re # Store definition of return value type self.restype_d = restype_d - # Store handlers on packing/unpacking routines - self.arg_list_pack = self.parent_routine.arg_list_pack - self.arg_list_sync = self.parent_routine.arg_list_sync - self.arg_list_unpack = self.parent_routine.arg_list_unpack - self.return_msg_unpack = self.parent_routine.return_msg_unpack - def __call__(self, *args): @@ -78,20 +72,20 @@ def __call__(self, *args): self.log.out('[callback-server] ... parameters are "%r". Packing and pushing to client ...' % (args,)) # Pack arguments and call RPC callback function (packed arguments are shipped to Unix side) - return_dict = self.handler(self.arg_list_pack(args, self.argtypes_d)) + return_dict = self.handler(self.data.arg_list_pack(args, self.argtypes_d)) # Log status self.log.out('[callback-server] ... received feedback from client, unpacking ...') # Unpack return dict (for pointers and structs) - self.arg_list_sync( + self.data.arg_list_sync( args, - self.arg_list_unpack(return_dict['args'], self.argtypes_d), + self.data.arg_list_unpack(return_dict['args'], self.argtypes_d), self.argtypes_d ) # Unpack return value - return_value = self.return_msg_unpack(return_dict['return_value'], self.restype_d) + return_value = self.data.return_msg_unpack(return_dict['return_value'], self.restype_d) # Log status self.log.out('[callback-server] ... unpacked, return.') diff --git a/src/zugbruecke/core/config.py b/src/zugbruecke/core/config.py index d088a251..ac6b5fff 100644 --- a/src/zugbruecke/core/config.py +++ b/src/zugbruecke/core/config.py @@ -60,17 +60,11 @@ def get_default_config(): cfg['stderr'] = True # Write log messages into file - cfg['logwrite'] = False + cfg['log_write'] = False # Overall log level cfg['log_level'] = 0 # No logs are generated by default - # Open server for collected logs from clients - cfg['log_server'] = True - - # Send log messages to remove sever - cfg['remote_log'] = False - # Define Wine & Wine-Python architecture cfg['arch'] = 'win32' diff --git a/src/zugbruecke/core/data/__init__.py b/src/zugbruecke/core/data/__init__.py new file mode 100644 index 00000000..a2058c8a --- /dev/null +++ b/src/zugbruecke/core/data/__init__.py @@ -0,0 +1,66 @@ +# -*- coding: utf-8 -*- + +""" + +ZUGBRUECKE +Calling routines in Windows DLLs from Python scripts running on unixlike systems +https://github.com/pleiszenburg/zugbruecke + + src/zugbruecke/core/data/__init__.py: Arguments, return values and memory + + Required to run on platform / side: [UNIX, WINE] + + Copyright (C) 2017-2018 Sebastian M. Ernst + + +The contents of this file are subject to the GNU Lesser General Public License +Version 2.1 ("LGPL" or "License"). You may not use this file except in +compliance with the License. You may obtain a copy of the License at +https://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt +https://github.com/pleiszenburg/zugbruecke/blob/master/LICENSE + +Software distributed under the License is distributed on an "AS IS" basis, +WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the +specific language governing rights and limitations under the License. + + +""" + + +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +# IMPORT +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +from ctypes import _FUNCFLAG_CDECL + +from .contents import contents_class +from .definition import definition_class +from .memory import memory_class + +from ..const import _FUNCFLAG_STDCALL + + +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +# CLASS: DATA +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +class data_class(contents_class, definition_class, memory_class): + + + cache_dict = { + 'func_type': { + _FUNCFLAG_CDECL: {}, + _FUNCFLAG_STDCALL: {} + }, + 'func_handle': {}, + 'struct_type': {} + } + + + def __init__(self, log, is_server, callback_client = None, callback_server = None): + + self.log = log + self.is_server = is_server + + self.callback_client = callback_client + self.callback_server = callback_server diff --git a/src/zugbruecke/core/arg_contents.py b/src/zugbruecke/core/data/contents.py similarity index 90% rename from src/zugbruecke/core/arg_contents.py rename to src/zugbruecke/core/data/contents.py index 39873a2f..c4d10fb8 100644 --- a/src/zugbruecke/core/arg_contents.py +++ b/src/zugbruecke/core/data/contents.py @@ -6,7 +6,7 @@ Calling routines in Windows DLLs from Python scripts running on unixlike systems https://github.com/pleiszenburg/zugbruecke - src/zugbruecke/core/arg_contents.py: (Un-) packing of argument contents + src/zugbruecke/core/data/contents.py: (Un-) packing of argument contents Required to run on platform / side: [UNIX, WINE] @@ -35,34 +35,52 @@ from pprint import pformat as pf import traceback -from .const import ( +from ..const import ( FLAG_POINTER, GROUP_VOID, GROUP_FUNDAMENTAL, GROUP_STRUCT, GROUP_FUNCTION ) -from .callback_client import callback_translator_client_class -from .callback_server import callback_translator_server_class +from ..callback_client import callback_translator_client_class +from ..callback_server import callback_translator_server_class # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ # CLASS: Content packing and unpacking # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ -class arg_contents_class(): +class contents_class(): def arg_list_pack(self, args_tuple, argtypes_list): - # Return parameter message list - MUST WORK WITH PICKLE - return [(d['n'], self.__pack_item__(a, d)) for a, d in zip(args_tuple, argtypes_list)] + # Everything is normal + if len(args_tuple) == len(argtypes_list): + return [(d['n'], self.__pack_item__(a, d)) for a, d in zip(args_tuple, argtypes_list)] + + # Function has likely not been configured but there are arguments + elif len(args_tuple) > 0 and len(argtypes_list) == 0: + return list(args_tuple) # let's try ... TODO catch pickling errors + + # Number of arguments is just wrong + else: + raise TypeError def arg_list_unpack(self, args_package_list, argtypes_list): - # Return args as list, will be converted into tuple on call - return [self.__unpack_item__(a[1], d) for a, d in zip(args_package_list, argtypes_list)] + # Everything is normal + if len(args_package_list) == len(argtypes_list): + return [self.__unpack_item__(a[1], d) for a, d in zip(args_package_list, argtypes_list)] + + # Function has likely not been configured but there are arguments + elif len(args_package_list) > 0 and len(argtypes_list) == 0: + return args_package_list + + # Number of arguments is just wrong + else: + raise TypeError def return_msg_pack(self, return_value, returntype_dict): @@ -233,6 +251,10 @@ def __sync_item__(self, old_arg, new_arg, arg_def_dict): # Grep the simple case first, scalars if arg_def_dict['s']: + # Do not do this for void pointers, likely handled by memsync + if arg_def_dict['g'] == GROUP_VOID: + return + # Strip away the pointers ... (all flags are pointers in this case) for flag in arg_def_dict['f']: if flag != FLAG_POINTER: @@ -434,6 +456,10 @@ def __unpack_item_struct__(self, args_list, struct_def_dict): # Step through arguments for field_def_dict, field_arg in zip(struct_def_dict['_fields_'], args_list): + # HACK is field_arg[1] is None, it's likely a function pointer sent back from Wine side - skip + if field_arg[1] is None: + continue + setattr( struct_inst, # struct instance to be modified field_arg[0], # parameter name (from tuple) diff --git a/src/zugbruecke/core/arg_definition.py b/src/zugbruecke/core/data/definition.py similarity index 81% rename from src/zugbruecke/core/arg_definition.py rename to src/zugbruecke/core/data/definition.py index 7a17b25d..e43e99fb 100644 --- a/src/zugbruecke/core/arg_definition.py +++ b/src/zugbruecke/core/data/definition.py @@ -6,7 +6,7 @@ Calling routines in Windows DLLs from Python scripts running on unixlike systems https://github.com/pleiszenburg/zugbruecke - src/zugbruecke/core/arg_definition.py: (Un-) packing of argument definitions + src/zugbruecke/core/data/definition.py: (Un-) packing of argument definitions Required to run on platform / side: [UNIX, WINE] @@ -32,16 +32,18 @@ # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ import ctypes +from ctypes import _FUNCFLAG_CDECL #from pprint import pformat as pf -from .const import ( +from ..const import ( + _FUNCFLAG_STDCALL, FLAG_POINTER, GROUP_VOID, GROUP_FUNDAMENTAL, GROUP_STRUCT, GROUP_FUNCTION ) -from .lib import ( +from ..lib import ( reduce_dict ) @@ -50,7 +52,7 @@ # CLASS: Definition packing and unpacking # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ -class arg_definition_class(): +class definition_class(): def apply_memsync_to_argtypes_definition(self, memsync, argtypes_d): @@ -65,15 +67,35 @@ def apply_memsync_to_argtypes_definition(self, memsync, argtypes_d): arg_type = argtypes_d[segment['p'][0]] # Step through path to argument type ... for path_element in segment['p'][1:]: + # Keep track of whether or not a match has been found so an error can be raised if not + found_match = False + # Find field with matching name + for field_index, field in enumerate(arg_type['_fields_']): + if field['n'] == path_element: + found_match = True + break + # Raise an error if the definition does not make sense + if not found_match: + raise # TODO # Go deeper ... - arg_type = arg_type['_fields_'][path_element] + arg_type = arg_type['_fields_'][field_index] # Reference processed argument types - start with depth 0 len_type = argtypes_d[segment['l'][0]] # Step through path to argument type ... for path_element in segment['l'][1:]: + # Keep track of whether or not a match has been found so an error can be raised if not + found_match = False + # Find field with matching name + for field_index, field in enumerate(len_type['_fields_']): + if field['n'] == path_element: + found_match = True + break + # Raise an error if the definition does not make sense + if not found_match: + raise # TODO # Go deeper ... - len_type = len_type['_fields_'][path_element] + len_type = len_type['_fields_'][field_index] # HACK make memory sync pointers type agnostic arg_type['g'] = GROUP_VOID @@ -88,6 +110,32 @@ def apply_memsync_to_argtypes_definition(self, memsync, argtypes_d): return memsync_handle + def generate_callback_decorator(self, flags, restype, *argtypes): + + if not(flags & _FUNCFLAG_STDCALL): + func_type_key = _FUNCFLAG_CDECL + else: + func_type_key = _FUNCFLAG_STDCALL + + try: + + # There already is a matching function pointer type available + return self.cache_dict['func_type'][func_type_key][(restype, argtypes, flags)] + + except KeyError: + + # Create new function pointer type class + class FunctionType(ctypes._CFuncPtr): + + _argtypes_ = argtypes + _restype_ = restype + _flags_ = flags + + # Store the new type and return + self.cache_dict['func_type'][func_type_key][(restype, argtypes, flags)] = FunctionType + return FunctionType + + def pack_definition_argtypes(self, argtypes): return [self.__pack_definition_dict__(arg) for arg in argtypes] @@ -138,6 +186,26 @@ def __generate_struct_from_definition__(self, struct_d_dict): field['n'], self.__unpack_definition_struct_dict__(field) )) + # Functions (PyCFuncPtrType) + elif field['g'] == GROUP_FUNCTION: + + # Add tuple with name and struct datatype + fields.append(( + field['n'], self.__unpack_definition_function_dict__(field) + )) + + # Handle generic pointers + elif field['g'] == GROUP_VOID: + + fields.append(( + field['n'], + self.__unpack_definition_flags__( + ctypes.c_void_p, + field['f'], + is_void_pointer = True + ) + )) + # Undhandled stuff (pointers of pointers etc.) TODO else: @@ -329,20 +397,11 @@ def __unpack_definition_function_dict__(self, datatype_d_dict): if not self.is_server: raise # TODO - # Figure out which "factory" to use, i.e. calling convention - if not(datatype_d_dict['_flags_'] & ctypes._FUNCFLAG_STDCALL): - FACTORY = ctypes.CFUNCTYPE - elif datatype_d_dict['_flags_'] & ctypes._FUNCFLAG_STDCALL: - FACTORY = ctypes.WINFUNCTYPE - else: - raise # TODO - # Generate function pointer type (used as parameter type and as decorator for Python function) - factory_type = FACTORY( + factory_type = self.generate_callback_decorator( + datatype_d_dict['_flags_'], self.unpack_definition_returntype(datatype_d_dict['_restype_']), - *self.unpack_definition_argtypes(datatype_d_dict['_argtypes_']), - use_errno = datatype_d_dict['_flags_'] & ctypes._FUNCFLAG_USE_ERRNO, - use_last_error = datatype_d_dict['_flags_'] & ctypes._FUNCFLAG_USE_LASTERROR + *self.unpack_definition_argtypes(datatype_d_dict['_argtypes_']) ) # Store function pointer type for subsequent use as decorator diff --git a/src/zugbruecke/core/arg_memory.py b/src/zugbruecke/core/data/memory.py similarity index 79% rename from src/zugbruecke/core/arg_memory.py rename to src/zugbruecke/core/data/memory.py index a4b376d7..b64f45f0 100644 --- a/src/zugbruecke/core/arg_memory.py +++ b/src/zugbruecke/core/data/memory.py @@ -6,7 +6,7 @@ Calling routines in Windows DLLs from Python scripts running on unixlike systems https://github.com/pleiszenburg/zugbruecke - src/zugbruecke/core/arg_memory.py: (Un-) packing of argument pointers + src/zugbruecke/core/data/memory.py: (Un-) packing of argument pointers Required to run on platform / side: [UNIX, WINE] @@ -33,8 +33,9 @@ import ctypes #from pprint import pformat as pf +#import traceback -from .memory import ( +from ..memory import ( generate_pointer_from_int_list, overwrite_pointer_with_int_list, serialize_pointer_into_int_list @@ -45,7 +46,7 @@ # CLASS: Memory content packing and unpacking # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ -class arg_memory_class(): +class memory_class(): def client_fix_memsync_ctypes(self, memsync): @@ -74,14 +75,20 @@ def client_pack_memory_list(self, args, memsync): # Step through path to pointer ... for path_element in segment['p']: # Go deeper ... - pointer = pointer[path_element] + if isinstance(path_element, int): + pointer = pointer[path_element] + else: + pointer = getattr(pointer, path_element) # Reference args - search for length length = args # Step through path to pointer ... for path_element in segment['l']: # Go deeper ... - length = length[path_element] + if isinstance(path_element, int): + length = length[path_element] + else: + length = getattr(length, path_element) # Compute actual length - might come from ctypes or a Python datatype if hasattr(length, 'value'): @@ -139,12 +146,20 @@ def server_unpack_memory_list(self, args, arg_memory_list, memsync): # Step through path to pointer ... for path_element in segment['p'][:-1]: # Go deeper ... - pointer = pointer[path_element] - - # Handle deepest instance - pointer[segment['p'][-1]] = generate_pointer_from_int_list(arg_memory_list[segment_index]) - - # Append to handle - memory_handle.append((pointer[segment['p'][-1]], len(arg_memory_list[segment_index]))) + if isinstance(path_element, int): + pointer = pointer[path_element] + else: + pointer = getattr(pointer.contents, path_element) + + if isinstance(segment['p'][-1], int): + # Handle deepest instance + pointer[segment['p'][-1]] = generate_pointer_from_int_list(arg_memory_list[segment_index]) + # Append to handle + memory_handle.append((pointer[segment['p'][-1]], len(arg_memory_list[segment_index]))) + else: + # Handle deepest instance + setattr(pointer.contents, segment['p'][-1], generate_pointer_from_int_list(arg_memory_list[segment_index])) + # Append to handle + memory_handle.append((getattr(pointer.contents, segment['p'][-1]), len(arg_memory_list[segment_index]))) return memory_handle diff --git a/src/zugbruecke/core/dll_client.py b/src/zugbruecke/core/dll_client.py index db71ee3e..5379b40d 100644 --- a/src/zugbruecke/core/dll_client.py +++ b/src/zugbruecke/core/dll_client.py @@ -51,7 +51,7 @@ def __init__(self, parent_session, dll_name, dll_type, hash_id): self.session = parent_session # For convenience ... - self.client = self.session.client + self.rpc_client = self.session.rpc_client # Get handle on log self.log = self.session.log @@ -63,10 +63,10 @@ def __init__(self, parent_session, dll_name, dll_type, hash_id): self.routines = {} # Expose routine registration - self.__register_routine_on_server__ = getattr(self.client, self.hash_id + '_register_routine') + self.__register_routine_on_server__ = getattr(self.rpc_client, self.hash_id + '_register_routine') # Expose string reprentation of dll object - self.__get_repr__ = getattr(self.client, self.hash_id + '_repr') + self.__get_repr__ = getattr(self.rpc_client, self.hash_id + '_repr') def __attach_to_routine__(self, name): @@ -84,25 +84,23 @@ def __attach_to_routine__(self, name): if name.startswith('__') and name.endswith('__'): raise AttributeError(name) - # Register routine in wine - success = self.__register_routine_on_server__(name) + try: - # If success ... - if success: + # Register routine in wine + self.__register_routine_on_server__(name) - # Create new instance of routine_client - self.routines[name] = routine_client_class(self, name) + except AttributeError as e: # Log status - self.log.out('[dll-client] ... registered (unconfigured) ...') + self.log.out('[dll-client] ... failed!') - # If failed ... - else: + raise e - # Log status - self.log.out('[dll-client] ... failed!') + # Create new instance of routine_client + self.routines[name] = routine_client_class(self, name) - raise # TODO + # Log status + self.log.out('[dll-client] ... registered (unconfigured) ...') # If name is a string ... if isinstance(name, str): @@ -119,6 +117,9 @@ def __attach_to_routine__(self, name): def __getattr__(self, name): + if name in ['__objclass__']: + raise AttributeError(name) + return self.__attach_to_routine__(name) diff --git a/src/zugbruecke/core/dll_server.py b/src/zugbruecke/core/dll_server.py index a4d04ce5..a8f0e176 100644 --- a/src/zugbruecke/core/dll_server.py +++ b/src/zugbruecke/core/dll_server.py @@ -66,11 +66,11 @@ def __init__(self, parent_session, dll_name, dll_type, handler): self.hash_id = get_hash_of_string(self.name) # Export registration of my functions directly - self.session.server.register_function( + self.session.rpc_server.register_function( self.__get_repr__, self.hash_id + '_repr' ) - self.session.server.register_function( + self.session.rpc_server.register_function( self.__register_routine__, self.hash_id + '_register_routine' ) @@ -110,30 +110,32 @@ def __register_routine__(self, routine_name): # Get handler on routine in dll as item routine_handler = self.handler[routine_name] - except: + except AttributeError as e: # Log status self.log.out('[dll-server] ... failed!') + raise e + + except: + # Push traceback to log self.log.err(traceback.format_exc()) - return False # Fail + raise # TODO # Generate new instance of routine class self.routines[routine_name] = routine_server_class(self, routine_name, routine_handler) # Export call and configration directly - self.session.server.register_function( + self.session.rpc_server.register_function( self.routines[routine_name], self.hash_id + '_' + str(routine_name) + '_handle_call' ) - self.session.server.register_function( + self.session.rpc_server.register_function( self.routines[routine_name].__configure__, self.hash_id + '_' + str(routine_name) + '_configure' ) # Log status self.log.out('[dll-server] ... done.') - - return True # Success diff --git a/src/zugbruecke/core/lib.py b/src/zugbruecke/core/lib.py index a20026d8..c4a99699 100644 --- a/src/zugbruecke/core/lib.py +++ b/src/zugbruecke/core/lib.py @@ -31,14 +31,11 @@ # IMPORT # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ -from ctypes import _FUNCFLAG_CDECL import hashlib import os import random import socket -from .const import _FUNCFLAG_STDCALL - # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ # LIBRARY ROUTINES @@ -73,18 +70,6 @@ def get_randhashstr(dig): return (('%0' + str(dig) + 'x') % random.randrange(16**dig)) -def generate_cache_dict(): - - return { - 'func_type': { - _FUNCFLAG_CDECL: {}, - _FUNCFLAG_STDCALL: {} - }, - 'func_handle': {}, - 'struct_type': {} - } - - def generate_session_id(): # A session id by default is an 8 digit hash string diff --git a/src/zugbruecke/core/log.py b/src/zugbruecke/core/log.py index eaab7c89..7f96ab2a 100644 --- a/src/zugbruecke/core/log.py +++ b/src/zugbruecke/core/log.py @@ -35,12 +35,6 @@ import sys import time -from .lib import get_free_port -from .rpc import ( - mp_client_class, - mp_server_class - ) - # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ # CONSTANTS @@ -69,7 +63,7 @@ class log_class: - def __init__(self, session_id, parameter): + def __init__(self, session_id, parameter, rpc_server = None, rpc_client = None): # Store id and parameter self.id = session_id @@ -88,28 +82,27 @@ def __init__(self, session_id, parameter): self.p['platform'] = 'UNIX' # Open logfiles - if self.p['logwrite']: + if self.p['log_write']: self.f = {} self.f['out'] = '%s_%s.txt' % (self.p['platform'], 'out') self.f['err'] = '%s_%s.txt' % (self.p['platform'], 'err') # Fire up server if required self.server_port = 0 - if self.p['log_server']: - self.__start_server__() + if rpc_server is not None: + self.server = rpc_server + self.server.register_function(self.__receive_message_from_client__, 'transfer_message') # Fire up client if required - if self.p['remote_log']: - self.__start_client__() + if rpc_client is not None: + self.client = rpc_client def terminate(self): if self.up: - # Stop server, if there is one - if self.p['log_server']: - self.__stop_server__() + # Nothing to do, just a placeholder # Log down self.up = False @@ -187,9 +180,9 @@ def __process_message_dict__(self, mesage_dict): self.__append_message_to_log__(mesage_dict) if self.p['std' + mesage_dict['pipe']]: self.__print_message__(mesage_dict) - if self.p['remote_log']: + if hasattr(self, 'client'): self.__push_message_to_server__(mesage_dict) - if self.p['logwrite']: + if self.p['log_write']: self.__store_message__(mesage_dict) @@ -203,37 +196,6 @@ def __receive_message_from_client__(self, message): self.__process_message_dict__(json.loads(message)) - def __start_client__(self): - - self.client = mp_client_class( - ('localhost', self.p['port_socket_log_main']), - 'zugbruecke_log_main' - ) - - - def __start_server__(self): - - # Generate new socket and store it - self.p['port_socket_log_main'] = get_free_port() - - # Create server - self.server = mp_server_class( - ('localhost', self.p['port_socket_log_main']), - 'zugbruecke_log_main' - ) - - # Register functions - self.server.register_function(self.__receive_message_from_client__, 'transfer_message') - - # Run server in its own thread - self.server.server_forever_in_thread() - - - def __stop_server__(self): - - self.server.terminate() - - def __store_message__(self, message): f = open(self.f[message['pipe']], 'a+') diff --git a/src/zugbruecke/core/memory.py b/src/zugbruecke/core/memory.py index 2fec7f0f..ab25cc3f 100644 --- a/src/zugbruecke/core/memory.py +++ b/src/zugbruecke/core/memory.py @@ -39,7 +39,7 @@ def generate_pointer_from_int_list(int_array): - return ctypes.pointer((ctypes.c_ubyte * len(int_array))(*int_array)) + return ctypes.cast(ctypes.pointer((ctypes.c_ubyte * len(int_array))(*int_array)), ctypes.c_void_p) def overwrite_pointer_with_int_list(ctypes_pointer, int_array): @@ -49,4 +49,6 @@ def overwrite_pointer_with_int_list(ctypes_pointer, int_array): def serialize_pointer_into_int_list(ctypes_pointer, size_bytes): - return (ctypes.c_ubyte * size_bytes).from_address(ctypes.c_void_p.from_buffer(ctypes_pointer).value)[:] + return (ctypes.c_ubyte * size_bytes).from_address(ctypes.c_void_p.from_buffer( + ctypes.cast(ctypes_pointer, ctypes.POINTER(ctypes.c_ubyte * size_bytes)) + ).value)[:] diff --git a/src/zugbruecke/core/routine_client.py b/src/zugbruecke/core/routine_client.py index 003a013a..69d7f6c3 100644 --- a/src/zugbruecke/core/routine_client.py +++ b/src/zugbruecke/core/routine_client.py @@ -35,20 +35,12 @@ from functools import partial from pprint import pformat as pf -from .arg_contents import arg_contents_class -from .arg_definition import arg_definition_class -from .arg_memory import arg_memory_class - # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ # DLL CLIENT CLASS # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ -class routine_client_class( - arg_contents_class, - arg_definition_class, - arg_memory_class - ): +class routine_client_class(): def __init__(self, parent_dll, routine_name): @@ -60,7 +52,7 @@ def __init__(self, parent_dll, routine_name): self.session = self.dll.session # For convenience ... - self.client = self.dll.client + self.rpc_client = self.dll.rpc_client # Get handle on log self.log = self.dll.log @@ -69,9 +61,7 @@ def __init__(self, parent_dll, routine_name): self.name = routine_name # Required by arg definitions and contents - self.cache_dict = self.session.cache_dict - self.callback_server = self.session.callback_server - self.is_server = False + self.data = self.session.data # Set call status self.called = False @@ -87,12 +77,12 @@ def __init__(self, parent_dll, routine_name): # Get handle on server-side configure self.__configure_on_server__ = getattr( - self.client, self.dll.hash_id + '_' + str(self.name) + '_configure' + self.rpc_client, self.dll.hash_id + '_' + str(self.name) + '_configure' ) # Get handle on server-side handle_call self.__handle_call_on_server__ = getattr( - self.client, self.dll.hash_id + '_' + str(self.name) + '_handle_call' + self.rpc_client, self.dll.hash_id + '_' + str(self.name) + '_handle_call' ) @@ -123,32 +113,39 @@ def __call__(self, *args): self.log.out('[routine-client] ... parameters are "%r". Packing and pushing to server ...' % (args,)) # Handle memory - mem_package_list, memory_transport_handle = self.client_pack_memory_list(args, self.memsync) + mem_package_list, memory_transport_handle = self.data.client_pack_memory_list(args, self.memsync) # Actually call routine in DLL! TODO Handle kw ... return_dict = self.__handle_call_on_server__( - self.arg_list_pack(args, self.argtypes_d), mem_package_list + self.data.arg_list_pack(args, self.argtypes_d), mem_package_list ) # Log status self.log.out('[routine-client] ... received feedback from server, unpacking ...') - # Unpack return dict (for pointers and structs) - self.arg_list_sync( + # Unpack return dict (call may have failed partially only) + self.data.arg_list_sync( args, - self.arg_list_unpack(return_dict['args'], self.argtypes_d), + self.data.arg_list_unpack(return_dict['args'], self.argtypes_d), self.argtypes_d ) - # Unpack return value of routine - return_value = self.return_msg_unpack(return_dict['return_value'], self.restype_d) + # Unpack memory (call may have failed partially only) + self.data.client_unpack_memory_list(return_dict['memory'], memory_transport_handle) + + # Unpacking a return value only makes sense if the call was a success + if return_dict['success']: - # Unpack memory - self.client_unpack_memory_list(return_dict['memory'], memory_transport_handle) + # Unpack return value of routine + return_value = self.data.return_msg_unpack(return_dict['return_value'], self.restype_d) # Log status self.log.out('[routine-client] ... unpacked, return.') + # Raise the original error if call was not a success + if not return_dict['success']: + raise return_dict['exception'] + # Return result. return_value will be None if there was not a result. return return_value @@ -156,19 +153,19 @@ def __call__(self, *args): def __configure__(self): # Prepare list of arguments by parsing them into list of dicts (TODO field name / kw) - self.argtypes_d = self.pack_definition_argtypes(self.__argtypes__) + self.argtypes_d = self.data.pack_definition_argtypes(self.__argtypes__) # Parse return type - self.restype_d = self.pack_definition_returntype(self.__restype__) + self.restype_d = self.data.pack_definition_returntype(self.__restype__) # Fix missing ctypes in memsync - self.client_fix_memsync_ctypes(self.__memsync__) + self.data.client_fix_memsync_ctypes(self.__memsync__) # Reduce memsync for transfer - self.memsync_d = self.pack_definition_memsync(self.__memsync__) + self.memsync_d = self.data.pack_definition_memsync(self.__memsync__) # Generate handles on relevant argtype definitions for memsync, adjust definitions with void pointers - self.memsync_handle = self.apply_memsync_to_argtypes_definition(self.__memsync__, self.argtypes_d) + self.memsync_handle = self.data.apply_memsync_to_argtypes_definition(self.__memsync__, self.argtypes_d) # Log status self.log.out(' memsync: \n%s' % pf(self.__memsync__)) diff --git a/src/zugbruecke/core/routine_server.py b/src/zugbruecke/core/routine_server.py index e7939129..f700e5d3 100644 --- a/src/zugbruecke/core/routine_server.py +++ b/src/zugbruecke/core/routine_server.py @@ -34,20 +34,12 @@ from pprint import pformat as pf import traceback -from .arg_contents import arg_contents_class -from .arg_definition import arg_definition_class -from .arg_memory import arg_memory_class - # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ # DLL SERVER CLASS # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ -class routine_server_class( - arg_contents_class, - arg_definition_class, - arg_memory_class - ): +class routine_server_class(): def __init__(self, parent_dll, routine_name, routine_handler): @@ -65,9 +57,7 @@ def __init__(self, parent_dll, routine_name, routine_handler): self.name = routine_name # Required by arg definitions and contents - self.cache_dict = self.session.cache_dict - self.callback_client = self.session.callback_client - self.is_server = True + self.data = self.session.data # Set routine handler self.handler = routine_handler @@ -82,10 +72,10 @@ def __call__(self, arg_message_list, arg_memory_list): self.log.out('[routine-server] Trying call routine "%s" ...' % self.name) # Unpack passed arguments, handle pointers and structs ... - args_list = self.arg_list_unpack(arg_message_list, self.argtypes_d) + args_list = self.data.arg_list_unpack(arg_message_list, self.argtypes_d) # Unpack pointer data - memory_handle = self.server_unpack_memory_list(args_list, arg_memory_list, self.memsync_d) + memory_handle = self.data.server_unpack_memory_list(args_list, arg_memory_list, self.memsync_d) # Default return value return_value = None @@ -97,13 +87,13 @@ def __call__(self, arg_message_list, arg_memory_list): return_value = self.handler(*tuple(args_list)) # Pack memory for return - arg_memory_list = self.server_pack_memory_list(memory_handle) + arg_memory_list = self.data.server_pack_memory_list(memory_handle) # Get new arg message list - arg_message_list = self.arg_list_pack(args_list, self.argtypes_d) + arg_message_list = self.data.arg_list_pack(args_list, self.argtypes_d) # Get new return message list - return_message = self.return_msg_pack(return_value, self.restype_d) + return_message = self.data.return_msg_pack(return_value, self.restype_d) # Log status self.log.out('[routine-server] ... done.') @@ -113,10 +103,11 @@ def __call__(self, arg_message_list, arg_memory_list): 'args': arg_message_list, 'return_value': return_message, # TODO handle memory allocated by DLL in "free form" pointers 'memory': arg_memory_list, - 'success': True + 'success': True, + 'exception': None } - except: + except Exception as e: # Log status self.log.out('[routine-server] ... failed!') @@ -129,7 +120,8 @@ def __call__(self, arg_message_list, arg_memory_list): 'args': arg_message_list, 'return_value': return_value, 'memory': arg_memory_list, - 'success': False + 'success': False, + 'exception': e } @@ -142,13 +134,16 @@ def __configure__(self, argtypes_d, restype_d, memsync_d): self.argtypes_d = argtypes_d # Parse and apply argtype definition dict to actual ctypes routine - self.handler.argtypes = self.unpack_definition_argtypes(argtypes_d) + _argtypes = self.data.unpack_definition_argtypes(argtypes_d) + # Only configure if there are definitions, otherwise calls with int parameters without definition fail + if len(_argtypes) > 0: + self.handler.argtypes = _argtypes # Store return value definition dict self.restype_d = restype_d # Parse and apply restype definition dict to actual ctypes routine - self.handler.restype = self.unpack_definition_returntype(restype_d) + self.handler.restype = self.data.unpack_definition_returntype(restype_d) # Log status self.log.out(' memsync: \n%s' % pf(self.memsync_d)) diff --git a/src/zugbruecke/core/rpc.py b/src/zugbruecke/core/rpc.py index 608f1ced..5bbdadef 100644 --- a/src/zugbruecke/core/rpc.py +++ b/src/zugbruecke/core/rpc.py @@ -36,6 +36,7 @@ Listener ) from threading import Thread +import time import traceback @@ -43,6 +44,35 @@ # CLASSES AND CONSTRUCTOR ROUTINES # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +def mp_client_safe_connect(socket_path, authkey, timeout_after_seconds = 30, wait_for_seconds = 0.01): + + # Already waited for ... + started_waiting_at = time.time() + + # Run loop until socket appears + while True: + + # Try to connect to server and get its status + try: + # Fire up xmlrpc client + mp_client = mp_client_class(socket_path, authkey) + # Get status from server and return handle + if mp_client.__get_handler_status__(): + return mp_client + except: + pass + + # Break the loop after timeout + if time.time() >= (started_waiting_at + timeout_after_seconds): + break + + # Wait before trying again + time.sleep(wait_for_seconds) + + # If client could not connect, raise an error + raise # TODO + + class mp_client_class: @@ -78,8 +108,17 @@ class mp_server_handler_class: def __init__(self): + # cache for registered functions self.__functions__ = {} + # Method for verifying server status + self.register_function(self.__get_handler_status__) + + + def __get_handler_status__(self): + + return True + def register_function(self, function_pointer, public_name = None): @@ -191,9 +230,9 @@ def serve_forever(self): traceback.print_exc() - def server_forever_in_thread(self): + def server_forever_in_thread(self, daemon = True): # Start the server in its own thread t = Thread(target = self.serve_forever) - t.daemon = True + t.daemon = daemon t.start() diff --git a/src/zugbruecke/core/session_client.py b/src/zugbruecke/core/session_client.py index b2efc012..4410ad68 100644 --- a/src/zugbruecke/core/session_client.py +++ b/src/zugbruecke/core/session_client.py @@ -33,7 +33,6 @@ import atexit from ctypes import ( - _CFuncPtr, _FUNCFLAG_CDECL, _FUNCFLAG_USE_ERRNO, _FUNCFLAG_USE_LASTERROR @@ -44,16 +43,16 @@ from .const import _FUNCFLAG_STDCALL from .config import get_module_config +from .data import data_class from .dll_client import dll_client_class from .interpreter import interpreter_session_class from .lib import ( - generate_cache_dict, get_free_port, get_location_of_file ) from .log import log_class from .rpc import ( - mp_client_class, + mp_client_safe_connect, mp_server_class ) from .wineenv import ( @@ -82,7 +81,7 @@ def ctypes_FormatError(self, code = None): self.__init_stage_2__() # Ask the server - return self.client.ctypes_FormatError(code) + return self.rpc_client.ctypes_FormatError(code) def ctypes_get_last_error(self): @@ -92,7 +91,7 @@ def ctypes_get_last_error(self): self.__init_stage_2__() # Ask the server - return self.client.ctypes_get_last_error() + return self.rpc_client.ctypes_get_last_error() def ctypes_GetLastError(self): @@ -102,7 +101,7 @@ def ctypes_GetLastError(self): self.__init_stage_2__() # Ask the server - return self.client.ctypes_GetLastError() + return self.rpc_client.ctypes_GetLastError() def ctypes_set_last_error(self, value): @@ -112,7 +111,7 @@ def ctypes_set_last_error(self, value): self.__init_stage_2__() # Ask the server - return self.client.ctypes_set_last_error(value) + return self.rpc_client.ctypes_set_last_error(value) def ctypes_WinError(self, code = None, descr = None): @@ -122,7 +121,7 @@ def ctypes_WinError(self, code = None, descr = None): self.__init_stage_2__() # Ask the server - return self.client.ctypes_WinError(code, descr) + return self.rpc_client.ctypes_WinError(code, descr) def ctypes_CFUNCTYPE(self, restype, *argtypes, **kw): @@ -131,25 +130,25 @@ def ctypes_CFUNCTYPE(self, restype, *argtypes, **kw): if self.stage == 1: self.__init_stage_2__() - return self.get_callback_decorator(_FUNCFLAG_CDECL, restype, *argtypes, **kw) + flags = _FUNCFLAG_CDECL + if kw.pop("use_errno", False): + flags |= _FUNCFLAG_USE_ERRNO + if kw.pop("use_last_error", False): + flags |= _FUNCFLAG_USE_LASTERROR + if kw: + raise ValueError("unexpected keyword argument(s) %s" % kw.keys()) - def ctypes_WINFUNCTYPE(self, restype, *argtypes, **kw): # EXPORT - - # If in stage 1, fire up stage 2 - if self.stage == 1: - self.__init_stage_2__() - - return self.get_callback_decorator(_FUNCFLAG_STDCALL, restype, *argtypes, **kw) + return self.data.generate_callback_decorator(flags, restype, *argtypes) - def get_callback_decorator(self, functype, restype, *argtypes, **kw): + def ctypes_WINFUNCTYPE(self, restype, *argtypes, **kw): # EXPORT # If in stage 1, fire up stage 2 if self.stage == 1: self.__init_stage_2__() - flags = functype + flags = _FUNCFLAG_STDCALL if kw.pop("use_errno", False): flags |= _FUNCFLAG_USE_ERRNO @@ -158,23 +157,7 @@ def get_callback_decorator(self, functype, restype, *argtypes, **kw): if kw: raise ValueError("unexpected keyword argument(s) %s" % kw.keys()) - try: - - # There already is a matching function pointer type available - return self.cache_dict['func_type'][functype][(restype, argtypes, flags)] - - except KeyError: - - # Create new function pointer type class - class FunctionType(_CFuncPtr): - - _argtypes_ = argtypes - _restype_ = restype - _flags_ = flags - - # Store the new type and return - self.cache_dict['func_type'][functype][(restype, argtypes, flags)] = FunctionType - return FunctionType + return self.data.generate_callback_decorator(flags, restype, *argtypes) def load_library(self, dll_name, dll_type, dll_param = {}): @@ -204,18 +187,22 @@ def load_library(self, dll_name, dll_type, dll_param = {}): dll_param['use_last_error'] = False # Log status - self.log.out('[session-client] Trying to access DLL "%s" of type "%s" ...' % (dll_name, dll_type)) + self.log.out('[session-client] Attaching to DLL file "%s" with calling convention "%s" ...' % (dll_name, dll_type)) - # Tell wine about the dll and its type TODO implement some sort of find_library - (success, hash_id) = self.__load_library_on_server__( - dll_name, dll_type, dll_param - ) + try: - # If it failed, raise an error - if not success: + # Tell wine about the dll and its type + hash_id = self.rpc_client.load_library( + dll_name, dll_type, dll_param + ) - # (Re-) raise an OSError if the above returned an error - raise # TODO + except OSError as e: + + # Log status + self.log.out('[session-client] ... failed!') + + # If DLL was not found, reraise error + raise e # Fire up new dll object self.dll_dict[dll_name] = dll_client_class( @@ -223,7 +210,7 @@ def load_library(self, dll_name, dll_type, dll_param = {}): ) # Log status - self.log.out('[session-client] ... touched and added to list.') + self.log.out('[session-client] ... attached.') # Return reference on existing dll object return self.dll_dict[dll_name] @@ -236,7 +223,7 @@ def path_unix_to_wine(self, in_path): self.__init_stage_2__() # Ask the server - return self.client.path_unix_to_wine(in_path) + return self.rpc_client.path_unix_to_wine(in_path) def path_wine_to_unix(self, in_path): @@ -246,13 +233,13 @@ def path_wine_to_unix(self, in_path): self.__init_stage_2__() # Ask the server - return self.client.path_wine_to_unix(in_path) + return self.rpc_client.path_wine_to_unix(in_path) def set_parameter(self, parameter): self.p.update(parameter) - self.client.set_parameter(parameter) + self.rpc_client.set_parameter(parameter) def terminate(self): @@ -266,15 +253,18 @@ def terminate(self): # Only if in stage 2: if self.stage == 2: - # Tell server via message to terminate - self.client.terminate() + # Wait for server to appear + self.__wait_for_server_status_change__(target_status = False) - # Terminate callback server - self.callback_server.terminate() + # Tell server via message to terminate + self.rpc_client.terminate() # Destruct interpreter session self.interpreter_session.terminate() + # Terminate callback server + self.rpc_server.terminate() + # Log status self.log.out('[session-client] TERMINATED.') @@ -293,19 +283,22 @@ def __init_stage_1__(self, parameter, force_stage_2): # Get and set session id self.id = self.p['id'] + # Start RPC server for callback routines + self.__start_rpc_server__() + # Start session logging - self.log = log_class(self.id, self.p) + self.log = log_class(self.id, self.p, rpc_server = self.rpc_server) # Log status self.log.out('[session-client] STARTING (STAGE 1) ...') self.log.out('[session-client] Configured Wine-Python version is %s for %s.' % (self.p['version'], self.p['arch'])) - self.log.out('[session-client] Log socket port: %d.' % self.p['port_socket_log_main']) + self.log.out('[session-client] Log socket port: %d.' % self.p['port_socket_unix']) # Store current working directory self.dir_cwd = os.getcwd() - # Set up a cache dict (packed and unpacked types) - self.cache_dict = generate_cache_dict() + # Set data cache and parser + self.data = data_class(self.log, is_server = False, callback_server = self.rpc_server) # Set up a dict for loaded dlls self.dll_dict = {} @@ -313,6 +306,9 @@ def __init_stage_1__(self, parameter, force_stage_2): # Mark session as up self.up = True + # Marking server component as down + self.server_up = False + # Set current stage to 1 self.stage = 1 @@ -341,126 +337,119 @@ def __init_stage_2__(self): self.dir_wineprefix = set_wine_env(self.p['dir'], self.p['arch']) create_wine_prefix(self.dir_wineprefix) - # Start RPC server for callback routines - self.__start_callback_server__() - # Prepare python command for ctypes server or interpreter self.__prepare_python_command__() # Initialize interpreter session self.interpreter_session = interpreter_session_class(self.id, self.p, self.log) - # If in ctypes mode ... - self.__start_ctypes_client__() + # Wait for server to appear + self.__wait_for_server_status_change__(target_status = True) - # Set current stage to 1 + # Try to connect to Wine side + self.__start_rpc_client__() + + # Set current stage to 2 self.stage = 2 # Log status self.log.out('[session-client] STARTED (STAGE 2).') - def __start_callback_server__(self): + def __set_server_status__(self, status): + + # Interface for session server through RPC + self.server_up = status + + + def __start_rpc_client__(self): + + # Fire up xmlrpc client + self.rpc_client = mp_client_safe_connect( + ('localhost', self.p['port_socket_wine']), + 'zugbruecke_wine' + ) + + + def __start_rpc_server__(self): # Get socket for callback bridge - self.p['port_socket_callback'] = get_free_port() + self.p['port_socket_unix'] = get_free_port() # Create server - self.callback_server = mp_server_class( - ('localhost', self.p['port_socket_callback']), - 'zugbruecke_callback_main', - log = self.log - ) + self.rpc_server = mp_server_class( + ('localhost', self.p['port_socket_unix']), + 'zugbruecke_unix' + ) # Log is added later + + # Interface to server to indicate its status + self.rpc_server.register_function(self.__set_server_status__, 'set_server_status') # Start server into its own thread - self.callback_server.server_forever_in_thread() + self.rpc_server.server_forever_in_thread() + + + def __prepare_python_command__(self): + + # Get socket for ctypes bridge + self.p['port_socket_wine'] = get_free_port() + + # Prepare command with minimal meta info. All other info can be passed via sockets. + self.p['command_dict'] = [ + os.path.join( + os.path.abspath(os.path.join(get_location_of_file(__file__), os.pardir)), + '_server_.py' + ), + '--id', self.id, + '--port_socket_wine', str(self.p['port_socket_wine']), + '--port_socket_unix', str(self.p['port_socket_unix']), + '--log_level', str(self.p['log_level']), + '--log_write', str(int(self.p['log_write'])) + ] - def __start_ctypes_client__(self): + def __wait_for_server_status_change__(self, target_status): + + # Does the status have to change? + if target_status == self.server_up: + + # No, so get out of here + return + + # Debug strings + STATUS_DICT = {True: 'up', False: 'down'} # Log status - self.log.out('[session-client] ctypes client connecting ...') + self.log.out('[session-client] Waiting for session-server be %s ...' % STATUS_DICT[target_status]) - # Status variable - ctypes_server_up = False # Time-step wait_for_seconds = 0.01 # Timeout timeout_after_seconds = 30.0 # Already waited for ... started_waiting_at = time.time() - # Connection trys - tried_this_many_times = 0 # Run loop until socket appears - while True: - - # Try to get server status - try: - - # Count attempts - tried_this_many_times += 1 - - # Fire up xmlrpc client - self.client = mp_client_class( - ('localhost', self.p['port_socket_ctypes']), - 'zugbruecke_server_main' - ) - - # Get status from server - server_status = self.client.get_status() - - # Check result - if server_status == 'up': - ctypes_server_up = True - break - - except: - - pass - - # Break the loop after timeout - if time.time() >= (started_waiting_at + timeout_after_seconds): - break + while not self.server_up: # Wait before trying again time.sleep(wait_for_seconds) - # Evaluate the result - if not ctypes_server_up: - - # Log status - self.log.out('[session-client] ... could not connect (after %0.2f seconds & %d attempts)! Error.' % - (time.time() - started_waiting_at, tried_this_many_times) - ) - raise # TODO - - else: + # Time out + if time.time() >= (started_waiting_at + timeout_after_seconds): + break - # Generate handles on server-side routines - self.__load_library_on_server__ = self.client.load_library + # Handle timeout + if not self.server_up: # Log status - self.log.out('[session-client] ... connected (after %0.2f seconds & %d attempts).' % - (time.time() - started_waiting_at, tried_this_many_times) + self.log.out('[session-client] ... wait timed out (after %0.2f seconds).' % + (time.time() - started_waiting_at) ) + raise # TODO - def __prepare_python_command__(self): - - # Get socket for ctypes bridge - self.p['port_socket_ctypes'] = get_free_port() - - # Prepare command with minimal meta info. All other info can be passed via sockets. - self.p['command_dict'] = [ - os.path.join( - os.path.abspath(os.path.join(get_location_of_file(__file__), os.pardir)), - '_server_.py' - ), - '--id', self.id, - '--port_socket_ctypes', str(self.p['port_socket_ctypes']), - '--port_socket_callback', str(self.p['port_socket_callback']), - '--port_socket_log_main', str(self.p['port_socket_log_main']), - '--log_level', str(self.p['log_level']), - '--logwrite', str(int(self.p['logwrite'])) - ] + # Log status + self.log.out('[session-client] ... session server is %s (after %0.2f seconds).' % + (STATUS_DICT[target_status], time.time() - started_waiting_at) + ) diff --git a/src/zugbruecke/core/session_server.py b/src/zugbruecke/core/session_server.py index 7018f873..72d5f753 100644 --- a/src/zugbruecke/core/session_server.py +++ b/src/zugbruecke/core/session_server.py @@ -32,14 +32,15 @@ # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ import ctypes +import time import traceback +from .data import data_class from .dll_server import dll_server_class -from .lib import generate_cache_dict from .log import log_class from .path import path_class from .rpc import ( - mp_client_class, + mp_client_safe_connect, mp_server_class ) @@ -57,8 +58,14 @@ def __init__(self, session_id, parameter): self.id = session_id self.p = parameter + # Connect to Unix side + self.rpc_client = mp_client_safe_connect( + ('localhost', self.p['port_socket_unix']), + 'zugbruecke_unix' + ) + # Start logging session and connect it with log on unix side - self.log = log_class(self.id, self.p) + self.log = log_class(self.id, self.p, rpc_client = self.rpc_client) # Status log self.log.out('[session-server] STARTING ...') @@ -66,9 +73,6 @@ def __init__(self, session_id, parameter): # Mark session as up self.up = True - # Create dict for struct type definitions - self.cache_dict = generate_cache_dict() - # Offer methods for converting paths path = path_class() self.path_unix_to_wine = path.unix_to_wine @@ -84,43 +88,41 @@ def __init__(self, session_id, parameter): 'oledll': ctypes.OleDLL } - # Connect to callback server - self.callback_client = mp_client_class( - ('localhost', self.p['port_socket_callback']), - 'zugbruecke_callback_main' - ) + # Set data cache and parser + self.data = data_class(self.log, is_server = True, callback_client = self.rpc_client) # Create server - self.server = mp_server_class( - ('localhost', self.p['port_socket_ctypes']), - 'zugbruecke_server_main', + self.rpc_server = mp_server_class( + ('localhost', self.p['port_socket_wine']), + 'zugbruecke_wine', log = self.log, terminate_function = self.__terminate__ ) - # Return status of server - self.server.register_function(self.__get_status__, 'get_status') # Register call: Accessing a dll - self.server.register_function(self.__load_library__, 'load_library') + self.rpc_server.register_function(self.__load_library__, 'load_library') # Expose routine for updating parameters - self.server.register_function(self.__set_parameter__, 'set_parameter') + self.rpc_server.register_function(self.__set_parameter__, 'set_parameter') # Register destructur: Call goes into xmlrpc-server first, which then terminates parent - self.server.register_function(self.server.terminate, 'terminate') + self.rpc_server.register_function(self.rpc_server.terminate, 'terminate') # Convert path: Unix to Wine - self.server.register_function(self.path_unix_to_wine, 'path_unix_to_wine') + self.rpc_server.register_function(self.path_unix_to_wine, 'path_unix_to_wine') # Convert path: Wine to Unix - self.server.register_function(self.path_wine_to_unix, 'path_wine_to_unix') + self.rpc_server.register_function(self.path_wine_to_unix, 'path_wine_to_unix') # Expose ctypes stuff self.__expose_ctypes_routines__() # Status log - self.log.out('[session-server] ctypes server is listening on port %d.' % self.p['port_socket_ctypes']) + self.log.out('[session-server] ctypes server is listening on port %d.' % self.p['port_socket_wine']) self.log.out('[session-server] STARTED.') self.log.out('[session-server] Serve forever ...') # Run server ... - self.server.serve_forever() + self.rpc_server.server_forever_in_thread(daemon = False) + + # Indicate to session client that the server is up + self.rpc_client.set_server_status(True) def __expose_ctypes_routines__(self): @@ -134,18 +136,7 @@ def __expose_ctypes_routines__(self): 'set_last_error' ]: - self.server.register_function(getattr(ctypes, routine), 'ctypes_' + routine) - - - def __get_status__(self): - """ - Exposed interface - """ - - if self.up: - return 'up' - else: - return 'down' + self.rpc_server.register_function(getattr(ctypes, routine), 'ctypes_' + routine) def __load_library__(self, dll_name, dll_type, dll_param): @@ -171,26 +162,31 @@ def __load_library__(self, dll_name, dll_type, dll_param): use_last_error = dll_param['use_last_error'] ) - # Load library - self.dll_dict[dll_name] = dll_server_class( - self, dll_name, dll_type, handler - ) + except OSError as e: # Log status - self.log.out('[session-server] ... done.') + self.log.out('[session-server] ... failed!') - # Return success and dll's hash id - return (True, self.dll_dict[dll_name].hash_id) # Success + # Reraise error + raise e except: - # Log status - self.log.out('[session-server] ... failed!') - # Push traceback to log self.log.err(traceback.format_exc()) - return (False, None) # Fail + raise # TODO + + # Load library + self.dll_dict[dll_name] = dll_server_class( + self, dll_name, dll_type, handler + ) + + # Log status + self.log.out('[session-server] ... attached.') + + # Return success and dll's hash id + return self.dll_dict[dll_name].hash_id def __set_parameter__(self, parameter): @@ -212,8 +208,11 @@ def __terminate__(self): # Terminate log self.log.terminate() + # Session down + self.up = False + # Status log self.log.out('[session-server] TERMINATED.') - # Session down - self.up = False + # Indicate to session client that server was terminated + self.rpc_client.set_server_status(False) diff --git a/src/zugbruecke/core/wineenv.py b/src/zugbruecke/core/wineenv.py index c7e257dd..a8b4ee88 100644 --- a/src/zugbruecke/core/wineenv.py +++ b/src/zugbruecke/core/wineenv.py @@ -84,7 +84,7 @@ def setup_wine_pip(arch, version, directory): def setup_wine_python(arch, version, directory, overwrite = False): # File name for python stand-alone zip file - pyarchive = 'python-%s-embed-%s.zip' % (version, arch) + pyarchive = 'python-%s-embed-%s.zip' % (version, 'amd64' if arch == 'win64' else arch) # Name of target subfolder pydir = '%s-python%s' % (arch, version) diff --git a/tests/test_bubblesort_struct.py b/tests/test_bubblesort_struct.py new file mode 100644 index 00000000..faa385b5 --- /dev/null +++ b/tests/test_bubblesort_struct.py @@ -0,0 +1,104 @@ +# -*- coding: utf-8 -*- + +""" + +ZUGBRUECKE +Calling routines in Windows DLLs from Python scripts running on unixlike systems +https://github.com/pleiszenburg/zugbruecke + + tests/test_bubblesort_struct.py: Test bidirectional memory sync for pointers in struct + + Required to run on platform / side: [UNIX, WINE] + + Copyright (C) 2017-2018 Sebastian M. Ernst + + +The contents of this file are subject to the GNU Lesser General Public License +Version 2.1 ("LGPL" or "License"). You may not use this file except in +compliance with the License. You may obtain a copy of the License at +https://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt +https://github.com/pleiszenburg/zugbruecke/blob/master/LICENSE + +Software distributed under the License is distributed on an "AS IS" basis, +WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the +specific language governing rights and limitations under the License. + + +""" + + +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +# IMPORT +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +import pytest + +from sys import platform +if any([platform.startswith(os_name) for os_name in ['linux', 'darwin', 'freebsd']]): + import zugbruecke as ctypes +elif platform.startswith('win'): + import ctypes + + +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +# CLASSES AND ROUTINES +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +class bubblesort_data(ctypes.Structure): + + + _fields_ = [ + ('a', ctypes.POINTER(ctypes.c_float)), + ('n', ctypes.c_int) + ] + + +class sample_class: + + + def __init__(self): + + self.__dll__ = ctypes.windll.LoadLibrary('tests/demo_dll.dll') + + self.__bubblesort_struct__ = self.__dll__.bubblesort_struct + self.__bubblesort_struct__.memsync = [ # Regular ctypes on Windows should ignore this statement + { + 'p': [0, 'a'], # "path" to argument containing the pointer + 'l': [0, 'n'], # "path" to argument containing the length + '_t': ctypes.c_float # type of argument (optional, default char/byte): sizeof(type) * length == bytes + } + ] + self.__bubblesort_struct__.argtypes = (ctypes.POINTER(bubblesort_data),) + + + def bubblesort_struct(self, values): + + ctypes_float_values = ((ctypes.c_float)*len(values))(*values) + ctypes_float_pointer_firstelement = ctypes.cast( + ctypes.pointer(ctypes_float_values), ctypes.POINTER(ctypes.c_float) + ) + + data = bubblesort_data( + ctypes_float_pointer_firstelement, + len(values) + ) + + self.__bubblesort_struct__(data) + values[:] = ctypes_float_values[:] + + +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +# TEST(s) +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +def test_bubblesort_struct(): + + sample = sample_class() + + test_vector = [5.74, 3.72, 6.28, 8.6, 9.34, 6.47, 2.05, 9.09, 4.39, 4.75] + sample.bubblesort_struct(test_vector) + test_vector = [round(element, 2) for element in test_vector] + result_vector = [2.05, 3.72, 4.39, 4.75, 5.74, 6.28, 6.47, 8.6, 9.09, 9.34] + vector_diff = sum([abs(test_vector[index] - result_vector[index]) for index in range(len(result_vector))]) + + assert pytest.approx(0.0, 0.0000001) == vector_diff diff --git a/tests/test_callback_simple_struct.py b/tests/test_callback_simple_struct.py new file mode 100644 index 00000000..1176de09 --- /dev/null +++ b/tests/test_callback_simple_struct.py @@ -0,0 +1,95 @@ +# -*- coding: utf-8 -*- + +""" + +ZUGBRUECKE +Calling routines in Windows DLLs from Python scripts running on unixlike systems +https://github.com/pleiszenburg/zugbruecke + + tests/test_callback_simple_struct.py: Demonstrates callback in struct + + Required to run on platform / side: [UNIX, WINE] + + Copyright (C) 2017-2018 Sebastian M. Ernst + + +The contents of this file are subject to the GNU Lesser General Public License +Version 2.1 ("LGPL" or "License"). You may not use this file except in +compliance with the License. You may obtain a copy of the License at +https://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt +https://github.com/pleiszenburg/zugbruecke/blob/master/LICENSE + +Software distributed under the License is distributed on an "AS IS" basis, +WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the +specific language governing rights and limitations under the License. + + +""" + + +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +# IMPORT +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +# import pytest + +from sys import platform +if any([platform.startswith(os_name) for os_name in ['linux', 'darwin', 'freebsd']]): + import zugbruecke as ctypes +elif platform.startswith('win'): + import ctypes + + +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +# CLASSES AND ROUTINES +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +conveyor_belt = ctypes.WINFUNCTYPE(ctypes.c_int16, ctypes.c_int16) + + +class conveyor_belt_data(ctypes.Structure): + + + _fields_ = [ + ('len', ctypes.c_int16), + ('get_data', conveyor_belt) + ] + + +class sample_class: + + + def __init__(self): + + self.__dll__ = ctypes.windll.LoadLibrary('tests/demo_dll.dll') + + self.__sum_elements_from_callback_in_struct__ = self.__dll__.sum_elements_from_callback_in_struct + self.__sum_elements_from_callback_in_struct__.argtypes = (ctypes.POINTER(conveyor_belt_data),) + self.__sum_elements_from_callback_in_struct__.restype = ctypes.c_int16 + + self.DATA = [1, 6, 8, 4, 9, 7, 4, 2, 5, 2] + + @conveyor_belt + def get_data(index): + print((index, self.DATA[index])) + return self.DATA[index] + + self.__get_data__ = get_data + + + def sum_elements_from_callback_in_struct(self): + + in_struct = conveyor_belt_data(len(self.DATA), self.__get_data__) + + return self.__sum_elements_from_callback_in_struct__(in_struct) + + +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +# TEST(s) +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +def test_callback_simple(): + + sample = sample_class() + + assert 48 == sample.sum_elements_from_callback_in_struct() diff --git a/tests/test_error_callargs.py b/tests/test_error_callargs.py new file mode 100644 index 00000000..bd1a3432 --- /dev/null +++ b/tests/test_error_callargs.py @@ -0,0 +1,92 @@ +# -*- coding: utf-8 -*- + +""" + +ZUGBRUECKE +Calling routines in Windows DLLs from Python scripts running on unixlike systems +https://github.com/pleiszenburg/zugbruecke + + tests/test_error_callargs.py: Test error handling when malformed arguments are passed + + Required to run on platform / side: [UNIX, WINE] + + Copyright (C) 2017-2018 Sebastian M. Ernst + + +The contents of this file are subject to the GNU Lesser General Public License +Version 2.1 ("LGPL" or "License"). You may not use this file except in +compliance with the License. You may obtain a copy of the License at +https://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt +https://github.com/pleiszenburg/zugbruecke/blob/master/LICENSE + +Software distributed under the License is distributed on an "AS IS" basis, +WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the +specific language governing rights and limitations under the License. + + +""" + + +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +# IMPORT +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +import pytest + +from sys import platform +if any([platform.startswith(os_name) for os_name in ['linux', 'darwin', 'freebsd']]): + import zugbruecke as ctypes +elif platform.startswith('win'): + import ctypes + + +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +# TEST(s) +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +def test_error_callargs_unconfigured_too_many_args(): + + dll = ctypes.windll.LoadLibrary('tests/demo_dll.dll') + square_int = dll.square_int + + with pytest.raises(ValueError): + a = square_int(1, 2, 3) + + +def test_error_callargs_unconfigured_right_number_of_args(): + + dll = ctypes.windll.LoadLibrary('tests/demo_dll.dll') + add_ints = dll.add_ints + + assert 7 == add_ints(3, 4) + + +def test_error_callargs_unconfigured_right_number_of_args_nondefault_float(): + + dll = ctypes.windll.LoadLibrary('tests/demo_dll.dll') + add_floats = dll.add_floats + + with pytest.raises(ctypes.ArgumentError): + a = add_floats(1.2, 3.6) + + +def test_error_callargs_configured_too_few_args(): + + dll = ctypes.windll.LoadLibrary('tests/demo_dll.dll') + subtract_ints = dll.subtract_ints + subtract_ints.argtypes = (ctypes.c_int16, ctypes.c_int16) + subtract_ints.restype = ctypes.c_int16 + + with pytest.raises(TypeError): + a = subtract_ints(7) + + +def test_error_callargs_configured_too_many_args(): + + dll = ctypes.windll.LoadLibrary('tests/demo_dll.dll') + pow_ints = dll.pow_ints + pow_ints.argtypes = (ctypes.c_int16, ctypes.c_int16) + pow_ints.restype = ctypes.c_int16 + + with pytest.raises(TypeError): + a = pow_ints(7, 2, 99) diff --git a/tests/test_error_missingdll.py b/tests/test_error_missingdll.py new file mode 100644 index 00000000..43856e36 --- /dev/null +++ b/tests/test_error_missingdll.py @@ -0,0 +1,80 @@ +# -*- coding: utf-8 -*- + +""" + +ZUGBRUECKE +Calling routines in Windows DLLs from Python scripts running on unixlike systems +https://github.com/pleiszenburg/zugbruecke + + tests/test_error_missingdll.py: Checks for proper error handling if DLL does not exist + + Required to run on platform / side: [UNIX, WINE] + + Copyright (C) 2017-2018 Sebastian M. Ernst + + +The contents of this file are subject to the GNU Lesser General Public License +Version 2.1 ("LGPL" or "License"). You may not use this file except in +compliance with the License. You may obtain a copy of the License at +https://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt +https://github.com/pleiszenburg/zugbruecke/blob/master/LICENSE + +Software distributed under the License is distributed on an "AS IS" basis, +WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the +specific language governing rights and limitations under the License. + + +""" + + +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +# IMPORT +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +import pytest + +from sys import platform +if any([platform.startswith(os_name) for os_name in ['linux', 'darwin', 'freebsd']]): + import zugbruecke as ctypes +elif platform.startswith('win'): + import ctypes + + +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +# TEST(s) +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +def test_missingdll_cll(): + + with pytest.raises(OSError): + dll = ctypes.cdll.LoadLibrary('tests/nonexistent_dll.dll') + + +def test_missingdll_windll(): + + with pytest.raises(OSError): + dll = ctypes.windll.LoadLibrary('tests/nonexistent_dll.dll') + + +def test_missingdll_oledll(): + + with pytest.raises(OSError): + dll = ctypes.oledll.LoadLibrary('tests/nonexistent_dll.dll') + + +def test_missingdll_cll_attr(): + + with pytest.raises(OSError): + dll = ctypes.cdll.nonexistent_dll + + +def test_missingdll_windll_attr(): + + with pytest.raises(OSError): + dll = ctypes.windll.nonexistent_dll + + +def test_missingdll_oledll_attr(): + + with pytest.raises(OSError): + dll = ctypes.oledll.nonexistent_dll diff --git a/tests/test_error_missingroutine.py b/tests/test_error_missingroutine.py new file mode 100644 index 00000000..ce7cf52e --- /dev/null +++ b/tests/test_error_missingroutine.py @@ -0,0 +1,52 @@ +# -*- coding: utf-8 -*- + +""" + +ZUGBRUECKE +Calling routines in Windows DLLs from Python scripts running on unixlike systems +https://github.com/pleiszenburg/zugbruecke + + tests/test_error_missingroutine.py: Checks for proper error handling if routine does not exist + + Required to run on platform / side: [UNIX, WINE] + + Copyright (C) 2017-2018 Sebastian M. Ernst + + +The contents of this file are subject to the GNU Lesser General Public License +Version 2.1 ("LGPL" or "License"). You may not use this file except in +compliance with the License. You may obtain a copy of the License at +https://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt +https://github.com/pleiszenburg/zugbruecke/blob/master/LICENSE + +Software distributed under the License is distributed on an "AS IS" basis, +WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the +specific language governing rights and limitations under the License. + + +""" + + +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +# IMPORT +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +import pytest + +from sys import platform +if any([platform.startswith(os_name) for os_name in ['linux', 'darwin', 'freebsd']]): + import zugbruecke as ctypes +elif platform.startswith('win'): + import ctypes + + +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +# TEST(s) +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +def test_missingroutine(): + + dll = ctypes.windll.LoadLibrary('tests/demo_dll.dll') + + with pytest.raises(AttributeError): + missing_routine = dll.missing_routine diff --git a/tests/test_sqrt_int.py b/tests/test_sqrt_int.py index 09db6a9f..09e245ef 100644 --- a/tests/test_sqrt_int.py +++ b/tests/test_sqrt_int.py @@ -64,4 +64,4 @@ def test_sqrt_int(): sample = sample_class() - assert 9 == sample.sqrt_int(3) + assert 3 == sample.sqrt_int(9)