30

This question is about how to pass a C++ object to a python function that is called in a (C++) embedded Python interpreter.

The following C++ class (MyClass.h) is designed for testing:

#ifndef MyClassH
#define MyClassH
#include <string>

using std::string;
class MyClass
{
    public:
                        MyClass(const string& lbl): label(lbl) {}
                        ~MyClass(){}
        string          getLabel() {return label;}

    private:
        string          label;
};
#endif

A python module, exposing the C++ class, can be generated by the following Swig interface file:

%module passmetopython

%{    #include "MyClass.h"    %}

%include "std_string.i"

//Expose to Python
%include "MyClass.h"

Below is a Python script using the python module

import passmetopython as pmtp

def execute(obj):
    #This function is to be called from C/C++, with a
    #MyClass object as an argument
    print ("Entering execute function")
    lbl = obj.getLabel();
    print ("Printing from within python execute function. Object label is: " + lbl)
    return True

def main():
    c = pmtp.MyClass("Test 1")
    retValue = execute(c)
    print("Return value: " + str(retValue))

#Test function from within python
if __name__ == '__main__':
    main()

This question is about how to get the python execute() function working, when called from c++, with a C++ object as an argument.

The following C++ program was written to test the functions (minimum amount of error checking):

#include "Python.h"
#include <iostream>
#include <sstream>
#include "MyClass.h"

using namespace std;

int main()
{
    MyClass obj("In C++");
    cout << "Object label: \"" << obj.getLabel() << "\"" << endl;

    //Setup the Python interpreter and eventually call the execute function in the
    //demo python script
    Py_Initialize();

    //Load python Demo script, "passmetopythonDemo.py"
    string PyModule("passmetopythonDemo");
    PyObject* pm = PyUnicode_DecodeFSDefault(PyModule.c_str());

    PyRun_SimpleString("import sys");
    stringstream cmd;
    cmd << "sys.path.append(\"" << "." << "\")";
    PyRun_SimpleString(cmd.str().c_str());
    PyObject* PyModuleP = PyImport_Import(pm);
    Py_DECREF(pm);

    //Now create PyObjects for the Python functions that we want to call
    PyObject* pFunc = PyObject_GetAttrString(PyModuleP, "execute");

    if(pFunc)
    {
        //Setup argument
        PyObject* pArgs = PyTuple_New(1);

        //Construct a PyObject* from long
        PyObject* pObj(NULL);

        /* My current attempt to create avalid argument to Python */
        pObj = PyLong_FromLong((long) &obj);


        PyTuple_SetItem(pArgs, 0, pObj);

        /***** Calling python here *****/
        cout<<endl<<"Calling function with an MyClass argument\n\n";
        PyObject* res = PyObject_CallObject(pFunc, pArgs);
        if(!res)
        {
            cerr << "Failed calling function..";
        }
    }

    return 0;
}

When running the above code, the execute() python function, with a MyClass object as an argument, fails and returns NULL. However, the Python function is entered, as I can see the output (Entering execute function) in the console output, indicating that the object passed is not, indeed, a valid MyClass object.

There are a lot of examples on how to pass simple types, like ints, doubles or string types to Python from C/C++. But there are very few example showing how to pass a C/C++ object/ pointer, which is kind of puzzling.

The above code, with a CMake file, can be checked out from github: https://github.com/TotteKarlsson/miniprojects/tree/master/passMeToPython

This code is not to use any boost python or other API's. Cython sounds interesting though, and if it can be used to simplify on the C++ side, it could be acceptable.

Totte Karlsson
  • 1,261
  • 1
  • 20
  • 55
  • Have you thought about serializing it in JSON or xml and then using a constructor to build the object in python? – Jake Steele May 30 '18 at 21:16
  • nope. My goal is to be able to share the memory for the object, so no copying should be needed. – Totte Karlsson May 30 '18 at 21:19
  • There's no `pFunc` object in your snippets. It doesn't work that way. `myObject` must be a **valid** `PyObject` (which I don't think it is - I can't be sure until I see `MyObject`'s definition). *"The C++ class have already been wrapped using swig"*: then I don't think that you should be required to write this *C* code. – CristiFati May 31 '18 at 14:58
  • Thanks CristiFati, In this question, the python interpreter is embedded in a C++ application, and so the python function above is called from C/C++. The MyObject is an empty "demo" C++ class, and its internals is irrelevant. Its only used for getting the concept right, concept being, as stated in the title, "how to pass a C++ object to Python?" – Totte Karlsson May 31 '18 at 16:17

2 Answers2

9

This is a partial answer to my own question. I'm saying partial, because I do believe there is a better way.

Building on this post http://swig.10945.n7.nabble.com/Pass-a-Swig-wrapped-C-class-to-embedded-Python-code-td8812.html I generated the swig runtime header, as described here, section 15.4: http://www.swig.org/Doc2.0/Modules.html#Modules_external_run_time

Including the generated header in the C++ code above, allow the following code to be written:

    PyObject* pObj = SWIG_NewPointerObj((void*)&obj, SWIG_TypeQuery("_p_MyClass"),  0 ); 

This code is using information from the Swig python wrap source files, namely the "swig" name of the type MyClass, i.e. _p_MyClass.

With the above PyObject* as an argument to the PyObject_CallObject function, the python execute() function in the code above executes fine, and the Python code, using the generated python module, do have proper access to the MyClass objects internal data. This is great.

Although the above code illustrate how to pass, and retrieve data between C++ and Python in a quite simple fashion, its not ideal, in my opinion.

The usage of the swig header file in the C++ code is really not that pretty, and in addition, it requires a user to "manually" look into swig generated wrapper code in order to find the "_p_MyClass" code.

There must be a better way!? Perhaps something should be added to the swig interface file in order to get this looking nicer?

Totte Karlsson
  • 1,261
  • 1
  • 20
  • 55
  • This is pretty much the answer. You should be able to write `SWIG_TypeQuery("MyClass *")` though if you prefer. – Flexo Jun 02 '18 at 21:39
  • What I might be tempted to do though would be make `execute` 'find' the `obj` instance via a wrapped C++ function instead of as an argument though, e.g. it calls `api.getInstance()`, where `api` is provided through the `PyImport_AppendInittab()`. – Flexo Jun 02 '18 at 21:47
  • Thanks Flexo, Perhaps inclusion of swig wrapper header not too bad. and the ability to query the type using your sueggestion, with "MyClass *" removes the necessity to having to look inside the swig wrapper code, which is a big improvement, imo. Not sure how to use the AppendInittab technique. – Totte Karlsson Jun 07 '18 at 18:01
-1
PyObject *pValue;
pValue = PyObject_CallMethod(pInstance, "add","(i)",x);
if (pValue)
    Py_DECREF(pValue);
else
    PyErr_Print();
Lorelorelore
  • 3,335
  • 8
  • 29
  • 40
PewDie
  • 9
  • 1