Создание Python-обвязки для библиотек, написанных на C/C++, с помощью SIP. Часть 2
- Введение ко второй части
- Делаем обвязку для библиотеки на языке C++
- Добавляем свойства
- Добавляем строки документации
- Переименовываем классы и методы
- Добавляем преобразование типов
- Заключение
- Ссылки
Введение ко второй части
В первой части статьи мы рассмотрели основы работы с утилитой 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 для проверки:
>>> 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 действительно содержит ту строку документации, которую мы ввели:
>>> 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:
'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). Таким образом, в этом примере используются сразу три способа использования директив.
Убедимся, что строка документации для свойств установлена:
'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):
>>> 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:
>>> 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 и ограничимся примером из документации:
{
%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 "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 "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, то с процессе сборки получили бы следующую ошибку:
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:
>>> 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++.
Ссылки
- Первая часть статьи
- Примеры для данной статьи
- Домашняя страница SIP
- Документация для SIP
- Документация для Python/C API
Вы можете подписаться на новости сайта через RSS, Группу Вконтакте или Канал в Telegram.