8
\$\begingroup\$

I wrote a very simple Python interface for C++ that uses the Python 2.7 C API. At the moment it can:

  • Manage reference counts/deal with "stolen" references (PyObject)
  • Load Python module
  • Invoke a function (templated) and get result
  • Convert between intrinsic Python and C++ types

Notes

I am not an expert in template meta-programming. Also, I wrote this project in approx. 6h today, so please take this into account. PYObjectBase could probably be implemented using a std::unique_ptr and a custom deleter, but for learning purpose I decided not to. I didn't comment much.

It should compile using MSVC (VS2017).

Code

#ifdef _DEBUG
#undef _DEBUG
#include <Python.h>
#define _DEBUG
#else
#include <Python.h>
#endif

#include <iostream>
#include <string>

namespace Scripting
{
    namespace Python
    {
        template<bool AUTO_RELEASE>
        class PYObjectBase
        {
        public:
            friend class PYObjectBase<!AUTO_RELEASE>;

            PYObjectBase(PyObject* a_Object)
                : m_Raw(a_Object)
            {
            }

            PYObjectBase()
                : PYObjectBase(nullptr)
            {
            }

            //COPY CTOR (DELETED)
            template<bool RHS_AUTO_RELEASE>
            PYObjectBase(const PYObjectBase<RHS_AUTO_RELEASE>& a_RHS) = delete;

            //MOVE CTOR
            template<bool RHS_AUTO_RELEASE>
            PYObjectBase(PYObjectBase<RHS_AUTO_RELEASE>&& a_RHS)
            {
                m_Raw = a_RHS.m_Raw;
                a_RHS.m_Raw = nullptr;
            }

            //COPY ASSIGN (DELETED)
            template<bool RHS_AUTO_RELEASE>
            PYObjectBase<AUTO_RELEASE>& operator=(const PYObjectBase<RHS_AUTO_RELEASE>&) = delete;

            //MOVE ASSIGN
            template<bool RHS_AUTO_RELEASE>
            PYObjectBase<AUTO_RELEASE>& operator=(const PYObjectBase<RHS_AUTO_RELEASE>&& a_RHS)
            {
                m_Raw = a_RHS.m_Raw;
                a_RHS.m_Raw = nullptr;
                return *this;
            }

            PYObjectBase<AUTO_RELEASE>& operator=(PyObject* a_RHS)
            {
                m_Raw = a_RHS;
                return *this;
            }

            ~PYObjectBase()
            {
                if (AUTO_RELEASE && m_Raw != nullptr)
                    Py_DECREF(m_Raw);
            }

            operator PyObject*()
            {
                return m_Raw;
            }

        private:
            PyObject* m_Raw;
        };

        typedef PYObjectBase<true> PYObject;
        typedef PYObjectBase<false> PYWeakObject;

        template<typename...ARGS>
        class PYArgs
        {
        public:
            static constexpr size_t Size = sizeof...(ARGS);

            PYArgs(ARGS&&...args)
            {
                m_Tuple = PyTuple_New(Size);
                this->ConvertArgs(std::forward<ARGS>(args)...);
            }

            PYObject Expand()
            {
                return m_Tuple;
            }

        private:
            template<typename HEAD, typename...TAIL>
            void ConvertArgs(HEAD&& head, TAIL&&...tail)
            {
                this->SetArg<HEAD>(std::move(head), sizeof...(TAIL) - 1);
                this->ConvertArgs(std::forward<TAIL>(tail)...);
            }

            template<typename ARG>
            void ConvertArgs(ARG&& a_Arg)
            {
                this->SetArg<ARG>(std::move(a_Arg), Size - 1);
            }

            template<typename ARG>
            void SetArg(ARG&& a_Arg, const size_t a_Index)
            {
                std::cout << a_Index << "->" << a_Arg << std::endl;

                PYConverter<ARG> convert;
                PYWeakObject value = convert.from(a_Arg);

                PyTuple_SetItem(m_Tuple, a_Index, value);
            }

        private:
            PYObject m_Tuple;
        };

        template<typename...ARGS>
        PYArgs<ARGS...> PYMakeArgs(ARGS&&...a_Args)
        {
            return { std::forward<ARGS>(a_Args)... };
        }

        template<typename T>
        struct PYConverter;

        template<>
        struct PYConverter<int>
        {
            int to(PYObject obj)
            {
                return static_cast<int>(PyInt_AsLong(obj));
            }

            PYObject from(const int& v)
            {
                return PyInt_FromLong(static_cast<long>(v));
            }
        };

        template<>
        struct PYConverter<double>
        {
            double to(PYObject obj)
            {
                return PyFloat_AsDouble(obj);
            }

            PYObject from(const double& v)
            {
                return PyFloat_FromDouble(v);
            }
        };

        template<>
        struct PYConverter<std::string>
        {
            std::string to(PYObject obj)
            {
                return PyString_AsString(obj);
            }

            PYObject from(const std::string& v)
            {
                return PyString_FromString(v.c_str());
            }
        };

        class PYContext
        {
        public:
            PYContext(const std::string& a_Module = "__main__")
            {
                Py_Initialize();
                PYObject moduleStr = PyString_FromString(a_Module.c_str());
                m_Module = PyImport_Import(moduleStr);
            }

            ~PYContext()
            {
                Py_Finalize();
            }

            void Run(const std::string& a_Source)
            {
                PyRun_SimpleString(a_Source.c_str());
            }

            template<typename RETURN, typename...ARGS>
            RETURN Invoke(const std::string& a_Func, ARGS&&...a_Args)
            {
                PYObject func = PyObject_GetAttrString(m_Module, a_Func.c_str());
                if (!PyCallable_Check(func))
                    throw std::runtime_error("PyCallable_Check() failed!");

                auto args = PYMakeArgs(std::forward<ARGS>(a_Args)...);
                PYObject ret = PyObject_CallObject(func, args.Expand());

                PYConverter<RETURN> convert;
                return convert.to(ret);
            }

        private:
            PYObject m_Module;
        };
    }
}

Usage example

int main()
{
    try
    {
        Scripting::Python::PYContext context;
        context.Run
        (
            R"(def mul(a, b):
                return a * b)"
        );

        auto ret = context.Invoke<double>("mul", 3.0, 4.0);
        std::cout << "RETURN VALUE IS: " << ret << std::endl;
    }
    catch (const std::runtime_error& e)
    {
        std::cerr << e.what();
    }

    std::cin.ignore();
    return 0;
}
\$\endgroup\$
4
\$\begingroup\$

I don't know Python, so this is a purely C++ review.

1 PYObjectBase

1.1 What is a const rvalue?

Move operations ownership of some resource generally, so how can a move operation's parameter be const? It logically does not make sense.

Your function...

template<bool RHS_AUTO_RELEASE> PYObjectBase<AUTO_RELEASE>& operator=(
    const PYObjectBase<RHS_AUTO_RELEASE>&& a_RHS);
{
    m_Raw = a_RHS.m_Raw; // valid, reading from const is fine.
    a_RHS.m_Raw = nullptr; // how is this compiling? You cannot modify a const value.
    return *this;
}

As my comment states, you cannot modify a const value. Your compiler should be giving you an error. Simply remove the const modifier from a_RHS to make it valid.

2 PYArgs

2.1 Those are not universal references!

PYArgs has this constructor:

PYArgs(ARGS&&... args)
    : m_Tuple{ PyTuple_New(Size) } // same point from PYObjectBase; leaving the rest to you
{
    // m_Tuple = PyTuple_New(Size); delete this
    // will expand as rvalues; might as well call std::move() on the pack
    this->ConvertArgs(std::forward<ARGS>(args)...);
}

The variadic template ARGS&&... args is not a pack forwarding references, it is an rvalue reference pack. For it to be a forwarding reference variadic template, the variadic template must be a function template parameter:

template<class... Types>
PYArgs(Types&&... types)
{
    m_Tuple = PyTuple_New(Size);
    // will now properly forward since Types&&... is universal reference variadic template
    this->ConvertArgs(std::forward<Types>(types)...);
}

As a side effect, your function:

template<typename...ARGS> PYArgs<ARGS...>
PYMakeArgs(ARGS&&...a_Args);

Is now behaving properly.

3 General

3.1 Mark your functions as noexcept if they're non-throwing.

In PYObjectBase:

  • PYObjectBase(PyObject*);
  • PYObjectBase();
  • template<bool RHS_AUTO_RELEASE> PYObjectBase(PYObjectBase<RHS_AUTO_RELEASE>&&);
  • PYObjectBase<AUTO_RELEASE>& operator=(PyObject* a_RHS);
  • template<bool RHS_AUTO_RELEASE> PYObjectBase<AUTO_RELEASE>& operator=( const PYObjectBase<RHS_AUTO_RELEASE>&& a_RHS);
  • ~PYObjectBase();
  • operator PyObject*();

I will leave the rest of the types to you.

3.2 Mark non-modifying functions as const

Functions that don't modify the logical object should be marked const.

For example:

operator PyObject*()    ------------>    operator PyObject*() noexcept const
{                                        {
    return m_Raw;                            return m_Raw;
}                                        }

Copying a pointer (m_Raw) is a non-throwing, and non-modifying operation. Thus, the function is additionally marked noexcept as well as const. I leave the rest of the functions to you. The benefit is that your type is now actually convertible to PyObject* in const contexts where it would otherwise not even compile. Here is an example of what I mean:

struct PYObjectBase
{
    operator int*()
    {
        return m_Raw;
    }

    int* m_Raw;
};

void f(PYObjectBase const& x)
{
    int* ptr = x; // compilation error; operator int*() is not marked const
}

3.3 Initialize your members in the constructor member initialization list

You do it in some constructors, but not in others. Do it whenever possible. You can start with PYObjectBase's move constructor:

template<bool RHS_AUTO_RELEASE>
PYObjectBase(PYObjectBase<RHS_AUTO_RELEASE>&& a_RHS)
    : m_Raw{ a_RHS.m_Raw } // member is initialized here
{
    //m_Raw = a_RHS.m_Raw; remove this line.
    a_RHS.m_Raw = nullptr;
}

PYArgs's constructor can also be modified similarly.

\$\endgroup\$
  • \$\begingroup\$ Thanks for the helpful comments. Regarding 1) Obviously a copy-paste error. fixed it. The template was not instantiated in my code hence no compiler error. Should I use const in PyConverter<T>? \$\endgroup\$ – pr8x May 30 '17 at 7:26
  • \$\begingroup\$ @mutex36 You could mark them const. You could also just make them static non-member functions of thePyConverter<T> class unless you specifically need to create PyConverter<T> instances elsewhere. \$\endgroup\$ – user2296177 May 30 '17 at 7:53
  • \$\begingroup\$ Basically, I want PYConverter to work similar to std::hash, so probably won't make it static. \$\endgroup\$ – pr8x May 30 '17 at 8:50
3
\$\begingroup\$

You use the recursive technique to call SetArg().
I personally prefer the iterative approach. This uses an initializer list.

        void ConvertArgs(ARGS&&... args)
        {
            std::size_t index = 0;
            // Initializer list is constructed from left to right (guranteed)
            // We use the comma operator so that each expression returns a zero
            // The first item in the list is provided in case `ARGS` is empty
            // as a zero element initializer list is not allowed.
            int ignore[] ={0, (SetArg(std::forward<ARGS>(args), index++), 0)...};
        }
        template<typename ARG>
        void SetArg(ARG&& a_Arg, const size_t a_Index)
        {
            std::cout << a_Index << "->" << a_Arg << std::endl;

            PYConverter<ARG> convert;
            PYWeakObject value = convert.from(a_Arg);

            PyTuple_SetItem(m_Tuple, a_Index, value);
        }
\$\endgroup\$
  • \$\begingroup\$ Is there any advantage of using that method? Or just personal preference? \$\endgroup\$ – pr8x May 30 '17 at 7:26
  • 1
    \$\begingroup\$ In the old days yes (there was a template recursion limit). Nowadays not so much. There is a small advantage on source code size (this is smaller at more compact) and thus easier to read (and I like easy to read). But I think that is a tiny bit subjective and technically they deliver the same results. So I think it is a matter of style. \$\endgroup\$ – Martin York May 30 '17 at 7:35

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.