Création de liaisons Python pour les bibliothèques C / C ++ à l'aide de SIP. Partie 2

Dans la première partie de l' article, nous avons examiné les bases de l'utilisation de l'utilitaire SIP conçu pour créer des liaisons Python pour les bibliothèques écrites en C et C ++. Nous avons examiné les fichiers de base que vous devez créer pour travailler avec SIP et avons commencé à examiner les directives et les annotations. Jusqu'à présent, nous avons fait la liaison pour une bibliothèque simple écrite en C. Dans cette partie, nous allons voir comment faire la liaison pour une bibliothèque C ++ qui contient des classes. En utilisant l'exemple de cette bibliothèque, nous verrons quelles techniques peuvent être utiles lorsque vous travaillez avec une bibliothèque orientée objet, et en même temps nous traiterons de nouvelles directives et annotations pour nous.

Tous les exemples de l'article sont disponibles dans le référentiel github à l' adresse : https://github.com/Jenyay/sip-examples .

Créer une liaison pour une bibliothèque en C ++


L'exemple suivant, que nous considérerons, se trouve dans le dossier pyfoo_cpp_01 .

Créez d'abord une bibliothèque pour laquelle nous ferons la liaison. La bibliothèque résidera toujours dans le dossier foo et contiendra une classe - Foo . Le fichier d'en-tête foo.h avec la déclaration de cette classe est le suivant:

#ifndef FOO_LIB
#define FOO_LIB

class Foo {
    private:
        int _int_val;
        char* _string_val;
    public:
        Foo(int int_val, const char* string_val);
        virtual ~Foo();

        void set_int_val(int val);
        int get_int_val();

        void set_string_val(const char* val);
        char* get_string_val();
};

#endif

Il s'agit d'une classe simple avec deux getters et setters qui définit et renvoie des valeurs de type int et char * . L'implémentation de la classe est la suivante:

#include <string.h>

#include "foo.h"

Foo::Foo(int int_val, const char* string_val): _int_val(int_val) {
    _string_val = nullptr;
    set_string_val(string_val);
}

Foo::~Foo(){
    delete[] _string_val;
    _string_val = nullptr;
}

void Foo::set_int_val(int val) {
    _int_val = val;
}

int Foo::get_int_val() {
    return _int_val;
}

void Foo::set_string_val(const char* val) {
    if (_string_val != nullptr) {
        delete[] _string_val;
    }

    auto count = strlen(val) + 1;
    _string_val = new char[count];
    strcpy(_string_val, val);
}

char* Foo::get_string_val() {
    return _string_val;
}

Pour tester les fonctionnalités de la bibliothèque, le dossier foo contient également le fichier main.cpp à l'aide de la classe Foo :

#include <iostream>

#include "foo.h"

using std::cout;
using std::endl;

int main(int argc, char* argv[]) {
    auto foo = Foo(10, "Hello");
    cout << "int_val: " << foo.get_int_val() << endl;
    cout << "string_val: " << foo.get_string_val() << endl;

    foo.set_int_val(0);
    foo.set_string_val("Hello world!");

    cout << "int_val: " << foo.get_int_val() << endl;
    cout << "string_val: " << foo.get_string_val() << endl;
}

Pour construire la bibliothèque foo , utilisez le Makefile suivant :

CC=g++
CFLAGS=-c -fPIC
DIR_OUT=bin

all: main

main: main.o libfoo.a
    $(CC) $(DIR_OUT)/main.o -L$(DIR_OUT) -lfoo -o $(DIR_OUT)/main

main.o: makedir main.cpp
    $(CC) $(CFLAGS) main.cpp -o $(DIR_OUT)/main.o

libfoo.a: makedir foo.cpp
    $(CC) $(CFLAGS) foo.cpp -o $(DIR_OUT)/foo.o
    ar rcs $(DIR_OUT)/libfoo.a $(DIR_OUT)/foo.o

makedir:
    mkdir -p $(DIR_OUT)

clean:
    rm -rf $(DIR_OUT)/*

La différence avec le Makefile dans les exemples précédents, en plus de changer le compilateur de gcc en g ++ , est qu'une autre option -fPIC a été ajoutée pour la compilation , qui indique au compilateur de placer le code dans la bibliothèque d'une certaine manière (le soi-disant "code indépendant de la position"). Étant donné que cet article ne concerne pas les compilateurs, nous n'examinerons pas plus en détail ce que fait ce paramètre et pourquoi il est nécessaire.

Commençons à lier pour cette bibliothèque. Les fichiers pyproject.toml et project.py sont presque inchangés par rapport aux exemples précédents. Voici à quoi ressemble maintenant le fichier pyproject.toml :

[build-system]
requires = ["sip >=5, <6"]
build-backend = "sipbuild.api"

[tool.sip.metadata]
name = "pyfoocpp"
version = "0.1"
license = "MIT"

[tool.sip.bindings.pyfoocpp]
headers = ["foo.h"]
libraries = ["foo"]

Maintenant, nos exemples écrits en C ++ seront empaquetés dans le paquet pyfoocpp Python , c'est peut-être le seul changement notable dans ce fichier.

Le fichier project.py reste le mĂŞme que dans l'exemple pyfoo_c_04 :

import os
import subprocess

from sipbuild import Project

class FooProject(Project):
    def _build_foo(self):
        cwd = os.path.abspath('foo')
        subprocess.run(['make'], cwd=cwd, capture_output=True, check=True)

    def build(self):
        self._build_foo()
        super().build()

    def build_sdist(self, sdist_directory):
        self._build_foo()
        return super().build_sdist(sdist_directory)

    def build_wheel(self, wheel_directory):
        self._build_foo()
        return super().build_wheel(wheel_directory)

    def install(self):
        self._build_foo()
        super().install()

Et ici, nous examinerons le fichier pyfoocpp.sip plus en détail. Permettez-moi de vous rappeler que ce fichier décrit l'interface du futur module Python: ce qu'il devrait inclure, à quoi devrait ressembler l'interface de classe, etc. Le fichier .sip n'est pas nécessaire pour répéter le fichier d'en-tête de bibliothèque, bien qu'ils aient beaucoup en commun. Dans cette classe, de nouvelles méthodes peuvent être ajoutées qui n'étaient pas dans la classe d'origine. Ceux. l'interface décrite dans le fichier .sip peut adapter les classes de bibliothèque aux principes acceptés en Python, si nécessaire. Dans le fichier pyfoocpp.sip , nous verrons de nouvelles directives pour nous.

Voyons d'abord ce que contient ce fichier:

%Module(name=foocpp, language="C++")
%DefaultEncoding "UTF-8"

class Foo {
    %TypeHeaderCode
    #include <foo.h>
    %End

    public:
        Foo(int, const char*);

        void set_int_val(int);
        int get_int_val();

        void set_string_val(const char*);
        char* get_string_val();
};

Les premières lignes doivent déjà être claires pour nous à partir des exemples précédents. Dans la directive % Module , nous indiquons le nom du module Python qui sera créé (c'est-à-dire, pour utiliser ce module, nous devrons utiliser les commandes import foocpp ou from foocpp import .... Dans la même directive, nous indiquons que nous avons maintenant le langage - C ++. La directive% DefaultEncoding définit le codage qui sera utilisé pour convertir la chaîne Python en types char , const char , char * et const char * .

Ensuite, la déclaration d'interface de la classe Foo suit . Immédiatement après la déclaration de la classe Fooil n'y a toujours pas utilisé la directive % TypeHeaderCode , qui se termine par la directive % End . La directive% TypeHeaderCode doit contenir du code qui déclare l'interface de la classe C ++ pour laquelle l'encapsuleur est créé. En règle générale, dans cette directive, il suffit d'inclure le fichier d'en-tête avec la déclaration de classe.

Après cela, les méthodes de classe qui seront converties en méthodes de la classe Foo pour le langage Python sont répertoriées . Il est important de noter qu'à ce stade, nous déclarons uniquement les méthodes publiques qui seront accessibles à partir de la classe Foo en Python (car il n'y a pas de membres privés et protégés en Python). Puisque nous avons utilisé la directive % DefaultEncoding au tout début, puis dans les méthodes qui prennent des arguments de type const char * , vous ne pouvez pas utiliser l'annotation Encoding pour spécifier l'encodage pour convertir ces paramètres en chaînes Python et vice versa.

Il nous suffit maintenant de compiler le paquet pyfoocpp Python et de le tester. Mais avant d'assembler un package de roue à part entière, utilisons la commande sip-build et voyons quels fichiers source SIP créera pour une compilation ultérieure, et essayons de trouver en eux quelque chose de similaire à la classe qui sera créée en code Python. Pour ce faire, la commande sip-build ci - dessus doit être appelée dans le dossier pyfoo_cpp_01 . Par conséquent, le dossier de génération sera créé. avec le contenu suivant:

construire
└── foocpp
    ├── apiversions.c
    ├── array.c
    ├── array.h
    ├── bool.cpp
    ├── build
    │   └── temp.linux-x86_64-3.8
    │       ├── apiversions.o
    │       ├── array.o
    │       ├── bool.o
    │       ├── descriptors.o
    │       ├── int_convertors.o
    │       ├── objmap.o
    │       ├── qtlib.o
    │       ├── sipfoocppcmodule.o
    │       ├── sipfoocppFoo.o
    │       ├── siplib.o
    │       ├── threads.o
    │       └── voidptr.o
    ├── descriptors.c
    ├── foocpp.cpython-38-x86_64-linux-gnu.so
    ├── int_convertors.c
    ├── objmap.c
    ├── qtlib.c
    ├── sipAPIfoocpp.h
    ├── sipfoocppcmodule.cpp
    ├── sipfoocppFoo.cpp
    ├── sip.h
    ├── sipint.h
    ├── siplib.c
    ├── threads.c
    └── voidptr.c


En tant que tâche supplémentaire, examinez attentivement le fichier sipfoocppFoo.cpp (nous ne l' expliquerons pas en détail dans cet article):

/*
 * Interface wrapper code.
 *
 * Generated by SIP 5.1.1
 */

#include "sipAPIfoocpp.h"

#line 6 "/home/jenyay/temp/2/pyfoocpp.sip"
    #include <foo.h>
#line 12 "/home/jenyay/temp/2/build/foocpp/sipfoocppFoo.cpp"

PyDoc_STRVAR(doc_Foo_set_int_val, "set_int_val(self, int)");

extern "C" {static PyObject *meth_Foo_set_int_val(PyObject *, PyObject *);}
static PyObject *meth_Foo_set_int_val(PyObject *sipSelf, PyObject *sipArgs)
{
    PyObject *sipParseErr = SIP_NULLPTR;

    {
        int a0;
         ::Foo *sipCpp;

        if (sipParseArgs(&sipParseErr, sipArgs, "Bi", &sipSelf, sipType_Foo, &sipCpp, &a0))
        {
            sipCpp->set_int_val(a0);

            Py_INCREF(Py_None);
            return Py_None;
        }
    }

    /* Raise an exception if the arguments couldn't be parsed. */
    sipNoMethod(sipParseErr, sipName_Foo, sipName_set_int_val, doc_Foo_set_int_val);

    return SIP_NULLPTR;
}

PyDoc_STRVAR(doc_Foo_get_int_val, "get_int_val(self) -> int");

extern "C" {static PyObject *meth_Foo_get_int_val(PyObject *, PyObject *);}
static PyObject *meth_Foo_get_int_val(PyObject *sipSelf, PyObject *sipArgs)
{
    PyObject *sipParseErr = SIP_NULLPTR;

    {
         ::Foo *sipCpp;

        if (sipParseArgs(&sipParseErr, sipArgs, "B", &sipSelf, sipType_Foo, &sipCpp))
        {
            int sipRes;

            sipRes = sipCpp->get_int_val();

            return PyLong_FromLong(sipRes);
        }
    }

    /* Raise an exception if the arguments couldn't be parsed. */
    sipNoMethod(sipParseErr, sipName_Foo, sipName_get_int_val, doc_Foo_get_int_val);

    return SIP_NULLPTR;
}

PyDoc_STRVAR(doc_Foo_set_string_val, "set_string_val(self, str)");

extern "C" {static PyObject *meth_Foo_set_string_val(PyObject *, PyObject *);}
static PyObject *meth_Foo_set_string_val(PyObject *sipSelf, PyObject *sipArgs)
{
    PyObject *sipParseErr = SIP_NULLPTR;

    {
        const char* a0;
        PyObject *a0Keep;
         ::Foo *sipCpp;

        if (sipParseArgs(&sipParseErr, sipArgs, "BA8", &sipSelf, sipType_Foo, &sipCpp, &a0Keep, &a0))
        {
            sipCpp->set_string_val(a0);
            Py_DECREF(a0Keep);

            Py_INCREF(Py_None);
            return Py_None;
        }
    }

    /* Raise an exception if the arguments couldn't be parsed. */
    sipNoMethod(sipParseErr, sipName_Foo, sipName_set_string_val, doc_Foo_set_string_val);

    return SIP_NULLPTR;
}

PyDoc_STRVAR(doc_Foo_get_string_val, "get_string_val(self) -> str");

extern "C" {static PyObject *meth_Foo_get_string_val(PyObject *, PyObject *);}
static PyObject *meth_Foo_get_string_val(PyObject *sipSelf, PyObject *sipArgs)
{
    PyObject *sipParseErr = SIP_NULLPTR;

    {
         ::Foo *sipCpp;

        if (sipParseArgs(&sipParseErr, sipArgs, "B", &sipSelf, sipType_Foo, &sipCpp))
        {
            char*sipRes;

            sipRes = sipCpp->get_string_val();

            if (sipRes == SIP_NULLPTR)
            {
                Py_INCREF(Py_None);
                return Py_None;
            }

            return PyUnicode_FromString(sipRes);
        }
    }

    /* Raise an exception if the arguments couldn't be parsed. */
    sipNoMethod(sipParseErr, sipName_Foo, sipName_get_string_val, doc_Foo_get_string_val);

    return SIP_NULLPTR;
}

/* Call the instance's destructor. */
extern "C" {static void release_Foo(void *, int);}
static void release_Foo(void *sipCppV, int)
{
    delete reinterpret_cast< ::Foo *>(sipCppV);
}

extern "C" {static void dealloc_Foo(sipSimpleWrapper *);}
static void dealloc_Foo(sipSimpleWrapper *sipSelf)
{
    if (sipIsOwnedByPython(sipSelf))
    {
        release_Foo(sipGetAddress(sipSelf), 0);
    }
}

extern "C" {static void *init_type_Foo(sipSimpleWrapper *, PyObject *, 
                 PyObject *, PyObject **, PyObject **, PyObject **);}
static void *init_type_Foo(sipSimpleWrapper *, PyObject *sipArgs, PyObject *sipKwds,
                                   PyObject **sipUnused, PyObject **, PyObject **sipParseErr)
{
     ::Foo *sipCpp = SIP_NULLPTR;

    {
        int a0;
        const char* a1;
        PyObject *a1Keep;

        if (sipParseKwdArgs(sipParseErr, sipArgs, sipKwds, SIP_NULLPTR, sipUnused, "iA8", &a0, &a1Keep, &a1))
        {
            sipCpp = new  ::Foo(a0,a1);
            Py_DECREF(a1Keep);

            return sipCpp;
        }
    }

    {
        const  ::Foo* a0;

        if (sipParseKwdArgs(sipParseErr, sipArgs, sipKwds, SIP_NULLPTR, sipUnused, "J9", sipType_Foo, &a0))
        {
            sipCpp = new  ::Foo(*a0);

            return sipCpp;
        }
    }

    return SIP_NULLPTR;
}

static PyMethodDef methods_Foo[] = {
    {sipName_get_int_val, meth_Foo_get_int_val, METH_VARARGS, doc_Foo_get_int_val},
    {sipName_get_string_val, meth_Foo_get_string_val, METH_VARARGS, doc_Foo_get_string_val},
    {sipName_set_int_val, meth_Foo_set_int_val, METH_VARARGS, doc_Foo_set_int_val},
    {sipName_set_string_val, meth_Foo_set_string_val, METH_VARARGS, doc_Foo_set_string_val}
};

PyDoc_STRVAR(doc_Foo, "\1Foo(int, str)\n"
"Foo(Foo)");

sipClassTypeDef sipTypeDef_foocpp_Foo = {
    {
        -1,
        SIP_NULLPTR,
        SIP_NULLPTR,
        SIP_TYPE_CLASS,
        sipNameNr_Foo,
        SIP_NULLPTR,
        SIP_NULLPTR
    },
    {
        sipNameNr_Foo,
        {0, 0, 1},
        4, methods_Foo,
        0, SIP_NULLPTR,
        0, SIP_NULLPTR,
        {SIP_NULLPTR, SIP_NULLPTR, SIP_NULLPTR, SIP_NULLPTR, SIP_NULLPTR,
         SIP_NULLPTR, SIP_NULLPTR, SIP_NULLPTR, SIP_NULLPTR, SIP_NULLPTR},
    },
    doc_Foo,
    -1,
    -1,
    SIP_NULLPTR,
    SIP_NULLPTR,
    init_type_Foo,
    SIP_NULLPTR,
    SIP_NULLPTR,
    SIP_NULLPTR,
    SIP_NULLPTR,
    dealloc_Foo,
    SIP_NULLPTR,
    SIP_NULLPTR,
    SIP_NULLPTR,
    release_Foo,
    SIP_NULLPTR,
    SIP_NULLPTR,
    SIP_NULLPTR,
    SIP_NULLPTR,
    SIP_NULLPTR,
    SIP_NULLPTR,
    SIP_NULLPTR
};

Maintenant, construisez le package à l'aide de la commande sip-wheel . Après avoir exécuté cette commande, si tout se passe bien, un fichier pyfoocpp-0.1-cp38-cp38-manylinux1_x86_64.whl ou avec un nom similaire sera créé . Installez-le à l'aide de la commande pip install --user pyfoocpp-0.1-cp38-cp38-manylinux1_x86_64.whl et exécutez l'interpréteur Python pour vérifier:

>>> from foocpp import Foo
>>> x = Foo(10, 'Hello')

>>> x.get_int_val()
10

>>> x.get_string_val()
'Hello'

>>> x.set_int_val(50)
>>> x.set_string_val('')

>>> x.get_int_val()
50

>>> x.get_string_val()
''

Travaux! Ainsi, nous venons de créer un module Python avec une liaison pour une classe en C ++. De plus, nous apporterons de la beauté à cette classe et ajouterons divers équipements.

Ajouter des propriétés


Les classes créées à l'aide de SIP ne sont pas nécessaires pour répéter exactement l'interface de classe C ++. Par exemple, dans notre classe Foo , il y a deux getters et deux setters, qui peuvent clairement être combinés en une propriété pour rendre la classe plus «Python». L'ajout de propriétés à l'aide de sip est assez facile au fur et à mesure, comme le montre un exemple dans le dossier pyfoo_cpp_02 .

Cet exemple est similaire au précédent, la principale différence réside dans le fichier pyfoocpp.sip , qui ressemble maintenant à ceci:

%Module(name=foocpp, language="C++")
%DefaultEncoding "UTF-8"

class Foo {
    %TypeHeaderCode
    #include <foo.h>
    %End

    public:
        Foo(int, const char*);

        void set_int_val(int);
        int get_int_val();
        %Property(name=int_val, get=get_int_val, set=set_int_val)

        void set_string_val(const char*);
        char* get_string_val();
        %Property(name=string_val, get=get_string_val, set=set_string_val)
};

Comme vous pouvez le voir, tout est assez simple. Pour ajouter une propriété, la directive % Property est prévue , qui a deux paramètres requis: nom pour spécifier le nom de la propriété, et obtenir pour spécifier une méthode qui renvoie une valeur (getter). Il peut ne pas y avoir de setter, mais si la propriété doit également être affectée de valeurs, la méthode setter est spécifiée comme valeur du paramètre set . Dans notre exemple, les propriétés sont créées de manière assez simple, car il existe déjà des fonctions qui fonctionnent comme des getters et des setters.

Nous pouvons uniquement collecter le package à l'aide de la commande sip-wheel , l'installer, après quoi nous vérifierons le fonctionnement des propriétés dans le mode de commande de l'interpréteur python:

>>> from foocpp import Foo
>>> x = Foo(10, "Hello")
>>> x.int_val
10

>>> x.string_val
'Hello'

>>> x.int_val = 50
>>> x.string_val = ''

>>> x.get_int_val()
50

>>> x.get_string_val()
''

Comme vous pouvez le voir dans l'exemple d'utilisation de la classe Foo , les propriétés int_val et string_val fonctionnent à la fois pour la lecture et l'écriture.

Ajouter des lignes de documentation


Nous continuerons d'améliorer notre classe Foo . L'exemple suivant, situé dans le dossier pyfoo_cpp_03, montre comment ajouter des lignes de documentation (docstring) à divers éléments d'une classe. Cet exemple est basé sur le précédent et le principal changement concerne le fichier pyfoocpp.sip . Voici son contenu:

%Module(name=foocpp, language="C++")
%DefaultEncoding "UTF-8"

class Foo {
%Docstring
Class example from C++ library
%End

    %TypeHeaderCode
    #include <foo.h>
    %End

    public:
        Foo(int, const char*);

        void set_int_val(int);
        %Docstring(format="deindented", signature="prepended")
            Set integer value
        %End

        int get_int_val();
        %Docstring(format="deindented", signature="prepended")
            Return integer value
        %End

        %Property(name=int_val, get=get_int_val, set=set_int_val)
        {
            %Docstring "deindented"
                The property for integer value
            %End
        };

        void set_string_val(const char*);
        %Docstring(format="deindented", signature="appended")
            Set string value
        %End

        char* get_string_val();
        %Docstring(format="deindented", signature="appended")
            Return string value
        %End

        %Property(name=string_val, get=get_string_val, set=set_string_val)
        {
            %Docstring "deindented"
                The property for string value
            %End
        };
};

Comme vous l'avez déjà compris, pour ajouter des lignes de documentation à n'importe quel élément de la classe, vous devez utiliser la directive % Docstring . Cet exemple montre plusieurs façons d'utiliser cette directive. Pour une meilleure compréhension de cet exemple, compilons immédiatement le paquet pyfoocpp à l'aide de la commande sip-wheel , installez-le, et nous déterminerons séquentiellement quel paramètre de cette directive affecte quoi, compte tenu des lignes de documentation résultantes en mode de commande Python. Permettez-moi de vous rappeler que les lignes de documentation sont stockées en tant que membres des objets __doc__ auxquels ces lignes appartiennent.

La première ligne de documentation concerne la classe Foo .. Comme vous pouvez le voir, toutes les lignes de documentation sont situées entre les directives% Docstring et % End . Les lignes 5-7 de cet exemple n'utilisent aucun paramètre supplémentaire de la directive % Docstring , la ligne de documentation sera donc écrite dans la classe Foo telle quelle . C'est pourquoi il n'y a pas de retrait dans les lignes 5-7, sinon le retrait avant la ligne de documentation tomberait également dans Foo .__ doc__. Nous nous assurerons que la classe Foo contient vraiment la ligne de documentation que nous avons introduite:

>>> from foocpp import Foo
>>> Foo.__doc__
'Class example from C++ library'

La directive % Docstring suivante , située sur les lignes 17-19, utilise deux paramètres à la fois. Le paramètre de format peut prendre l'une des deux valeurs: «brut» ou «désindenté». Dans le premier cas, les lignes de documentation sont enregistrées au fur et à mesure qu'elles sont écrites et dans le second cas, les caractères d'espacement initiaux (mais pas les tabulations) sont supprimés. La valeur par défaut du cas si le paramètre de format n'est pas spécifié peut être définie à l'aide de la directive % DefaultDocstringFormat (nous la considérerons un peu plus tard), et si elle n'est pas spécifiée, alors on suppose que format = "raw" .

En plus des lignes de documentation spécifiées, SIP ajoute une description de sa signature (quels types de variables sont attendus à l'entrée et quel type la fonction renvoie) aux lignes de documentation des fonctions. Le paramètre de signature indique où placer une telle signature: avant la ligne de documentation spécifiée ( signature = "pré-ajoutée" ), après celle-ci ( signature = "ajoutée" ) ou ne pas ajouter la signature ( signature = "supprimée" ).

Notre exemple définit le paramètre signature = "prepended" pour les fonctions get_int_val et set_int_val , ainsi que signature = " appended " pour les fonctions get_string_val et set_string_val. Le paramètre format = "deindented" a également été ajouté pour supprimer les espaces au début de la ligne de documentation. Vérifions le fonctionnement de ces paramètres en Python:

>>> Foo.get_int_val.__doc__
'get_int_val(self) -> int\nReturn integer value'

>>> Foo.set_int_val.__doc__
'set_int_val(self, int)\nSet integer value'

>>> Foo.get_string_val.__doc__
'Return string value\nget_string_val(self) -> str'

>>> Foo.set_string_val.__doc__
'Set string value\nset_string_val(self, str)'

Comme vous pouvez le voir, en utilisant le paramètre de signature de la directive % Docstring , vous pouvez modifier la position de la description de la signature de fonction dans la ligne de documentation.

Pensez maintenant à ajouter une ligne de documentation aux propriétés. Notez que dans ce cas, les directives% Docstring ... % End sont placées entre accolades après la directive% Property. Ce format d'enregistrement est décrit dans la documentation de la directive % Property .

Notez également comment nous spécifions le paramètre de directive % Docstring . Un tel format pour écrire des directives est possible si nous ne définissons que le premier paramètre de la directive (dans ce cas, le paramètre format) Ainsi, dans cet exemple, trois méthodes d'utilisation des directives sont utilisées à la fois.

Assurez-vous que la ligne de documentation des propriétés est définie:

>>> Foo.int_val.__doc__
'The property for integer value'

>>> Foo.string_val.__doc__
'The property for string value'

>>> help(Foo)
Help on class Foo in module foocpp:

class Foo(sip.wrapper)
 |  Class example from C++ library
 |  
 |  Method resolution order:
 |      Foo
 |      sip.wrapper
 |      sip.simplewrapper
 |      builtins.object
 |  
 |  Methods defined here:
 |  
 |  get_int_val(...)
 |      get_int_val(self) -> int
 |      Return integer value
 |  
 |  get_string_val(...)
 |      Return string value
 |      get_string_val(self) -> str
 |  
 |  set_int_val(...)
 |      set_int_val(self, int)
 |      Set integer value
 |  
 |  set_string_val(...)
 |      Set string value
 |      set_string_val(self, str)
...


Simplifions cet exemple en définissant les valeurs par défaut des paramètres de format et de signature à l'aide des directives % DefaultDocstringFormat et % DefaultDocstringSignature . L'utilisation de ces directives est illustrée dans l'exemple du dossier pyfoo_cpp_04 . Le fichier pyfoocpp.sip dans cet exemple contient le code suivant:

%Module(name=foocpp, language="C++")
%DefaultEncoding "UTF-8"
%DefaultDocstringFormat "deindented"
%DefaultDocstringSignature "prepended"

class Foo {
    %Docstring
    Class example from C++ library
    %End

    %TypeHeaderCode
    #include <foo.h>
    %End

    public:
        Foo(int, const char*);

        void set_int_val(int);
        %Docstring
            Set integer value
        %End

        int get_int_val();
        %Docstring
            Return integer value
        %End

        %Property(name=int_val, get=get_int_val, set=set_int_val)
        {
            %Docstring
                The property for integer value
            %End
        };

        void set_string_val(const char*);
        %Docstring
            Set string value
        %End

        char* get_string_val();
        %Docstring
            Return string value
        %End

        %Property(name=string_val, get=get_string_val, set=set_string_val)
        {
            %Docstring
                The property for string value
            %End
        };
};

Au début du fichier, les lignes % DefaultDocstringFormat "deindented" et % DefaultDocstringSignature "prepended" ont été ajoutées , puis tous les paramètres de la directive % Docstring ont été supprimés.

Après avoir assemblé et installé cet exemple, nous pouvons voir à quoi ressemble la description de la classe Foo , que la commande help (Foo) affiche :

>>> from foocpp import Foo
>>> help(Foo)

class Foo(sip.wrapper)
 |  Class example from C++ library
 |  
 |  Method resolution order:
 |      Foo
 |      sip.wrapper
 |      sip.simplewrapper
 |      builtins.object
 |  
 |  Methods defined here:
 |  
 |  get_int_val(...)
 |      get_int_val(self) -> int
 |      Return integer value
 |  
 |  get_string_val(...)
 |      get_string_val(self) -> str
 |      Return string value
 |  
 |  set_int_val(...)
 |      set_int_val(self, int)
 |      Set integer value
 |  
 |  set_string_val(...)
 |      set_string_val(self, str)
 |      Set string value
...

Tout a l'air assez soigné et du même type.

Renommer des classes et des méthodes


Comme nous l'avons déjà dit, l'interface fournie par les liaisons Python ne doit pas nécessairement correspondre à l'interface fournie par la bibliothèque C / C ++. Nous avons ajouté des propriétés aux classes ci-dessus, et nous allons maintenant examiner une autre technique qui peut être utile si des conflits de noms de classe ou de fonctions surviennent, par exemple, si un nom de fonction correspond à un mot-clé Python. Pour ce faire, vous pouvez renommer des classes, des fonctions, des exceptions et d'autres entités.

Pour renommer une entité, l'annotation PyName est utilisée , dont la valeur doit être affectée à un nouveau nom d'entité. Le travail avec l'annotation PyName est illustré dans l'exemple du dossier pyfoo_cpp_05 . Cet exemple est basé sur l'exemple précédent.pyfoo_cpp_04 et en diffère par le fichier pyfoocpp.sip , dont le contenu ressemble maintenant à ceci:

%Module(name=foocpp, language="C++")
%DefaultEncoding "UTF-8"
%DefaultDocstringFormat "deindented"
%DefaultDocstringSignature "prepended"

class Foo /PyName=Bar/ {
    %Docstring
    Class example from C++ library
    %End

    %TypeHeaderCode
    #include <foo.h>
    %End

    public:
        Foo(int, const char*);

        void set_int_val(int) /PyName=set_integer_value/;
        %Docstring
            Set integer value
        %End

        int get_int_val() /PyName=get_integer_value/;
        %Docstring
            Return integer value
        %End

        %Property(name=int_val, get=get_integer_value, set=set_integer_value)
        {
            %Docstring
                The property for integer value
            %End
        };

        void set_string_val(const char*) /PyName=set_string_value/;
        %Docstring
            Set string value
        %End

        char* get_string_val() /PyName=get_string_value/;
        %Docstring
            Return string value
        %End

        %Property(name=string_val, get=get_string_value, set=set_string_value)
        {
            %Docstring
                The property for string value
            %End
        };
};

Dans cet exemple, nous avons renommé la classe Foo en classe Bar et avons également attribué d'autres noms à toutes les méthodes à l'aide de l'annotation PyName . Je pense que tout ici est assez simple et clair, la seule chose à laquelle il faut prêter attention est la création de propriétés. Dans la directive % Property , les paramètres get et set doivent spécifier les noms des méthodes, car ils seront appelés dans la classe Python, et non les noms qu'ils ont été initialement appelés en code C ++.

Compilez l'exemple, installez-le et voyez Ă  quoi ressemblera cette classe en Python:

>>> from foocpp import Bar
>>> help(Bar)

Help on class Bar in module foocpp:

class Bar(sip.wrapper)
 |  Class example from C++ library
 |  
 |  Method resolution order:
 |      Bar
 |      sip.wrapper
 |      sip.simplewrapper
 |      builtins.object
 |  
 |  Methods defined here:
 |  
 |  get_integer_value(...)
 |      get_integer_value(self) -> int
 |      Return integer value
 |  
 |  get_string_value(...)
 |      get_string_value(self) -> str
 |      Return string value
 |  
 |  set_integer_value(...)
 |      set_integer_value(self, int)
 |      Set integer value
 |  
 |  set_string_value(...)
 |      set_string_value(self, str)
 |      Set string value
 |  
 |  ----------------------------------------------------------------------
 |  Data descriptors defined here:
 |  
 |  __weakref__
 |      list of weak references to the object (if defined)
 |  
 |  int_val
 |      The property for integer value
 |  
 |  string_val
 |      The property for string value
 |  
 |  ----------------------------------------------------------------------
...

Ça a marché! Nous avons réussi à renommer la classe elle-même et ses méthodes.

Parfois, les bibliothèques utilisent l'accord que les noms de toutes les classes commencent par un préfixe, par exemple, avec la lettre «Q» dans Qt ou «wx» dans wxWidgets. Si, dans votre liaison Python, vous souhaitez renommer toutes les classes en supprimant ces préfixes, afin de ne pas définir l'annotation PyName pour chaque classe, vous pouvez utiliser la directive % AutoPyName . Nous ne considérerons pas cette directive dans cet article, nous dirons seulement que la directive % AutoPyName doit être située à l'intérieur de la directive % Module et nous limiter à un exemple de la documentation:

%Module PyQt5.QtCore
{
    %AutoPyName(remove_leading="Q")
}

Ajouter une conversion de type


Exemple d'utilisation de la classe std :: wstring


Jusqu'à présent, nous avons examiné les fonctions et les classes qui ont travaillé avec des types simples comme int et char * . Pour ces types, SIP a automatiquement créé un convertisseur vers et depuis les classes Python. Dans l'exemple suivant, qui se trouve dans le dossier pyfoo_cpp_06 , nous considérerons le cas où les méthodes de classe acceptent et retournent des objets plus complexes, par exemple des chaînes de STL. Pour simplifier l'exemple et ne pas compliquer la conversion d'octets en Unicode et vice versa, la classe de chaîne std :: wstring sera utilisée dans cet exemple . L'idée de cet exemple est de montrer comment vous pouvez définir manuellement les règles de conversion des classes C ++ vers et depuis les classes Python.

Pour cet exemple, nous allons changer la classe Foo de la bibliothèque foo. Maintenant, la définition de classe ressemblera à ceci (fichier foo.h ):

#ifndef FOO_LIB
#define FOO_LIB

#include <string>

using std::wstring;

class Foo {
    private:
        int _int_val;
        wstring _string_val;
    public:
        Foo(int int_val, wstring string_val);

        void set_int_val(int val);
        int get_int_val();

        void set_string_val(wstring val);
        wstring get_string_val();
};

#endif

L'implémentation de la classe Foo dans le fichier foo.cpp :

#include <string>

#include "foo.h"

using std::wstring;

Foo::Foo(int int_val, wstring string_val):
    _int_val(int_val), _string_val(string_val) {}

void Foo::set_int_val(int val) {
    _int_val = val;
}

int Foo::get_int_val() {
    return _int_val;
}

void Foo::set_string_val(wstring val) {
    _string_val = val;
}

wstring Foo::get_string_val() {
    return _string_val;
}

Et le fichier main.cpp pour vérifier les fonctionnalités de la bibliothèque:

#include <iostream>

#include "foo.h"

using std::cout;
using std::endl;

int main(int argc, char* argv[]) {
    auto foo = Foo(10, L"Hello");
    cout << L"int_val: " << foo.get_int_val() << endl;
    cout << L"string_val: " << foo.get_string_val().c_str() << endl;

    foo.set_int_val(0);
    foo.set_string_val(L"Hello world!");

    cout << L"int_val: " << foo.get_int_val() << endl;
    cout << L"string_val: " << foo.get_string_val().c_str() << endl;
}

Les fichiers foo.h , foo.cpp et main.cpp , comme précédemment, se trouvent dans le dossier foo . Le processus de création du makefile et de la bibliothèque n'a pas changé. Il n'y a pas non plus de modifications importantes dans les fichiers pyproject.toml et project.py .

Mais le fichier pyfoocpp.sip est devenu beaucoup plus compliqué:

%Module(name=foocpp, language="C++")
%DefaultEncoding "UTF-8"

class Foo {
    %TypeHeaderCode
    #include <foo.h>
    %End

    public:
        Foo(int, std::wstring);

        void set_int_val(int);
        int get_int_val();
        %Property(name=int_val, get=get_int_val, set=set_int_val)

        void set_string_val(std::wstring);
        std::wstring get_string_val();
        %Property(name=string_val, get=get_string_val, set=set_string_val)
};

%MappedType std::wstring
{
%TypeHeaderCode
#include <string>
%End

%ConvertFromTypeCode
    // Convert an std::wstring to a Python (Unicode) string
    PyObject* newstring;
    newstring = PyUnicode_FromWideChar(sipCpp->data(), -1);
    return newstring;
%End

%ConvertToTypeCode
    // Convert a Python (Unicode) string to an std::wstring
    if (sipIsErr == NULL) {
        return PyUnicode_Check(sipPy);
    }
    if (PyUnicode_Check(sipPy)) {
        *sipCppPtr = new std::wstring(PyUnicode_AS_UNICODE(sipPy));
        return 1;
    }
    return 0;
%End
};

À des fins d' illustration , le fichier pyfoocpp.sip n'ajoute pas de lignes de documentation. Si nous ne conservions que la déclaration de la classe Foo dans le fichier pyfoocpp.sip sans la directive % MappedType suivante , nous obtiendrions l'erreur suivante pendant le processus de génération:

$ sip-wheel

These bindings will be built: pyfoocpp.
Generating the pyfoocpp bindings...
sip-wheel: std::wstring is undefined

Nous devons décrire explicitement comment un objet de type std :: wstring sera converti en un objet Python, et également décrire la transformation inverse. Pour décrire la conversion, nous devons travailler à un niveau assez bas dans le langage C et utiliser l' API Python / C . Étant donné que l'API Python / C est un gros sujet, digne même d'un article séparé, mais d'un livre, dans cette section, nous ne considérerons que les fonctions utilisées dans l'exemple, sans entrer dans trop de détails.

Pour déclarer des conversions d'objets C ++ en Python et vice versa, la directive % MappedType est prévue , à l'intérieur de laquelle il peut y avoir trois autres directives: % TypeHeaderCode , % ConvertToTypeCode et % ConvertFromTypeCode. Après l'expression % MappedType , vous devez spécifier le type pour lequel les convertisseurs seront créés. Dans notre cas, la directive commence par l'expression % MappedType std :: wstring . Nous avons déjà rencontré la

directive % TypeHeaderCode dans la section Création d'une liaison pour une bibliothèque en C ++ . Permettez-moi de vous rappeler que cette directive est destinée à déclarer les types utilisés ou à inclure les fichiers d'en-tête dans lesquels ils sont déclarés. Dans cet exemple , la chaîne du fichier d'en-tête , où la classe std :: string est déclarée, est connectée à l'intérieur de la directive % TypeHeaderCode . Maintenant, nous devons décrire les transformations



% ConvertFromTypeCode. Conversion d'objets C ++ en Python


Nous commençons par convertir les objets std :: wstring en la classe Python str . Cette conversion dans l'exemple est la suivante:

%ConvertFromTypeCode
    // Convert an std::wstring to a Python (Unicode) string
    PyObject* newstring;
    newstring = PyUnicode_FromWideChar(sipCpp->data(), -1);
    return newstring;
%End

À l'intérieur de cette directive, nous avons la variable sipCpp - un pointeur vers un objet à partir du code C ++, par lequel nous devons créer un objet Python et renvoyer l'objet créé à partir de la directive à l'aide de l' instruction return . Dans ce cas, la variable sipCpp est de type std :: wstring * . Pour créer la classe str , utilisez la fonction PyUnicode_FromWideChar de l'API Python / C. Cette fonction accepte un tableau (pointeur) de type const wchar_t * w comme premier paramètre , et la taille de ce tableau comme deuxième paramètre. Si vous passez la valeur -1 comme deuxième paramètre, la fonction PyUnicode_FromWideChar elle-même calculera la longueur à l'aide de la fonctionwcslen .

Pour obtenir le tableau wchar_t * , utilisez la méthode data de la classe std :: wstring .

La fonction PyUnicode_FromWideChar renvoie un pointeur sur PyObject ou NULL en cas d'erreur. PyObject est n'importe quel objet Python, dans ce cas, ce sera la classe str . Dans l'API Python / C, le travail avec les objets s'effectue généralement via des pointeurs PyObject * , dans ce cas, nous renvoyons le pointeur PyObject * à partir de la directive % ConvertFromTypeCode .

% ConvertToTypeCode. Convertir des objets Python en C ++


La conversion inverse d'un objet Python (essentiellement de PyObject * ) en classe std :: wstring est décrite dans la directive % ConvertToTypeCode . Dans l'exemple pyfoo_cpp_06, la conversion est la suivante:

%ConvertToTypeCode
    // Convert a Python (Unicode) string to an std::wstring
    if (sipIsErr == NULL) {
        return PyUnicode_Check(sipPy);
    }
    if (PyUnicode_Check(sipPy)) {
        *sipCppPtr = new std::wstring(PyUnicode_AS_UNICODE(sipPy));
        return 1;
    }
    return 0;
%End

Le code de la directive % ConvertToTypeCode semble plus compliqué, car pendant le processus de conversion, il est appelé plusieurs fois à des fins différentes. Dans la directive % ConvertToTypeCode , SIP crée plusieurs variables que nous pouvons (ou devons) utiliser.

L'une de ces variables, PyObject * sipPy, est un objet Python dont vous avez besoin pour créer une instance de la classe std :: wstring dans ce cas . Le résultat devra être écrit dans une autre variable - sipCppPtr est un double pointeur vers l'objet créé, c'est-à-dire dans notre cas, cette variable sera de type std :: wstring ** .

Un autre % ConvertToTypeCode créé à l'intérieur de la directivela variable est int * sipIsErr . Si la valeur de cette variable est NULL , la directive % ConvertToTypeCode est appelée uniquement pour vérifier si la conversion de type est possible. Dans ce cas, nous ne sommes pas obligés d'effectuer la transformation, mais il suffit de vérifier si cela est possible en principe. Si possible, la directive doit renvoyer une valeur non nulle, sinon, si la conversion n'est pas possible, ils doivent retourner 0. Si ce pointeur n'est pas NULL , alors vous devez effectuer la conversion, et si une erreur se produit pendant la conversion, le code d'erreur entier peut être enregistré dans cette variable (étant donné que cette variable est un pointeur vers int * ).

Dans cet exemple, pour vérifier que sipPy est une chaîne unicode (classe str ), la macro PyUnicode_Check est utilisée , qui prend un argument de type PyObject * si l'argument passé est une chaîne unicode ou une classe qui en dérive.

La conversion en un objet C ++ est effectuée à l'aide de la chaîne * sipCppPtr = new std :: wstring (PyUnicode_AS_UNICODE (sipPy)); . Ici, la macro PyUnicode_AS_UNICODE est appelée à partir de l'API Python / C, qui renvoie un tableau de type Py_UNICODE * , qui équivaut à wchar_t * . Ce tableau est passé au constructeur de la classe std :: wstring. Comme mentionné ci-dessus, le résultat est stocké dans la variable sipCppPtr .

Pour le moment, la directive PyUnicode_AS_UNICODE est déconseillée et il est recommandé d'utiliser d'autres macros, mais cette macro est utilisée pour simplifier l'exemple.

Si la conversion a réussi, la directive % ConvertToTypeCode doit renvoyer une valeur non nulle (dans ce cas 1), et en cas d'erreur, elle doit retourner 0.

VĂ©rifier


Nous avons décrit la conversion du type std :: wstring en str et vice versa, maintenant nous pouvons nous assurer que le package est correctement construit et que la liaison fonctionne comme il se doit. Pour construire, appelez sip-wheel , puis installez le package à l'aide de pip et vérifiez l'opérabilité en mode de commande Python:

>>> from foocpp import Foo
>>> x = Foo(10, 'Hello')

>>> x.string_val
'Hello'

>>> x.string_val = ''
>>> x.string_val
''

>>> x.get_string_val()
''

Comme vous pouvez le voir, tout fonctionne, il n'y a pas non plus de problème avec la langue russe, c'est-à-dire Les conversions de chaînes Unicode ont fonctionné correctement.

Conclusion


Dans cet article, nous avons couvert les bases de l'utilisation de SIP pour créer des liaisons Python pour des bibliothèques écrites en C et C ++. Tout d'abord (dans la première partie ), nous avons créé une bibliothèque simple en C et trouvé les fichiers qui doivent être créés pour fonctionner avec SIP. Le fichier pyproject.toml contient des informations sur le package (nom, numéro de version, licence et chemins d'accès aux fichiers d'en-tête et d'objet). À l'aide du fichier project.py , vous pouvez influencer le processus de génération du package Python, par exemple, commencer à créer la bibliothèque C / C ++ ou laisser l'utilisateur spécifier l'emplacement des fichiers d'en-tête et d'objet de la bibliothèque.

Dans le fichier * .sipdécrit l'interface du module Python listant les fonctions et classes qui seront contenues dans le module. Des directives et des annotations sont utilisées pour décrire l'interface dans le fichier * .sip . L'interface de classe Python ne doit pas nécessairement correspondre à l'interface de classe C ++. Par exemple, vous pouvez ajouter des propriétés aux classes à l'aide de la directive % Property , renommer des entités à l'aide de l'annotation / PyName / et ajouter des lignes de documentation à l'aide de la directive % Docstring .

Types élémentaires comme int , char , char *etc. SIP se convertit automatiquement en classes Python similaires, mais si vous devez effectuer une conversion plus complexe, vous devez la programmer vous-même dans la directive % MappedType à l'aide de l'API Python / C. La conversion de la classe Python en C ++ doit être effectuée dans la directive imbriquée % ConvertToTypeCode . La conversion d'un type C ++ en une classe Python doit être effectuée dans la directive imbriquée % ConvertFromTypeCode .

Certaines directives comme % DefaultEncoding , % DefaultDocstringFormat et % DefaultDocstringSignature sont auxiliaires et vous permettent de définir des valeurs par défaut pour les cas où certains paramètres d'annotation ne sont pas définis explicitement.

Dans cet article, nous n'avons examiné que les directives et les annotations les plus simples et les plus simples, mais bon nombre d'entre elles ont été ignorées. Par exemple, il existe des directives pour gérer GIL, pour créer de nouvelles exceptions Python, pour gérer manuellement la mémoire et les récupérateurs de place, pour peaufiner les classes pour différents systèmes d'exploitation, et bien d'autres qui peuvent être utiles lors de la création de liaisons de bibliothèque C / C ++ complexes. Nous avons également ignoré le problème de la création de packages pour différents systèmes d'exploitation, en nous limitant à la construction sous Linux à l'aide de compilateurs gcc / g ++.

Références



All Articles