P_Viper Internal¶
Introduction¶
P_Viper is the Python C/API binding of Viper is like a safe strong-typed layer over python required for C++ interoperability. P_Viper raises an exception immediately when you use the wrong type, but P_Viper is smart enough to use native python value and collection as input since the metadata drives everything in Viper.
Filename Convention¶
Wrapped Classes¶
Wrapped class for Viper Classes.
- Viper::TypeSet --> "P_Viper_TypeSet.hpp"
- Viper::ValueSet --> "P_Viper_ValueSet.hpp"
- Viper::Definitions --> "P_Viper_Definitions.hpp"
- Viper::DSMDefinitions --> "P_Viper_DSMDefinitions.hpp"
- ...
Wrapper Features¶
Classes involved in the implementation of Python wrapper.
- P_ViperSRef.hpp
- P_ViperCaptureIO.hpp
- P_ViperWrapper.hpp
- P_ViperEncoder.hpp
- P_ViperEncoderDeep.hpp
- ...
The Wrapped Pattern¶
Since Python is in C and Viper is in C++, we need to:
- Use a pointer to hide the C++ implementation of Viper in a custom Python Object.
- Catch all c++ exceptions and return a value adapted for convention defined by Python for reporting errors.
- Handle the Python reference counter with
Py_XDECREFin C++ block for Python new reference convention.
The Pattern for embedding a std::shared_ptr<T>¶
We used a pointer std::shared_ptr<T> * v to implement the shared ownership
policy between the Viper runtime and the Python runtime.
#include "Viper_TypeVector.hpp"
typedef struct {
PyObject_HEAD
std::shared_ptr<Viper::TypeVector> * v; // embed a C++ std::shared_ptr<T> in a C struct;
} P_Viper_TypeVector;
...
// create a new C Python Object that embed a std::shared_ptr of a Viper Object.
PyObject * P_Viper_TypeVector_New(std::shared_ptr<Viper::TypeVector> const & v);
...
#endif
The Pattern for New¶
PyObject * P_Viper_TypeVector_New(std::shared_ptr<TypeVector> const & v) {
auto const pn_o {_PyObject_New(reinterpret_cast<PyTypeObject *>(P_Viper_TypeVector_Type))};
auto const self {reinterpret_cast<P_Viper_TypeVector *>(pn_o)};
self->v = new std::shared_ptr(v); // new => increment the reference counter of the C++ Object.
return pn_o;
}
The Pattern for Free¶
static void tp_dealloc(P_Viper_Blob * self) {
delete self->v; // delete => decrement the reference counter of the C++ Object
auto const type {Py_TYPE(self)};
type->tp_free(self);
Py_DECREF(type);
}
The Pattern for Catching C++ Exception¶
To protect the Python runtime from the C++ exception, we use the macro P_TRY
to create a safe context, and the macro P_CATCH_P to catch the exception for a
C/Python API that require a null pointer to indicate an error or the macro P_CATCH_I
to catch the exception for a C/Python API that returns an error with an integer;
ex: return a PyObject * (nullptr => error)
static PyObject * tp_repr(P_Viper_TypeTuple * self) {
P_TRY {
auto const v {*self->v};
return P_ViperWrapper::wrapString(v->representation());
} P_CATCH_P
}
ex: return a integer (-1 => error)
static int tp_set_vector_size(P_Viper_Fuzzer * self, PyObject * pb_value, void *) {
P_TRY {
auto const v {*self->v};
v->vectorSize = P_ViperWrapper::checkUInt64(pb_value);
return 0;
} P_CATCH_I
}
The Pattern for Python 'new reference' Convention¶
To automatically handle new reference returned from the C/Python API we use
the wrapper P_ViperSRef.
The Python documentation for PySequence_GetItem.
PyObject *PySequence_GetItem(PyObject *o, Py_ssize_t i)
Return value: New reference. Part of the Stable ABI.
We use a P_ViperSRef value to handle the returned reference of
PySequence_GetItem in a C++ block.
// Extract types
std::vector<std::shared_ptr<Type>> types;
auto const size {PySequence_Length(pb_types)};
for (Py_ssize_t index {}; index < size; ++index) {
P_ViperSRef ps_item {PySequence_GetItem(pb_types, index)}; // use of P_ViperSRef in a C++ block.
auto const itemType {P_ViperWrapper::unwrapTypeOrPyErr(ps_item.get())};
if (!itemType)
return nullptr;
types.push_back(itemType);
}
The Pattern for Methods¶
Method Naming Convention tp_m_set.¶
tpfor type.mfor method.
Variable Naming Convention¶
Variables used to interop with the C/Python API are named
p<reference_type>_<name>.
pb_<name>for a borrowed reference.pn_<name>for a new reference.ps_<name>for a new Reference managed by a C++ scope throughP_ViperSRef(loop and exception).
The variables used to interop with C++/Viper API use lowerPascalCase convention.
Anatomy of a Wrapped Method¶
1) Collect the arguments with PyArg_ParseTupleAndKeywords with "O!" for
registered wrapped type and "O" for seamless value.
2) Unwrap a Viper object from the Python object.
3) Catch C++ exception.
4) Call P_ViperWrapper::checkValue to potentially convert a Python Object to
the corresponding Viper Object.
5) Unwrap C++ this from Python self.
6) Call the method.
7) return a wrapped value or Py_RETURN_NONE;
8) Or convert the C++ exception to the 'Return Error Convention' of Python.
static PyObject * tp_m_set(P_Viper_CommitMutating * self, PyObject * args, PyObject * kwargs) {
// Naming convention for C variables
PyObject * pb_attachment {}, * pb_key {}, * pb_value {};
// 1) Parse Python Arguments with the help of 'O!' and *_Type and 'O'.
static char const * const kws[] = {"attachment", "key", "value", nullptr};
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O!O!O", (char **) kws,
P_Viper_Attachment_Type, &pb_attachment,
P_Viper_Key_Type, &pb_key,
&pb_value))
return nullptr;
// 2) Unwrap viper object (naming convention for C++ variable).
auto const attachment {*reinterpret_cast<P_Viper_Attachment *>(pb_attachment)->v};
auto const key {*reinterpret_cast<P_Viper_Key *>(pb_key)->v};
P_TRY { // 3) Catch exception
// 4) Create a Viper Value from a python object by consuming the type of required value.
auto const value {P_ViperWrapper::checkValue(pb_value, attachment->documentType)};
// 5) Extract C++ this from C/Python self.
auto const v {*self->v};
// 6) call the method
v->set(attachment, key, value);
// 7) Return something
Py_RETURN_NONE;
} P_CATCH_P // 8) convert the C++ exception to 'Return Error Convention'.
}
The Seamless Wrapper¶
The seamless wrapper, convert Python Object to Viper::Value and
Viper::Value to Python Object.
The conversion from a Viper::Value to a Python object is only done when a
Python statement constructs a new Python object from others Python objects like
c = a + b.
The namespace P_Viper::Wrapper defines utility functions to handle the
conversion when it's necessary. The conversion from Python Object to Viper::Value
is driven by the Viper's type system.
Functions defined in the namespace P_ViperDecoder try to construct a new
Viper::Value by decoding the mapping from Python Object to Viper::Value.
- int -->
Viper::ValueUInt8,Viper::ValueUInt16... - real -->
Viper::ValueFloat,Viper::ValueDouble... - bytes -->
Viper::ValueBlob - dict -->
Viper::ValueMap - list -->
Viper::ValueVector - set -->
Viper::ValueSet - ...
The Function P_Wrapper::wrapValueOrEncode creates Python object bool,
int, float, str only for Viper::ValueBool, Viper::ValueUInt[8|16|32|64],
Viper::ValueInt[8|16|32|64], Viper::ValueFloat,
Viper::ValueDouble and Viper::ValueString other Viper::Values are just wrapped in
their corresponding Python object P_Viper_Value_... by embedding the std::shared_ptr.
Viper::ValueVector-->struct P_Viper_ValueVectorViper::ValueSet-->struct P_Viper_ValueSetViper::ValueMap-->struct P_Viper_ValueVectorViper::ValueBlob-->struct P_Viper_ValueBlob- ...