Все примеры для статьи доступны в репозитории на 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/
Добавить комментарий