diff --git a/CHANGES.rst b/CHANGES.rst index e7937c12..f001cb3b 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,6 +1,13 @@ Changes ======= +0.0.10 (2018-03-23) +------------------- + +* FEATURE: Support for functions calculating the length of memory sections in memsync protocol, see issue #33. +* FEATURE: Support for string buffers (and null-terminated strings), see issue #7. +* FIX: Memsync definition sometimes lost information during first call of function, second call subsequently failed, see issue #36. + 0.0.9 (2018-03-21) ------------------ diff --git a/demo_dll/demo_dll.c b/demo_dll/demo_dll.c index 4de08168..4c104abb 100644 --- a/demo_dll/demo_dll.c +++ b/demo_dll/demo_dll.c @@ -169,6 +169,29 @@ void __stdcall DEMODLL bubblesort_struct( } +void __stdcall DEMODLL bubblesort_segments( + float *a, + int number_of_segments, + int elements_per_segment + ) +{ + int i, j; + int n = number_of_segments * elements_per_segment; + for (i = 0; i < n - 1; ++i) + { + for (j = 0; j < n - i - 1; ++j) + { + if (a[j] > a[j + 1]) + { + float tmp = a[j]; + a[j] = a[j + 1]; + a[j + 1] = tmp; + } + } + } +} + + void __stdcall DEMODLL mix_rgb_colors( int8_t color_a[3], int8_t color_b[3], @@ -294,6 +317,66 @@ int16_t __stdcall DEMODLL get_const_int(void) } +void __stdcall DEMODLL replace_letter_in_null_terminated_string_a( + char *in_string, + char old_letter, + char new_letter + ) +{ + int i; + for (i = 0; i < strlen(in_string); i++) { + if(in_string[i] == old_letter) { + in_string[i] = new_letter; + } + } +} + + +void __stdcall DEMODLL replace_letter_in_null_terminated_string_b( + char *in_string, + char old_letter, + char new_letter + ) +{ + int i; + for (i = 0; i < strlen(in_string); i++) { + if(in_string[i] == old_letter) { + in_string[i] = new_letter; + } + } +} + + +void __stdcall DEMODLL replace_letter_in_null_terminated_string_unicode_a( + wchar_t *in_string, + wchar_t old_letter, + wchar_t new_letter + ) +{ + int i; + for (i = 0; i < wcslen(in_string); i++) { + if(in_string[i] == old_letter) { + in_string[i] = new_letter; + } + } +} + + +void __stdcall DEMODLL replace_letter_in_null_terminated_string_unicode_b( + wchar_t *in_string, + wchar_t old_letter, + wchar_t new_letter + ) +{ + int i; + for (i = 0; i < wcslen(in_string); i++) { + if(in_string[i] == old_letter) { + in_string[i] = new_letter; + } + } +} + + float __stdcall DEMODLL simple_demo_routine( float param_a, float param_b @@ -369,6 +452,36 @@ int16_t __stdcall DEMODLL sum_elements_from_callback_in_struct( } +int16_t __stdcall DEMODLL use_optional_callback_a( + int16_t in_data, + conveyor_belt process_data + ) +{ + int16_t tmp; + if(process_data) { + tmp = process_data(in_data); + } else { + tmp = in_data; + } + return tmp * 2; +} + + +int16_t __stdcall DEMODLL use_optional_callback_b( + int16_t in_data, + conveyor_belt process_data + ) +{ + int16_t tmp; + if(process_data) { + tmp = process_data(in_data); + } else { + tmp = in_data; + } + return tmp * 2; +} + + // +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ // DLL infrastructure // +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ diff --git a/demo_dll/demo_dll.h b/demo_dll/demo_dll.h index cac85b08..10e2f9b8 100644 --- a/demo_dll/demo_dll.h +++ b/demo_dll/demo_dll.h @@ -111,6 +111,12 @@ void __stdcall DEMODLL bubblesort_struct( bubblesort_data *data ); +void __stdcall DEMODLL bubblesort_segments( + float *a, + int number_of_segments, + int elements_per_segment + ); + void __stdcall DEMODLL mix_rgb_colors( int8_t color_a[3], int8_t color_b[3], @@ -161,6 +167,30 @@ int16_t __stdcall DEMODLL pow_ints( int16_t __stdcall DEMODLL get_const_int(void); +void __stdcall DEMODLL replace_letter_in_null_terminated_string_a( + char *in_string, + char old_letter, + char new_letter + ); + +void __stdcall DEMODLL replace_letter_in_null_terminated_string_b( + char *in_string, + char old_letter, + char new_letter + ); + +void __stdcall DEMODLL replace_letter_in_null_terminated_string_unicode_a( + wchar_t *in_string, + wchar_t old_letter, + wchar_t new_letter + ); + +void __stdcall DEMODLL replace_letter_in_null_terminated_string_unicode_b( + wchar_t *in_string, + wchar_t old_letter, + wchar_t new_letter + ); + struct test { char el_char; @@ -204,6 +234,16 @@ int16_t __stdcall DEMODLL sum_elements_from_callback_in_struct( struct conveyor_belt_data *data ); +int16_t __stdcall DEMODLL use_optional_callback_a( + int16_t in_data, + conveyor_belt process_data + ); + +int16_t __stdcall DEMODLL use_optional_callback_b( + int16_t in_data, + conveyor_belt process_data + ); + // +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ // DLL infrastructure diff --git a/docs/memsync.rst b/docs/memsync.rst index a3ccb707..7897f555 100644 --- a/docs/memsync.rst +++ b/docs/memsync.rst @@ -20,8 +20,8 @@ through the ``memsync`` protocol. ``memsync`` implements special directives, which do not interfere with *ctypes* should the code be required to run on *Windows* as well. -A simple example ----------------- +A simple example: An array of floating point numbers of variable length +----------------------------------------------------------------------- Consider the following example DLL routine in C: @@ -149,6 +149,135 @@ The complete example, which will run on *Unix* and on *Windows* looks just like test_vector = [5.74, 3.72, 6.28, 8.6, 9.34, 6.47, 2.05, 9.09, 4.39, 4.75] bubblesort(test_vector) + +A more complex example: Computing the size of the memory from multiple arguments +-------------------------------------------------------------------------------- + +There are plenty of cases where you will encounter function (or structure) +definitions like the following: + +.. code:: C + + void __stdcall __declspec(dllimport) process_image( + float *image_data, + int image_width, + int image_height + ); + +The ``image_data`` parameter is a flattened 1D array representing a 2D image. +It's length is defined by its width and its height. So the length of array equals +``image_width * image_height``. For cases like this, ``memsync`` has the ability +to dynamically compute the length of the memory through custom functions. +Let's have a look at how the above function would be configured in *Python*: + +.. code:: python + + process_image.argtypes = (ctypes.POINTER(ctypes.c_float), ctypes.c_int, ctypes.c_int) + process_image.memsync = [ + { + 'p': [0], + 'l': ([1], [2]), + '_f': lambda x, y: x * y, + '_t': ctypes.c_float + } + ] + +The above definition will extract the values of the ``image_width`` and +``image_height`` parameters for every function call and feed them into the +specified lambda function. + +Using string buffers, null-terminated strings and Unicode +--------------------------------------------------------- + +Let's assume you are confronted with a regular Python (3) string. With the help of a +DLL function, you want to replace all occurrences of a letter with another letter. + +.. code:: python + + some_string = 'zategahuba' + +The DLL function's definition looks like this: + +.. code:: C + + void __stdcall __declspec(dllimport) replace_letter( + char *in_string, + char old_letter, + char new_letter + ); + +In Python, it can be configured as follows: + +.. code:: python + + replace_letter.argtypes = ( + ctypes.POINTER(ctypes.c_char), + ctypes.c_char, + ctypes.c_char + ) + replace_letter.memsync = [ + { + 'p': [0], + 'l': ([0],), + '_f': lambda x: ctypes.sizeof(x) + } + ] + +The above configuration exploits the field for specifying a function for computing +the length of the memory section, ``_f``. The function is pointed to the parameter +containing the string buffer and determines its length. + +While Python (3) strings are actually Unicode strings, the function accepts an +array of type ``char`` - a bytes array in Python terms. I.e. you have to encode the +string before it is copied into a string buffer. The following example illustrates +how the function ``replace_letter`` can be called on the string ``some_string``, +exchanging all letters ``a`` with ``e``. Subsequently, the result is printed. + +.. code:: python + + string_buffer = ctypes.create_string_buffer(some_string.encode('utf-8')) + replace_letter(string_buffer, 'a'.encode('utf-8'), 'e'.encode('utf-8')) + print(string_buffer.value.decode('utf-8')) + +The process differs if the DLL function accepts Unicode strings. Let's assume +the DLL function is defined as follows: + +.. code:: C + + void __stdcall __declspec(dllimport) replace_letter_w( + wchar_t *in_string, + wchar_t old_letter, + wchar_t new_letter + ); + +In Python, it can be configured like this: + +.. code:: python + + replace_letter_w.argtypes = ( + ctypes.POINTER(ctypes.c_wchar), + ctypes.c_wchar, + ctypes.c_wchar + ) + replace_letter_w.memsync = [ + { + 'p': [0], + 'l': ([0],), + 'w': ctypes.sizeof(ctypes.c_wchar), + '_f': lambda x: ctypes.sizeof(x) + } + ] + +One key aspect has changed: ``memsync`` contains another field, ``w``. It must +be initialized with the actual length of a Unicode character in the current environment. +Now you can call the function as follows: + +.. code:: python + + unicode_buffer = ctypes.create_unicode_buffer(some_string) + replace_letter_w(unicode_buffer, 'a', 'e') + print(unicode_buffer.value) + Attribute: ``memsync`` (list of dict) ---------------------------------------- @@ -157,7 +286,9 @@ section, which must be kept in sync. It has the following keys: * ``p`` (:ref:`path to pointer `) * ``l`` (:ref:`path to length `) -* ``_t`` (:ref:`data type of pointer `) +* ``w`` (:ref:`size of Unicode character `, optional) +* ``_t`` (:ref:`data type of pointer `, optional) +* ``_f`` (:ref:`custom length function `, optional) * ``_c`` (:ref:`custom data type `, optional) .. _pathpointer: @@ -198,21 +329,33 @@ You should be able to extrapolate from here. .. _pathlength: -Key: ``l``, path to length (list of int and/or str) -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Key: ``l``, path to length (list of int and/or str OR tuple of lists of int and/or str) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ This parameter works just like the :ref:`path to pointer ` parameter. It is expected to tell the parser, where it can find a number (int) which represents the length of the memory block. +It is expected to be either a single path list like ``[0, 'field_a']`` or a tuple +of multiple (or even zero) path lists, if the optional ``_f`` key is defined. + +.. _unicodechar: + +Key: ``w``, size of Unicode character (optional) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +If a Unicode string (buffer) is passed into a function, this parameter must be +initialized with the length of one Unicode character in bytes in the current +environment - ``ctypes.sizeof(ctypes.c_wchar)`` in most cases. + .. _pointertype: -Key: ``_t``, data type of pointer (PyCSimpleType or PyCStructType) -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Key: ``_t``, data type of pointer (PyCSimpleType or PyCStructType) (optional) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ This parameter will be fed into ``ctypes.sizeof`` for determining its size in bytes. The result is then multiplied with the ``length`` to get an actual size of the -memory block in bytes. +memory block in bytes. If it is not explicitly defined, it defaults to ``ctypes.c_ubyte``. For details on ``sizeof``, consult the `Python documentation on sizeof`_. It will accept `fundamental types`_ as well as `structure types`_. @@ -221,6 +364,15 @@ It will accept `fundamental types`_ as well as `structure types`_. .. _fundamental types: https://docs.python.org/3/library/ctypes.html?highlight=ctypes#fundamental-data-types .. _structure types: https://docs.python.org/3/library/ctypes.html?highlight=ctypes#ctypes.Structure +.. _length function: + +Key: ``_f``, custom function for computing the length of the memory segment (optional) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +This field can be used to plug in a function or lambda expression for computing the ``length`` +of the memory section from multiple parameters. The function is expected to accept a number of +arguments equal to the number of elements of the tuple of length paths defined in ``l``. + .. _customtype: Key: ``_c``, custom data type (optional) diff --git a/setup.py b/setup.py index 6b489923..1665830d 100644 --- a/setup.py +++ b/setup.py @@ -46,7 +46,7 @@ # Bump version HERE! -_version_ = '0.0.9' +_version_ = '0.0.10' # List all versions of Python which are supported diff --git a/src/zugbruecke/core/data/contents.py b/src/zugbruecke/core/data/contents.py index c4d10fb8..f223effc 100644 --- a/src/zugbruecke/core/data/contents.py +++ b/src/zugbruecke/core/data/contents.py @@ -116,6 +116,7 @@ def arg_list_sync(self, old_arguments_list, new_arguments_list, argtypes_list): old_arg, new_arg, arg_def_dict ) + def __item_pointer_strip__(self, arg_in): # Handle pointer object diff --git a/src/zugbruecke/core/data/definition.py b/src/zugbruecke/core/data/definition.py index e43e99fb..3cacc1bd 100644 --- a/src/zugbruecke/core/data/definition.py +++ b/src/zugbruecke/core/data/definition.py @@ -55,60 +55,18 @@ class definition_class(): - def apply_memsync_to_argtypes_definition(self, memsync, argtypes_d): - - # Start empty handle list - memsync_handle = [] + def apply_memsync_to_argtypes_definition(self, memsync_d, argtypes_d): # Iterate over memory segments, which must be kept in sync - for segment in memsync: - - # Reference processed argument types - start with depth 0 - 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_'][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_'][field_index] + for memsync_item in memsync_d: + + # Get type of pointer argument + arg_type = self.__get_argument_type_by_memsync_path__(memsync_item['p'], argtypes_d) # HACK make memory sync pointers type agnostic arg_type['g'] = GROUP_VOID arg_type['t'] = None # no type string - # Add to list - memsync_handle.append({ - 'p': arg_type, # Handle on pointer argument definition - 'l': len_type # Handle on length argument definition - }) - - return memsync_handle - def generate_callback_decorator(self, flags, restype, *argtypes): @@ -222,6 +180,28 @@ def __generate_struct_from_definition__(self, struct_d_dict): ) + def __get_argument_type_by_memsync_path__(self, memsync_path, argtypes_d): + + # Reference processed argument types - start with depth 0 + arg_type = argtypes_d[memsync_path[0]] + # Step through path to argument type ... + for path_element in memsync_path[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_'][field_index] + + return arg_type + + def __pack_definition_dict__(self, datatype, field_name = None): # Not all datatypes have a name, let's handle that diff --git a/src/zugbruecke/core/data/memory.py b/src/zugbruecke/core/data/memory.py index b64f45f0..a9c47b0b 100644 --- a/src/zugbruecke/core/data/memory.py +++ b/src/zugbruecke/core/data/memory.py @@ -41,6 +41,8 @@ serialize_pointer_into_int_list ) +WCHAR_BYTES = ctypes.sizeof(ctypes.c_wchar) + # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ # CLASS: Memory content packing and unpacking @@ -49,17 +51,17 @@ class memory_class(): - def client_fix_memsync_ctypes(self, memsync): + def client_fix_memsync_ctypes(self, memsync_d): # Iterate over memory segments, which must be kept in sync - for segment in memsync: + for memsync_item in memsync_d: # Defaut type, if nothing is given, is unsigned byte - if '_t' not in segment.keys(): - segment['_t'] = ctypes.c_ubyte + if '_t' not in memsync_item.keys(): + memsync_item['_t'] = ctypes.c_ubyte - def client_pack_memory_list(self, args, memsync): + def client_pack_memory_list(self, args, memsync_d): # Start empty package for transfer mem_package_list = [] @@ -68,48 +70,16 @@ def client_pack_memory_list(self, args, memsync): memory_handle = [] # Iterate over memory segments, which must be kept in sync - for segment_index, segment in enumerate(memsync): - - # Reference args - search for pointer - pointer = args - # Step through path to pointer ... - for path_element in segment['p']: - # Go deeper ... - 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 ... - 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'): - length_value = length.value * ctypes.sizeof(segment['_t']) - else: - length_value = length * ctypes.sizeof(segment['_t']) + for memsync_item in memsync_d: - # Convert argument into ctypes datatype TODO more checks needed! - if '_c' in segment.keys(): - arg_value = ctypes.pointer(segment['_c'].from_param(pointer)) - else: - arg_value = pointer - - # Serialize the data ... - data = serialize_pointer_into_int_list(arg_value, length_value) + # Pack data for one pointer + item_data, item_pointer = self.__pack_memory_item__(args, memsync_item) # Append data to package - mem_package_list.append(data) + mem_package_list.append(item_data) # Append actual pointer to handler list - memory_handle.append(arg_value) + memory_handle.append(item_pointer) return mem_package_list, memory_handle @@ -121,45 +91,132 @@ def client_unpack_memory_list(self, mem_package_list, memory_handle): overwrite_pointer_with_int_list(pointer, mem_package_list[pointer_index]) - def server_pack_memory_list(self, memory_handle): + def server_pack_memory_list(self, memory_handle, memsync_d): # Generate new list for arrays of ints to be shipped back to the client mem_package_list = [] # Iterate through pointers and serialize them - for pointer in memory_handle: - mem_package_list.append(serialize_pointer_into_int_list(*pointer)) + for pointer, memsync_item in zip(memory_handle, memsync_d): + + memory_list = serialize_pointer_into_int_list(*pointer) + + if 'w' in memsync_item.keys(): + memory_list = self.__adjust_wchar_length__( + memory_list, WCHAR_BYTES, memsync_item['w'] + ) + + mem_package_list.append(memory_list) return mem_package_list - def server_unpack_memory_list(self, args, arg_memory_list, memsync): + def server_unpack_memory_list(self, args, arg_memory_list, memsync_d): # Generate temporary handle for faster packing memory_handle = [] # Iterate over memory segments, which must be kept in sync - for segment_index, segment in enumerate(memsync): - - # Reference args - search for pointer - pointer = args - # Step through path to pointer ... - for path_element in segment['p'][:-1]: - # Go deeper ... - if isinstance(path_element, int): - pointer = pointer[path_element] - else: - pointer = getattr(pointer.contents, path_element) - - if isinstance(segment['p'][-1], int): + for memsync_item_index, memsync_item in enumerate(memsync_d): + + # Search for pointer + pointer = self.__get_argument_by_memsync_path__(args, memsync_item['p'][:-1]) + + memory_list = arg_memory_list[memsync_item_index] + + if 'w' in memsync_item.keys(): + memory_list = self.__adjust_wchar_length__( + memory_list, memsync_item['w'], WCHAR_BYTES + ) + + if isinstance(memsync_item['p'][-1], int): # Handle deepest instance - pointer[segment['p'][-1]] = generate_pointer_from_int_list(arg_memory_list[segment_index]) + pointer[memsync_item['p'][-1]] = generate_pointer_from_int_list(memory_list) # Append to handle - memory_handle.append((pointer[segment['p'][-1]], len(arg_memory_list[segment_index]))) + memory_handle.append((pointer[memsync_item['p'][-1]], len(memory_list))) else: # Handle deepest instance - setattr(pointer.contents, segment['p'][-1], generate_pointer_from_int_list(arg_memory_list[segment_index])) + setattr(pointer.contents, memsync_item['p'][-1], generate_pointer_from_int_list(memory_list)) # Append to handle - memory_handle.append((getattr(pointer.contents, segment['p'][-1]), len(arg_memory_list[segment_index]))) + memory_handle.append((getattr(pointer.contents, memsync_item['p'][-1]), len(memory_list))) return memory_handle + + + def __adjust_wchar_length__(self, in_byte_list, old_len, new_len): + + def mix_lists(*in_lists): + def mix_lists_generator(): + for item_tuple in zip(*in_lists): + for item in item_tuple: + yield item + return list(mix_lists_generator()) + + if old_len == new_len: + return in_byte_list + + elif new_len > old_len: + tmp = [in_byte_list[byte_nr::old_len] for byte_nr in range(0, old_len)] + for _ in range(0, new_len - old_len): + tmp.append([0 for __ in range(0, len(tmp[0]))]) + return mix_lists(*tmp) + + else: + return mix_lists(*[in_byte_list[byte_nr::old_len] for byte_nr in range(0, new_len)]) + + + def __get_argument_by_memsync_path__(self, args, memsync_path): + + # Reference args as initial value + element = args + + # Step through path + for path_element in memsync_path: + + # Go deeper ... # TODO use __item_pointer_strip__ ? + if isinstance(path_element, int): + element = element[path_element] + else: + element = getattr(self.__item_pointer_strip__(element), path_element) + + return element + + + def __pack_memory_item__(self, args, memsync_d): + + # Search for pointer + pointer = self.__get_argument_by_memsync_path__(args, memsync_d['p']) + + # Is there a function defining the length? + if '_f' in memsync_d.keys() and isinstance(memsync_d['l'], tuple): + + # Start list for length function arguments + length_func_arg_list = [] + + # Iterate over length components + for length_component in memsync_d['l']: + + # Append length argument to list + length_func_arg_list.append(self.__get_argument_by_memsync_path__(args, length_component)) + + # Compute length + length = memsync_d['_f'](*length_func_arg_list) + + else: + + # Search for length + length = self.__get_argument_by_memsync_path__(args, memsync_d['l']) + + # Compute actual length - might come from ctypes or a Python datatype + length_value = getattr(length, 'value', length) * ctypes.sizeof(memsync_d['_t']) + + # Convert argument into ctypes datatype TODO more checks needed! + if '_c' in memsync_d.keys(): + arg_value = ctypes.pointer(memsync_d['_c'].from_param(pointer)) + else: + arg_value = pointer + + # Serialize the data ... + data = serialize_pointer_into_int_list(arg_value, length_value) + + return data, arg_value diff --git a/src/zugbruecke/core/memory.py b/src/zugbruecke/core/memory.py index ab25cc3f..37707466 100644 --- a/src/zugbruecke/core/memory.py +++ b/src/zugbruecke/core/memory.py @@ -44,11 +44,9 @@ def generate_pointer_from_int_list(int_array): def overwrite_pointer_with_int_list(ctypes_pointer, int_array): - (ctypes.c_ubyte * len(int_array)).from_address(ctypes.c_void_p.from_buffer(ctypes_pointer).value)[:] = int_array[:] + ctypes.cast(ctypes_pointer, ctypes.POINTER(ctypes.c_ubyte * len(int_array))).contents[:] = 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.cast(ctypes_pointer, ctypes.POINTER(ctypes.c_ubyte * size_bytes)) - ).value)[:] + return ctypes.cast(ctypes_pointer, ctypes.POINTER(ctypes.c_ubyte * size_bytes)).contents[:] diff --git a/src/zugbruecke/core/routine_client.py b/src/zugbruecke/core/routine_client.py index 69d7f6c3..2fe8df25 100644 --- a/src/zugbruecke/core/routine_client.py +++ b/src/zugbruecke/core/routine_client.py @@ -113,7 +113,7 @@ 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.data.client_pack_memory_list(args, self.memsync) + mem_package_list, memory_transport_handle = self.data.client_pack_memory_list(args, self.memsync_d) # Actually call routine in DLL! TODO Handle kw ... return_dict = self.__handle_call_on_server__( @@ -121,7 +121,7 @@ def __call__(self, *args): ) # Log status - self.log.out('[routine-client] ... received feedback from server, unpacking ...') + self.log.out('[routine-client] ... received feedback from server, unpacking & syncing arguments ...') # Unpack return dict (call may have failed partially only) self.data.arg_list_sync( @@ -130,12 +130,18 @@ def __call__(self, *args): self.argtypes_d ) + # Log status + self.log.out('[routine-client] ... overwriting memory ...') + # 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']: + # Log status + self.log.out('[routine-client] ... unpacking return value ...') + # Unpack return value of routine return_value = self.data.return_msg_unpack(return_dict['return_value'], self.restype_d) @@ -161,11 +167,12 @@ def __configure__(self): # Fix missing ctypes in memsync self.data.client_fix_memsync_ctypes(self.__memsync__) - # Reduce memsync for transfer - self.memsync_d = self.data.pack_definition_memsync(self.__memsync__) + # Store and reduce memsync for transfer + self.memsync_d = self.__memsync__ + memsync_d_packed = self.data.pack_definition_memsync(self.__memsync__) - # Generate handles on relevant argtype definitions for memsync, adjust definitions with void pointers - self.memsync_handle = self.data.apply_memsync_to_argtypes_definition(self.__memsync__, self.argtypes_d) + # Adjust definitions with void pointers + self.data.apply_memsync_to_argtypes_definition(self.__memsync__, self.argtypes_d) # Log status self.log.out(' memsync: \n%s' % pf(self.__memsync__)) @@ -176,7 +183,7 @@ def __configure__(self): # Pass argument and return value types as strings ... result = self.__configure_on_server__( - self.argtypes_d, self.restype_d, self.memsync_d + self.argtypes_d, self.restype_d, memsync_d_packed ) diff --git a/src/zugbruecke/core/routine_server.py b/src/zugbruecke/core/routine_server.py index f700e5d3..425438db 100644 --- a/src/zugbruecke/core/routine_server.py +++ b/src/zugbruecke/core/routine_server.py @@ -71,23 +71,50 @@ def __call__(self, arg_message_list, arg_memory_list): # Log status self.log.out('[routine-server] Trying call routine "%s" ...' % self.name) - # Unpack passed arguments, handle pointers and structs ... - args_list = self.data.arg_list_unpack(arg_message_list, self.argtypes_d) + try: + + # Unpack passed arguments, handle pointers and structs ... + args_list = self.data.arg_list_unpack(arg_message_list, self.argtypes_d) + + # Unpack pointer data + memory_handle = self.data.server_unpack_memory_list(args_list, arg_memory_list, self.memsync_d) + + # Default return value + return_value = None - # Unpack pointer data - memory_handle = self.data.server_unpack_memory_list(args_list, arg_memory_list, self.memsync_d) + except Exception as e: + + # Push traceback to log + self.log.err(traceback.format_exc()) - # Default return value - return_value = None + raise e - # This is risky try: # Call into dll return_value = self.handler(*tuple(args_list)) + except Exception as e: + + # Log status + self.log.out('[routine-server] ... failed!') + + # Push traceback to log + self.log.err(traceback.format_exc()) + + # Pack return package and return it + return { + 'args': arg_message_list, + 'return_value': return_value, + 'memory': arg_memory_list, + 'success': False, + 'exception': e + } + + try: + # Pack memory for return - arg_memory_list = self.data.server_pack_memory_list(memory_handle) + arg_memory_list = self.data.server_pack_memory_list(memory_handle, self.memsync_d) # Get new arg message list arg_message_list = self.data.arg_list_pack(args_list, self.argtypes_d) @@ -107,43 +134,42 @@ def __call__(self, arg_message_list, arg_memory_list): 'exception': None } - except Exception as e: - - # Log status - self.log.out('[routine-server] ... failed!') + except: # Push traceback to log self.log.err(traceback.format_exc()) - # Pack return package and return it - return { - 'args': arg_message_list, - 'return_value': return_value, - 'memory': arg_memory_list, - 'success': False, - 'exception': e - } + raise e def __configure__(self, argtypes_d, restype_d, memsync_d): - # Store memory sync instructions - self.memsync_d = memsync_d - # Store argtype definition dict self.argtypes_d = argtypes_d - # Parse and apply argtype definition dict to actual ctypes routine - _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.data.unpack_definition_returntype(restype_d) + # Store memory sync instructions + self.memsync_d = memsync_d + + try: + + # Parse and apply argtype definition dict to actual ctypes routine + _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 + + # Parse and apply restype definition dict to actual ctypes routine + self.handler.restype = self.data.unpack_definition_returntype(restype_d) + + except Exception as e: + + # Push traceback to log + self.log.err(traceback.format_exc()) + + raise e # Log status self.log.out(' memsync: \n%s' % pf(self.memsync_d)) diff --git a/tests/test_bubblesort_segments.py b/tests/test_bubblesort_segments.py new file mode 100644 index 00000000..bfa81986 --- /dev/null +++ b/tests/test_bubblesort_segments.py @@ -0,0 +1,91 @@ +# -*- coding: utf-8 -*- + +""" + +ZUGBRUECKE +Calling routines in Windows DLLs from Python scripts running on unixlike systems +https://github.com/pleiszenburg/zugbruecke + + tests/test_bubblesort_segments.py: Memory sync with lambda function + + 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 sample_class: + + + def __init__(self): + + self.__dll__ = ctypes.windll.LoadLibrary('tests/demo_dll.dll') + + self.__bubblesort_segments__ = self.__dll__.bubblesort_segments + self.__bubblesort_segments__.memsync = [ # Regular ctypes on Windows should ignore this statement + { + 'p': [0], # "path" to argument containing the pointer + 'l': ([1], [2]), # "path" to arguments containing information on length + '_f': lambda x, y: x * y, # function computing length from relevant arguments + '_t': ctypes.c_float # type of argument (optional, default char/byte): sizeof(type) * length == bytes + } + ] + self.__bubblesort_segments__.argtypes = (ctypes.POINTER(ctypes.c_float), ctypes.c_int, ctypes.c_int) + + + def bubblesort_segments(self, values, number_of_segments, elements_per_segment): + + 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) + ) + self.__bubblesort_segments__(ctypes_float_pointer_firstelement, number_of_segments, elements_per_segment) + values[:] = ctypes_float_values[:] + + +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +# TEST(s) +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +def test_bubblesort(): + + 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] # 5, 2 + segments = 2 + sample.bubblesort_segments(test_vector, segments, len(test_vector) // 2) + 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_optional.py b/tests/test_callback_optional.py new file mode 100644 index 00000000..b1f492c6 --- /dev/null +++ b/tests/test_callback_optional.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_callback_optional.py: Optional callback routines as arguments + + 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 sample_class_a: + + + def __init__(self): + + self.__dll__ = ctypes.windll.LoadLibrary('tests/demo_dll.dll') + + conveyor_belt = ctypes.WINFUNCTYPE(ctypes.c_int16, ctypes.c_int16) + + self.__use_optional_callback__ = self.__dll__.use_optional_callback_a + self.__use_optional_callback__.argtypes = (ctypes.c_int16, conveyor_belt) + self.__use_optional_callback__.restype = ctypes.c_int16 + + @conveyor_belt + def process_data(in_data): + return in_data ** 2 + + self.__process_data__ = process_data + + + def use_optional_callback(self, some_data): + + return self.__use_optional_callback__(some_data, self.__process_data__) + + +class sample_class_b: + + + def __init__(self): + + self.__dll__ = ctypes.windll.LoadLibrary('tests/demo_dll.dll') + + self.__use_optional_callback__ = self.__dll__.use_optional_callback_b + self.__use_optional_callback__.argtypes = (ctypes.c_int16, ctypes.c_void_p) + self.__use_optional_callback__.restype = ctypes.c_int16 + + + def do_not_use_optional_callback(self, some_data): + + return self.__use_optional_callback__(some_data, None) + + +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +# TEST(s) +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +def test_use_optional_callback(): + + sample = sample_class_a() + + assert 18 == sample.use_optional_callback(3) + + +def test_do_not_use_optional_callback(): + + sample = sample_class_b() + + assert 14 == sample.do_not_use_optional_callback(7) diff --git a/tests/test_null_terminated_string.py b/tests/test_null_terminated_string.py new file mode 100644 index 00000000..d11002e4 --- /dev/null +++ b/tests/test_null_terminated_string.py @@ -0,0 +1,310 @@ +# -*- coding: utf-8 -*- + +""" + +ZUGBRUECKE +Calling routines in Windows DLLs from Python scripts running on unixlike systems +https://github.com/pleiszenburg/zugbruecke + + tests/test_null_terminated_string.py: Demonstrates null terminated strings + + 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 sample_class_a: + + + def __init__(self): + + self.__dll__ = ctypes.windll.LoadLibrary('tests/demo_dll.dll') + + self.__replace_letter_in_null_terminated_string__ = self.__dll__.replace_letter_in_null_terminated_string_a + self.__replace_letter_in_null_terminated_string__.argtypes = ( + ctypes.POINTER(ctypes.c_char), # Generate pointer to char manually + ctypes.c_char, + ctypes.c_char + ) + self.__replace_letter_in_null_terminated_string__.memsync = [ + { + 'p': [0], + 'l': ([0],), + '_f': lambda x: ctypes.sizeof(x) + } + ] + + + def replace_letter_in_null_terminated_string(self, in_string, old_letter, new_letter): + + BUFFER_LENGTH = 128 + + string_buffer = ctypes.create_string_buffer(BUFFER_LENGTH) + string_buffer.value = in_string.encode('utf-8') + + self.__replace_letter_in_null_terminated_string__( + string_buffer, + old_letter.encode('utf-8'), + new_letter.encode('utf-8') + ) + + return string_buffer.value.decode('utf-8') + + + def replace_letter_in_null_terminated_string_buff(self, in_string, old_letter, new_letter): + + string_buffer = ctypes.create_string_buffer(in_string.encode('utf-8')) + + self.__replace_letter_in_null_terminated_string__( + string_buffer, + old_letter.encode('utf-8'), + new_letter.encode('utf-8') + ) + + return string_buffer.value.decode('utf-8') + + +class sample_class_b: + + + def __init__(self): + + self.__dll__ = ctypes.windll.LoadLibrary('tests/demo_dll.dll') + + self.__replace_letter_in_null_terminated_string__ = self.__dll__.replace_letter_in_null_terminated_string_b + self.__replace_letter_in_null_terminated_string__.argtypes = ( + ctypes.c_char_p, # Use built-in char pointer type + ctypes.c_char, + ctypes.c_char + ) + self.__replace_letter_in_null_terminated_string__.memsync = [ + { + 'p': [0], + 'l': ([0],), + '_f': lambda x: ctypes.sizeof(x) + } + ] + + + def replace_letter_in_null_terminated_string(self, in_string, old_letter, new_letter): + + BUFFER_LENGTH = 128 + + string_buffer = ctypes.create_string_buffer(BUFFER_LENGTH) + string_buffer.value = in_string.encode('utf-8') + + self.__replace_letter_in_null_terminated_string__( + string_buffer, + old_letter.encode('utf-8'), + new_letter.encode('utf-8') + ) + + return string_buffer.value.decode('utf-8') + + + def replace_letter_in_null_terminated_string_buff(self, in_string, old_letter, new_letter): + + string_buffer = ctypes.create_string_buffer(in_string.encode('utf-8')) + + self.__replace_letter_in_null_terminated_string__( + string_buffer, + old_letter.encode('utf-8'), + new_letter.encode('utf-8') + ) + + return string_buffer.value.decode('utf-8') + + +class sample_class_unicode_a: + + + def __init__(self): + + self.__dll__ = ctypes.windll.LoadLibrary('tests/demo_dll.dll') + + self.__replace_letter_in_null_terminated_string_unicode__ = self.__dll__.replace_letter_in_null_terminated_string_unicode_a + self.__replace_letter_in_null_terminated_string_unicode__.argtypes = ( + ctypes.POINTER(ctypes.c_wchar), # Generate pointer to wchar manually + ctypes.c_wchar, + ctypes.c_wchar + ) + self.__replace_letter_in_null_terminated_string_unicode__.memsync = [ + { + 'p': [0], + 'l': ([0],), + 'w': ctypes.sizeof(ctypes.c_wchar), + '_f': lambda x: ctypes.sizeof(x) + } + ] + + + def replace_letter_in_null_terminated_string_unicode(self, in_string, old_letter, new_letter): + + BUFFER_LENGTH = 128 + + string_buffer = ctypes.create_unicode_buffer(BUFFER_LENGTH) + string_buffer.value = in_string + + self.__replace_letter_in_null_terminated_string_unicode__( + string_buffer, + old_letter, + new_letter + ) + + return string_buffer.value + + + def replace_letter_in_null_terminated_string_buff_unicode(self, in_string, old_letter, new_letter): + + string_buffer = ctypes.create_unicode_buffer(in_string) + + self.__replace_letter_in_null_terminated_string_unicode__( + string_buffer, + old_letter, + new_letter + ) + + return string_buffer.value + + +class sample_class_unicode_b: + + + def __init__(self): + + self.__dll__ = ctypes.windll.LoadLibrary('tests/demo_dll.dll') + + self.__replace_letter_in_null_terminated_string_unicode__ = self.__dll__.replace_letter_in_null_terminated_string_unicode_b + self.__replace_letter_in_null_terminated_string_unicode__.argtypes = ( + ctypes.c_wchar_p, # Use built-in wchar pointer type + ctypes.c_wchar, + ctypes.c_wchar + ) + self.__replace_letter_in_null_terminated_string_unicode__.memsync = [ + { + 'p': [0], + 'l': ([0],), + 'w': ctypes.sizeof(ctypes.c_wchar), + '_f': lambda x: ctypes.sizeof(x) + } + ] + + + def replace_letter_in_null_terminated_string_unicode(self, in_string, old_letter, new_letter): + + BUFFER_LENGTH = 128 + + string_buffer = ctypes.create_unicode_buffer(BUFFER_LENGTH) + string_buffer.value = in_string + + self.__replace_letter_in_null_terminated_string_unicode__( + string_buffer, + old_letter, + new_letter + ) + + return string_buffer.value + + + def replace_letter_in_null_terminated_string_buff_unicode(self, in_string, old_letter, new_letter): + + string_buffer = ctypes.create_unicode_buffer(in_string) + + self.__replace_letter_in_null_terminated_string_unicode__( + string_buffer, + old_letter, + new_letter + ) + + return string_buffer.value + + +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +# TEST(s) +# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +def test_replace_letter_in_null_terminated_string_a(): + + sample = sample_class_a() + + assert 'zetegehube' == sample.replace_letter_in_null_terminated_string('zategahuba', 'a', 'e') + + +def test_replace_letter_in_null_terminated_string_buff_a(): + + sample = sample_class_a() + + assert 'zetegehube' == sample.replace_letter_in_null_terminated_string_buff('zategahuba', 'a', 'e') + + +def test_replace_letter_in_null_terminated_string_b(): + + sample = sample_class_b() + + assert 'zetegehube' == sample.replace_letter_in_null_terminated_string('zategahuba', 'a', 'e') + + +def test_replace_letter_in_null_terminated_string_buff_b(): + + sample = sample_class_b() + + assert 'zetegehube' == sample.replace_letter_in_null_terminated_string_buff('zategahuba', 'a', 'e') + + +def test_replace_letter_in_null_terminated_string_unicode_a(): + + sample = sample_class_unicode_a() + + assert 'zetegehube' == sample.replace_letter_in_null_terminated_string_unicode('zategahuba', 'a', 'e') + + +def test_replace_letter_in_null_terminated_string_buff_unicode_a(): + + sample = sample_class_unicode_a() + + assert 'zetegehube' == sample.replace_letter_in_null_terminated_string_buff_unicode('zategahuba', 'a', 'e') + + +def test_replace_letter_in_null_terminated_string_unicode_b(): + + sample = sample_class_unicode_b() + + assert 'zetegehube' == sample.replace_letter_in_null_terminated_string_unicode('zategahuba', 'a', 'e') + + +def test_replace_letter_in_null_terminated_string_buff_unicode_b(): + + sample = sample_class_unicode_b() + + assert 'zetegehube' == sample.replace_letter_in_null_terminated_string_buff_unicode('zategahuba', 'a', 'e')