Создание 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 с объявлением этого класса выглядит следующим образом:

  1. #ifndef FOO_LIB
  2. #define FOO_LIB
  3.  
  4.  
  5. class Foo {
  6.     private:
  7.         int _int_val;
  8.         char* _string_val;
  9.     public:
  10.         Foo(int int_val, const char* string_val);
  11.         virtual ~Foo();
  12.  
  13.         void set_int_val(int val);
  14.         int get_int_val();
  15.  
  16.         void set_string_val(const char* val);
  17.         char* get_string_val();
  18. };
  19.  
  20. #endif

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

  1. #include <string.h>
  2.  
  3. #include "foo.h"
  4.  
  5.  
  6. Foo::Foo(int int_val, const char* string_val): _int_val(int_val) {
  7.     _string_val = nullptr;
  8.     set_string_val(string_val);
  9. }
  10.  
  11.  
  12. Foo::~Foo(){
  13.     delete[] _string_val;
  14.     _string_val = nullptr;
  15. }
  16.  
  17.  
  18. void Foo::set_int_val(int val) {
  19.     _int_val = val;
  20. }
  21.  
  22.  
  23. int Foo::get_int_val() {
  24.     return _int_val;
  25. }
  26.  
  27.  
  28. void Foo::set_string_val(const char* val) {
  29.     if (_string_val != nullptr) {
  30.         delete[] _string_val;
  31.     }
  32.  
  33.     auto count = strlen(val) + 1;
  34.     _string_val = new char[count];
  35.     strcpy(_string_val, val);
  36. }
  37.  
  38.  
  39. char* Foo::get_string_val() {
  40.     return _string_val;
  41. }

Для проверки работоспособности библиотеки в папке foo также содержится файл main.cpp, использующий класс Foo:

  1. #include <iostream>
  2.  
  3. #include "foo.h"
  4.  
  5. using std::cout;
  6. using std::endl;
  7.  
  8.  
  9. int main(int argc, char* argv[]) {
  10.     auto foo = Foo(10, "Hello");
  11.     cout << "int_val: " << foo.get_int_val() << endl;
  12.     cout << "string_val: " << foo.get_string_val() << endl;
  13.  
  14.     foo.set_int_val(0);
  15.     foo.set_string_val("Hello world!");
  16.  
  17.     cout << "int_val: " << foo.get_int_val() << endl;
  18.     cout << "string_val: " << foo.get_string_val() << endl;
  19. }

Для сборки библиотеки foo используется следующий Makefile:

  1. CC=g++
  2. CFLAGS=-c -fPIC
  3. DIR_OUT=bin
  4.  
  5. all: main
  6.  
  7. main: main.o libfoo.a
  8.     $(CC) $(DIR_OUT)/main.o -L$(DIR_OUT) -lfoo -o $(DIR_OUT)/main
  9.  
  10. main.o: makedir main.cpp
  11.     $(CC) $(CFLAGS) main.cpp -o $(DIR_OUT)/main.o
  12.  
  13. libfoo.a: makedir foo.cpp
  14.     $(CC) $(CFLAGS) foo.cpp -o $(DIR_OUT)/foo.o
  15.     ar rcs $(DIR_OUT)/libfoo.a $(DIR_OUT)/foo.o
  16.  
  17. makedir:
  18.     mkdir -p $(DIR_OUT)
  19.  
  20. clean:
  21.     rm -rf $(DIR_OUT)/*

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

Начнем делать обвязку для этой библиотеки. Файлы pyproject.toml и project.py почти не изменятся по сравнению с предыдущими примерами. Вот как теперь выглядит файл pyproject.toml:

  1. [build-system]
  2. requires = ["sip >=5, <6"]
  3. build-backend = "sipbuild.api"
  4.  
  5. [tool.sip.metadata]
  6. name = "pyfoocpp"
  7. version = "0.1"
  8. license = "MIT"
  9.  
  10. [tool.sip.bindings.pyfoocpp]
  11. headers = ["foo.h"]
  12. libraries = ["foo"]

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

Файл project.py остался такой же, как и в примере pyfoo_c_04:

  1. import os
  2. import subprocess
  3.  
  4. from sipbuild import Project
  5.  
  6.  
  7. class FooProject(Project):
  8.     def _build_foo(self):
  9.         cwd = os.path.abspath('foo')
  10.         subprocess.run(['make'], cwd=cwd, capture_output=True, check=True)
  11.  
  12.     def build(self):
  13.         self._build_foo()
  14.         super().build()
  15.  
  16.     def build_sdist(self, sdist_directory):
  17.         self._build_foo()
  18.         return super().build_sdist(sdist_directory)
  19.  
  20.     def build_wheel(self, wheel_directory):
  21.         self._build_foo()
  22.         return super().build_wheel(wheel_directory)
  23.  
  24.     def install(self):
  25.         self._build_foo()
  26.         super().install()

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

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

  1. %Module(name=foocpp, language="C++")
  2. %DefaultEncoding "UTF-8"
  3.  
  4. class Foo {
  5.     %TypeHeaderCode
  6.     #include <foo.h>
  7.     %End
  8.  
  9.     public:
  10.         Foo(int, const char*);
  11.  
  12.         void set_int_val(int);
  13.         int get_int_val();
  14.  
  15.         void set_string_val(const char*);
  16.         char* get_string_val();
  17. };

Первые строки нам уже должны быть понятны по предыдущим примерам. В директиве %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 (мы его не будем подробно обсуждать в этой статье):

  1. /*
  2.  * Interface wrapper code.
  3.  *
  4.  * Generated by SIP 5.1.1
  5.  */
  6.  
  7. #include "sipAPIfoocpp.h"
  8.  
  9. #line 6 "/home/jenyay/temp/2/pyfoocpp.sip"
  10.     #include <foo.h>
  11. #line 12 "/home/jenyay/temp/2/build/foocpp/sipfoocppFoo.cpp"
  12.  
  13.  
  14.  
  15. PyDoc_STRVAR(doc_Foo_set_int_val, "set_int_val(self, int)");
  16.  
  17. extern "C" {static PyObject *meth_Foo_set_int_val(PyObject *, PyObject *);}
  18. static PyObject *meth_Foo_set_int_val(PyObject *sipSelf, PyObject *sipArgs)
  19. {
  20.     PyObject *sipParseErr = SIP_NULLPTR;
  21.  
  22.     {
  23.         int a0;
  24.          ::Foo *sipCpp;
  25.  
  26.         if (sipParseArgs(&sipParseErr, sipArgs, "Bi", &sipSelf, sipType_Foo, &sipCpp, &a0))
  27.         {
  28.             sipCpp->set_int_val(a0);
  29.  
  30.             Py_INCREF(Py_None);
  31.             return Py_None;
  32.         }
  33.     }
  34.  
  35.     /* Raise an exception if the arguments couldn't be parsed. */
  36.     sipNoMethod(sipParseErr, sipName_Foo, sipName_set_int_val, doc_Foo_set_int_val);
  37.  
  38.     return SIP_NULLPTR;
  39. }
  40.  
  41.  
  42. PyDoc_STRVAR(doc_Foo_get_int_val, "get_int_val(self) -> int");
  43.  
  44. extern "C" {static PyObject *meth_Foo_get_int_val(PyObject *, PyObject *);}
  45. static PyObject *meth_Foo_get_int_val(PyObject *sipSelf, PyObject *sipArgs)
  46. {
  47.     PyObject *sipParseErr = SIP_NULLPTR;
  48.  
  49.     {
  50.          ::Foo *sipCpp;
  51.  
  52.         if (sipParseArgs(&sipParseErr, sipArgs, "B", &sipSelf, sipType_Foo, &sipCpp))
  53.         {
  54.             int sipRes;
  55.  
  56.             sipRes = sipCpp->get_int_val();
  57.  
  58.             return PyLong_FromLong(sipRes);
  59.         }
  60.     }
  61.  
  62.     /* Raise an exception if the arguments couldn't be parsed. */
  63.     sipNoMethod(sipParseErr, sipName_Foo, sipName_get_int_val, doc_Foo_get_int_val);
  64.  
  65.     return SIP_NULLPTR;
  66. }
  67.  
  68.  
  69. PyDoc_STRVAR(doc_Foo_set_string_val, "set_string_val(self, str)");
  70.  
  71. extern "C" {static PyObject *meth_Foo_set_string_val(PyObject *, PyObject *);}
  72. static PyObject *meth_Foo_set_string_val(PyObject *sipSelf, PyObject *sipArgs)
  73. {
  74.     PyObject *sipParseErr = SIP_NULLPTR;
  75.  
  76.     {
  77.         const char* a0;
  78.         PyObject *a0Keep;
  79.          ::Foo *sipCpp;
  80.  
  81.         if (sipParseArgs(&sipParseErr, sipArgs, "BA8", &sipSelf, sipType_Foo, &sipCpp, &a0Keep, &a0))
  82.         {
  83.             sipCpp->set_string_val(a0);
  84.             Py_DECREF(a0Keep);
  85.  
  86.             Py_INCREF(Py_None);
  87.             return Py_None;
  88.         }
  89.     }
  90.  
  91.     /* Raise an exception if the arguments couldn't be parsed. */
  92.     sipNoMethod(sipParseErr, sipName_Foo, sipName_set_string_val, doc_Foo_set_string_val);
  93.  
  94.     return SIP_NULLPTR;
  95. }
  96.  
  97.  
  98. PyDoc_STRVAR(doc_Foo_get_string_val, "get_string_val(self) -> str");
  99.  
  100. extern "C" {static PyObject *meth_Foo_get_string_val(PyObject *, PyObject *);}
  101. static PyObject *meth_Foo_get_string_val(PyObject *sipSelf, PyObject *sipArgs)
  102. {
  103.     PyObject *sipParseErr = SIP_NULLPTR;
  104.  
  105.     {
  106.          ::Foo *sipCpp;
  107.  
  108.         if (sipParseArgs(&sipParseErr, sipArgs, "B", &sipSelf, sipType_Foo, &sipCpp))
  109.         {
  110.             char*sipRes;
  111.  
  112.             sipRes = sipCpp->get_string_val();
  113.  
  114.             if (sipRes == SIP_NULLPTR)
  115.             {
  116.                 Py_INCREF(Py_None);
  117.                 return Py_None;
  118.             }
  119.  
  120.             return PyUnicode_FromString(sipRes);
  121.         }
  122.     }
  123.  
  124.     /* Raise an exception if the arguments couldn't be parsed. */
  125.     sipNoMethod(sipParseErr, sipName_Foo, sipName_get_string_val, doc_Foo_get_string_val);
  126.  
  127.     return SIP_NULLPTR;
  128. }
  129.  
  130.  
  131. /* Call the instance's destructor. */
  132. extern "C" {static void release_Foo(void *, int);}
  133. static void release_Foo(void *sipCppV, int)
  134. {
  135.     delete reinterpret_cast< ::Foo *>(sipCppV);
  136. }
  137.  
  138.  
  139. extern "C" {static void dealloc_Foo(sipSimpleWrapper *);}
  140. static void dealloc_Foo(sipSimpleWrapper *sipSelf)
  141. {
  142.     if (sipIsOwnedByPython(sipSelf))
  143.     {
  144.         release_Foo(sipGetAddress(sipSelf), 0);
  145.     }
  146. }
  147.  
  148.  
  149. extern "C" {static void *init_type_Foo(sipSimpleWrapper *, PyObject *,
  150.                  PyObject *, PyObject **, PyObject **, PyObject **);}
  151. static void *init_type_Foo(sipSimpleWrapper *, PyObject *sipArgs, PyObject *sipKwds,
  152.                                    PyObject **sipUnused, PyObject **, PyObject **sipParseErr)
  153. {
  154.      ::Foo *sipCpp = SIP_NULLPTR;
  155.  
  156.     {
  157.         int a0;
  158.         const char* a1;
  159.         PyObject *a1Keep;
  160.  
  161.         if (sipParseKwdArgs(sipParseErr, sipArgs, sipKwds, SIP_NULLPTR, sipUnused, "iA8", &a0, &a1Keep, &a1))
  162.         {
  163.             sipCpp = new  ::Foo(a0,a1);
  164.             Py_DECREF(a1Keep);
  165.  
  166.             return sipCpp;
  167.         }
  168.     }
  169.  
  170.     {
  171.         const  ::Foo* a0;
  172.  
  173.         if (sipParseKwdArgs(sipParseErr, sipArgs, sipKwds, SIP_NULLPTR, sipUnused, "J9", sipType_Foo, &a0))
  174.         {
  175.             sipCpp = new  ::Foo(*a0);
  176.  
  177.             return sipCpp;
  178.         }
  179.     }
  180.  
  181.     return SIP_NULLPTR;
  182. }
  183.  
  184.  
  185. static PyMethodDef methods_Foo[] = {
  186.     {sipName_get_int_val, meth_Foo_get_int_val, METH_VARARGS, doc_Foo_get_int_val},
  187.     {sipName_get_string_val, meth_Foo_get_string_val, METH_VARARGS, doc_Foo_get_string_val},
  188.     {sipName_set_int_val, meth_Foo_set_int_val, METH_VARARGS, doc_Foo_set_int_val},
  189.     {sipName_set_string_val, meth_Foo_set_string_val, METH_VARARGS, doc_Foo_set_string_val}
  190. };
  191.  
  192. PyDoc_STRVAR(doc_Foo, "\1Foo(int, str)\n"
  193. "Foo(Foo)");
  194.  
  195.  
  196. sipClassTypeDef sipTypeDef_foocpp_Foo = {
  197.     {
  198.         -1,
  199.         SIP_NULLPTR,
  200.         SIP_NULLPTR,
  201.         SIP_TYPE_CLASS,
  202.         sipNameNr_Foo,
  203.         SIP_NULLPTR,
  204.         SIP_NULLPTR
  205.     },
  206.     {
  207.         sipNameNr_Foo,
  208.         {0, 0, 1},
  209.         4, methods_Foo,
  210.         0, SIP_NULLPTR,
  211.         0, SIP_NULLPTR,
  212.         {SIP_NULLPTR, SIP_NULLPTR, SIP_NULLPTR, SIP_NULLPTR, SIP_NULLPTR,
  213.          SIP_NULLPTR, SIP_NULLPTR, SIP_NULLPTR, SIP_NULLPTR, SIP_NULLPTR},
  214.     },
  215.     doc_Foo,
  216.     -1,
  217.     -1,
  218.     SIP_NULLPTR,
  219.     SIP_NULLPTR,
  220.     init_type_Foo,
  221.     SIP_NULLPTR,
  222.     SIP_NULLPTR,
  223.     SIP_NULLPTR,
  224.     SIP_NULLPTR,
  225.     dealloc_Foo,
  226.     SIP_NULLPTR,
  227.     SIP_NULLPTR,
  228.     SIP_NULLPTR,
  229.     release_Foo,
  230.     SIP_NULLPTR,
  231.     SIP_NULLPTR,
  232.     SIP_NULLPTR,
  233.     SIP_NULLPTR,
  234.     SIP_NULLPTR,
  235.     SIP_NULLPTR,
  236.     SIP_NULLPTR
  237. };

Теперь соберем пакет с помощью команды 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, который теперь выглядит следующим образом:

  1. %Module(name=foocpp, language="C++")
  2. %DefaultEncoding "UTF-8"
  3.  
  4. class Foo {
  5.     %TypeHeaderCode
  6.     #include <foo.h>
  7.     %End
  8.  
  9.     public:
  10.         Foo(int, const char*);
  11.  
  12.         void set_int_val(int);
  13.         int get_int_val();
  14.         %Property(name=int_val, get=get_int_val, set=set_int_val)
  15.  
  16.         void set_string_val(const char*);
  17.         char* get_string_val();
  18.         %Property(name=string_val, get=get_string_val, set=set_string_val)
  19. };

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

Нам остается только лишь собрать пакет с помощью команды sip-wheel, установить его, после этого проверим работу свойств в командном режиме интерпретатора python:

  1. >>> from foocpp import Foo
  2. >>> x = Foo(10, "Hello")
  3. >>> x.int_val
  4. 10
  5.  
  6. >>> x.string_val
  7. 'Hello'
  8.  
  9. >>> x.int_val = 50
  10. >>> x.string_val = 'Привет'
  11.  
  12. >>> x.get_int_val()
  13. 50
  14.  
  15. >>> x.get_string_val()
  16. 'Привет'

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

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

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

  1. %Module(name=foocpp, language="C++")
  2. %DefaultEncoding "UTF-8"
  3.  
  4. class Foo {
  5. %Docstring
  6. Class example from C++ library
  7. %End
  8.  
  9.     %TypeHeaderCode
  10.     #include <foo.h>
  11.     %End
  12.  
  13.     public:
  14.         Foo(int, const char*);
  15.  
  16.         void set_int_val(int);
  17.         %Docstring(format="deindented", signature="prepended")
  18.             Set integer value
  19.         %End
  20.  
  21.         int get_int_val();
  22.         %Docstring(format="deindented", signature="prepended")
  23.             Return integer value
  24.         %End
  25.  
  26.         %Property(name=int_val, get=get_int_val, set=set_int_val)
  27.         {
  28.             %Docstring "deindented"
  29.                 The property for integer value
  30.             %End
  31.         };
  32.  
  33.         void set_string_val(const char*);
  34.         %Docstring(format="deindented", signature="appended")
  35.             Set string value
  36.         %End
  37.  
  38.         char* get_string_val();
  39.         %Docstring(format="deindented", signature="appended")
  40.             Return string value
  41.         %End
  42.  
  43.         %Property(name=string_val, get=get_string_val, set=set_string_val)
  44.         {
  45.             %Docstring "deindented"
  46.                 The property for string value
  47.             %End
  48.         };
  49. };

Как вы уже поняли, для того, чтобы добавить строки документации к какому-либо элементу класса, нужно воспользоваться директивой %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 в этом примере содержит следующий код:

  1. %Module(name=foocpp, language="C++")
  2. %DefaultEncoding "UTF-8"
  3. %DefaultDocstringFormat "deindented"
  4. %DefaultDocstringSignature "prepended"
  5.  
  6. class Foo {
  7.     %Docstring
  8.     Class example from C++ library
  9.     %End
  10.  
  11.     %TypeHeaderCode
  12.     #include <foo.h>
  13.     %End
  14.  
  15.     public:
  16.         Foo(int, const char*);
  17.  
  18.         void set_int_val(int);
  19.         %Docstring
  20.             Set integer value
  21.         %End
  22.  
  23.         int get_int_val();
  24.         %Docstring
  25.             Return integer value
  26.         %End
  27.  
  28.         %Property(name=int_val, get=get_int_val, set=set_int_val)
  29.         {
  30.             %Docstring
  31.                 The property for integer value
  32.             %End
  33.         };
  34.  
  35.         void set_string_val(const char*);
  36.         %Docstring
  37.             Set string value
  38.         %End
  39.  
  40.         char* get_string_val();
  41.         %Docstring
  42.             Return string value
  43.         %End
  44.  
  45.         %Property(name=string_val, get=get_string_val, set=set_string_val)
  46.         {
  47.             %Docstring
  48.                 The property for string value
  49.             %End
  50.         };
  51. };

В начале файла добавлены строки %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, содержимое которого теперь выглядит следующим образом:

  1. %Module(name=foocpp, language="C++")
  2. %DefaultEncoding "UTF-8"
  3. %DefaultDocstringFormat "deindented"
  4. %DefaultDocstringSignature "prepended"
  5.  
  6. class Foo /PyName=Bar/ {
  7.     %Docstring
  8.     Class example from C++ library
  9.     %End
  10.  
  11.     %TypeHeaderCode
  12.     #include <foo.h>
  13.     %End
  14.  
  15.     public:
  16.         Foo(int, const char*);
  17.  
  18.         void set_int_val(int) /PyName=set_integer_value/;
  19.         %Docstring
  20.             Set integer value
  21.         %End
  22.  
  23.         int get_int_val() /PyName=get_integer_value/;
  24.         %Docstring
  25.             Return integer value
  26.         %End
  27.  
  28.         %Property(name=int_val, get=get_integer_value, set=set_integer_value)
  29.         {
  30.             %Docstring
  31.                 The property for integer value
  32.             %End
  33.         };
  34.  
  35.         void set_string_val(const char*) /PyName=set_string_value/;
  36.         %Docstring
  37.             Set string value
  38.         %End
  39.  
  40.         char* get_string_val() /PyName=get_string_value/;
  41.         %Docstring
  42.             Return string value
  43.         %End
  44.  
  45.         %Property(name=string_val, get=get_string_value, set=set_string_value)
  46.         {
  47.             %Docstring
  48.                 The property for string value
  49.             %End
  50.         };
  51. };

В этом примере мы переименовали класс 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):

  1. #ifndef FOO_LIB
  2. #define FOO_LIB
  3.  
  4. #include <string>
  5.  
  6. using std::wstring;
  7.  
  8.  
  9. class Foo {
  10.     private:
  11.         int _int_val;
  12.         wstring _string_val;
  13.     public:
  14.         Foo(int int_val, wstring string_val);
  15.  
  16.         void set_int_val(int val);
  17.         int get_int_val();
  18.  
  19.         void set_string_val(wstring val);
  20.         wstring get_string_val();
  21. };
  22.  
  23. #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 стал заметно сложнее:

  1. %Module(name=foocpp, language="C++")
  2. %DefaultEncoding "UTF-8"
  3.  
  4. class Foo {
  5.     %TypeHeaderCode
  6.     #include <foo.h>
  7.     %End
  8.  
  9.     public:
  10.         Foo(int, std::wstring);
  11.  
  12.         void set_int_val(int);
  13.         int get_int_val();
  14.         %Property(name=int_val, get=get_int_val, set=set_int_val)
  15.  
  16.         void set_string_val(std::wstring);
  17.         std::wstring get_string_val();
  18.         %Property(name=string_val, get=get_string_val, set=set_string_val)
  19. };
  20.  
  21.  
  22. %MappedType std::wstring
  23. {
  24. %TypeHeaderCode
  25. #include <string>
  26. %End
  27.  
  28. %ConvertFromTypeCode
  29.     // Convert an std::wstring to a Python (Unicode) string
  30.     PyObject* newstring;
  31.     newstring = PyUnicode_FromWideChar(sipCpp->data(), -1);
  32.     return newstring;
  33. %End
  34.  
  35. %ConvertToTypeCode
  36.     // Convert a Python (Unicode) string to an std::wstring
  37.     if (sipIsErr == NULL) {
  38.         return PyUnicode_Check(sipPy);
  39.     }
  40.     if (PyUnicode_Check(sipPy)) {
  41.         *sipCppPtr = new std::wstring(PyUnicode_AS_UNICODE(sipPy));
  42.         return 1;
  43.     }
  44.     return 0;
  45. %End
  46. };

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

  1. %ConvertFromTypeCode
  2.     // Convert an std::wstring to a Python (Unicode) string
  3.     PyObject* newstring;
  4.     newstring = PyUnicode_FromWideChar(sipCpp->data(), -1);
  5.     return newstring;
  6. %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 преобразование выглядит следующим образом:

  1. %ConvertToTypeCode
  2.     // Convert a Python (Unicode) string to an std::wstring
  3.     if (sipIsErr == NULL) {
  4.         return PyUnicode_Check(sipPy);
  5.     }
  6.     if (PyUnicode_Check(sipPy)) {
  7.         *sipCppPtr = new std::wstring(PyUnicode_AS_UNICODE(sipPy));
  8.         return 1;
  9.     }
  10.     return 0;
  11. %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++.

Ссылки

Вы можете подписаться на новости сайта через RSS, Группу Вконтакте или Канал в Telegram.
0 stars Пока еще нет оценок



 04.07.2020 - 22:50

Великолепная статья!


Подписаться на комментарии
Автор:
Тема:
 Ваш комментарий
 
 
Введите код 201