Criando ligações Python para bibliotecas C / C ++ usando SIP. Parte 2

Na primeira parte do artigo, examinamos os conceitos básicos de trabalho com o utilitário SIP projetado para criar ligações Python para bibliotecas escritas em C e C ++. Examinamos os arquivos básicos que você precisa criar para trabalhar com o SIP e começamos a examinar diretivas e anotações. Até agora, fizemos a ligação para uma biblioteca simples escrita em C. Nesta parte, descobriremos como fazer a ligação para uma biblioteca C ++ que contém classes. Usando o exemplo desta biblioteca, veremos quais técnicas podem ser úteis ao trabalhar com uma biblioteca orientada a objetos e, ao mesmo tempo, lidaremos com novas diretivas e anotações para nós.

Todos os exemplos deste artigo estão disponíveis no repositório do github em: https://github.com/Jenyay/sip-examples .

Criando uma ligação para uma biblioteca em C ++


O exemplo a seguir, que consideraremos, está na pasta pyfoo_cpp_01 .

Primeiro, crie uma biblioteca para a qual faremos a ligação. A biblioteca ainda residirá na pasta foo e conterá uma classe - Foo . O arquivo de cabeçalho foo.h com a declaração desta classe é o seguinte:

#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 é uma classe simples com dois getters e setters que configuram e retornam valores do tipo int e char * . A implementação da classe é a seguinte:

#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 testar a funcionalidade da biblioteca, a pasta foo também contém o arquivo main.cpp usando a 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;
}

Para criar a biblioteca foo , use o seguinte 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)/*

A diferença do Makefile nos exemplos anteriores, além de alterar o compilador de gcc para g ++ , é que outra opção -fPIC foi adicionada para compilação , o que instrui o compilador a colocar o código na biblioteca de uma certa maneira (o chamado "código independente de posição"). Como este artigo não trata de compiladores, não examinaremos em mais detalhes o que esse parâmetro faz e por que é necessário.

Vamos começar a amarrar para esta biblioteca. Os arquivos pyproject.toml e project.py permanecem praticamente inalterados nos exemplos anteriores. Aqui está a aparência do arquivo pyproject.toml agora :

[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"]

Agora, nossos exemplos escritos em C ++ serão empacotados no pacote pyfoocpp Python , talvez essa seja a única alteração perceptível nesse arquivo.

O arquivo project.py permanece o mesmo que no exemplo 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()

E aqui consideraremos o arquivo pyfoocpp.sip com mais detalhes. Deixe-me lembrá-lo de que este arquivo descreve a interface para o futuro módulo Python: o que deve incluir, a aparência da interface da classe etc. O arquivo .sip não é necessário para repetir o arquivo de cabeçalho da biblioteca, embora eles tenham muito em comum. Dentro desta classe, podem ser adicionados novos métodos que não estavam na classe original. Essa. a interface descrita no arquivo .sip pode adaptar as classes da biblioteca aos princípios aceitos no Python, se necessário. No arquivo pyfoocpp.sip , veremos novas diretivas para nós.

Primeiro, vamos ver o que esse arquivo contém:

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

As primeiras linhas já devem estar claras para os exemplos anteriores. Na diretiva % Module , indicamos o nome do módulo Python que será criado (ou seja, para usar este módulo, precisaremos usar os comandos import foocpp ou foocpp import .... Na mesma diretiva, indicamos que agora temos a linguagem - C ++. A diretiva% DefaultEncoding define a codificação que será usada para converter a string Python nos tipos char , const char , char * e const char * .

Em seguida, a declaração da interface da classe Foo segue . Imediatamente após a declaração da classe Fooainda não é usada a diretiva % TypeHeaderCode , que termina com a diretiva % End . A diretiva% TypeHeaderCode deve conter um código que declare a interface da classe C ++ para a qual o wrapper está sendo criado. Como regra, nesta diretiva é suficiente incluir o arquivo de cabeçalho na declaração de classe.

Depois disso, os métodos de classe que serão convertidos nos métodos da classe Foo para a linguagem Python são listados . É importante observar que, neste ponto, declaramos apenas métodos públicos que serão acessíveis a partir da classe Foo no Python (já que não há membros privados e protegidos no Python). Como usamos a diretiva % DefaultEncoding no início, em métodos que usam argumentos do tipo const char * , não é possível usar a anotação Encoding para especificar a codificação para converter esses parâmetros em seqüências de caracteres Python e vice-versa.

Agora só precisamos compilar o pacote pyfoocpp Python e testá-lo. Mas antes de montar um pacote completo de roda, vamos usar o comando sip-build e ver quais arquivos de origem o SIP criará para compilação posterior e tentar encontrar neles algo semelhante à classe que será criada no código Python. Para fazer isso, o comando sip-build acima deve ser chamado na pasta pyfoo_cpp_01 . Como resultado, a pasta de compilação será criada. com o seguinte conteúdo:

Construir
Fo── 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 uma tarefa adicional, considere cuidadosamente o arquivo sipfoocppFoo.cpp (não o discutiremos em detalhes neste artigo):

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

Agora construa o pacote usando o comando sip-wheel . Depois de executar este comando, se tudo der certo, será criado um arquivo pyfoocpp-0.1-cp38-cp38-manylinux1_x86_64.whl ou com um nome semelhante. Instale-o usando o comando pip install --user pyfoocpp-0.1-cp38-cp38-manylinux1_x86_64.whl e execute o interpretador 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()
''

Trabalho! Assim, acabamos de criar um módulo Python com uma ligação para uma classe em C ++. Além disso, traremos beleza a esta classe e adicionaremos várias comodidades.

Adicionar propriedades


As classes criadas usando o SIP não são necessárias para repetir exatamente a interface da classe C ++. Por exemplo, em nossa classe Foo , existem dois getters e dois setters, que podem ser claramente combinados em uma propriedade para tornar a classe mais "Python". Adicionar propriedades usando sip é fácil o suficiente, como mostra um exemplo na pasta pyfoo_cpp_02 .

Este exemplo é semelhante ao anterior, a principal diferença está no arquivo pyfoocpp.sip , que agora se parece com o seguinte:

%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 você pode ver, tudo é bem simples. Para adicionar uma propriedade, a propriedade% directiva tem por objectivo , que tem dois parâmetros necessários: Nome para especificar o nome da propriedade, e obter para especificar um método que retorna um valor (getter). Pode não haver um setter, mas se também for necessário atribuir valores à propriedade, o método setter será especificado como o valor do parâmetro set . No nosso exemplo, as propriedades são criadas de maneira bastante direta, pois já existem funções que funcionam como getters e setters.

Só podemos coletar o pacote usando o comando sip-wheel , instalá-lo, depois verificaremos a operação das propriedades no modo de comando do interpretador 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 você pode ver a partir do exemplo da utilização do Foo classe , os int_val e propriedades string_val trabalhar tanto para ler e escrever.

Adicionar linhas de documentação


Continuaremos a melhorar nossa classe Foo . O exemplo a seguir, localizado na pasta pyfoo_cpp_03, mostra como adicionar linhas de documentação (docstring) a vários elementos de uma classe. Este exemplo é baseado no anterior e a principal alteração refere-se ao arquivo pyfoocpp.sip . Aqui está o seu conteúdo:

%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 você já entendeu, para adicionar linhas de documentação a qualquer elemento da classe, você precisa usar a diretiva % Docstring . Este exemplo mostra várias maneiras de usar esta diretiva. Para uma melhor compreensão deste exemplo, vamos compilar imediatamente o pacote pyfoocpp usando o comando sip-wheel , instalá-lo e lidaremos com qual parâmetro desta diretiva afeta o que, considerando as linhas de documentação resultantes no modo de comando Python. Deixe-me lembrá-lo de que as linhas de documentação são armazenadas como membros dos objetos __doc__ aos quais essas linhas pertencem.

A primeira linha de documentação é para a classe Foo .. Como você pode ver, todas as linhas de documentação estão localizadas entre as diretivas% Docstring e % End . As linhas 5-7 deste exemplo não usam parâmetros adicionais da diretiva % Docstring ; portanto, a linha de documentação será gravada na classe Foo como está. É por isso que não há recuo nas linhas 5-7, caso contrário, os recuos na frente da linha de documentação também cairiam em Foo .__ doc__. Garantiremos que a classe Foo realmente contenha a linha de documentação que introduzimos:

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

A diretiva % Docstring a seguir , localizada nas linhas 17-19, usa dois parâmetros ao mesmo tempo. O parâmetro format pode assumir um dos dois valores: “bruto” ou “desindentado”. No primeiro caso, as linhas de documentação são salvas à medida que são gravadas e, no segundo, os caracteres de espaço inicial (mas não as guias) são excluídos. O valor padrão para o caso, se o parâmetro format não for especificado, pode ser configurado usando a diretiva % DefaultDocstringFormat (consideraremos um pouco mais tarde) e, se não for especificado, será assumido que format = "raw" .

Além das linhas de documentação especificadas, o SIP adiciona uma descrição de sua assinatura (que tipos de variáveis ​​são esperados na entrada e que tipo a função retorna) às linhas de documentação de funções. O parâmetro signature indica onde colocar essa assinatura: antes da linha de documentação especificada ( assinatura = "pré- anexada" ) , depois dela ( assinatura = "anexada" ) ou não a assinatura ( assinatura = "descartada" ).

Nosso exemplo define a assinatura = "prefixado" parâmetro para os get_int_val e funções set_int_val , bem como a assinatura = "anexado" para os get_string_val e funções set_string_val. O parâmetro format = "deindented" também foi adicionado para remover espaços no início da linha de documentação. Vamos verificar como esses parâmetros funcionam no 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 você pode ver, usando o parâmetro de assinatura da diretiva % Docstring , você pode alterar a posição da descrição da assinatura da função na linha de documentação.

Agora considere adicionar uma linha de documentação às propriedades. Observe que, nesse caso, as diretivas% Docstring ... % End são colocadas entre chaves após a diretiva% Property. Este formato de gravação é descrito na documentação da diretiva % Property .

Observe também como especificamos o parâmetro da diretiva % Docstring . Esse formato para escrever diretivas é possível se definirmos apenas o primeiro parâmetro da diretiva (nesse caso, o parâmetro format) Portanto, neste exemplo, três métodos de uso de diretivas são usados ​​ao mesmo tempo.

Verifique se a linha de documentação para as propriedades está definida:

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


Vamos simplificar este exemplo, definindo os valores padrão para os parâmetros de formato e assinatura usando as diretivas % DefaultDocstringFormat e % DefaultDocstringSignature . O uso dessas diretivas é mostrado no exemplo da pasta pyfoo_cpp_04 . O arquivo pyfoocpp.sip neste exemplo contém o seguinte 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
        };
};

No início do arquivo, as linhas % DefaultDocstringFormat "deindented" e % DefaultDocstringSignature "prepended" foram adicionadas e, em seguida, todos os parâmetros da diretiva % Docstring foram removidos.

Após montar e instalar este exemplo, podemos ver como é a descrição da classe Foo agora , que o comando help (Foo) exibe :

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

Tudo parece bem arrumado e do mesmo tipo.

Renomear classes e métodos


Como já dissemos, a interface fornecida pelas ligações do Python não precisa corresponder à interface que a biblioteca C / C ++ fornece. Adicionamos propriedades às classes acima e agora veremos mais uma técnica que pode ser útil se surgirem conflitos de nomes ou funções de classe, por exemplo, se um nome de função corresponder a alguma palavra-chave Python. Para fazer isso, você pode renomear classes, funções, exceções e outras entidades.

Para renomear uma entidade, a anotação PyName é usada , cujo valor precisa ser atribuído a um novo nome de entidade. O trabalho com a anotação PyName é mostrado no exemplo da pasta pyfoo_cpp_05 . Este exemplo é baseado no exemplo anterior.pyfoo_cpp_04 e difere dele pelo arquivo pyfoocpp.sip , cujo conteúdo agora se parece com isso:

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

Neste exemplo, renomeamos a classe Foo para a classe Bar e também atribuímos outros nomes a todos os métodos usando a anotação PyName . Eu acho que tudo aqui é bastante simples e claro, a única coisa que merece atenção é a criação de propriedades. Na diretiva % Property , os parâmetros get e set devem especificar os nomes dos métodos, como serão chamados na classe Python, e não os nomes para os quais foram originalmente chamados no código C ++.

Compile o exemplo, instale-o e veja como essa classe ficará no 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
 |  
 |  ----------------------------------------------------------------------
...

Funcionou! Conseguimos renomear a própria classe e seus métodos.

Às vezes, as bibliotecas usam o acordo de que os nomes de todas as classes começam com um prefixo, por exemplo, com a letra "Q" em Qt ou "wx" em wxWidgets. Se na sua ligação Python você deseja renomear todas as classes, se livrando desses prefixos, para não definir a anotação PyName para cada classe, você pode usar a diretiva % AutoPyName . Não consideraremos essa diretiva neste artigo, diremos apenas que a diretiva % AutoPyName deve estar localizada dentro da diretiva % Module e nos restringir a um exemplo da documentação:

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

Adicionar conversão de tipo


Exemplo usando a classe std :: wstring


Até agora, vimos funções e classes que funcionaram com tipos simples como int e char * . Para esses tipos, o SIP criou automaticamente um conversor de e para as classes Python. No exemplo a seguir, localizado na pasta pyfoo_cpp_06 , consideraremos o caso em que os métodos de classe aceitam e retornam objetos mais complexos, por exemplo, cadeias de caracteres de STL. Para simplificar o exemplo e não complicar a conversão de bytes em Unicode e vice-versa, a classe de string std :: wstring será usada neste exemplo . A idéia deste exemplo é mostrar como você pode definir manualmente as regras para a conversão de classes C ++ de e para classes Python.

Neste exemplo, mudaremos a classe Foo da biblioteca foo. Agora a definição da classe ficará assim (arquivo 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

A implementação da classe Foo no arquivo 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;
}

E o arquivo main.cpp para verificar a funcionalidade da 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;
}

Os arquivos foo.h , foo.cpp e main.cpp , como antes, estão localizados na pasta foo . O processo de criação de makefile e biblioteca não mudou. Também não há alterações significativas nos arquivos pyproject.toml e project.py .

Mas o arquivo pyfoocpp.sip tornou- se notavelmente mais 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
};

Para fins ilustrativos , o arquivo pyfoocpp.sip não adiciona linhas de documentação. Se deixássemos apenas a declaração da classe Foo no arquivo pyfoocpp.sip sem a diretiva % MappedType subsequente, obteríamos o seguinte erro durante o processo de compilação:

$ sip-wheel

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

Precisamos descrever explicitamente como um objeto do tipo std :: wstring será convertido em algum objeto Python e também descrever a transformação inversa. Para descrever a conversão, teremos de trabalhar em um nível bastante baixo na linguagem C e usar a API Python / C . Como a API Python / C é um tópico importante, digno de um artigo separado, mas de um livro, nesta seção, consideraremos apenas as funções usadas no exemplo, sem entrar em muitos detalhes.

Para declarar conversões de objetos C ++ para Python e vice-versa, a diretiva % MappedType é destinada , dentro da qual pode haver três outras diretivas: % TypeHeaderCode , % ConvertToTypeCode e % ConvertFromTypeCode. Após a expressão % MappedType , especifique o tipo para o qual os conversores serão criados. No nosso caso, a diretiva começa com a expressão % MappedType std :: wstring . cumprimos a

diretiva % TypeHeaderCode na seção Fazendo uma ligação para uma biblioteca em C ++ . Deixe-me lembrá-lo de que esta diretiva se destina a declarar os tipos usados ​​ou a incluir os arquivos de cabeçalho nos quais eles são declarados. Neste exemplo , a string do arquivo de cabeçalho , onde a classe std :: string é declarada, é conectada à diretiva % TypeHeaderCode . Agora precisamos descrever as transformações



% ConvertFromTypeCode. Convertendo objetos C ++ em Python


Começamos convertendo objetos std :: wstring para a classe Python str . Essa conversão no exemplo é a seguinte:

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

Dentro dessa diretiva, temos a variável sipCpp - um ponteiro para um objeto do código C ++, pelo qual precisamos criar um objeto Python e retornar o objeto criado a partir da diretiva usando a instrução return . Nesse caso, a variável sipCpp é do tipo std :: wstring * . Para criar a classe str , use a função PyUnicode_FromWideChar da API Python / C. Essa função aceita uma matriz (ponteiro) do tipo const wchar_t * w como o primeiro parâmetro e o tamanho dessa matriz como o segundo parâmetro. Se você passar o valor -1 como o segundo parâmetro, a própria função PyUnicode_FromWideChar calculará o comprimento usando a funçãowcslen .

Para obter a matriz wchar_t * , use o método de dados da classe std :: wstring .

A função PyUnicode_FromWideChar retorna um ponteiro para PyObject ou NULL no caso de um erro. PyObject é qualquer objeto Python, nesse caso, será a classe str . Na API Python / C, o trabalho com objetos geralmente ocorre por meio de ponteiros PyObject * ; portanto, nesse caso, retornamos o ponteiro PyObject * da diretiva % ConvertFromTypeCode .

% ConvertToTypeCode. Converter objetos Python em C ++


A conversão inversa de um objeto Python (essencialmente de PyObject * ) para a classe std :: wstring é descrita na diretiva % ConvertToTypeCode . No exemplo pyfoo_cpp_06, a conversão é a seguinte:

%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

O código da diretiva % ConvertToTypeCode parece mais complicado, porque durante o processo de conversão é chamado várias vezes para propósitos diferentes. Dentro da diretiva % ConvertToTypeCode , o SIP cria várias variáveis ​​que podemos (ou devemos) usar.

Uma dessas variáveis, PyObject * sipPy, é um objeto Python que você precisa para criar uma instância da classe std :: wstring nesse caso . O resultado precisará ser gravado em outra variável - sipCppPtr é um ponteiro duplo para o objeto criado, ou seja, no nosso caso, essa variável será do tipo std :: wstring ** .

Outro % ConvertToTypeCode criado dentro da diretivaa variável é int * sipIsErr . Se o valor dessa variável for NULL , a diretiva % ConvertToTypeCode será chamada apenas para verificar se a conversão de tipo é possível. Nesse caso, não somos obrigados a realizar a transformação, mas apenas precisamos verificar se é possível em princípio. Se possível, a diretiva deve retornar um valor diferente de zero; caso contrário, se a conversão não for possível, eles devem retornar 0. Se esse ponteiro não for NULL , será necessário executar a conversão e, se ocorrer um erro durante a conversão, o código de erro inteiro poderá ser salvo. nessa variável (dado que essa variável é um ponteiro para int * ).

Neste exemplo, para verificar se sipPy é uma string unicode (classe str ), é usada a macro PyUnicode_Check , que recebe um argumento do tipo PyObject * se o argumento passado for uma string unicode ou uma classe derivada dela.

A conversão para um objeto C ++ é realizada usando a string * sipCppPtr = new std :: wstring (PyUnicode_AS_UNICODE (sipPy)); . Isso chama a macro PyUnicode_AS_UNICODE da API Python / C, que retorna uma matriz do tipo Py_UNICODE * , que é equivalente a wchar_t * . Essa matriz é passada para o construtor da classe std :: wstring. Como mencionado acima, o resultado é armazenado na variável sipCppPtr .

No momento, a diretiva PyUnicode_AS_UNICODE está obsoleta e é recomendável usar outras macros, mas essa macro é usada para simplificar o exemplo.

Se a conversão foi bem-sucedida, a diretiva % ConvertToTypeCode deve retornar um valor diferente de zero (neste caso, 1) e, no caso de um erro, deve retornar 0.

Verifica


Descrevemos a conversão do tipo std :: wstring para str e vice-versa, agora podemos garantir que o pacote seja construído com êxito e que a ligação funcione como deveria. Para construir, chame sip-wheel , instale o pacote usando pip e verifique a operabilidade no 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 você pode ver, tudo funciona, também não há problemas com o idioma russo, ou seja, Conversões de seqüência de caracteres Unicode executadas corretamente.

Conclusão


Neste artigo, abordamos o básico do uso do SIP para criar ligações Python para bibliotecas escritas em C e C ++. Primeiro (na primeira parte ), criamos uma biblioteca simples em C e descobrimos os arquivos que precisam ser criados para trabalhar com o SIP. O arquivo pyproject.toml contém informações sobre o pacote (nome, número da versão, licença e caminhos para os arquivos de cabeçalho e objeto). Usando o arquivo project.py , você pode influenciar o processo de criação do pacote Python, por exemplo, começar a criar a biblioteca C / C ++ ou permitir que o usuário especifique o local dos arquivos de cabeçalho e de objeto da biblioteca.

No arquivo * .sipdescreve a interface do módulo Python, listando as funções e classes que estarão contidas no módulo. Diretivas e anotações são usadas para descrever a interface no arquivo * .sip . A interface da classe Python não precisa corresponder à interface da classe C ++. Por exemplo, você pode adicionar propriedades a classes usando a diretiva % Property , renomear entidades usando a anotação / PyName / e adicionar linhas de documentação usando a diretiva % Docstring .

Tipos elementares como int , char , char *etc. O SIP converte automaticamente em classes Python semelhantes, mas se você precisar executar uma conversão mais complexa, precisará programá-lo dentro da diretiva % MappedType usando a API Python / C. A conversão da classe Python para C ++ deve ser feita na diretiva aninhada % ConvertToTypeCode . A conversão de um tipo C ++ para uma classe Python deve ser feita na diretiva aninhada % ConvertFromTypeCode .

Algumas diretivas como % DefaultEncoding , % DefaultDocstringFormat e % DefaultDocstringSignature são auxiliares e permitem definir valores padrão para casos em que alguns parâmetros de anotação não são explicitamente definidos.

Neste artigo, examinamos apenas as diretrizes e anotações básicas e mais simples, mas muitas delas foram ignoradas. Por exemplo, existem diretrizes para gerenciar o GIL, para criar novas exceções em Python, para gerenciar manualmente coletores de memória e lixo, para ajustar classes para diferentes sistemas operacionais e muitas outras que podem ser úteis ao criar ligações complexas da biblioteca C / C ++. Também contornamos a questão de criar pacotes para diferentes sistemas operacionais, limitando-nos a criar no Linux usando compiladores gcc / g ++.

Referências



All Articles