C++/Python wxWidgets3/wxPython4 avec CodeBlocks et pybind11

C++/Python wxWidgets3/wxPython4 avec CodeBlocks et pybind11

Au travers de l’application wxCppPy je vais vous apprendre à créer une application wxWidgets qui charge des scripts python et qui se laisse modifier et manipuler par wxPython. A savoir que wxPython4, qui porte le nom de version Phoenix, ne possède plus d’exemple pour interfacer wxPython avec wxWidgets. Je vais donc vous expliquer comment y parvenir.

Les liens du projet

Librairies et outils utilisés

Et bien sûr :

La hiérarchie de la solution:

La solution est composée de trois projets:

  • Un projet API qui défini les accès à la GUI et un objet Vector3 servant de démonstration.
  • Un projet API_Py qui exporte l’API vers un module python dans l’environnement python choisi. Le module python se nomme wxcpppy.
  • Un projet GUI qui défini une interface graphique qui est manipuler par les scripts python qu’elle importe.

Description du projet API :

API défini un objet Vector3 et les accès à l’interface wxWidgets. L’objet Vector sert d’exemple pour exporter des objets via pybind11.
Il dépend de wxWidgets et de CppClay. CppClay est utilisé pour instancier un singleton qui sera utilisé par le projet GUI et le module python wxcpppy.
La documentation générée par Doxygen est ici.

Description du projet API_Py:

API_Py génère le module wxcpppy. Il exporte API vers python. API_Py compile une première fois via Codeblocks ce qui permet d’accéder aux erreurs directement dans les fichiers. Après cette première compilation il en exécute une deuxièmes via le script setup.py. Pour se faire il utilise pip qui installera le module wxcpppy dans l’environnement python choisi.
La documentation générée par Doxygen est ici.

Description du projet GUI:

GUI définit une application graphique et permet de charger des scripts python qui pourront la manipuler.
La documentation générée par Doxygen est ici.

Le fonctionnement:

Définir les accès à l’interface graphique.

API propose l’interface suivante pour que soit stocké et partagé des références à des éléments graphiques de la GUI:

void SetMenuBar(wxMenuBar* inMenuBar);
void SetToolBar(wxAuiToolBar* inAuiToolBar);
void SetBottomNotebook(wxAuiNotebook* inNoteBook);
void SetLeftNoteBook(wxAuiNotebook* inNoteBook);
void SetCenterNoteBook(wxAuiNotebook* inNoteBook);
void SetMotherFrame(wxFrame* inFrame);

Il suffit donc d’utiliser ces fonctions de l’API à l’initialisation des éléments graphique de la GUI. Pour que les scripts python aient accès à ces références je les stocke dans un singleton.

API::wx::Access::GetSingleton().SetMenuBar(m_MenuBar);

Ce qui signifie que le projet API génère une librairie dynamique (.so/.dll). En statique API_Py et GUI auraient chacun sa propre instance du singleton API::wx::Access, chose qu’on ne veux pas. Je profite d’énoncer cette notion pour dire qu’il en sera de même pour lier API_Py et GUI à python et wxWidgets.

Quand à API_Py il utilise les accès à ces éléments graphiques pour les exporter vers python.
Pour simplifier l’accès j’ai créer directement des fonctions tel que :

API::wx::GetMenuBar();

Exporter l’API vers python avec pybind11.

Le point d’entré de la création du module wxcpppy est dans le fichier Module.cpp.
Le module wxcpppy est référencé avec la variable «m». «m.doc()» permet de définir la «docstring» du module.

Le R »mabalise(blabla blala c’est ma documentation)mabalise » est un string litteral.

Je vous met ici quelques liens pour rédiger vos propres «docstring».

Python:

pybind11:

Exporter les fonctionnalités faisant intervenir des objets c++.

Avant d’aborder le cas particulier des accès à la GUI voyons l’utilisation normale de pybind11 avec l’objet API::Vector3.

Je vous conseil de lier la documentation de pybind11. Elle est complète pour expliquer comment exporter des objets c++.

REMARQUE: Pour le coup cette partie n’est pas complète, il me reste pas mal de de fonctionnalité à exporter.

Exporter des fonctionnalités faisant intervenir des objets provenant de modules python.

En spécialisant type_caster Je n’ai pas réussi à ce que pybind11 convertisse automatiquement les objets wxWidgets vers des pybind11::handle.

Je l’ai donc fait manuellement en créant la fonction wxCast:

template <typename T>
pybind11::handle wxCast(T* src)
{
    wxASSERT(src);

    // As always, first grab the GIL
    wxPyBlock_t blocked = wxPyBeginBlockThreads();
    wxString typeName(src->GetClassInfo()->GetClassName());
    PyObject* obj = wxPyConstructObject(src, typeName, false);
    // Finally, after all Python stuff is done, release the GIL
    wxPyEndBlockThreads(blocked);

    wxASSERT(obj != nullptr);
    return obj;
}

La source est un objet de wxWidgets c++. La fonction wxPyConstructObject fournie par wxPython permet de retrouver l’objet à partir de l’instance et de son type c++ sous forme de string. Pour ce cas précis tous les objets de wxWidgets utiliser hérite de wxObject, ce qui permet de retrouver le nom de la classe grâce au wxClassInfo.

pybind11 assure automatiquement la conversion du PyObject obtenue en pybind11::handle ce qui permet d’exporter les fonctions en python.
Pour utiliser les fonctions exporter il faudra bien veiller à faire un import wx et import wx.aui dans le cas des objets dépendant de wxAui.

Exécuter des scripts python pour manipuler l’interface wxWidgets.

L’initialisation de l’interpréteur avec wxWidgets :

Via pybind ça ne passe pas. L’initialisation de l’interpréteur python et le chargement des scripts python se fait directement via «Python.h» et «wxPython/wxpy_api.h».
Dans notre cas j’ai implémenté l’initialisation de l’interpréteur python dans App.cpp.

bool App::Init_wxPython()
{
    Py_SetPythonHome(const_cast<wchar_t*>(m_PythonEnvPath.wc_str()));
    Py_Initialize();
    PyEval_InitThreads();

    if(!wxPyGetAPIPtr())
    {
        wxLogError(wxT("***** Error importing the wxPython API! *****"));
        PyErr_Print();
        Py_Finalize();
        return false;
    }

    // Save the current Python thread state and release the
    // Global Interpreter Lock.
    m_mainTState = wxPyBeginAllowThreads();

    CreatePyApp();

    return true;
}

Tout est nettoyer lors de l’appel à OnExit. Il est préférable de ne pas utiliser le destructeur ~App car OnExit est appelé si seulement OnInit à réussi à initialiser l’application.

int App::OnExit()
{
    if(m_PyApp)
       Py_DECREF(m_PyApp);

    wxPyEndAllowThreads(m_mainTState);
    Py_Finalize();
    return 0;
}

wxPyApp:

Si vous parcourez les sources de wxPython vous vous rendrez compte que à l’inverse de wxWidgets, wxPython défini en dure dans le code c++ une classe wxApp.
Dans le dossier si/cpp/:
A la ligne 20213 sipAPI_core.h défini une déclaration anticipée de wxPyApp et l’accès se fait via wxGetApp();
La déclaration et l’implémentation de wxPyApp se font dans le fichier sip_corewxPyApp.cpp ce qui empêche de définir un application commune entre notre GUI c++ et l’API wxPython.

str/_app.i

Bref depuis wxPython/phoenix c’est à dire la version 4 de wxPython nous avons cette erreur:
‘No wx.App created yet’

Tant que wxPython ne règle pas cette erreur. Nous devons appeler le code python suivant et stocker l’instance de cette wx.App() fantoche pour toute l’exécution du programme.

char* python_code = "\
# in case of wxPhoenix we need to create a wxApp first and store it\n\
# to prevent removal by gabage collector\n\
import wx\n\
theApp = wx.App()\n\
";

Ce code est exécuté une fois l’interpréteur python initialisé. Cf: App::CreatePyApp().
wxPyApp sera désalloué lors de l’appel à App::OnExit.

Je vous met quelques liens qui parlent de ce problème:
wxWidgets bogue
KiCad

GIL Global interpreter lock:

Dans CPython, «global interpreter lock» alias GIL est un mutex qui protège l’accès aux objets python. Cela empêche d’utiliser entièrement le potentiel du multi-tâches. Le GIL peut devenir un goulot d’étranglement. Pour en savoir plus voici ce lien. Avec wxPython il faut utiliser wxPyBeginBlockThreads et wxPyEndBlockThreads au lieu des traditionnels PyGILState_Ensure et PyGILState_Release

PyGILState_STATE gstate;
gstate = PyGILState_Ensure();

/* Perform Python actions here. */
result = CallSomeFunction();
/* evaluate result or handle exception */

/* Release the thread. No Python API allowed beyond this point. */
PyGILState_Release(gstate);

ou via pybind11.
L’expemple suivant provient de ce lien.

%%file wrap7.cpp
/*
<%
cfg['compiler_args'] = ['-std=c++11', '-fopenmp']
cfg['linker_args'] = ['-lgomp']
setup_pybind11(cfg)
%>
*/
#include <cmath>
#include <omp.h>
#include <pybind11/pybind11.h>
#include <pybind11/numpy.h>

namespace py = pybind11;

// Passing in an array of doubles
void twice(py::array_t<double> xs) {
    py::gil_scoped_acquire acquire;

    py::buffer_info info = xs.request();
    auto ptr = static_cast<double *>(info.ptr);

    int n = 1;
    for (auto r: info.shape) {
      n *= r;
    }

    #pragma omp parallel for
    for (int i = 0; i <n; i++) {
        *ptr++ *= 2;
    }
}

PYBIND11_PLUGIN(wrap7) {
  pybind11::module m("wrap7", "auto-compiled c++ extension");
  m.def("twice", [](py::array_t<double> xs) {
      /* Release GIL before calling into C++ code */
      py::gil_scoped_release release;
      return twice(xs);
    });

  return m.ptr();
}

Voici un exemple avec wxPython:

void MainFrame::LoadwxPython(const wxString& inScriptPath)
{
    // As always, first grab the GIL
    wxPyBlock_t blocked = wxPyBeginBlockThreads();

    PyObject* globals = PyDict_New();
    #if PY_MAJOR_VERSION >= 3
        PyObject* builtins = PyImport_ImportModule("builtins");
    #else
        PyObject* builtins = PyImport_ImportModule("__builtin__");
    #endif

    wxASSERT(builtins);
    if(builtins)
    {
        // Cette fonction plante si builtins est null https://bugs.python.org/issue5627
        PyDict_SetItemString(globals, "__builtins__", builtins);
        Py_DECREF(builtins);
    }

    PyObject* result = nullptr;

    // Execute the code to make the make_window function
    auto path = inScriptPath.ToStdString();
    std::ifstream file(path);
    if(file)
    {
        std::stringstream buffer;
        buffer << file.rdbuf();
        file.close();

        result = PyRun_String(buffer.str().c_str(), Py_file_input, globals, globals);
    }

    if(result)
        Py_DECREF(result);
    else
        PyErr_Print(); // Was there an exception?

    if(globals)
        Py_DECREF(globals);
    else
        PyErr_Print(); // Was there an exception?

    // Finally, after all Python stuff is done, release the GIL
    wxPyEndBlockThreads(blocked);
}

On remarquera que wxPyBeginBlockThreads et wxPyEndBlockThreads font exactement le même travail que PyGILState_Ensure et PyGILState_Release.

Compilation du module wxCppPy via python.

La compilation du module wxcpppy ce fait via le projet API_Py et utilise l’environnement virtuel python avec lequel vous travailler. Et c’est Codeblocks qui va fournir cette information.

ATTENTION: J’ai lié à toutes les librairies dynamique de wxPython installer dans l’environnement virtuel car je n’ai pas fini de configurer cet export. En dehors de l’application GUI un bogue persiste. Le module ne peut être chargé et donc l’auto-complétion ne fonctionne pas en-dehors du projet GUI.

Voici les variables d’environnements à définir pour faire fonctionner la solution wxCppPy.

Le processus de compilation du projet API_Py est le suivant:

  • Compiler via Codeblocks le projet API_Py et généré une librairie dynamique.
  • Compiler et installer wxcpppy via pip.

La compilation via pip suit les étapes suivantes:
Codeblocks appel install.sh

cd "${PROJECT_DIR}"
./install.sh ${PYTHONENV_PATH} ${PYBIND11_PATH} ${WXPYTHON_PATH} ${CPPCLAY_PATH} ${CPPCLAYDLL_PATH}

install.sh appel wxcpppyconfig.py pour généer un fichier de configuration

$1/bin/python wxcpppyconfig.py --pybind11_path="$2" --wxpython_path="$3" --cppclay_path="$4" --cppclaydll_path="$5"

install.sh appel ensuite setup.py via la commande suivante:

$1/bin/pip install ./

Le module sera installer dans le dossier:
python_envirronement/lib64/pythonx.yz/site-packages
La librairie dynamique sera nommée ainsi :
wxcpppy.cpython-xyz-x86_64-linux-gnu
x étant la version majeur
y la version mineur
z la balise nommée abiflags

Le nom de librairie peut s’obtenir de deux manières. Vous pouvez utilisez la commande suivante avec python3-config :

wxcpppy`python3-config --extension-suffix`

ou si vous souhaitez passer par un script python, vous pouvez utiliser la fonction suivante:

def get_extension_suffix():
    # https://stackoverflow.com/questions/42020937/why-pyvenv-does-not-install-python-config
    ext_suffix = sysconfig.get_config_var('EXT_SUFFIX')
    if ext_suffix is None:
        ext_suffix = sysconfig.get_config_var('SO')

    return ext_suffix

Je vous conseil de lire le script wxcpppyconfig.py ou ce lien.

Au final python s’occupe d’appeler le compilateur c++ par défaut en utilisant la configuration défini dans setup.py.

Les problèmes à résoudre et les fonctionnalités à implémenter:

Redirection des flux python vers le feuillet de log

J’ai pas encore essayé de le faire. Mais c’est une chose que j’estime importante à démontrer. Le peu que j’ai vue lors de mes lectures, c’est que c’est faisable avec pybind11.

L’auto-complétion du module wxCppPy dans un IDE

A partir du moment où j’ai mis les références à wxPython dans le projet API_Py, PyCharm et Python, en ligne de commande, ne peuvent charger wxcpppy sans erreurs.
L’erreur est la suivante:

# ImportError : venv/lib/python3.5/site-packages/wxcpppy.cpython-35m-x86_64-linux-gnu.so: undefined symbol: _ZTI8wxThread

Pour le moment l’auto-complétion fonctionne seulement via la GUI de la solution.

Finir l’export des fonctionnalités du Vector3

Il me manque pas mal de fonctionnalités à finir d’exporter vers python.

suryavarman