Создание Python-обвязки для библиотек, написанных на C/C++, с помощью SIP. Часть 2

от автора

В первой части статьи мы рассмотрели основы работы с утилитой SIP, предназначенной для создания Python-обвязок (Python bindings) для библиотек, написанных на языках C и C++. Мы рассмотрели основные файлы, которые нужно создать для работы с SIP и начали рассматривать директивы и аннотации. До сих пор мы делали обвязку для простой библиотеки, написанной на языке C. В этой части мы разберемся, как делать обвязку для библиотеки на языке C++, которая содержит классы. На примере этой библиотеки мы посмотрим, какие приемы могут быть полезны при работе с объектно-ориентированной библиотекой, а заодно разберемся с новыми для нас директивами и аннотациями.

Все примеры для статьи доступны в репозитории на github по ссылке: https://github.com/Jenyay/sip-examples.

Делаем обвязку для библиотеки на языке C++

Следующий пример, который мы будем рассматривать, находится в папке pyfoo_cpp_01.

Для начала создадим библиотеку, для которой мы будем делать обвязку. Библиотека будет по-прежнему располагаться в папке foo и содержать один класс — Foo. Заголовочный файл foo.h с объявлением этого класса выглядит следующим образом:

#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 

Это простой класс с двумя геттерами и сеттерами, устанавливающие и возвращающие значения типа int и char*. Реализация класса выглядит следующим образом:

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

Для проверки работоспособности библиотеки в папке foo также содержится файл main.cpp, использующий класс 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; } 

Для сборки библиотеки foo используется следующий 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)/* 

Отличие от Makefile в предыдущих примерах, помимо изменение компилятора с gcc на g++, заключается в том, что для компиляции был добавлен еще один параметр -fPIC, который указывает компилятору размещать код в библиотеке определенным образом (так называемый «позиционно-независимый код»). Поскольку эта статья не про компиляторы, то не будем более подробно разбираться с тем, что этот параметр делает и зачем он нужен.

Начнем делать обвязку для этой библиотеки. Файлы pyproject.toml и project.py почти не изменятся по сравнению с предыдущими примерами. Вот как теперь выглядит файл 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"] 

Теперь наши примеры, написанные на языке C++ будут упаковываться в Python-пакет pyfoocpp, это, пожалуй, единственное заметное изменение в этом файле.

Файл project.py остался такой же, как и в примере 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() 

А вот файл pyfoocpp.sip мы рассмотрим более подробно. Напомню, что этот файл описывает интерфейс для будущего Python-модуля: что он должен в себя включать, как должен выглядеть интерфейс классов и т.д. Файл .sip не обязан повторять заголовочный файл библиотеки, хоть у них и будет много общего. Внутри этого класса могут добавляться новые методы, которых не было в исходном классе. Т.е. интерфейс, описанный в файле .sip может подстраивать классы библиотеки под принципы, принятые в языке Python, если это необходимо. В файле pyfoocpp.sip мы увидим новые для нас директивы.

Для начала посмотрим, что этот файл содержит:

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

Первые строки нам уже должны быть понятны по предыдущим примерам. В директиве %Module мы указываем имя Python-модуля, который будет создан (т.е. для использования этого модуля мы должны будем использовать команды import foocpp или from foocpp import …. В этой же директиве мы указываем, что язык у нас теперь — C++. Директива %DefaultEncoding задает кодировку, которая будет использоваться для преобразования строки Python в типы char, const char, char* и const char*.

Затем следует объявление интерфейса класса Foo. Сразу после объявления класса Foo встречается не используемая до сих пор директива %TypeHeaderCode, которая заканчивается директивой %End. Директива %TypeHeaderCode должна содержать код, объявляющий интерфейс класса C++, для которого создается обертка. Как правило, в этой директиве достаточно подключить заголовочный файл с объявлением класса.

После этого перечислены методы класса, которые будут преобразованы в методы класса Foo для языка Python. Важно отметить, что в этом месте мы объявляем только публичные методы, которые будут доступны из класса Foo в Python (поскольку в Python нет приватных и защищенных членов). Поскольку мы в самом начале использовали директиву %DefaultEncoding, то в методах, принимающих аргументы типа const char*, можно не использовать аннотацию Encoding для указания кодировки для преобразования этих параметров в строки Python и обратно.

Теперь нам остается собрать Python-пакет pyfoocpp и проверить его. Но прежде чем собирать полноценный wheel-пакет, давайте воспользуемся командой sip-build и посмотрим, какие исходные файлы для последующей компиляции создаст SIP, и попытаемся найти в них что-то похожее на тот класс, который будет создаваться в коде на языке Python. Для этого вышеуказанную команду sip-build нужно вызвать в папке pyfoo_cpp_01. В результате будет создана папка build со следующим содержимым:

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

В качестве дополнительного задания рассмотрите внимательнее файл sipfoocppFoo.cpp (мы его не будем подробно обсуждать в этой статье):

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

Теперь соберем пакет с помощью команды sip-wheel. После выполнения этой команды, если все пройдет успешно, будет создан файл pyfoocpp-0.1-cp38-cp38-manylinux1_x86_64.whl или с похожим именем. Установим его с помощью команды pip install —user pyfoocpp-0.1-cp38-cp38-manylinux1_x86_64.whl и запустим интерпретатор Python для проверки:

>>> 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() 'Привет' 

Работает! Таким образом, мы с вами только что сделали Python-модуль с обвязкой для класса на C++. Дальше будем наводить в этом классе красоту и добавлять разные удобства.

Добавляем свойства

Классы, созданные с помощью SIP не обязаны в точности повторять интерфейс классов C++. Например, в нашем классе Foo имеется два геттера и два сеттера, которые явно можно объединить в свойство, чтобы класс стал более «питоновским». Добавить свойства с помощью сип достаточно легко, как это делается, показывает пример в папке pyfoo_cpp_02.

Этот пример аналогичен предыдущему, главное отличие заключается в файле pyfoocpp.sip, который теперь выглядит следующим образом:

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

Как видите, все достаточно просто. Чтобы добавить свойство, предназначена директива %Property, у которой имеется два обязательных параметра: name для задания имени свойства, а также get для указания метода, который возвращает какое-либо значение (геттер). Сеттера может не быть, но если свойству нужно также присваивать значения, то метод-сеттер указывается в качестве значения параметра set. В нашем примере свойства создаются достаточно прямолинейно, поскольку уже имеются функции, работающие как геттеры и сеттеры.

Нам остается только лишь собрать пакет с помощью команды sip-wheel, установить его, после этого проверим работу свойств в командном режиме интерпретатора 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() 'Привет' 

Как видно из примера использования класса Foo, свойства int_val и string_val работают и на чтение, и на запись.

Добавляем строки документации

Продолжим улучшать наш класс Foo. Следующий пример, который расположен в папке pyfoo_cpp_03 показывает, как добавлять к различным элементам класса строки документации (docstring). Этот пример сделан на основе предыдущего, и главное изменение в нем касается файла pyfoocpp.sip. Вот его содержимое:

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

Как вы уже поняли, для того, чтобы добавить строки документации к какому-либо элементу класса, нужно воспользоваться директивой %Docstring. В этом примере показано несколько способов использования этой директивы. Для лучшего понимания этого примера давайте сразу скомпилируем пакет pyfoocpp с помощью команды sip-wheel, установим его и будем последовательно разбираться с тем, какой параметр этой директивы на что влияет, рассматривая получившиеся строки документации в командном режиме Python. Напомню, что строки документации сохраняются в члены __doc__ объектов, к которым относятся эти строки.

Первая строка документации относится к классу Foo. Как вы видите, все строки документации расположены между директивами %Docstring и %End. В строках 5-7 этого примера не используются никакие дополнительные параметры директивы %Docstring, поэтому строка документации будет записана в класс Foo как есть. Именно поэтому в строках 5-7 нет отступов, иначе отступы перед строкой документации тоже попали бы в Foo.__doc__. Убедимся в том, что класс Foo действительно содержит ту строку документации, которую мы ввели:

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

Следующая директива %Docstring, расположенная на 17-19 строках, использует сразу два параметра. Параметр format может принимать одно из двух значений: «raw» или «deindented». В первом случае строки документации сохраняются в том виде, как они записаны, а во втором — удаляются начальные символы пробелов (но не табуляции). Значение по умолчанию для случая, если параметр format не указан, можно задать с помощью директивы %DefaultDocstringFormat (мы ее рассмотрим чуть позже), а если она не указана, то считается, что format=«raw».

Помимо заданных строк документации, SIP добавляет к строкам документации функций описание ее сигнатуры (какие типы переменных ожидаются на входе и какой тип функция возвращает). Параметр signature указывает, куда помещать такую сигнатуру: до указанной строки документации (signature=«prepended»), после нее (signature=«appended») или не добавлять сигнатуру (signature=«discarded»).

Наш пример устанавливает параметр signature=«prepended» для функций get_int_val и set_int_val, а также signature=«appended» для функций get_string_val и set_string_val. Также был добавлен параметр format=«deindented» для того, чтобы удалить пробелы в начале строки документации. Проверим работу этих параметров в 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)' 

Как видим, с помощью параметра signature директивы %Docstring можно менять положение описания сигнатуры функции в строке документации.

Теперь рассмотрим добавление строки документации в свойства. Обратите внимание, что в этом случае директивы %Docstring%End заключены в фигурные скобки после директивы %Property. Такой формат записи описан в документации к директиве %Property.

Также обратите внимание, как мы указываем параметр директивы %Docstring. Такой формат записи директив возможен, если мы устанавливаем только первый параметр директивы (в данном случае параметр format). Таким образом, в этом примере используются сразу три способа использования директив.

Убедимся, что строка документации для свойств установлена:

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

Давайте упростим этот пример, установив значения по умолчанию для параметров format и signature с помощью директив %DefaultDocstringFormat и %DefaultDocstringSignature. Использование этих директив показано в примере из папки pyfoo_cpp_04. Файл pyfoocpp.sip в этом примере содержит следующий код:

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

В начале файла добавлены строки %DefaultDocstringFormat «deindented» и %DefaultDocstringSignature «prepended», а далее все параметры из директивы %Docstring были убраны.

После сборки и установки этого примера можем посмотреть, как теперь выглядит описание класса Foo, которое выводит команда help(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 ... 

Все выглядит достаточно аккуратно и однотипно.

Переименовываем классы и методы

Как мы уже говорили, интерфейс, предоставляемый обвязкой на языке Python не обязательно должен совпадать с тем интерфейсом, который предоставляет библиотека на языке C/C++. Выше мы добавляли свойства в классы, а сейчас рассмотрим еще один прием, который может быть полезен, если возникают конфликты имен классов или функций, например, если имя функции совпадает с каким-нибудь ключевым словом языка Python. Для этого предусмотрена возможность переименования классов, функций, исключений и других сущностей.

Для переименования сущности используется аннотация PyName, значению которой нужно присвоить новое имя сущности. Работа с аннотацией PyName показана в примере из папки pyfoo_cpp_05. Этот пример создан на основе предыдущего примера pyfoo_cpp_04 и отличается от него файлом pyfoocpp.sip, содержимое которого теперь выглядит следующим образом:

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

В этом примере мы переименовали класс Foo в класс Bar, а также присвоили другие имена всем методам с помощью аннотации PyName. Думаю, что все здесь достаточно просто и понятно, единственное, на что стоит обратить внимание — это создание свойств. В директиве %Property в качестве параметров get и set нужно указывать имена методов, как они будут называться в Python-классе, а не те имена, как они назывались изначально к коде на C++.

Скомпилируем пример, установим его и посмотрим, как этот класс будет выглядеть в языке 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  |    |  ---------------------------------------------------------------------- ... 

Сработало! Нам удалось переименовать сам класс и его методы.

Иногда в библиотеках используется договоренность, что имена всех классов начинаются с какого-либо префикса, например, с буквы «Q» в Qt или «wx» в wxWidgets. Если в своей Python-обвязке вы хотите переименовать все классы, избавившись от таких префиксов, то для того, чтобы не задавать аннотацию PyName для каждого класса, можно воспользоваться директивой %AutoPyName. Мы не будем рассматривать эту директиву в данной статье, скажем только, что директива %AutoPyName должна располагаться внутри директивы %Module и ограничимся примером из документации:

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

Добавляем преобразование типов

Пример с использованием класса std::wstring

До сих пор мы рассматривали функции и классы, которые работали с простейшими типами вроде int и char*. Для таких типов SIP автоматически создавал конвертер в классы Python и обратно. В следующем примере, который расположен в папке pyfoo_cpp_06, мы рассмотрим случай, когда методы класса принимают и возвращают более сложные объекты, например, строки из STL. Чтобы упростить пример и не усложнять преобразование байтов в Unicode и обратно, в этом примере будет использоваться класс строк std::wstring. Идея этого примера — показать, как можно вручную задавать правила преобразования классов C++ в классы Python и обратно.

Для этого примера мы изменим класс Foo из библиотеки foo. Теперь определение класса будет выглядеть следующим образом (файл 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 

Реализация класса Foo в файле 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; } 

И файл main.cpp для проверки работоспособности библиотеки:

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

Файлы foo.h, foo.cpp и main.cpp, как и раньше, располагаются в папке foo. Makefile и процесс сборки библиотеки не изменился. Также нет существенных изменений в файлах pyproject.toml и project.py.

А вот файл pyfoocpp.sip стал заметно сложнее:

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

Для наглядности файл pyfoocpp.sip не добавляет строки документации. Если бы мы в файле pyfoocpp.sip оставили только объявление класса Foo без последующей директивы %MappedType, то с процессе сборки получили бы следующую ошибку:

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

Нам нужно явно описать, как объект типа std::wstring будет преобразовываться в какой-либо Python-объект, а также описать обратное преобразование. Для описания преобразования нам нужно будет работать на достаточно низком уровне на языке C и использовать Python/C API. Поскольку Python/C API — это большая тема, достойная даже не отдельной статьи, а книги, то в этом разделе мы рассмотрим только те функции, которые используются в примере, не особо углубляясь в подробности.

Для объявления преобразований из объектов C++ в Python и наоборот предназначена директива %MappedType, внутри которой могут находиться три другие директивы: %TypeHeaderCode, %ConvertToTypeCode и %ConvertFromTypeCode. После выражения %MappedType нужно указать тип, для которого будут создаваться конвертеры. В нашем случае директива начинается с выражения %MappedType std::wstring.

С директивой %TypeHeaderCode мы уже встречались в разделе Делаем обвязку для библиотеки на языке C++. Напомню, что эта директива предназначена для того, чтобы объявить используемые типы или подключить заголовочные файлы, в которых они объявлены. В данном примере внутри директивы %TypeHeaderCode подключается заголовочный файл string, где объявлен класс std::string.

Теперь нам нужно описать преобразования

%ConvertFromTypeCode. Преобразование объектов C++ в Python

Начнем с преобразования объектов std::wstring в класс str языка Python. Данное преобразование в примере выглядит следующим образом:

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

Внутри этой директивы у нас имеется переменная sipCpp — указатель на объект из кода на C++, по которому нужно создать Python-объект и вернуть созданный объект из директивы с помощью оператора return. В данном случае переменная sipCpp имеет тип std::wstring*. Чтобы создать класс str, используется функция PyUnicode_FromWideChar из Python/C API. Эта функция в качестве первого параметра принимает массив (указатель) типа const wchar_t *w, а в качестве второго параметра — размер этого массива. Если в качестве второго параметра передать значение -1, то функция PyUnicode_FromWideChar сама рассчитает длину с помощью функции wcslen.

Чтобы получить массив wchar_t* используется метод data из класса std::wstring.

Функция PyUnicode_FromWideChar возвращает указатель на PyObject или NULL в случае ошибки. PyObject представляет собой любой Python-объект, в данном случае это будет класс str. В Python/C API работа с объектами происходит обычно через указатели PyObject*, поэтому и в данном случае из директивы %ConvertFromTypeCode мы возвращаем указатель PyObject*.

%ConvertToTypeCode. Преобразование объектов Python в C++

Обратное преобразование из объекта Python (по сути из PyObject*) в класс std::wstring описывается в директиве %ConvertToTypeCode. В примере pyfoo_cpp_06 преобразование выглядит следующим образом:

%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 

Код директивы %ConvertToTypeCode выглядит более сложно, потому что в процессе преобразования он вызывается несколько раз с разными целями. Внутри директивы %ConvertToTypeCode SIP создает несколько переменных, которые мы можем (или должны) использовать.

Одна из таких переменных PyObject *sipPy представляет собой Python-объект, по которому нужно создать в данном случае экземпляр класса std::wstring. Результат нужно будет записать в другую переменную — sipCppPtr — это двойной указатель на создаваемый объект, т.е. в нашем случае эта переменная будет иметь тип std::wstring**.

Еще одна создаваемая внутри директивы %ConvertToTypeCode переменная — int *sipIsErr. Если значение этой переменной равно NULL, значит директива %ConvertToTypeCode вызывается только с целью проверки, возможно ли преобразование типа. В этом случае мы не обязаны выполнять преобразование, а должны только проверить, возможно ли оно в принципе. Если возможно, то из директивы должны вернуть не нулевое значение, в противном случае, если преобразование невозможно, должны вернуть 0. Если этот указатель не равен NULL, значит нужно выполнить преобразование, а в случае возникновения ошибки в процессе преобразования, целочисленный код ошибки можно сохранить в эту переменную (с учетом того, что эта переменная является указателем на int*).

В данном примере для проверки того, что sipPy представляет собой юникодную строку (класс str) используется макрос PyUnicode_Check, который принимает в качестве параметра аргумент типа PyObject*, если переданный аргумент представляет собой юникодную строку или класс, производный от нее.

Преобразование в объект C++ осуществляется с помощью строки *sipCppPtr = new std::wstring(PyUnicode_AS_UNICODE(sipPy));. Здесь вызывается макрос PyUnicode_AS_UNICODE из Python/C API, который возвращает массив типа Py_UNICODE*, что эквивалентно wchar_t*. Этот массив передается в конструктор класса std::wstring. Как уже было сказано выше, результат сохраняется в переменной sipCppPtr.

В данный момент директива PyUnicode_AS_UNICODE объявлена устаревшей и рекомендуется использовать другие макросы, но для упрощения примера используется именно этот макрос.

Если преобразование прошло успешно, директива %ConvertToTypeCode должна вернуть не нулевое значение (в данном случае 1), а в случае ошибки должна вернуть 0.

Проверка

Мы описали преобразование типа std::wstring в str и обратно, теперь можем убедиться, что пакет успешно собирается и обвязка работает, как надо. Для сборки вызываем sip-wheel, затем устанавливаем пакет с помощью pip и проверяем работоспособность в командном режиме Python:

>>> from foocpp import Foo >>> x = Foo(10, 'Hello')  >>> x.string_val 'Hello'  >>> x.string_val = 'Привет' >>> x.string_val 'Привет'  >>> x.get_string_val() 'Привет' 

Как видим, все работает, с русским языком тоже проблем нет, т.е. преобразования юникодных строк выполнено корректно.

Заключение

В этой статье мы рассмотрели основы использования SIP для создания Python-обвязок для библиотек, написанных на C и C++. Сначала (в первой части) мы создали простую библиотеку на языке C и разобрались с файлами, которые необходимо создать для работы с SIP. В файле pyproject.toml содержится информация о пакете (название, номер версии, лицензия и пути до заголовочных и объектных файлов). С помощью файла project.py можно влиять на процесс сборки пакета Python, например, запускать сборку C/C++-библиотеки или дать возможность пользователю указывать расположение заголовочных и объектных файлов библиотеки.

В файле *.sip описывается интерфейс Python-модуля с перечислением функций и классов, которые будут содержаться в модуле. Для описания интерфейса в файле *.sip используются директивы и аннотации. Интерфейс классов Python не обязательно должен совпадать с интерфейсом классов C++. Например, в классы можно добавлять свойства с помощью директивы %Property, переименовывать сущности с помощью аннотации /PyName/, добавлять строки документации с помощью директивы %Docstring.

Элементарные типы вроде int, char, char* и т.п. SIP автоматически преобразует в аналогичные классы Python, но если нужно выполнять более сложное преобразование, то его нужно запрограммировать самостоятельно внутри директивы %MappedType, используя Python/C API. Преобразование из класса Python в C++ должно осуществляться во вложенной директиве %ConvertToTypeCode. Преобразование из типа C++ в класс Python должно осуществляться во вложенной директиве %ConvertFromTypeCode.

Некоторые директивы вроде %DefaultEncoding, %DefaultDocstringFormat и %DefaultDocstringSignature являются вспомогательными и позволяют устанавливать значения по умолчанию для случаев, когда какие-то параметры аннотаций не установлены явно.

В этой статье мы рассмотрели только лишь основные и самые простые директивы и аннотации, но многие из них обошли вниманием. Например, существуют директивы для управления GIL, для создания новых Python-исключений, для ручного управления памятью и сборщиком мусора, для подстройки классов под разные операционные системы и многие другие, которые могут быть полезны при создании обвязок сложных C/C++-библиотек. Также мы обошли вопрос сборки пакетов под разные операционные системы, ограничившись сборкой под Linux с помощью компиляторов gcc/g++.

Ссылки

ссылка на оригинал статьи https://habr.com/ru/post/495636/