Crear enlaces de Python para bibliotecas C / C ++ usando SIP. Parte 2

En la primera parte del artículo, examinamos los conceptos básicos del trabajo con la utilidad SIP diseñada para crear enlaces de Python para bibliotecas escritas en C y C ++. Analizamos los archivos básicos que necesita crear para trabajar con SIP y comenzamos a buscar directivas y anotaciones. Hasta ahora, hemos realizado el enlace para una biblioteca simple escrita en C. En esta parte, descubriremos cómo hacer el enlace para una biblioteca C ++ que contiene clases. Usando el ejemplo de esta biblioteca, veremos qué técnicas pueden ser útiles al trabajar con una biblioteca orientada a objetos, y al mismo tiempo trataremos con nuevas directivas y anotaciones para nosotros.

Todos los ejemplos del artículo están disponibles en el repositorio de github en: https://github.com/Jenyay/sip-examples .

Hacer un enlace para una biblioteca en C ++


El siguiente ejemplo, que consideraremos, está en la carpeta pyfoo_cpp_01 .

Primero, cree una biblioteca para la cual haremos el enlace. La biblioteca seguirá residiendo en la carpeta foo y contendrá una clase: Foo . El archivo de encabezado foo.h con la declaración de esta clase es el siguiente:

#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

Esta es una clase simple con dos captadores y establecedores que establece y devuelve valores de tipo int y char * . La implementación de la clase es la siguiente:

#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;
}

Para probar la funcionalidad de la biblioteca, la carpeta foo también contiene el archivo main.cpp usando la clase 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;
}

Para construir la biblioteca foo , use el siguiente Makefile :

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 diferencia con el Makefile en los ejemplos anteriores, además de cambiar el compilador de gcc a g ++ , es que se agregó otra opción -fPIC para la compilación , que le dice al compilador que coloque el código en la biblioteca de cierta manera (el llamado "código independiente de la posición"). Como este artículo no trata sobre compiladores, no examinaremos con más detalle qué hace este parámetro y por qué es necesario.

Comencemos por esta biblioteca. Los archivos pyproject.toml y project.py casi no han cambiado desde los ejemplos anteriores. Así es como se ve el archivo 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"]

Ahora nuestros ejemplos escritos en C ++ se empaquetarán en el paquete Pyfoocpp Python , este es quizás el único cambio notable en este archivo.

El archivo project.py permanece igual que en el ejemplo 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()

Y aquí consideraremos el archivo pyfoocpp.sip con más detalle. Permítame recordarle que este archivo describe la interfaz para el futuro módulo de Python: qué debería incluir, cómo debería ser la interfaz de clase, etc. No se requiere que el archivo .sip repita el archivo de encabezado de la biblioteca, aunque tendrán mucho en común. Dentro de esta clase, se pueden agregar nuevos métodos que no estaban en la clase original. Aquellos. la interfaz descrita en el archivo .sip puede adaptar las clases de la biblioteca a los principios aceptados en Python, si es necesario. En el archivo pyfoocpp.sip veremos nuevas directivas para nosotros.

Primero, veamos qué contiene este archivo:

%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();
};

Las primeras líneas ya deberían estar claras para nosotros de los ejemplos anteriores. En la directiva % Module , indicamos el nombre del módulo Python que se creará (es decir, para usar este módulo, necesitaremos usar los comandos import foocpp o de foocpp import ... En la misma directiva, indicamos que ahora tenemos el idioma - C ++. La directiva% DefaultEncoding establece la codificación que se utilizará para convertir la cadena Python a los tipos char , const char , char * y const char * .

Luego sigue la declaración de la interfaz de la clase Foo . Inmediatamente después de la declaración de la clase Footodavía no se usa la directiva % TypeHeaderCode , que termina con la directiva % End . La directiva% TypeHeaderCode debe contener código que declare la interfaz de la clase C ++ para la que se está creando el contenedor. Como regla, en esta directiva es suficiente incluir el archivo de encabezado con la declaración de clase.

Después de eso, se enumeran los métodos de clase que se convertirán a los métodos de la clase Foo para el lenguaje Python. Es importante tener en cuenta que en este punto solo declaramos métodos públicos que serán accesibles desde la clase Foo en Python (ya que no hay miembros privados y protegidos en Python). Como utilizamos la directiva % DefaultEncoding al principio, luego, en los métodos que toman argumentos de tipo const char * , no puede usar la anotación de codificación para especificar la codificación para convertir estos parámetros a cadenas de Python y viceversa.

Ahora solo necesitamos compilar el paquete Pyfoocpp Python y probarlo. Pero antes de ensamblar un paquete de rueda completo, usemos el comando sip-build y veamos qué archivos fuente creará SIP para una compilación posterior, e intentemos encontrar en ellos algo similar a la clase que se creará en el código Python. Para hacer esto, se debe llamar al comando sip-build anterior en la carpeta pyfoo_cpp_01 . Como resultado, se creará la carpeta de compilación . con los siguientes contenidos:

construir
└── 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


Como una tarea adicional, considere cuidadosamente el archivo sipfoocppFoo.cpp (no lo discutiremos en detalle en este artículo):

/*
 * 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
};

Ahora construya el paquete usando el comando sip-wheel . Después de ejecutar este comando, si todo va bien, se creará un archivo pyfoocpp-0.1-cp38-cp38-manylinux1_x86_64.whl o con un nombre similar. Instálelo utilizando el comando pip install --user pyfoocpp-0.1-cp38-cp38-manylinux1_x86_64.whl y ejecute el intérprete de Python para verificar:

>>> 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()
''

¡Trabajos! Por lo tanto, acabamos de crear un módulo Python con un enlace para una clase en C ++. Además, traeremos belleza a esta clase y agregaremos varias comodidades.

Agregar propiedades


No se requiere que las clases creadas usando SIP repitan exactamente la interfaz de clase C ++. Por ejemplo, en nuestra clase Foo , hay dos captadores y dos establecedores, que se pueden combinar claramente en una propiedad para hacer que la clase sea más "Python". Agregar propiedades usando sip es bastante fácil ya que se hace, muestra un ejemplo en la carpeta pyfoo_cpp_02 .

Este ejemplo es similar al anterior, la principal diferencia está en el archivo pyfoocpp.sip , que ahora se ve así:

%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)
};

Como puede ver, todo es bastante simple. Para agregar una propiedad, se pretende la directiva % Property , que tiene dos parámetros obligatorios: nombre para especificar el nombre de la propiedad, y llegar a especificar un método que devuelve un valor (getter). Es posible que no haya un establecedor, pero si la propiedad también necesita valores asignados, el método del establecedor se especifica como el valor del parámetro establecido . En nuestro ejemplo, las propiedades se crean de una manera bastante sencilla, ya que ya hay funciones que funcionan como captadores y definidores.

Solo podemos recolectar el paquete usando el comando sip-wheel , instalarlo, después de eso verificaremos el funcionamiento de las propiedades en el modo de comando del intérprete de 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()
''

Como puede ver en el ejemplo del uso de la clase Foo , las propiedades int_val y string_val funcionan tanto para leer como para escribir.

Agregar líneas de documentación


Continuaremos mejorando nuestra clase de Foo . El siguiente ejemplo, que se encuentra en la carpeta pyfoo_cpp_03, muestra cómo agregar líneas de documentación (docstring) a varios elementos de una clase. Este ejemplo se basa en el anterior, y el cambio principal se refiere al archivo pyfoocpp.sip . Aquí están sus contenidos:

%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
        };
};

Como ya entendió, para agregar líneas de documentación a cualquier elemento de la clase, debe usar la directiva % Docstring . Este ejemplo muestra varias formas de usar esta directiva. Para una mejor comprensión de este ejemplo, compilemos de inmediato el paquete pyfoocpp utilizando el comando sip-wheel , instálelo y descubriremos secuencialmente qué parámetro de esta directiva afecta qué, teniendo en cuenta las líneas de documentación resultantes en el modo de comando Python. Permítame recordarle que las líneas de documentación se almacenan como miembros de los objetos __doc__ a los que pertenecen estas líneas.

La primera línea de documentación es para la clase Foo .. Como puede ver, todas las líneas de documentación se encuentran entre las directivas% Docstring y % End . Las líneas 5-7 de este ejemplo no utilizan ningún parámetro adicional de la directiva % Docstring , por lo que la línea de documentación se escribirá en la clase Foo tal como está. Es por eso que no hay sangría en las líneas 5-7, de lo contrario, las sangrías frente a la línea de documentación también caerían en Foo .__ doc__. Nos aseguraremos de que la clase Foo realmente contenga la línea de documentación que presentamos:

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

La siguiente directiva % Docstring , ubicada en las líneas 17-19, usa dos parámetros a la vez. El parámetro de formato puede tomar uno de dos valores: "crudo" o "desinfectado". En el primer caso, las líneas de documentación se guardan a medida que se escriben, y en el segundo caso, los caracteres de espacio iniciales (pero no las pestañas) se eliminan. El valor predeterminado para el caso si no se especifica el parámetro de formato se puede establecer utilizando la directiva % DefaultDocstringFormat (lo consideraremos un poco más adelante), y si no se especifica, se supone que format = "raw" .

Además de las líneas de documentación especificadas, SIP agrega una descripción de su firma (qué tipos de variables se esperan en la entrada y qué tipo devuelve la función) a las líneas de documentación de funciones. El parámetro de firma indica dónde colocar dicha firma: antes de la línea de documentación especificada ( firma = "antepuesta" ), después ( firma = "adjunta" ) o no agrega la firma ( firma = "descartada" ).

Nuestro ejemplo establece el parámetro signature = "prepended" para las funciones get_int_val y set_int_val , así como signature = " appended " para las funciones get_string_val y set_string_val. El parámetro format = "deindented" también se agregó para eliminar espacios al comienzo de la línea de documentación. Veamos cómo funcionan estos parámetros 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)'

Como puede ver, utilizando el parámetro de firma de la directiva % Docstring , puede cambiar la posición de la descripción de firma de función en la línea de documentación.

Ahora considere agregar una línea de documentación a las propiedades. Tenga en cuenta que en este caso, las directivas% Docstring ... % End están encerradas entre llaves después de la directiva% Property. Este formato de grabación se describe en la documentación de la directiva % Property .

Observe también cómo especificamos el parámetro de directiva % Docstring . Tal formato para escribir directivas es posible si establecemos solo el primer parámetro de la directiva (en este caso, el parámetro de formato) Por lo tanto, en este ejemplo, se utilizan tres métodos para usar directivas a la vez.

Asegúrese de que la línea de documentación para las propiedades esté establecida:

>>> 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)
...


Simplifiquemos este ejemplo estableciendo los valores predeterminados para los parámetros de formato y firma utilizando las directivas% DefaultDocstringFormat y % DefaultDocstringSignature . El uso de estas directivas se muestra en el ejemplo de la carpeta pyfoo_cpp_04 . El archivo pyfoocpp.sip en este ejemplo contiene el siguiente código:

%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
        };
};

Al principio del archivo, se agregaron las líneas % DefaultDocstringFormat "deindented" y % DefaultDocstringSignature "prepended" , y luego se eliminaron todos los parámetros de la directiva % Docstring .

Después de ensamblar e instalar este ejemplo, podemos ver cómo se ve la descripción de la clase Foo ahora , que muestra el comando de ayuda (Foo) :

>>> 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
...

Todo parece bastante ordenado y del mismo tipo.

Renombrar clases y métodos


Como ya dijimos, la interfaz proporcionada por los enlaces de Python no tiene que coincidir con la interfaz que proporciona la biblioteca C / C ++. Agregamos propiedades a las clases anteriores, y ahora veremos una técnica más que puede ser útil si surgen conflictos de nombres de clase o funciones, por ejemplo, si el nombre de una función coincide con alguna palabra clave de Python. Para hacer esto, puede renombrar clases, funciones, excepciones y otras entidades.

Para cambiar el nombre de una entidad, se utiliza la anotación PyName , cuyo valor debe asignarse a un nuevo nombre de entidad. El trabajo con la anotación PyName se muestra en el ejemplo de la carpeta pyfoo_cpp_05 . Este ejemplo se basa en el ejemplo anterior.pyfoo_cpp_04 y difiere de él por el archivo pyfoocpp.sip , cuyo contenido ahora se ve así:

%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
        };
};

En este ejemplo, cambiamos el nombre de la clase Foo a la clase Bar , y también asignamos otros nombres a todos los métodos usando la anotación PyName . Creo que todo aquí es bastante simple y claro, lo único que vale la pena prestar atención es la creación de propiedades. En la directiva % Property , los parámetros get y set deben especificar los nombres de los métodos, como se llamarán en la clase Python, y no los nombres que originalmente se llamaron al código C ++.

Compile el ejemplo, instálelo y vea cómo se verá esta clase 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
 |  
 |  ----------------------------------------------------------------------
...

¡Funcionó! Logramos cambiar el nombre de la clase en sí y sus métodos.

Algunas veces las bibliotecas usan el acuerdo de que los nombres de todas las clases comienzan con un prefijo, por ejemplo, con la letra "Q" en Qt o "wx" en wxWidgets. Si en su enlace de Python desea cambiar el nombre de todas las clases, deshacerse de dichos prefijos, para no establecer la anotación PyName para cada clase, puede usar la directiva % AutoPyName . No consideraremos esta directiva en este artículo, solo diremos que la directiva % AutoPyName debe ubicarse dentro de la directiva % Module y restringirnos a un ejemplo de la documentación:

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

Agregar conversión de tipo


Ejemplo usando la clase std :: wstring


Hasta ahora, hemos analizado funciones y clases que han funcionado con tipos simples como int y char * . Para estos tipos, SIP creó automáticamente un convertidor hacia y desde las clases de Python. En el siguiente ejemplo, que se encuentra en la carpeta pyfoo_cpp_06 , consideraremos el caso cuando los métodos de clase aceptan y devuelven objetos más complejos, por ejemplo, cadenas de STL. Para simplificar el ejemplo y no complicar la conversión de bytes a Unicode y viceversa, la clase de cadena std :: wstring se usará en este ejemplo . La idea de este ejemplo es mostrar cómo puede establecer manualmente las reglas para convertir las clases de C ++ ay desde las clases de Python.

Para este ejemplo, cambiaremos la clase Foo de la biblioteca foo. Ahora la definición de clase se verá así (archivo 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

La implementación de la clase Foo en el archivo 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;
}

Y el archivo main.cpp para verificar la funcionalidad de la biblioteca:

#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;
}

Los archivos foo.h , foo.cpp y main.cpp , como antes, se encuentran en la carpeta foo . El proceso de compilación del archivo MAKE y la biblioteca no ha cambiado. Tampoco hay cambios significativos en los archivos pyproject.toml y project.py .

Pero el archivo pyfoocpp.sip se ha vuelto notablemente más complicado:

%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
};

Con fines ilustrativos , el archivo pyfoocpp.sip no agrega líneas de documentación. Si dejáramos solo la declaración de la clase Foo en el archivo pyfoocpp.sip sin la directiva % MappedType posterior, obtendríamos el siguiente error durante el proceso de compilación:

$ sip-wheel

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

Necesitamos describir explícitamente cómo un objeto de tipo std :: wstring se convertirá en algún objeto de Python, y también describir la transformación inversa. Para describir la conversión, tendremos que trabajar a un nivel bastante bajo en el lenguaje C y utilizar la API Python / C . Dado que la API Python / C es un gran tema, digno de incluso un artículo separado, pero un libro, en esta sección consideraremos solo aquellas funciones que se usan en el ejemplo, sin entrar en demasiados detalles.

Para declarar conversiones de objetos C ++ a Python y viceversa, se pretende la directiva % MappedType , dentro de la cual puede haber otras tres directivas: % TypeHeaderCode , % ConvertToTypeCode y % ConvertFromTypeCode. Después de la expresión % MappedType , debe especificar el tipo para el que se crearán los convertidores. En nuestro caso, la directiva comienza con la expresión % MappedType std :: wstring . Ya hemos cumplido con la

directiva % TypeHeaderCode en la sección Creación de un enlace para una biblioteca en C ++ . Permítame recordarle que esta directiva tiene la intención de declarar los tipos utilizados o incluir los archivos de encabezado en los que se declaran. En este ejemplo , la cadena del archivo de encabezado , donde se declara la clase std :: string , está conectada dentro de la directiva % TypeHeaderCode . Ahora tenemos que describir las transformaciones.



% ConvertFromTypeCode. Convertir objetos C ++ a Python


Comenzamos convirtiendo objetos std :: wstring a la clase Python str . Esta conversión en el ejemplo es la siguiente:

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

Dentro de esta directiva, tenemos la variable sipCpp : un puntero a un objeto desde el código C ++, por el cual necesitamos crear un objeto Python y devolver el objeto creado desde la directiva usando la instrucción return . En este caso, la variable sipCpp es de tipo std :: wstring * . Para crear la clase str , use la función PyUnicode_FromWideChar de la API Python / C. Esta función acepta una matriz (puntero) de tipo const wchar_t * w como primer parámetro , y el tamaño de esta matriz como segundo parámetro. Si pasa el valor -1 como el segundo parámetro, la función PyUnicode_FromWideChar calculará la longitud utilizando la funciónwcslen .

Para obtener la matriz wchar_t * , use el método de datos de la clase std :: wstring .

La función PyUnicode_FromWideChar devuelve un puntero a PyObject o NULL en caso de error. PyObject es cualquier objeto de Python, en este caso será la clase str . En la API Python / C, el trabajo con objetos generalmente ocurre a través de punteros PyObject * , por lo que en este caso, devolvemos el puntero PyObject * de la directiva % ConvertFromTypeCode .

% ConvertToTypeCode. Convertir objetos de Python a C ++


La conversión inversa de un objeto Python (esencialmente de PyObject * ) a la clase std :: wstring se describe en la directiva % ConvertToTypeCode . En el ejemplo pyfoo_cpp_06, la conversión es la siguiente:

%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

El código de la directiva % ConvertToTypeCode parece más complicado, porque durante el proceso de conversión se llama varias veces para diferentes propósitos. Dentro de la directiva % ConvertToTypeCode , SIP crea varias variables que podemos (o debemos) usar.

Una de estas variables, PyObject * sipPy, es un objeto Python que necesita para crear una instancia de la clase std :: wstring en este caso . El resultado deberá escribirse en otra variable: sipCppPtr es un puntero doble al objeto creado, es decir en nuestro caso, esta variable será de tipo std :: wstring ** .

Otro % ConvertToTypeCode creado dentro de la directivala variable es int * sipIsErr . Si el valor de esta variable es NULL , entonces se llama a la directiva % ConvertToTypeCode solo para verificar si es posible la conversión de tipos. En este caso, no estamos obligados a realizar la transformación, pero solo necesitamos comprobar si es posible en principio. Si es posible, la directiva debe devolver un valor distinto de cero, de lo contrario, si la conversión no es posible, debe devolver 0. Si este puntero no es NULL , entonces debe realizar la conversión, y si se produce un error durante la conversión, se puede guardar el código de error entero en esta variable (dado que esta variable es un puntero a int * ).

En este ejemplo, para verificar que sipPy es una cadena unicode (clase str ), se usa la macro PyUnicode_Check , que toma un argumento de tipo PyObject * si el argumento pasado es una cadena unicode o una clase derivada de ella.

La conversión a un objeto C ++ se realiza utilizando la cadena * sipCppPtr = new std :: wstring (PyUnicode_AS_UNICODE (sipPy)); . Esto llama a la macro PyUnicode_AS_UNICODE de la API Python / C, que devuelve una matriz de tipo Py_UNICODE * , que es equivalente a wchar_t * . Esta matriz se pasa al constructor de la clase std :: wstring. Como se mencionó anteriormente, el resultado se almacena en la variable sipCppPtr .

Por el momento, la directiva PyUnicode_AS_UNICODE está en desuso y se recomienda usar otras macros, pero esta macro se usa para simplificar el ejemplo.

Si la conversión fue exitosa, la directiva % ConvertToTypeCode debería devolver un valor distinto de cero (en este caso 1), y en caso de error debería devolver 0.

Cheque


Describimos la conversión de tipo std :: wstring a str y viceversa, ahora podemos asegurarnos de que el paquete se compila correctamente y que el enlace funciona como debería. Para construir, llame a sip-wheel , luego instale el paquete usando pip y verifique la operabilidad en el modo de comando Python:

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

>>> x.string_val
'Hello'

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

>>> x.get_string_val()
''

Como puede ver, todo funciona, tampoco hay problemas con el idioma ruso, es decir, Las conversiones de cadena Unicode se realizaron correctamente.

Conclusión


En este artículo, cubrimos los conceptos básicos del uso de SIP para crear enlaces de Python para bibliotecas escritas en C y C ++. Primero (en la primera parte ) creamos una biblioteca simple en C y descubrimos los archivos que deben crearse para funcionar con SIP. El archivo pyproject.toml contiene información sobre el paquete (nombre, número de versión, licencia y rutas a los archivos de encabezado y objeto). Usando el archivo project.py , puede influir en el proceso de construcción del paquete Python, por ejemplo, comenzar a construir la biblioteca C / C ++ o permitir al usuario especificar la ubicación de los archivos de encabezado y objeto de la biblioteca.

En el archivo * .sipdescribe la interfaz del módulo de Python que enumera las funciones y clases que se incluirán en el módulo. Las directivas y anotaciones se usan para describir la interfaz en el archivo * .sip . La interfaz de clase Python no tiene que coincidir con la interfaz de clase C ++. Por ejemplo, puede agregar propiedades a las clases usando la directiva % Property , cambiar el nombre de las entidades usando la anotación / PyName / y agregar líneas de documentación usando la directiva % Docstring .

Tipos elementales como int , char , char *etc. SIP convierte automáticamente a clases similares de Python, pero si necesita realizar una conversión más compleja, debe programarla usted mismo dentro de la directiva % MappedType usando la API Python / C. La conversión de la clase Python a C ++ debe hacerse en la directiva anidada % ConvertToTypeCode . La conversión de un tipo C ++ a una clase Python debe realizarse en la directiva anidada % ConvertFromTypeCode .

Algunas directivas como % DefaultEncoding , % DefaultDocstringFormat y % DefaultDocstringSignature son auxiliares y le permiten establecer valores predeterminados para los casos en que algunos parámetros de anotación no se establecen explícitamente.

En este artículo examinamos solo las directivas y anotaciones básicas y más simples, pero muchas de ellas fueron ignoradas. Por ejemplo, existen directivas para administrar GIL, para crear nuevas excepciones de Python, para administrar manualmente la memoria y los recolectores de basura, para ajustar las clases para diferentes sistemas operativos y muchos otros que pueden ser útiles al crear enlaces complejos de bibliotecas C / C ++. También pasamos por alto el tema de construir paquetes para diferentes sistemas operativos, limitándonos a construir bajo Linux usando compiladores gcc / g ++.

Referencias



All Articles