Skip to content

CFFI Wrapper

Víctor Diví edited this page May 4, 2023 · 5 revisions

The CFFI Wrapper Layer is the middle wrapper of PyMEOS, and it is generated by the build_pymeos_functions.py on top of the CFFI Layer.
It is located inside the pymeos_cffi package, and the files used to build it are the following:

  • builder/meos.h
    C header file that is going to be wrapped by CFFI. It is a slightly modified version of the meos.h file provided by the MEOS Library, and contains some extra type/functions definitions needed in PyMEOS. More information about this file can be found in the CFFI Layer Wiki Page
  • builder/build_pymeos_functions.py
    Parses the custom header file in search of functions to wrap and creates the python script functions.py containing all the wrappers that will be exported by the library.
  • builder/build_pymeos_functions_modifiers.py
    Contains some functions used by build_pymeos_functions.py to edit functions that need extra customization that cannot (or should not) be generalized.
  • builder/objects.py
    Contains information about some C types and their relationship with the corresponding Python types.

Generation

To check if the wrapping works properly before packaging for distribution, you can generate it by running build_pymeos_functions.py from the pymeos_cffi directory:

foo@bar:~/PyMEOS/pymeos_cffi$ python3 ./pymeos_cffi/builder/build_pymeos_functions.py

For easier copying:

python3 ./pymeos_cffi/builder/build_pymeos_functions.py

If it was successful, the file pymeos_cffi/functions.py should have a wrapper for every function exported in the header file. You can check the difference with the previous version to see which wrappers have been modified (or added/removed) to easily check if the changes that should have happened have happened.

Customizing function wrappers

There are some cases in which a function exported by MEOS is not so easily wrapped. These cases include, but may not be limited to:

  • Function with output parameters that in Python should be transformed to having multiple return values
  • Function with optional (nullable) parameters that will cause a crash when being cast to their supposed C type
  • Function with pointer parameters that are actually arrays and have to be cast differently
  • Function with an output parameter and boolean/int/void return type indicating (when bool/int) whether the operation was successful that should be transformed to return that output parameter or raise an exception on error

For these cases, some methods to ease wrapping are provided that will require developer intervention (the first time they occur):

Nullable Parameters

Nullable parameters need to be checked before casting to their C type, since the cast will crash if they are None:

# Will crash if duration is None
duration_converted = _ffi.cast('const Interval *', duration)
# Fix: Assign NULL if duration is None
duration_converted = _ffi.cast('const Interval *', duration) if duration is not None else _ffi.NULL

To register a function parameter as nullable, add an entry in the nullable_parameters dictionary in build_pymeos_functions.py with the format (function name, parameter name).

Output Parameters

Output parameters need to be removed from the wrapper function parameter list and instead moved to the return type:

# Normal wrapper generated
def periodset_timestamps(ps: 'const PeriodSet *', count: int) -> 'TimestampTz *':
    ps_converted = _ffi.cast('const PeriodSet *', ps)
    count = _ffi.cast('int *', count)
    result = _lib.periodset_timestamps(ps_converted, count)
    return result if result != _ffi.NULL else None
# Wrapper with output parameter
def periodset_timestamps(ps: 'const PeriodSet *') -> "Tuple['TimestampTz *', 'int']":
    ps_converted = _ffi.cast('const PeriodSet *', ps)
    count = _ffi.new('int *')
    result = _lib.periodset_timestamps(ps_converted, count)
    return result if result != _ffi.NULL else None, count[0]

To register a function parameter as output, add an entry in the output_parameters dictionary in build_pymeos_functions.py with the format (function name, parameter name).

⚠️ All parameters that end with _out or that are pointer parameters named count are considered output parameters.
If at some point this behaviour is no longer desired, the following should be done:

  • Modify the is_output_parameter function in build_pymeos_functions.py (leaving only the return statement that checks the dictionary).
  • Add all output parameters that satisfied these conditions in the output_parameters dictionary.
  • Remove this message

⚠️ When having input/output parameters, you'll have to add a custom modification (see below)

Result Parameters

Result parameters are similar to output parameters, but they differ in that they replace the actual returned object of the functions, instead of just being added to it:

# Normal wrapper generated
def tfloat_value_at_timestamp(temp: 'const Temporal *', t: int, strict: bool, result: float) -> 'bool':
    temp_converted = _ffi.cast('const Temporal *', temp)
    t_converted = _ffi.cast('TimestampTz', t)
    result_converted = _ffi.cast('double *', result)
    result = _lib.tfloat_value_at_timestamp(temp_converted, t_converted, strict, result_converted)
    return result if result != _ffi.NULL else None

# Wrapper with result parameter
def tfloat_value_at_timestamp(temp: 'const Temporal *', t: int, strict: bool) -> 'double':
    temp_converted = _ffi.cast('const Temporal *', temp)
    t_converted = _ffi.cast('TimestampTz', t)
    out_result = _ffi.new('double *')
    result = _lib.tfloat_value_at_timestamp(temp_converted, t_converted, strict, out_result)
    if result:
        return out_result[0] if out_result[0] != _ffi.NULL else None
    raise Exception(f'C call went wrong: {result}')

To register a function parameter as result, add an entry in the result_parameters dictionary in build_pymeos_functions.py with the format (function name, parameter name).

⚠️ All parameters named result are considered result parameters.
If at some point this behaviour is no longer desired, the following should be done:

  • Modify the is_result_parameter function in build_pymeos_functions.py (leaving only the return statement that checks the dictionary).
  • Add all result parameters that satisfied these conditions in the result_parameters dictionary.
  • Remove this message

Custom modifications

There are some cases when the previous modifications are not what is needed and further customization is needed to properly wrap a function. For example, a parameter that is supposed to be an array but it's declared as a pointer (parameters with double pointer types ** are cast as arrays properly) and input/output parameters.
For these cases, you can define a custom function to perform the necessary changes and add an entry in the function_modifiers dictionary with the format (function name, modifier function).
For sake of organization and clarity, these modifier functions should be declared in the file build_pymeos_functions_modifiers.py, and should be named after the function they modify appending _modifier at the end (e.g. the modifier of the function timestampset_make should be named timestampset_make_modifier).
When called, the function will be passed a string containing the wrapper function generated, and it must return a string with the modified wrapper. The returned string will be written instead of the generated wrapper in the functions.py file.

If necessary, TODO notes can be added to a wrapper function by adding an entry in the function_notes dictionary with the format (function name, message) (the message will be prepended with TODO: ).