Skip to content

Embedding Further

PyHelper Class

So far we need to:

  • Initialize the python interpreter.
  • Create python objects.
  • Increment and decrement the reference count for python objects as needed.
  • Close the python interpreter once we're finished with it.

With that said, let's create PyHelper.hpp which encapsulates this functionality.

#pragma once

#define PY_SSIZE_T_CLEAN
#include <Python.h>
#include <iostream>

class CPyInstance {
  public:
    CPyInstance() {
        Py_SetProgramName(L"red_engine");
        Py_Initialize();
        PyRun_SimpleString("import sys");
        PyRun_SimpleString("sys.path.append(\".\")");
    }

    ~CPyInstance() {
        Py_Finalize();
    }
};

class CPyObject {
  public:
    CPyObject(): pyObj(nullptr) {}

    CPyObject(PyObject* p) : pyObj(p) {}

    ~CPyObject() {
        Release();
    }

    PyObject* GetObj() {
        return pyObj;
    }

    PyObject* SetObj(PyObject* p) {
        return (pyObj=p);
    }

    PyObject* AddRef() {
        if(pyObj) {
            Py_INCREF(pyObj);
        }
        return pyObj;
    }

    void Release() {
        if(pyObj) {
            Py_DECREF(pyObj);
        }
        pyObj = nullptr;
    }

    PyObject* operator->() {
        return pyObj;
    }

    bool Is() const {
        return pyObj ? true : false;
    }

    operator PyObject*() {
        return pyObj;
    }

    PyObject* operator=(PyObject* p) {
        pyObj = p;
        return pyObj;
    }

  private:
    PyObject* pyObj;
};

There are two classes created in PyHelper.hpp. CPyInstance responsibility is to initialize the python interpreter, perform any additional setup, and shutdown the intepreter once finished. CPyObject is a wrapper class for PyObject which is a python object. Instead of having to explicitly decrement with Py_DECREF we instead use this CPyObject which decrements the reference count once the object is out of scope.

Now that we have a helper class, let's put it to use.

def play(message : str) -> int:
    print(f"{message} (from python)!")
    return 0

We have updated our python function to now accept an argument.

#include "./scripting/pyhelper.hpp"

int main(int argv, char** args) {
    CPyInstance pyInstance;

    // Load Module
    CPyObject pModuleName = PyUnicode_FromString("assets.scripts.game");
    CPyObject pModule = PyImport_Import(pModuleName);
    assert(pModule != nullptr && "Not able to load python module!");

    // Function
    CPyObject pFunc = PyObject_GetAttrString(pModule, "play");
    assert(pFunc != nullptr && "Not able to find function named 'play'!");
    CPyObject pArgs = Py_BuildValue("(s)", "hello world!");
    assert(pArgs != nullptr);
    CPyObject pValue = PyObject_CallObject(pFunc, pArgs);

    return 0;
}

This is similar to the code snippet we've created in the previous section, but instead of using PyObject we are using CPyObject. Py_BuildValue builds a tuple of arguements that we can then pass to a python function. With the argument defined, we can now call PyObject_CallObject with an argument. If you would like to double check your code you can view the source code for this section here.

Creating A Python Instance in C++

We're able to import modules and call functions, but there may be times where we'll want to interact with an instance of a python class. Furthermore, we don't want to have to import a module and query its attributes each time we want to use a function as that will affect performance. In this section we'll create a class to manage active python objects.

#pragma once

#include <string>
#include <unordered_map>

#include "./pyhelper.hpp"

struct PythonModuleObject {
    CPyObject module;
    std::unordered_map<std::string, CPyObject> classes;
};

class PythonObjectManager {
  public:
    CPyObject CreateClassInstance(const std::string &classPath, const std::string &className) {
        CPyObject pClass = GetClass(classPath, className);
        CPyObject pClassInstance = PyObject_CallObject(pClass, nullptr);
        assert(pClassInstance != nullptr && "Class instance is NULL!");
        pClassInstance.AddRef();
        return pClassInstance;
    }

  private:
    std::unordered_map<std::string, PythonModuleObject> modules;

    CPyObject GetClass(const std::string &classPath, const std::string &className) {
        if (modules.find(classPath) == modules.end()) {
            CPyObject pModuleName = PyUnicode_FromString(classPath.c_str());
            CPyObject pModule = PyImport_Import(pModuleName);
            assert(pModule != nullptr && "Python module is NULL!");
            modules.emplace(classPath, PythonModuleObject{
                .module = pModule,
                .classes = {}
            });
        }
        if (modules[classPath].classes.find(className) == modules[classPath].classes.end()) {
            CPyObject pModuleDict = PyModule_GetDict(modules[classPath].module);
            CPyObject pClass = PyDict_GetItemString(pModuleDict, className.c_str());
            assert(pClass != nullptr && "Python class is NULL!");
            modules[classPath].classes.emplace(className, pClass);
        }
        return modules[classPath].classes[className];
    }
};

Within python_object_manager.h we first create PythonModuleObject which is a struct to hold a python module's object data. It will hold all the python class objects loaded for the module.

Next is the main class which is PythonObjectManager. The GetClass function will return a class object for a module. We use this within CreateClassInstance to create a python instance of a class.

class Player:
    def talk(self, message: str) -> None:
        print(f"Player says '{message}'!")

The script game.py has been updated to include a class named Player. We can finally create an instance of this class and call a function from this instance from c++.

#include "./scripting/python_object_manager.h"

int main(int argv, char** args) {
    CPyInstance pyInstance;
    PythonObjectManager pObjectManager;

    CPyObject pClassInstance = pObjectManager.CreateClassInstance("assets.scripts.game", "Player");
    PyObject_CallMethod(pClassInstance, "talk", "(s)", "Hello!");

    return 0;
}

There is even less code in main even though we're creating an instance! The only thing to really point out is PyObject_CallMethod which calls a function on an instance of a class. Source code for this section can be viewed here. Now that we have a solid grasp on how to create python instances and call functions on them it's time to create our own modules in C++ in order to call the Red Engine api from within python scripts.