From 85c37dd624b9305a2a45e0ea19ead1bb829191a6 Mon Sep 17 00:00:00 2001 From: Alex Dewar Date: Sun, 18 Aug 2019 12:38:27 +0100 Subject: [PATCH] Add optional support for Python 3 By default, CMake will now use the system default version of Python. --- CMakeLists.txt | 10 ++--- include/wrappy/wrappy.h | 31 ++++++++++---- tests/stdlib.cpp | 6 ++- wrappy.cpp | 91 +++++++++++++++++++++++++++++------------ 4 files changed, 95 insertions(+), 43 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 9757695..be150d0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,12 +2,8 @@ cmake_minimum_required (VERSION 2.8) project (wrappy) # Dependencies -find_package(PythonLibs 2.7 REQUIRED) -# If i had one word to describe python versioning, -# it would be "broken as fuck" -if (NOT PYTHON_VERSION VERSION_LESS 3.0) - error("Requires python 2.x") -endif() +# If PYTHON_VERSION isn't set, use the default version on this system +find_package(PythonLibs ${PYTHON_VERSION} REQUIRED) option( WRAPPY_BUILD_DEMOS "Build the wrappy tests" ON) @@ -20,7 +16,7 @@ add_library(wrappy SHARED wrappy.cpp) set_target_properties(wrappy PROPERTIES VERSION 1.0.0) set_target_properties(wrappy PROPERTIES SOVERSION 1) -target_include_directories(wrappy PRIVATE ${PYTHON_INCLUDE_DIRS}) +include_directories(wrappy PUBLIC ${PYTHON_INCLUDE_DIRS}) target_include_directories(wrappy PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include/) if(${CMAKE_VERSION} VERSION_LESS 3.1) diff --git a/include/wrappy/wrappy.h b/include/wrappy/wrappy.h index 5436994..0469fa0 100644 --- a/include/wrappy/wrappy.h +++ b/include/wrappy/wrappy.h @@ -1,5 +1,10 @@ #pragma once +// Python header must be included first since they insist on +// unconditionally defining some system macros +// (http://bugs.python.org/issue1045893, still broken in python3.4) +#include + #include #include #include @@ -21,6 +26,16 @@ typedef _object PyObject; namespace wrappy { +/* + * Python 3 uses unicode strings, so we have to allocate memory and copy the + * byte array out. Using std::string seems the tidiest way of doing this. + */ +#if PY_MAJOR_VERSION >= 3 +typedef std::string string_t; +#else +typedef const char *string_t; +#endif + class WrappyError : public std::runtime_error { public: WrappyError(const std::string& str) @@ -38,7 +53,7 @@ class PythonObject { // not to do non-const things with it. long long num() const; double floating() const; - const char* str() const; + string_t str() const; PyObject* get() const; PythonObject attr(const std::string& x) const; // returns self.x @@ -71,14 +86,14 @@ class PythonObject { PyObject* obj_; }; -// Note that this is an input iterator, iterators cannot +// Note that this is an input iterator, iterators cannot // be stored, rewound, or compared to anything but "end" struct PythonIterator { PythonIterator& operator++(); // pre-increment PythonObject operator*(); // dereference - // *only* for comparison to "end", python iterators have + // *only* for comparison to "end", python iterators have // no concept of position or comparability - bool operator!=(const PythonIterator&); + bool operator!=(const PythonIterator&); private: PythonIterator(bool, PythonObject); @@ -102,12 +117,12 @@ void addModuleSearchPath(const std::string& path); // There is one quirk of call() for the case of member methods: // -// call("module.A.foo") +// call("module.A.foo") // // calls the unbound method "foo", so it is necessary to provide an instance // of A as the first argument, while // -// auto a = call("module.A"); call(a, "foo"); +// auto a = call("module.A"); call(a, "foo"); // // calls the method "foo" that is already bound to a, so providing an explicit // self argument in that case is an error. @@ -130,8 +145,8 @@ PythonObject construct(PythonObject); // identity PythonObject construct(const std::vector&); // python list // TODO there is no good way to actually call these constructed functions -typedef PythonObject (*Lambda)(const std::vector& args, const std::map& kwargs); -typedef PythonObject (*LambdaWithData)(const std::vector& args, const std::map& kwargs, void* userdata); +typedef PythonObject (*Lambda)(const std::vector& args, const std::map& kwargs); +typedef PythonObject (*LambdaWithData)(const std::vector& args, const std::map& kwargs, void* userdata); PythonObject construct(Lambda); PythonObject construct(LambdaWithData, void*); diff --git a/tests/stdlib.cpp b/tests/stdlib.cpp index f9a5689..2f5bfcb 100644 --- a/tests/stdlib.cpp +++ b/tests/stdlib.cpp @@ -32,8 +32,10 @@ BOOST_AUTO_TEST_CASE(builtins) args[0] = wrappy::construct(255ll); auto longval = wrappy::callWithArgs("hex", args); - BOOST_CHECK_EQUAL(intval.str(), "0xff"); - BOOST_CHECK_EQUAL(longval.str(), "0xffL"); + std::string intval_str = intval.str(); + std::string longval_str = longval.str(); + BOOST_TEST(intval.str() == "0xff"); + BOOST_TEST(longval.str() == "0xffL"); } BOOST_AUTO_TEST_CASE(error) diff --git a/wrappy.cpp b/wrappy.cpp index ab1e81a..cf904e5 100644 --- a/wrappy.cpp +++ b/wrappy.cpp @@ -1,8 +1,3 @@ -// Python header must be included first since they insist on -// unconditionally defining some system macros -// (http://bugs.python.org/issue1045893, still broken in python3.4) -#include - #include #include @@ -40,7 +35,11 @@ void wrappyInitialize() Py_Initialize(); // Setting a dummy value since many libraries require sys.argv[0] to exist - char* dummy_args[] = {const_cast("wrappy"), nullptr}; +#if PY_MAJOR_VERSION >= 3 + wchar_t *dummy_args[] = {const_cast(L"wrappy"), nullptr}; +#else + char *dummy_args[] = {const_cast("wrappy"), nullptr}; +#endif PySys_SetArgvEx(1, dummy_args, 0); wrappy::None = PythonObject(PythonObject::borrowed{}, Py_None); @@ -177,9 +176,18 @@ double PythonObject::floating() const return PyFloat_AsDouble(obj_); } -const char* PythonObject::str() const +string_t PythonObject::str() const { +#if PY_MAJOR_VERSION >= 3 + // In Python 3, strings are unicode, so we have to do the necessary conversion here + PythonObject str{owning{}, PyUnicode_AsEncodedString(get(), "UTF-8", "strict")}; + if (!str) { + throw WrappyError("Wrappy: Could not encode string"); + } + return PyBytes_AsString(str.get()); +#else return PyString_AsString(obj_); +#endif } PythonObject::operator bool() const @@ -200,7 +208,11 @@ PythonObject construct(long long ll) PythonObject construct(int i) { +#if PY_MAJOR_VERSION >= 3 + return PythonObject(PythonObject::owning {}, PyLong_FromLong(i)); +#else return PythonObject(PythonObject::owning {}, PyInt_FromLong(i)); +#endif } PythonObject construct(double d) @@ -210,7 +222,13 @@ PythonObject construct(double d) PythonObject construct(const std::string& str) { +#if PY_MAJOR_VERSION >= 3 + return PythonObject(PythonObject::owning {}, + PyUnicode_FromKindAndData(PyUnicode_1BYTE_KIND, + str.c_str(), str.size())); +#else return PythonObject(PythonObject::owning {}, PyString_FromString(str.c_str())); +#endif } PythonObject construct(const std::vector& v) @@ -234,12 +252,14 @@ void addModuleSearchPath(const std::string& path) std::string pathString("path"); auto syspath = PySys_GetObject(&pathString[0]); // Borrowed reference - PythonObject pypath(PythonObject::owning {}, - PyString_FromString(path.c_str())); + PythonObject pypath = construct(path); + // nullptr already checked for in Python 3 API +#if PY_MAJOR_VERSION < 3 if (!pypath) { throw WrappyError("Wrappy: Can't allocate memory for string."); } +#endif auto pos = PyList_Insert(syspath, 0, pypath.get()); if (pos < 0) { @@ -354,7 +374,7 @@ PythonObject callWithArgs( PythonObject function = loadObject(from, name); if (!function) { - throw WrappyError("Wrappy: " + throw WrappyError("Wrappy: " "Lookup of function " + functionName + " failed."); } @@ -435,30 +455,53 @@ std::vector to_vector(PyObject* pyargs) return args; } -std::map to_map(PyObject* pykwargs) +std::map to_map(PyObject* pykwargs) { if (!PyDict_Check(pykwargs)) { throw WrappyError("Trampoling kwargs was no dict"); } - std::map kwargs; + std::map kwargs; PyObject *key, *value; Py_ssize_t pos = 0; while (PyDict_Next(pykwargs, &pos, &key, &value)) { - const char* str = PyString_AsString(key); + PythonObject str(PythonObject::borrowed{}, key); PythonObject obj(PythonObject::borrowed{}, value); - kwargs.emplace(str, obj); + kwargs.emplace(str.str(), obj); } return kwargs; } +// In Python 3, CObjects were deprecated and removed in favour of PyCapsules +#if PY_MAJOR_VERSION >= 3 +#define check_cobject PyCapsule_CheckExact +#define get_cobject_desc PyCapsule_GetContext +#define cobject_as_ptr(capsule) PyCapsule_GetPointer(capsule, nullptr) +#define cobject_from_ptr(ptr) PyCapsule_New(ptr, nullptr, nullptr) + +PyObject *cobject_from_ptr_and_desc(void *ptr, void *desc) +{ + PyObject *obj = cobject_from_ptr(ptr); + if (!PyCapsule_SetContext(obj, desc)) { + throw WrappyError("Wrappy: Could not set context for object"); + } + return obj; +} +#else +#define check_cobject PyCObject_Check +#define get_cobject_desc PyCObject_GetDesc +#define cobject_as_ptr PyCObject_AsVoidPtr +#define cobject_from_ptr(ptr) PyCObject_FromVoidPtr(ptr, nullptr) +#define cobject_from_ptr_and_desc(ptr, desc) PyCObject_FromVoidPtrAndDesc(ptr, desc, nullptr) +#endif + PyObject* trampolineWithData(PyObject* data, PyObject* pyargs, PyObject* pykwargs) { - if (!PyCObject_Check(data)) { + if (!check_cobject(data)) { throw WrappyError("Trampoline data corrupted"); } - LambdaWithData fun = reinterpret_cast(PyCObject_AsVoidPtr(data)); - void* userdata = PyCObject_GetDesc(data); + LambdaWithData fun = reinterpret_cast(cobject_as_ptr(data)); + void* userdata = get_cobject_desc(data); auto args = to_vector(pyargs); auto kwargs = to_map(pykwargs); @@ -467,11 +510,11 @@ PyObject* trampolineWithData(PyObject* data, PyObject* pyargs, PyObject* pykwarg PyObject* trampolineNoData(PyObject* data, PyObject* pyargs, PyObject* pykwargs) { - if (!PyCObject_Check(data)) { + if (!check_cobject(data)) { throw WrappyError("Trampoline data corrupted"); } - Lambda fun = reinterpret_cast(PyCObject_AsVoidPtr(data)); + Lambda fun = reinterpret_cast(cobject_as_ptr(data)); auto args = to_vector(pyargs); auto kwargs = to_map(pykwargs); @@ -487,7 +530,7 @@ PyMethodDef trampolineWithDataMethod {"trampoline2", reinterpret_cast(lambda), nullptr); + PyObject* pydata = cobject_from_ptr(reinterpret_cast(lambda)); return PythonObject(PythonObject::owning{}, PyCFunction_New(&trampolineNoDataMethod, pydata)); } @@ -495,15 +538,11 @@ PythonObject construct(LambdaWithData lambda, void* userdata) { PyObject* pydata; if (!userdata) { - pydata = PyCObject_FromVoidPtr(reinterpret_cast(lambda), nullptr); + pydata = cobject_from_ptr(reinterpret_cast(lambda)); } else { // python returns an error if FromVoidPtrAndDesc is called with desc being null - pydata = PyCObject_FromVoidPtrAndDesc(reinterpret_cast(lambda), userdata, nullptr); + pydata = cobject_from_ptr_and_desc(reinterpret_cast(lambda), userdata); } return PythonObject(PythonObject::owning{}, PyCFunction_New(&trampolineWithDataMethod, pydata)); } - -typedef PythonObject (*Lambda)(const std::vector& args, const std::map& kwargs); -typedef PythonObject (*LambdaWithData)(const std::vector& args, const std::map& kwargs, void* userdata); - } // end namespace wrappy