Extending and Developing the Medial C++ API for Python
This document outlines the low-level C++ details required to further develop the Python bindings for our API.
Objectives
The goals are to:
- Use our API from Python.
- Employ Python as a "Glue Language".
- Enable prototyping, exploration, and discovery in Python.
- Ensure interoperability with other frameworks.
- Minimize maintenance overhead when updating code.
Implementation Approach
For maintainability, SWIG is chosen as the binding solution. Other options considered:
- Cython: Adds another language to the process.
- Boost::python: Limited support/resources; requires special definitions for each exported member.
- ctypes: Direct C calls, but parameter definitions are error-prone and may lack binary compatibility across platforms.
Why SWIG?
- Mature project with extensive resources. TensorFlow uses SWIG for example.
- Handles memory and ownership complexities.
- Good NumPy support.
Our Implementation: "MedPyExport"
- Located in
$MR_ROOT/Libs/Internal/MedPyExport
. - Wraps classes and exports only necessary functions for Python.
- Minimal learning curve for contributors familiar with C++.
- SWIG interface files (
.i
) are auto-generated during build.
Notes & Pitfalls
- Always use
std::vector
for vectors. - Restart Jupyter Notebook after adding new functions/classes to reload wrappers.
- Use distinct names for NumPy parameters of different types.
- Method overloading is not supported (may be possible to implement).
- Avoid "big-data" loops; use NumPy/Pandas for vectorized operations.
- Build system is separate from the main CMake files.
- No direct Pandas DataFrame conversion yet.
- SWIG scans only simple C++ headers; keep implementation in
.cpp
files. - Compilation works on both Windows and Linux (tested execution on Linux/Jupyter).
Build System
- Targeted for Linux machines
- Windows compilation works but does not produce a Python-loadable binary.
- Uses a specialized CMake file for SWIG integration.
Directory Structure
Located in $MR_ROOT/Libs/Internal/MedPyExport
:
MedPyExport
: Source files.generate_binding
: CMake and binding generation files.MedPython
: SWIG interface files.scripts
: Helper scripts for compilation.
Key Files
make.sh
: Bash script to start CMake build.MedPython/MedPython.i
: Main SWIG interface.MedPython/medial-numpy.i
: NumPy SWIG interface.MedPython/MedPython.h
: Entry point for headers scanned by SWIG.MedPython/MedPython.c
: Entry point for C files scanned by SWIG.MedPython/pythoncode.i
: Python code for the binding.MedPython/scripts/make_apply.py
: Script to generate NumPy apply directives.MedPython/apply_directives.i
: Auto-generated SWIG directives.
Extending the Code
Example: Exporting a Class
- Include
MedPyCommon.h
for utilities/macros. - Exported class names start with
MP
to avoid conflicts. - Use a pointer to the wrapped object.
- Keep headers simple for SWIG; implementation in
.cpp
files.
Add all exported headers to MedPyExport.h
:
Python Usage:
Adding a New Class
- Basic types are mapped automatically.
- Use
std::vector
for vectors.
Implementation Example:
Properties
Implement getter/setter methods with MEDPY_GET_
and MEDPY_SET_
prefixes:
Python Usage:
- Omit the setter for read-only properties.
Static Const Variables
Mapped to class variables in Python:
Implementation:
Python Usage:
Iterators
Array Iterator Example:
- Class name ends with
VectorAdaptor
. - Implements
__len__
and__getitem__
.
Map Iterator Example:
- Class name ends with
MapAdaptor
. - Implements
__len__
,__getitem__
, and optionally__setitem__
, etc.
NumPy Arrays
Input Arrays:
In-place Arrays:
Output Arrays:
Variant Output Example:
Supported NumPy Types:
Helper Functions & Macros
- Docstrings:
MEDPY_DOC(function_or_class_name, docstring)
- Ignore for SWIG:
MEDPY_IGNORE(...)
or#ifdef SWIG ... #endif
- Buffer to Vector:
buf_to_vector(int_in_buf, int_in_buf_len, idx);
- Vector to Buffer:
vector_to_buf(o->weights, float_out_buf, float_out_buf_len);