В первой части статьи мы рассмотрели основы работы с утилитой SIP, предназначенной для создания Python-обвязок (Python bindings) для библиотек, написанных на языках C и C++. Мы рассмотрели основные файлы, которые нужно создать для работы с SIP и начали рассматривать директивы и аннотации. До сих пор мы делали обвязку для простой библиотеки, написанной на языке C. В этой части мы разберемся, как делать обвязку для библиотеки на языке C++, которая содержит классы. На примере этой библиотеки мы посмотрим, какие приемы могут быть полезны при работе с объектно-ориентированной библиотекой, а заодно разберемся с новыми для нас директивами и аннотациями.
Все примеры для статьи доступны в репозитории на github по ссылке: https://github.com/Jenyay/sip-examples.
Следующий пример, который мы будем рассматривать, находится в папке pyfoo_cpp_01.
Для начала создадим библиотеку, для которой мы будем делать обвязку. Библиотека будет по-прежнему располагаться в папке foo и содержать один класс — Foo. Заголовочный файл foo.h с объявлением этого класса выглядит следующим образом:
Это простой класс с двумя геттерами и сеттерами, устанавливающие и возвращающие значения типа int и char*. Реализация класса выглядит следующим образом:
Для проверки работоспособности библиотеки в папке foo также содержится файл main.cpp, использующий класс Foo:
Для сборки библиотеки foo используется следующий Makefile:
Отличие от Makefile в предыдущих примерах, помимо изменение компилятора с gcc на g++, заключается в том, что для компиляции был добавлен еще один параметр -fPIC, который указывает компилятору размещать код в библиотеке определенным образом (так называемый «позиционно-независимый код»). Поскольку эта статья не про компиляторы, то не будем более подробно разбираться с тем, что этот параметр делает и зачем он нужен.
Начнем делать обвязку для этой библиотеки. Файлы pyproject.toml и project.py почти не изменятся по сравнению с предыдущими примерами. Вот как теперь выглядит файл pyproject.toml:
Теперь наши примеры, написанные на языке C++ будут упаковываться в Python-пакет pyfoocpp, это, пожалуй, единственное заметное изменение в этом файле.
Файл project.py остался такой же, как и в примере pyfoo_c_04:
А вот файл pyfoocpp.sip мы рассмотрим более подробно. Напомню, что этот файл описывает интерфейс для будущего Python-модуля: что он должен в себя включать, как должен выглядеть интерфейс классов и т.д. Файл .sip не обязан повторять заголовочный файл библиотеки, хоть у них и будет много общего. Внутри этого класса могут добавляться новые методы, которых не было в исходном классе. Т.е. интерфейс, описанный в файле .sip может подстраивать классы библиотеки под принципы, принятые в языке Python, если это необходимо. В файле pyfoocpp.sip мы увидим новые для нас директивы.
Для начала посмотрим, что этот файл содержит:
Первые строки нам уже должны быть понятны по предыдущим примерам. В директиве %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 со следующим содержимым:
В качестве дополнительного задания рассмотрите внимательнее файл sipfoocppFoo.cpp (мы его не будем подробно обсуждать в этой статье):
Теперь соберем пакет с помощью команды sip-wheel. После выполнения этой команды, если все пройдет успешно, будет создан файл pyfoocpp-0.1-cp38-cp38-manylinux1_x86_64.whl или с похожим именем. Установим его с помощью команды pip install --user pyfoocpp-0.1-cp38-cp38-manylinux1_x86_64.whl и запустим интерпретатор Python для проверки:
Работает! Таким образом, мы с вами только что сделали Python-модуль с обвязкой для класса на C++. Дальше будем наводить в этом классе красоту и добавлять разные удобства.
Классы, созданные с помощью SIP не обязаны в точности повторять интерфейс классов C++. Например, в нашем классе Foo имеется два геттера и два сеттера, которые явно можно объединить в свойство, чтобы класс стал более «питоновским». Добавить свойства с помощью сип достаточно легко, как это делается, показывает пример в папке pyfoo_cpp_02.
Этот пример аналогичен предыдущему, главное отличие заключается в файле pyfoocpp.sip, который теперь выглядит следующим образом:
Как видите, все достаточно просто. Чтобы добавить свойство, предназначена директива %Property, у которой имеется два обязательных параметра: name для задания имени свойства, а также get для указания метода, который возвращает какое-либо значение (геттер). Сеттера может не быть, но если свойству нужно также присваивать значения, то метод-сеттер указывается в качестве значения параметра set. В нашем примере свойства создаются достаточно прямолинейно, поскольку уже имеются функции, работающие как геттеры и сеттеры.
Нам остается только лишь собрать пакет с помощью команды sip-wheel, установить его, после этого проверим работу свойств в командном режиме интерпретатора python:
Как видно из примера использования класса Foo, свойства int_val и string_val работают и на чтение, и на запись.
Продолжим улучшать наш класс Foo. Следующий пример, который расположен в папке pyfoo_cpp_03 показывает, как добавлять к различным элементам класса строки документации (docstring). Этот пример сделан на основе предыдущего, и главное изменение в нем касается файла pyfoocpp.sip. Вот его содержимое:
Как вы уже поняли, для того, чтобы добавить строки документации к какому-либо элементу класса, нужно воспользоваться директивой %Docstring. В этом примере показано несколько способов использования этой директивы. Для лучшего понимания этого примера давайте сразу скомпилируем пакет pyfoocpp с помощью команды sip-wheel, установим его и будем последовательно разбираться с тем, какой параметр этой директивы на что влияет, рассматривая получившиеся строки документации в командном режиме Python. Напомню, что строки документации сохраняются в члены __doc__ объектов, к которым относятся эти строки.
Первая строка документации относится к классу Foo. Как вы видите, все строки документации расположены между директивами %Docstring и %End. В строках 5-7 этого примера не используются никакие дополнительные параметры директивы %Docstring, поэтому строка документации будет записана в класс Foo как есть. Именно поэтому в строках 5-7 нет отступов, иначе отступы перед строкой документации тоже попали бы в Foo.__doc__. Убедимся в том, что класс Foo действительно содержит ту строку документации, которую мы ввели:
Следующая директива %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:
Как видим, с помощью параметра signature директивы %Docstring можно менять положение описания сигнатуры функции в строке документации.
Теперь рассмотрим добавление строки документации в свойства. Обратите внимание, что в этом случае директивы %Docstring...%End заключены в фигурные скобки после директивы %Property. Такой формат записи описан в документации к директиве %Property.
Также обратите внимание, как мы указываем параметр директивы %Docstring. Такой формат записи директив возможен, если мы устанавливаем только первый параметр директивы (в данном случае параметр format). Таким образом, в этом примере используются сразу три способа использования директив.
Убедимся, что строка документации для свойств установлена:
Давайте упростим этот пример, установив значения по умолчанию для параметров format и signature с помощью директив %DefaultDocstringFormat и %DefaultDocstringSignature. Использование этих директив показано в примере из папки pyfoo_cpp_04. Файл pyfoocpp.sip в этом примере содержит следующий код:
В начале файла добавлены строки %DefaultDocstringFormat «deindented» и %DefaultDocstringSignature «prepended», а далее все параметры из директивы %Docstring были убраны.
После сборки и установки этого примера можем посмотреть, как теперь выглядит описание класса Foo, которое выводит команда help(Foo):
Все выглядит достаточно аккуратно и однотипно.
Как мы уже говорили, интерфейс, предоставляемый обвязкой на языке Python не обязательно должен совпадать с тем интерфейсом, который предоставляет библиотека на языке C/C++. Выше мы добавляли свойства в классы, а сейчас рассмотрим еще один прием, который может быть полезен, если возникают конфликты имен классов или функций, например, если имя функции совпадает с каким-нибудь ключевым словом языка Python. Для этого предусмотрена возможность переименования классов, функций, исключений и других сущностей.
Для переименования сущности используется аннотация PyName, значению которой нужно присвоить новое имя сущности. Работа с аннотацией PyName показана в примере из папки pyfoo_cpp_05. Этот пример создан на основе предыдущего примера pyfoo_cpp_04 и отличается от него файлом pyfoocpp.sip, содержимое которого теперь выглядит следующим образом:
В этом примере мы переименовали класс Foo в класс Bar, а также присвоили другие имена всем методам с помощью аннотации PyName. Думаю, что все здесь достаточно просто и понятно, единственное, на что стоит обратить внимание — это создание свойств. В директиве %Property в качестве параметров get и set нужно указывать имена методов, как они будут называться в Python-классе, а не те имена, как они назывались изначально к коде на C++.
Скомпилируем пример, установим его и посмотрим, как этот класс будет выглядеть в языке Python:
Сработало! Нам удалось переименовать сам класс и его методы.
Иногда в библиотеках используется договоренность, что имена всех классов начинаются с какого-либо префикса, например, с буквы «Q» в Qt или «wx» в wxWidgets. Если в своей Python-обвязке вы хотите переименовать все классы, избавившись от таких префиксов, то для того, чтобы не задавать аннотацию PyName для каждого класса, можно воспользоваться директивой %AutoPyName. Мы не будем рассматривать эту директиву в данной статье, скажем только, что директива %AutoPyName должна располагаться внутри директивы %Module и ограничимся примером из документации:
До сих пор мы рассматривали функции и классы, которые работали с простейшими типами вроде int и char*. Для таких типов SIP автоматически создавал конвертер в классы Python и обратно. В следующем примере, который расположен в папке pyfoo_cpp_06, мы рассмотрим случай, когда методы класса принимают и возвращают более сложные объекты, например, строки из STL. Чтобы упростить пример и не усложнять преобразование байтов в Unicode и обратно, в этом примере будет использоваться класс строк std::wstring. Идея этого примера — показать, как можно вручную задавать правила преобразования классов C++ в классы Python и обратно.
Для этого примера мы изменим класс Foo из библиотеки foo. Теперь определение класса будет выглядеть следующим образом (файл foo.h):
Реализация класса Foo в файле foo.cpp:
И файл main.cpp для проверки работоспособности библиотеки:
Файлы foo.h, foo.cpp и main.cpp, как и раньше, располагаются в папке foo. Makefile и процесс сборки библиотеки не изменился. Также нет существенных изменений в файлах pyproject.toml и project.py.
А вот файл pyfoocpp.sip стал заметно сложнее:
Для наглядности файл pyfoocpp.sip не добавляет строки документации. Если бы мы в файле pyfoocpp.sip оставили только объявление класса Foo без последующей директивы %MappedType, то с процессе сборки получили бы следующую ошибку:
Нам нужно явно описать, как объект типа 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.
Теперь нам нужно описать преобразования
Начнем с преобразования объектов std::wstring в класс str языка Python. Данное преобразование в примере выглядит следующим образом:
Внутри этой директивы у нас имеется переменная 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*.
Обратное преобразование из объекта Python (по сути из PyObject*) в класс std::wstring описывается в директиве %ConvertToTypeCode. В примере pyfoo_cpp_06 преобразование выглядит следующим образом:
Код директивы %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:
Как видим, все работает, с русским языком тоже проблем нет, т.е. преобразования юникодных строк выполнено корректно.
В этой статье мы рассмотрели основы использования 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++.
Все примеры для статьи доступны в репозитории на 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++.