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):
#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;
}
}
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);
}
}
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;
}
}
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);
}
}
sipNoMethod(sipParseErr, sipName_Foo, sipName_get_string_val, doc_Foo_get_string_val);
return SIP_NULLPTR;
}
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
PyObject* newstring;
newstring = PyUnicode_FromWideChar(sipCpp->data(), -1);
return newstring;
%End
%ConvertToTypeCode
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 . Já cumprimos adiretiva % 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
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
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