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

МЕНЮ


Искусственный интеллект
Поиск
Регистрация на сайте
Помощь проекту

ТЕМЫ


Новости ИИРазработка ИИВнедрение ИИРабота разума и сознаниеМодель мозгаРобототехника, БПЛАТрансгуманизмОбработка текстаТеория эволюцииДополненная реальностьЖелезоКиберугрозыНаучный мирИТ индустрияРазработка ПОТеория информацииМатематикаЦифровая экономика

Авторизация



RSS


RSS новости


Иногда во время работы над проектом на языке Python возникает желание использовать библиотеку, которая написана не на Python, а, например, на C или C++. Причины для этого могут быть разные Во-первых, Python — язык замечательный, но в некоторых ситуациях недостаточно быстрый. И если вы видите, что производительность ограничена особенностями языка Python, то имеет смысл часть программы написать на другом языке (в этой статье мы будем говорить про C и C++), оформить эту часть программы в виде библиотеки, сделать Python-обвязки (Python bindings) поверх нее и использовать полученный таким образом модуль как обычную Python-библиотеку. Во-вторых, часто случается ситуация, когда вы знаете, что есть библиотека, которая решает требуемую задачу, но, к сожалению, эта библиотека написана не на Python, а на тех же C или C++. В этом случае также мы можем сделать Python-обвязку над библиотекой и пользоваться ей, не задумываясь о том, что библиотека изначально не была написана на Python.
Для создания Python-обвязок существуют разные инструменты, начиная от более низкоуровневых вроде Python/C API и до более высокоуровневых вроде SWIG и SIP. У меня не было цели сравнения разных способов создания Python-обвязок, а хотелось бы рассказать об основах использования одного инструмента, а именно SIP. Изначально SIP разрабатывался для создания обвязки вокруг библиотеки Qt — PyQt, а также используется при разработке других крупных Python-библиотек, например, wxPython. В этой статье в качестве компилятора для C будет использоваться gcc, а в качестве компилятора C++ — g++. Все примеры проверялись под Arch Linux и Python 3.8. Для того, чтобы не усложнять примеры, тема компиляции под разные операционные системы и с помощью разных компиляторов (например, Visual Studio) не входит в рамки этой статьи. Все примеры для данной статьи вы можете скачать из репозитория на github. Репозиторий с исходниками SIP расположен по адресу https://www.riverbankcomputing.com/hg/sip/. В качестве системы контроля версий для SIP используется Mercurial.

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

Пишем библиотеку на C

Этот пример находится в папке pyfoo_c_01 в исходниках, но в данной статье мы будем подразумевать, что мы все делаем с чистого листа.

Начнем с простого примера. Для начала сделаем простую C-библиотеку, которую потом будем запускать из скрипта на Python. Пусть в нашей библиотеке будет единственная функция

int foo(char*); 

которая будет принимать строку и возвращать ее длину, умноженную на 2.

Заголовочный файл foo.h может выглядеть, например, так:

#ifndef FOO_LIB #define FOO_LIB  int foo(char* str);  #endif 

И файл с реализацией foo.cpp:

#include <string.h>  #include "foo.h"  int foo(char* str) { 	return strlen(str) * 2; } 

Для проверки работоспособности библиотеки напишем простую программу main.c:

#include <stdio.h>  #include "foo.h"  int main(int argc, char* argv[]) { 	char* str = "0123456789"; 	printf("%d ", foo(str)); } 

Для аккуратности создадим Makefile:

CC=gcc CFLAGS=-c 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.c 	$(CC) $(CFLAGS) main.c -o $(DIR_OUT)/main.o  libfoo.a: makedir foo.c 	$(CC) $(CFLAGS) foo.c -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)/* 

Пусть все исходники библиотеки foo расположены в подпапке foo в папке с исходниками:

 foo_c_01/ ??? foo     ??? foo.c     ??? foo.h     ??? main.c     ??? Makefile 


Заходим в папку foo и компилируем исходники с помощью команды

make 

В процессе компиляции будет выведен текст

mkdir -p bin gcc -c main.c -o bin/main.o gcc -c foo.c -o bin/foo.o ar rcs bin/libfoo.a bin/foo.o gcc bin/main.o -Lbin -lfoo -o bin/main 

Результат компиляции будет помещен в папку bin внутри папки foo:

 foo_c_01/ ??? foo     ??? bin     ?   ??? foo.o     ?   ??? libfoo.a     ?   ??? main     ?   ??? main.o     ??? foo.c     ??? foo.h     ??? main.c     ??? Makefile 


Мы скомпилировали библиотеку для статической линковки и программу, которая ее использует под названием main. После компиляции можно убедиться, что программа main запускается.

Давайте сделаем Python-обвязку над библиотекой foo.

Основы работы с SIP

Для начала SIP нужно установить. Делается это стандартно, как и для всех остальных библиотек с помощью pip:

pip install --user sip 

Разумеется, если вы работаете в виртуальном окружении, то параметр --user, сообщающий о том, что библиотеку SIP нужно установить в папку пользователя, а не глобально в систему, указывать не надо.

Что нам нужно сделать, чтобы библиотеку foo можно было бы вызывать из кода на Python? Как минимум нужно создать два файла: один из них в формате TOML и назвать его pyproject.toml, а второй — файл с расширением .sip. Давайте последовательно разбираться с каждым из них.

Нам нужно договориться о структуре исходников. Внутри папки pyfoo_c содержится папка foo, в которой расположены исходники для библиотеки. После компиляции внутри папки foo создается папка bin, которая будет содержать все скомпилированные файлы. Позже мы добавим возможность пользователю указывать пути до заголовочных и объектных файлов библиотеки через командную строку.

Файлы, необходимые для SIP, будут расположены в той же папке, что и папка foo.

pyproject.toml

Файл pyproject.toml — это не изобретение разработчиков SIP, а формат описания проекта на языке Python, описанный в PEP 517 «A build-system independent format for source trees» и в PEP 518 «Specifying Minimum Build System Requirements for Python Projects». Это файл в формате TOML, который можно рассматривать как более продвинутую версию формата ini, в котором параметры хранятся в виде «ключ=значение», при этом параметры могут располагаться не просто в разделах вроде [foo], которые в терминах TOML называются таблицами, но и в подразделах вида [foo.bar.spam]. Параметры могут могут содержать в качестве значения не только строки, но и списки, числа и булевы значения. Этот файл по задумке должен описывать все, что необходимо для сборки Python-пакета, причем не обязательно с помощью SIP. Правда, как мы увидим чуть позже, этого файла в некоторых случаях будет не достаточно, и ему в дополнение нужно будет создать небольшой скрипт на Python. Но давайте обо всем по порядку. Полное описание всех возможных параметров файла pyproject.toml, которые относятся к SIP, можно найти на странице документации SIP. Создадим для нашего примера файл pyproject.toml на том же уровне, что и папка foo:

 foo_c_01/ ??? foo ?   ??? bin ?   ?   ??? foo.o ?   ?   ??? libfoo.a ?   ?   ??? main ?   ?   ??? main.o ?   ??? foo.c ?   ??? foo.h ?   ??? main.c ?   ??? Makefile ??? pyproject.toml 

Содержимое pyproject.toml будет следующее:

[build-system] requires = ["sip >=5, <6"] build-backend = "sipbuild.api"  [tool.sip.metadata] name = "pyfoo" version = "0.1" license = "MIT"  [tool.sip.bindings.pyfoo] headers = ["foo.h"] libraries = ["foo"] include-dirs = ["foo"] library-dirs = ["foo/bin"] 

Раздел [build-system] («таблица» в терминах TOML) является стандартным и описан в PEP 518. Он содержит два параметра:
  • requires — список пакетов, необходимых для сборки нашего пакета. Формат описания зависимостей пакета описан в PEP 508 «Dependency specification for Python Software Packages». В данном случае нам требуется только пакет sip версии 5.x.
  • build-backend описывает, с помощью чего мы будем собирать наш пакет. Строго говоря, этот параметр в виде строки должен содержать полное название Python-объекта, который будет заниматься сборкой. Если не задумываться над глубоким содержимым этого параметра, то для пакетов, собираемых с помощью SIP, это значение должно равняться «sipbuild.api».

Другие параметры описаны в разделах [tool.sip.*].

Раздел [tool.sip.metadata] содержит общую информацию о пакете: имя собираемого пакета (у нас пакет будет называться pyfoo, но не путайте это имя с именем модуля, который мы потом будем импортировать в Python), номер версии пакета (в нашем случае номер версии «0.1») и лицензия (например, "MIT"). Самое важное с точки зрения сборки описано в разделе [tool.sip.bindings.pyfoo].

Обратите внимание на имя пакета в заголовке раздела. В этот раздел мы добавили два параметра:

  • headers — список заголовочных файлов, которые необходимы для использования библиотеки foo.
  • libraries — список объектных файлов, скомпилированных для статической линковки.
  • include-dirs — путь, где искать дополнительные заголовочные файлы помимо тех, что прилагаются к компилятору C. В данном случае, где искать файл foo.h.
  • library-dirs — путь, где искать дополнительные объектные файлы помимо тех, что прилагаются к компилятору C. В данном случае это папка, в которой создается скомпилированный файл библиотеки foo.

Итак, первый необходимый файл для SIP мы создали. Теперь переходим к созданию следующего файла, который будет описывать содержимое будущего Python-модуля.

pyfoo.sip

Создадим файл pyfoo.sip в той же папке, что и файл pyproject.toml:

 foo_c_01/ ??? foo ?   ??? bin ?   ?   ??? foo.o ?   ?   ??? libfoo.a ?   ?   ??? main ?   ?   ??? main.o ?   ??? foo.c ?   ??? foo.h ?   ??? main.c ?   ??? Makefile ??? pyfoo.sip ??? pyproject.toml 

Файл с расширением .sip описывает интерфейс исходной библиотеки, который будет преобразован в модуль на Python. Этот файл имеет собственный формат, который мы сейчас рассмотрим, и напоминает заголовочный файл C/C++ с дополнительной разметкой, которая должна помочь SIP создать Python-модуль.

В нашем примере этот файл должен называться pyfoo.sip, потому что до этого в файле pyproject.toml мы создали раздел [tool.sip.bindings.pyfoo]. В общем случае таких разделов может быть несколько и, соответственно, должно быть несколько файлов *.sip. Но если у нас несколько sip-файлов, то это особый случай с точки зрения SIP, и в этой статье мы его не рассматриваем. Обратите внимание, что в общем случае имя файла .sip (и, соответственно, имя раздела) может не совпадать с именем пакета, которое указано в параметре name в разделе [tool.sip.metadata].

Рассмотрим файл pyfoo.sip из нашего примера:

%Module(name=foo, language="C")  int foo(char*); 

Строки, которые начинаются с символа "%", называются директивами. Они должны подсказывать SIP, как нужно правильно собирать и оформлять Python-модуль. Полный список директив описан на этой странице документации. Некоторые директивы имеют дополнительные параметры. Параметры могут быть не обязательными. В этом примере мы используем две директивы, с некоторыми другими директивами познакомимся в следующих примерах. Файл pyfoo.sip начинается с директивы %Module(name=foo, language=«C»). Обратите внимание, что значение первого параметра (name) мы указали без кавычек, а значение второго параметра (language) с кавычками, как строки в C/C++. Это требование данной директивы, описанное в документации к директиве %Module. В директиве %Module обязательным является только параметр name, который задает имя Python-модуля, из которого мы будем импортировать функцию библиотеки. В данном случае модуль называется foo, он будет содержать функцию foo, поэтому после сборки и установки мы будем ее импортировать с помощью кода:

from foo import foo 

Мы могли бы сделать этот модуль вложенным в другой модуль, заменив эту строку, например, такой:

%Module(name=foo.bar, language="C") ... 

Тогда импортировать функцию foo нужно было бы следующим образом:

from foo.bar import foo 

Параметр language директивы %Module указывает язык, на котором написана исходная библиотека. Значение этого параметра может быть либо «C», либо «C++». Если этот параметр не указать, то SIP будет считать, что библиотека написана на C++.

Теперь посмотрим на последнюю строчку файла pyfoo.sip:

int foo(char*); 

Это описание интерфейса функции из библиотеки, которую мы хотим поместить в Python-модуль. На основе этого объявления sip создаст Python-функцию. Думаю, что здесь все должно быть ясно.

Собираем и проверяем

Теперь все готово для того, чтобы собрать Python-пакет с обвязкой для библиотеки на C. В первую очередь нужно собрать саму библиотеку. Переходим в папку pyfoo_c_01/foo/ и запускаем сборку с помощью команды make:

$ make  mkdir -p bin gcc -c main.c -o bin/main.o gcc -c foo.c -o bin/foo.o ar rcs bin/libfoo.a bin/foo.o gcc bin/main.o -Lbin -lfoo -o bin/main 

Если все прошло успешно, то внутри папки foo будет создана папка bin, в котором среди прочих файлов будет собранная библиотека libfoo.a. Напомню, что здесь, чтобы не отвлекаться от основной темы, мы говорим только про сборку под Linux с помощью gcc.

Переходим обратно в папку pyfoo_c_01. Теперь пришло время познакомиться с командами SIP. После установки SIP станут доступны следующие команды командной строки (страница документации):

  • sip-build. Создает объектный файл Python-расширения (Python extension).
  • sip-install. Создает объектный файл Python-расширения и устанавливает его.
  • sip-sdist. Создает пакет в виде архива .tar.gz, который можно установить с помощью pip.
  • sip-wheel. Создает пакет в формате wheel (файл с расширением .whl).
  • sip-module. Создает модуль, в который включается только служебные инструменты, необходимые самому SIP. Это нужно, если вы создаете библиотеку, разбитую на несколько пакетов. В этой статье мы не будем рассматривать такой случай, мы будем создавать только так называемый standalone project, то есть наш пакет будет единый, он будет включать и библиотеку, для которой мы делаем обвязку, и все служебные инструменты.
  • sip-distinfo. Создает и заполняет папку .dist-info, которая используется в пакете в формате wheel.

Эти команды нужно запускать из папки, где расположен файл pyproject.toml.

Для начала, чтобы лучше понять работу SIP, запустим команду sip-build, причем с параметром --verbose для более подробного вывода в консоль, и посмотрим, что происходит в процессе сборки.

$ sip-build --verbose

These bindings will be built: pyfoo.
Generating the pyfoo bindings…
Compiling the 'foo' module…
building 'foo' extension
creating build
creating build/temp.linux-x86_64-3.8
gcc -pthread -Wno-unused-result -Wsign-compare -DNDEBUG -g -fwrapv -O3 -Wall -march=x86-64 -mtune=generic -O3 -pipe -fno-plt -fno-semantic-interposition -march=x86-64 -mtune=generic -O3 -pipe -fno-plt -march=x86-64 -mtune=generic -O3 -pipe -fno-plt -fPIC -DSIP_PROTECTED_IS_PUBLIC -Dprotected=public -I. -I../../foo -I/usr/include/python3.8 -c sipfoocmodule.c -o build/temp.linux-x86_64-3.8/sipfoocmodule.o
sipfoocmodule.c: В функции «func_foo»:
sipfoocmodule.c:29:22: предупреждение: неявная декларация функции «foo» [-Wimplicit-function-declaration]
29 | sipRes = foo(a0);
| ^~~
gcc -pthread -Wno-unused-result -Wsign-compare -DNDEBUG -g -fwrapv -O3 -Wall -march=x86-64 -mtune=generic -O3 -pipe -fno-plt -fno-semantic-interposition -march=x86-64 -mtune=generic -O3 -pipe -fno-plt -march=x86-64 -mtune=generic -O3 -pipe -fno-plt -fPIC -DSIP_PROTECTED_IS_PUBLIC -Dprotected=public -I. -I../../foo -I/usr/include/python3.8 -c array.c -o build/temp.linux-x86_64-3.8/array.o
gcc -pthread -Wno-unused-result -Wsign-compare -DNDEBUG -g -fwrapv -O3 -Wall -march=x86-64 -mtune=generic -O3 -pipe -fno-plt -fno-semantic-interposition -march=x86-64 -mtune=generic -O3 -pipe -fno-plt -march=x86-64 -mtune=generic -O3 -pipe -fno-plt -fPIC -DSIP_PROTECTED_IS_PUBLIC -Dprotected=public -I. -I../../foo -I/usr/include/python3.8 -c bool.cpp -o build/temp.linux-x86_64-3.8/bool.o
gcc -pthread -Wno-unused-result -Wsign-compare -DNDEBUG -g -fwrapv -O3 -Wall -march=x86-64 -mtune=generic -O3 -pipe -fno-plt -fno-semantic-interposition -march=x86-64 -mtune=generic -O3 -pipe -fno-plt -march=x86-64 -mtune=generic -O3 -pipe -fno-plt -fPIC -DSIP_PROTECTED_IS_PUBLIC -Dprotected=public -I. -I../../foo -I/usr/include/python3.8 -c objmap.c -o build/temp.linux-x86_64-3.8/objmap.o
gcc -pthread -Wno-unused-result -Wsign-compare -DNDEBUG -g -fwrapv -O3 -Wall -march=x86-64 -mtune=generic -O3 -pipe -fno-plt -fno-semantic-interposition -march=x86-64 -mtune=generic -O3 -pipe -fno-plt -march=x86-64 -mtune=generic -O3 -pipe -fno-plt -fPIC -DSIP_PROTECTED_IS_PUBLIC -Dprotected=public -I. -I../../foo -I/usr/include/python3.8 -c qtlib.c -o build/temp.linux-x86_64-3.8/qtlib.o
gcc -pthread -Wno-unused-result -Wsign-compare -DNDEBUG -g -fwrapv -O3 -Wall -march=x86-64 -mtune=generic -O3 -pipe -fno-plt -fno-semantic-interposition -march=x86-64 -mtune=generic -O3 -pipe -fno-plt -march=x86-64 -mtune=generic -O3 -pipe -fno-plt -fPIC -DSIP_PROTECTED_IS_PUBLIC -Dprotected=public -I. -I../../foo -I/usr/include/python3.8 -c int_convertors.c -o build/temp.linux-x86_64-3.8/int_convertors.o
gcc -pthread -Wno-unused-result -Wsign-compare -DNDEBUG -g -fwrapv -O3 -Wall -march=x86-64 -mtune=generic -O3 -pipe -fno-plt -fno-semantic-interposition -march=x86-64 -mtune=generic -O3 -pipe -fno-plt -march=x86-64 -mtune=generic -O3 -pipe -fno-plt -fPIC -DSIP_PROTECTED_IS_PUBLIC -Dprotected=public -I. -I../../foo -I/usr/include/python3.8 -c voidptr.c -o build/temp.linux-x86_64-3.8/voidptr.o
gcc -pthread -Wno-unused-result -Wsign-compare -DNDEBUG -g -fwrapv -O3 -Wall -march=x86-64 -mtune=generic -O3 -pipe -fno-plt -fno-semantic-interposition -march=x86-64 -mtune=generic -O3 -pipe -fno-plt -march=x86-64 -mtune=generic -O3 -pipe -fno-plt -fPIC -DSIP_PROTECTED_IS_PUBLIC -Dprotected=public -I. -I../../foo -I/usr/include/python3.8 -c apiversions.c -o build/temp.linux-x86_64-3.8/apiversions.o
gcc -pthread -Wno-unused-result -Wsign-compare -DNDEBUG -g -fwrapv -O3 -Wall -march=x86-64 -mtune=generic -O3 -pipe -fno-plt -fno-semantic-interposition -march=x86-64 -mtune=generic -O3 -pipe -fno-plt -march=x86-64 -mtune=generic -O3 -pipe -fno-plt -fPIC -DSIP_PROTECTED_IS_PUBLIC -Dprotected=public -I. -I../../foo -I/usr/include/python3.8 -c descriptors.c -o build/temp.linux-x86_64-3.8/descriptors.o
gcc -pthread -Wno-unused-result -Wsign-compare -DNDEBUG -g -fwrapv -O3 -Wall -march=x86-64 -mtune=generic -O3 -pipe -fno-plt -fno-semantic-interposition -march=x86-64 -mtune=generic -O3 -pipe -fno-plt -march=x86-64 -mtune=generic -O3 -pipe -fno-plt -fPIC -DSIP_PROTECTED_IS_PUBLIC -Dprotected=public -I. -I../../foo -I/usr/include/python3.8 -c threads.c -o build/temp.linux-x86_64-3.8/threads.o
gcc -pthread -Wno-unused-result -Wsign-compare -DNDEBUG -g -fwrapv -O3 -Wall -march=x86-64 -mtune=generic -O3 -pipe -fno-plt -fno-semantic-interposition -march=x86-64 -mtune=generic -O3 -pipe -fno-plt -march=x86-64 -mtune=generic -O3 -pipe -fno-plt -fPIC -DSIP_PROTECTED_IS_PUBLIC -Dprotected=public -I. -I../../foo -I/usr/include/python3.8 -c siplib.c -o build/temp.linux-x86_64-3.8/siplib.o
siplib.c: В функции «slot_richcompare»:
siplib.c:9536:16: предупреждение: «st», возможно, используется без инициализации в данной функции [-Wmaybe-uninitialized]
9536 | slot = findSlotInClass(ctd, st);
| ^~~~~~~~~~~~~~~~~~~~~~~~
siplib.c:10671:19: замечание: «st» было объявлено здесь
10671 | sipPySlotType st;
| ^~
siplib.c: В функции «parsePass2»:
siplib.c:5625:32: предупреждение: «owner», возможно, используется без инициализации в данной функции [-Wmaybe-uninitialized]
5625 | *owner = arg;
| ~~~~~~~^~
g++ -pthread -shared -Wl,-O1,--sort-common,--as-needed,-z,relro,-z,now -fno-semantic-interposition -Wl,-O1,--sort-common,--as-needed,-z,relro,-z,now build/temp.linux-x86_64-3.8/sipfoocmodule.o build/temp.linux-x86_64-3.8/array.o build/temp.linux-x86_64-3.8/bool.o build/temp.linux-x86_64-3.8/objmap.o build/temp.linux-x86_64-3.8/qtlib.o build/temp.linux-x86_64-3.8/int_convertors.o build/temp.linux-x86_64-3.8/voidptr.o build/temp.linux-x86_64-3.8/apiversions.o build/temp.linux-x86_64-3.8/descriptors.o build/temp.linux-x86_64-3.8/threads.o build/temp.linux-x86_64-3.8/siplib.o -L../../foo/bin -L/usr/lib -lfoo -o /home/jenyay/projects/soft/sip-examples/pyfoo_c_01/build/foo/foo.cpython-38-x86_64-linux-gnu.so
The project has been built.

Мы не будем сильно углубляться в работу SIP, но из вывода видно, что происходит компиляция каких-то исходников. Эти исходники можно увидеть в созданной этой командой папке build/foo/:

 pyfoo_c_01 ??? build ?   ??? foo ?       ??? 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 ?       ?       ??? sipfoocmodule.o ?       ?       ??? siplib.o ?       ?       ??? threads.o ?       ?       ??? voidptr.o ?       ??? descriptors.c ?       ??? foo.cpython-38-x86_64-linux-gnu.so ?       ??? int_convertors.c ?       ??? objmap.c ?       ??? qtlib.c ?       ??? sipAPIfoo.h ?       ??? sipfoocmodule.c ?       ??? sip.h ?       ??? sipint.h ?       ??? siplib.c ?       ??? threads.c ?       ??? voidptr.c ??? foo ?   ??? bin ?   ?   ??? foo.o ?   ?   ??? libfoo.a ?   ?   ??? main ?   ?   ??? main.o ?   ??? foo.c ?   ??? foo.h ?   ??? main.c ?   ??? Makefile ??? pyfoo.sip ??? pyproject.toml 


В папке build/foo появились вспомогательные исходники. Из любопытства посмотрим файл sipfoocmodule.c, поскольку он непосредственно относится к модулю foo, который будет создан:

/*  * Module code.  *  * Generated by SIP 5.1.1  */  #include "sipAPIfoo.h"  /* Define the strings used by this module. */ const char sipStrings_foo[] = {     'f', 'o', 'o', 0, };  PyDoc_STRVAR(doc_foo, "foo(str) -> int");  static PyObject *func_foo(PyObject *sipSelf,PyObject *sipArgs) {     PyObject *sipParseErr = SIP_NULLPTR;      {         char* a0;          if (sipParseArgs(&sipParseErr, sipArgs, "s", &a0))         {             int sipRes;              sipRes = foo(a0);              return PyLong_FromLong(sipRes);         }     }      /* Raise an exception if the arguments couldn't be parsed. */     sipNoFunction(sipParseErr, sipName_foo, doc_foo);      return SIP_NULLPTR; }  /* This defines this module. */ sipExportedModuleDef sipModuleAPI_foo = {     0,     SIP_ABI_MINOR_VERSION,     sipNameNr_foo,     0,     sipStrings_foo,     SIP_NULLPTR,     SIP_NULLPTR,     0,     SIP_NULLPTR,     SIP_NULLPTR,     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, SIP_NULLPTR, SIP_NULLPTR},     SIP_NULLPTR,     SIP_NULLPTR,     SIP_NULLPTR,     SIP_NULLPTR,     SIP_NULLPTR,     SIP_NULLPTR,     SIP_NULLPTR,     SIP_NULLPTR };  /* The SIP API and the APIs of any imported modules. */ const sipAPIDef *sipAPI_foo;  /* The Python module initialisation function. */ #if defined(SIP_STATIC_MODULE) PyObject *PyInit_foo(void) #else PyMODINIT_FUNC PyInit_foo(void) #endif {     static PyMethodDef sip_methods[] = {         {sipName_foo, func_foo, METH_VARARGS, doc_foo},         {SIP_NULLPTR, SIP_NULLPTR, 0, SIP_NULLPTR}     };      static PyModuleDef sip_module_def = {         PyModuleDef_HEAD_INIT,         "foo",         SIP_NULLPTR,         -1,         sip_methods,         SIP_NULLPTR,         SIP_NULLPTR,         SIP_NULLPTR,         SIP_NULLPTR     };      PyObject *sipModule, *sipModuleDict;     /* Initialise the module and get it's dictionary. */     if ((sipModule = PyModule_Create(&sip_module_def)) == SIP_NULLPTR)         return SIP_NULLPTR;      sipModuleDict = PyModule_GetDict(sipModule);      if ((sipAPI_foo = sip_init_library(sipModuleDict)) == SIP_NULLPTR)         return SIP_NULLPTR;      /* Export the module and publish it's API. */     if (sipExportModule(&sipModuleAPI_foo, SIP_ABI_MAJOR_VERSION, SIP_ABI_MINOR_VERSION, 0) < 0)     {         Py_DECREF(sipModule);         return SIP_NULLPTR;     }     /* Initialise the module now all its dependencies have been set up. */     if (sipInitModule(&sipModuleAPI_foo,sipModuleDict) < 0)     {         Py_DECREF(sipModule);         return SIP_NULLPTR;     }      return sipModule; } 

Если вы работали с Python/C API, то увидите знакомые функции. Особо обратите внимание на функцию func_foo, начинающейся с 18 строки.

В результате компиляции этих исходников будет создан файл build/foo/foo.cpython-38-x86_64-linux-gnu.so, именно он и содержит Python-расширение, которое еще нужно правильно установить.

Для того, чтобы одной командой скомпилировать расширение и сразу его установить, можно воспользоваться командой sip-install, но мы ей пользоваться не будем, потому что по умолчанию пытается установить созданное Python-расширение глобально в систему. У этой команды есть параметр --target-dir, с помощью которого можно указать путь, куда нужно устанавливать расширение, но мы лучше воспользуемся другими инструментами, создающими пакеты, которые затем можно будет установить с помощью pip.

Сначала воспользуемся командой sip-sdist. Использовать ее очень просто:

$ sip-sdist  The sdist has been built. 

После этого будет создан файл pyfoo-0.1.tar.gz, который можно установить с помощью команды:

pip install --user pyfoo-0.1.tar.gz 

В результате будет показана следующая информация и пакет установится:

Processing ./pyfoo-0.1.tar.gz   Installing build dependencies ... done   Getting requirements to build wheel ... done     Preparing wheel metadata ... done Building wheels for collected packages: pyfoo   Building wheel for pyfoo (PEP 517) ... done   Created wheel for pyfoo: filename=pyfoo-0.1-cp38-cp38-manylinux1_x86_64.whl size=337289 sha256=762fc578...   Stored in directory: /home/jenyay/.cache/pip/wheels/54/dc/d8/cc534fff... Successfully built pyfoo Installing collected packages: pyfoo   Attempting uninstall: pyfoo     Found existing installation: pyfoo 0.1     Uninstalling pyfoo-0.1:       Successfully uninstalled pyfoo-0.1 Successfully installed pyfoo-0.1 

Давайте убедимся, что нам удалось сделать Python-обвязку. Запускаем Python и пытаемся вызвать функцию. Напомню, что согласно нашим настройкам, пакет pyfoo содержит модуль foo, в котором имеется функция foo.

>>> from foo import foo >>> foo(b'123456') 12 

Обратите внимание, что в качестве параметра функции мы передаем не просто строку, а строку байтов b'123456' — прямой аналог char* в C. Чуть позже мы добавим преобразование char* в str и обратно. Результат получился ожидаемым. Напомню, что функция foo возвращает удвоенный размер массива типа char*, переданного ей в качестве параметра.

Давайте попробуем передать в функцию foo обычную Python-строку вместо списка байтов.

>>> from foo import foo >>> foo('123456')  Traceback (most recent call last):   File "<stdin>", line 1, in <module> TypeError: foo(str): argument 1 has unexpected type 'str' 

Созданная обвязка не смогла преобразовать строку в char*, как ее научить это делать, мы рассмотрим в следующем разделе.

Поздравляю, мы сделали первую обвязку над библиотекой, написанной на языке C.

Выйдем из интерпретатора Python и соберем сборку в формате wheel. Как вы скорее всего знаете, wheel — это сравнительно новый формат пакетов, который в последнее время используется повсеместно. Описание формата содержится в PEP 427 «The Wheel Binary Package Format 1.0», но описание особенностей формата wheel — тема, достойная отдельной большой статьи. Для нас важно, что пакет в формате wheel пользователь может легко установить с помощью pip. Пакет в формате wheel собирается ничуть не сложнее, чем пакет в формате sdist. Для этого в папке с файлом pyproject.toml нужно выполнить команду

sip-wheel 

После запуска этой команды будет показан процесс сборки и могут быть предупреждения от компилятора:

$ sip-wheel

These bindings will be built: pyfoo.
Generating the pyfoo bindings…
Compiling the 'foo' module…
sipfoocmodule.c: В функции «func_foo»:
sipfoocmodule.c:29:22: предупреждение: неявная декларация функции «foo» [-Wimplicit-function-declaration]
29 | sipRes = foo(a0);
| ^~~
siplib.c: В функции «slot_richcompare»:
siplib.c:9536:16: предупреждение: «st», возможно, используется без инициализации в данной функции [-Wmaybe-uninitialized]
9536 | slot = findSlotInClass(ctd, st);
| ^~~~~~~~~~~~~~~~~~~~~~~~
siplib.c:10671:19: замечание: «st» было объявлено здесь
10671 | sipPySlotType st;
| ^~
siplib.c: В функции «parsePass2»:
siplib.c:5625:32: предупреждение: «owner», возможно, используется без инициализации в данной функции [-Wmaybe-uninitialized]
5625 | *owner = arg;
| ~~~~~~~^~
The wheel has been built.

Когда сборка завершится (наш маленький проект компилируется быстро), в папке проекта появится файл с именем pyfoo-0.1-cp38-cp38-manylinux1_x86_64.whl или похожим. Имя созданного файла может отличаться в зависимости от вашей операционной системы и версии Python.

Теперь мы можем установить этот пакет с помощью pip:

pip install --user --upgrade pyfoo-0.1-cp38-cp38-manylinux1_x86_64.whl 

Здесь используется параметр --upgrade, чтобы pip заменил модуль pyfoo, установленный ранее.

Дальше модуль foo и пакета pyfoo можно использовать, как было показано выше.

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

В предыдущем разделе мы столкнулись с проблемой, что функция foo может принимать только набор байтов, но не строки. Сейчас мы исправим этот недостаток. Для этого мы воспользуемся еще одним инструментом SIP — аннотациями. Аннотации используются внутри файлов .sip и применяются к каким-то элементам кода: функциям, классам, аргументам функций, исключениям, переменным и др. Аннотации записываются между прямыми слешами: /аннотация/.

Аннотация может работать в качестве флага, который может находиться в состоянии установлен или не установлен, например: /ReleaseGIL/, или некоторым аннотациям нужно присваивать какие-либо значения, например: /Encoding=«UTF-8»/. Если к какому-то объекту нужно применить несколько аннотаций, то они разделяются запятыми внутри слешей: /аннотация_1, аннотация_2/.

В следующем примере, который находится в папке pyfoo_c_02, добавим в файл pyfoo.sip аннотацию для параметра функции foo:

%Module(name=foo, language="C")  int foo(char* /Encoding="UTF-8"/); 

Аннотация Encoding указывает, в какую кодировку должна быть закодирована строка, которая будет передаваться в функцию. Значения этой аннотации могут быть следующие: «ASCII», «Latin-1», «UTF-8» или «None». Если аннотация Encoding не указана или равна None, то параметр для такой функции не подвергается никакой кодировке и передается в функцию как есть, но в этом случае параметр в коде на Python должен тип bytes, т.е. массив байтов, что мы и видели в предыдущем примере. Если кодировка указана, то этот параметр может быть строкой (типом str в Python). Аннотация Encoding может применяться только к параметрам типа char, const char, char* или const char*.

Проверим, как теперь работает функция foo из модуля foo. Для этого, как и ранее, нужно сначала скомпилировать библиотеку foo, вызвав внутри папки foo команду make, а затем из папки примера pyfoo_c_02 вызвать команду, например, sip-wheel. Будет создан файл pyfoo-0.2-cp38-cp38-manylinux1_x86_64.whl или с похожим названием, который можно установить с помощью команды:

pip install --user --upgrade pyfoo-0.2-cp38-cp38-manylinux1_x86_64.whl 

Если все прошло успешно, запускаем интерпретатор Python и пробуем вызвать функцию foo со строковым аргументом:

>>> from foo import foo  >>> foo(b'qwerty') 12  >>> foo('qwerty') 12  >>> foo('йцукен') 24 

Сначала мы убеждаемся, что использование типа bytes по-прежнему возможно. После этого убеждаемся, что теперь мы можем передавать в функцию foo также и строковые аргументы. Обратите внимание, что функция foo для строкового аргумента с русскими буквами вернула значение в два раза больше, чем для строки, содержащей только латинские буквы. Это произошло из-за того, что функция foo считает не длину строки в символах (и удваивает ее), а длину массива char*, а поскольку в кодировке UTF-8 русские буквы занимают 2 байта, то и размер массива char* после преобразования из строки Python получился в два раза длиннее.

Отлично! Мы решили проблему с аргументом функции foo, но что, если у нас в библиотеке будут десятки или сотни таких функций, для каждой из них придется указывать кодировку параметров? Часто кодировка в программе используется одна и та же, и нет цели для разных функций указывать разные кодировки. В этом случае в SIP есть возможность указать кодировку по умолчанию, а если для какой-то функции кодировка нужна какая-то другая, то ее можно переопределить с помощью аннотации Encoding.

Чтобы задать кодировку параметров функции по умолчанию предназначена директива %DefaultEncoding. Ее использование показано в примере, расположенном в папке pyfoo_c_03.

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

%Module(name=foo, language="C") %DefaultEncoding "UTF-8"  int foo(char*); 

Теперь, если у аргумента функции типа char, char* и т.п. нет аннотации Encoding, то кодировка берется из директивы %DefaultEncoding, а если ее нет, то преобразование не производится, и для всех параметров char* и т.п. нужно передавать не строки, а bytes.

Пример из папки pyfoo_c_03 собирается и проверяется так же, как и пример из папки pyfoo_c_02.

Коротко о project.py. Автоматизируем сборку

До сих пор для создания Python-обвязки мы использовали два служебных файла — pyproject.toml и pyfoo.sip. Теперь мы познакомимся с еще одним таким файлом, который должен называться project.py. С помощью этого скрипта мы можем влиять на процесс сборки нашего пакета. Давайте займемся автоматизацией сборки. Для того, чтобы собрать примеры pyfoo_c_01pyfoo_c_03 из предыдущих разделов, нужно было сначала зайти в папку foo/, выполнить там компиляцию с помощью команды make, вернуться в папку, где расположен файл pyproject.toml и только тогда запустить сборку пакета с помощью одной из команд sip-*.

Теперь наша цель — сделать так, чтобы при выполнении команд sip-build, sip-sdist и sip-wheel сначала запускалась сборка C-библиотеки foo, а потом уже запускалась непосредственно сама команда.

Пример, создаваемый в этом разделе, находится в папке pyfoo_c_04 исходников.

Чтобы изменить процесс сборки, мы можем в файле project.py (имя файла должно быть именно таким) объявить класс, производный от класса sipbuild.Project. У этого класса есть методы, которые мы можем переопределить на свои. В данный момент нас интересуют следующие методы:

  • build. Вызывается в процессе вызова команды sip-build.
  • build_sdist. Вызывается в процессе вызова команды sip-sdist.
  • build_wheel. Вызывается в процессе вызова команды sip-wheel.
  • install. Вызывается в процессе вызова команды sip-install.

То есть мы можем переопределить поведение этих команд. Строго говоря, перечисленные методы объявлены в абстрактном классе sipbuild.AbstractProject, от которого создан производный класс sipbuild.Project. Создадим файл project.py со следующим содержимым:

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

Мы объявили класс FooProject, производный от класса sipbuild.Project и преопределили в нем методы build, build_sdist, build_wheel и install. Во всех этих методах мы вызываем одноименные методы из базового класса, вызвав перед этим метод _build_foo, который запускает выполнение команды make в папке foo.

Обратите внимание, что методы build_sdist и build_wheel должны вернуть имя созданного ими файла. Это не написано в документации, но указано в исходниках SIP.

Теперь нам не нужно запускать команду make вручную для сборки библиотеки foo, это будет сделано автоматически.

Если теперь в папке pyfoo_c_04 выполнить команду sip-wheel, то будет создан файл с именем pyfoo-0.4-cp38-cp38-manylinux1_x86_64.whl или аналогичный в зависимости от вашей операционной системы и версии Python.

Этот пакет можно установить с помощью команды:

pip install --user --upgrade pyfoo-0.4-cp38-cp38-manylinux1_x86_64.whl 

После этого можно убедиться, что функция foo из модуля foo по-прежнему работает.

Добавляем параметры командной строки для сборки

Следующий пример содержится в папке pyfoo_c_05, а пакет имеет номер версии 0.5 (см. настройки в файле pyproject.toml). Этот пример создан на основе примера из документации с некоторыми исправлениями. В этом примере мы переделаем наш файл project.py и добавим новые параметры командной строки для сборки.

В наших примерах мы собираем очень простую библиотеку foo, а в реальных проектах библиотека может быть достаточно большой и тогда не будет смысла ее включать в исходники проекта Python-обвязки. Напомню, что SIP изначально создавался для создания обвязки для такого огромной библиотеки как Qt. Можно, конечно, возразить, что для организации исходников могут помочь подмодули из git, но не в этом суть. Предположим, что библиотека может находиться не в папке с исходниками обвязки. В этом случае возникает вопрос, где сборщик SIP должен искать заголовочные и объектные файлы библиотеки? В этом случае пути размещения библиотеки у разных пользователей могут быть свои.

Чтобы решить эту проблему, добавим два новых параметра командной строки в систему сборки, с помощью которых можно будет указывать путь до файла foo.h (параметр --foo-include-dir) и до объектного файла библиотеки (параметр --foo-library-dir). Кроме того будем подразумевать, что если эти параметры не указаны, то библиотека foo расположена по-прежнему вместе с исходниками обвязки.

Нам нужно снова создать файл project.py, а в нем объявить класс, производный от sipbuild.Project. Давайте сначала посмотрим на новую версию файла project.py, а потом разберемся, как он работает.

import os  from sipbuild import Option, Project  class FooProject(Project):     """ Проект с дополнительными параметрами командной строки для задания путей до заголовочных и объектных файлов библиотеки foo.     """      def get_options(self):         """ Возвращает список опций командной строки. """          tools = ['build', 'install', 'sdist', 'wheel']          # Получить стандартные опции.         options = super().get_options()          # Добавить новые опции         inc_dir_option = Option('foo_include_dir',                                 help="the directory containing foo.h",                                 metavar="DIR",                                 default=os.path.abspath('foo'),                                 tools=tools)         options.append(inc_dir_option)          lib_dir_option = Option('foo_library_dir',                                 help="the directory containing the foo library",                                 metavar="DIR",                                 default=os.path.abspath('foo/bin'),                                 tools=tools)          options.append(lib_dir_option)          return options      def apply_user_defaults(self, tool):         """ Применить настройки по умолчанию. """          # Применить стандартные настройки по умолчанию         super().apply_user_defaults(tool)          # Чтобы гарантировать, что пути до заголовочных файлов и собранной библиотеки абсолютные         self.foo_include_dir = os.path.abspath(self.foo_include_dir)         self.foo_library_dir = os.path.abspath(self.foo_library_dir)      def update(self, tool):         """ Обновить конфигурацию проекта. """          # Получить обвязки pyfoo         # (в файле pyproject.toml раздел [tool.sip.bindings.pyfoo])         foo_bindings = self.bindings['pyfoo']          # Установим параметр include_dirs для обвязки         if self.foo_include_dir is not None:             foo_bindings.include_dirs = [self.foo_include_dir]          # Установим параметр library_dirs для обвязки         if self.foo_library_dir is not None:             foo_bindings.library_dirs = [self.foo_library_dir]          super().update(tool) 

Мы снова создали класс FooProject, производный от sipbuild.Project. В этом примере отключена автоматическая сборка библиотеки foo, потому что теперь подразумевается, что она может находиться в каком-нибудь другом месте, и к моменту создания обвязки уже должны быть готовы заголовочные и объектные файлы.

В классе FooProject переопределены три метода: get_options, apply_user_defaults и update. Рассмотрим их более внимательно.

Начнем с метода get_options. Этот метод должен возвращать список экземпляров класса sipbuild.Option. Каждый элемент списка — это опция командной строки. Внутри переопределенного метода мы получаем список опций по умолчанию (переменная options) с помощью вызова одноименного метода базового класса, затем создаем две новые опции (--foo_include_dir и --foo_library_dir) и добавляем их в список, после чего возвращаем этот список из функции.

Конструктор класса Option принимает один обязательный параметр (имя опции) и достаточно большое количество необязательных, описывающие тип значения для этого параметра, значение по умолчанию, описание параметра и некоторые другие. В этом примере используются следующие параметры конструктора Option:

  • help задает описание параметра, которое можно увидеть, если запустить команду вроде sip-wheel -h
  • metavar — строковое значение, которое для пользователя описывает, что должно представлять собой значение данного параметра. В нашем примере параметр metavar равен «DIR», чтобы подсказать пользователю, что значение этого параметра — директория.
  • default — значение по умолчанию для параметра. В нашем примере подразумевается, что если не указаны пути к заголовочным и объектным файлам, то библиотека foo расположена там же, где и в предыдущих примерах (в папке с исходниками обвязки).
  • tools — список строк, описывающих к каким командам должна применяться данная опция. В нашем примере мы добавляем параметры к sip-build, sip-install, sip-sdist и sip-wheel, поэтому tools = ['build', 'install', 'sdist', 'wheel'].

Следующий перегруженный метод apply_user_defaults предназначен для установки значений параметров, которые пользователь может передать через командную строку. Метод apply_user_defaults из базового класса создает для каждого параметра командной строки, созданного в методе get_options, переменную (член класса), поэтому важно вызвать одноименный метод базового класса до использования созданных переменных, чтобы все созданные по параметрам командной строки переменные были созданы и проинициализированы значениями по умолчанию. После этого в нашем примере будут созданы переменные self.foo_include_dir и self.foo_library_dir. Если пользователь не указал соответствующие им параметры командной строки, то они будут принимать значения по умолчанию согласно параметрам конструктора класса Option (параметр default). Если параметр default не задан, то в зависимости от типа ожидаемого значения параметра он будет инициализирован либо None, либо пустым списком, либо 0.

Внутри метода apply_user_defaults делаем так, чтобы пути в переменных self.foo_include_dir и self.foo_library_dir всегда были абсолютными. Это нужно чтобы не зависеть от того, какой будет рабочая папка в момент запуска сборки.

Последний перегруженный метод в этом классе — update. Этот метод вызывается, когда нужно применить к проекту выполненные до этого изменения. Например, изменить или добавить параметры, заданные в файле pyproject.toml. В предыдущих примерах мы устанавливали пути до заголовочных и объектных файлов с помощью параметров include-dirs и library-dirs соответственно внутри раздела [tool.sip.bindings.pyfoo]. Теперь эти параметры мы будем устанавливать из скрипта project.py, поэтому в файле pyproject.toml эти параметры удалим:

[build-system] requires = ["sip >=5, <6"] build-backend = "sipbuild.api"  [tool.sip.metadata] name = "pyfoo" version = "0.3" license = "MIT"  [tool.sip.bindings.pyfoo] headers = ["foo.h"] libraries = ["foo"] 

Внутри метода update мы из словаря self.bindings по ключу pyfoo достаем экземпляр класса sipbuild.Bindings. Имя ключа соответствует разделу [tool.sip.bindings.pyfoo] из файла pyproject.toml, и полученный таким образом экземпляр класса описывает настройки, описанные в этом разделе. Затем членам этого класса include_dirs и library_dirs (имена членов соответствуют параметрам include-dirs и library-dirs с заменой дефиса на нижнее подчеркивание) присваиваем списки, содержащие пути, хранящиеся в членах self.foo_include_dir и self.foo_library_dir. В этом примере для аккуратности производится проверка на то, что значения self.foo_include_dir и self.foo_library_dir не равны None, но в данном примере это условие всегда выполняется, потому что у созданных нами параметров командной строки есть значения по умолчанию.

Таким образом мы подготовили файлы настроек для того, чтобы при сборке можно было указывать пути до заголовочных и объектных файлов. Проверим, что получилось.

Для начала убедимся, что работают значения по умолчанию. Для этого нужно зайти в папку pyfoo_c_05/foo и собрать библиотеку с помощью команды make, поскольку мы отключили автоматическую сборку библиотеки в этом примере.

После этого заходим в папку pyfoo_c_05 и запускаем команду sip-wheel. В результате выполнения этой команды будет создан файл pyfoo-0.5-cp38-cp38-manylinux1_x86_64.whl или с похожим названием.

Теперь перенесем папку foo куда-нибудь за пределы папки pyfoo_c_05 и снова запустим команду sip-wheel. В результате получим ожидаемую ошибку, сообщающую, что у нас нет объектного файла библиотеки:

usr/bin/ld: невозможно найти -lfoo collect2: ошибка: выполнение ld завершилось с кодом возврата 1 sip-wheel: Unable to compile the 'foo' module: command 'g++' failed with exit status 1 

После этого запустим sip-wheel с использованием новых параметром командной строки:

sip-wheel --foo-include-dir ".../foo" --foo-library-dir ".../foo/bin" 

Вместо многоточия нужно указать путь до папки, куда вы перенесли папку foo с собранной библиотекой. В результате сборка должна закончиться успешно созданием файла .whl. Созданный модуль можно установить и протестировать так же, как это делали в предыдущих разделах.

Проверяем порядок вызова методов из project.py

Следующий пример, который мы рассмотрим, будет совсем простым, он продемонстрирует порядок вызова методов класса Project, которые мы перегружали в предыдущих разделах. Это может быть полезно для того, чтобы понять, когда можно инициализировать переменные. Данный пример находится в папке pyfoo_c_06 в репозитории с исходниками.

Суть этого примера состоит в том, чтобы в классе FooProject, который расположен в файле project.py, перегрузить все методы, которые мы использовали до этого, и добавить в них вызовы функции print, которая бы выводила имя метода, в котором она находится:

from sipbuild import Project  class FooProject(Project):     def get_options(self):         print('get_options()')         options = super().get_options()         return options      def apply_user_defaults(self, tool):         print('apply_user_defaults()')         super().apply_user_defaults(tool)      def apply_nonuser_defaults(self, tool):         print('apply_nonuser_defaults()')         super().apply_nonuser_defaults(tool)      def update(self, tool):         print('update()')         super().update(tool)      def build(self):         print('build()')         super().build()      def build_sdist(self, sdist_directory):         print('build_sdist()')         return super().build_sdist(sdist_directory)      def build_wheel(self, wheel_directory):         print('build_wheel()')         return super().build_wheel(wheel_directory)      def install(self):         print('install()')         super().install() 

Внимательные читатели должны заметить, что помимо ранее использованных методов, в этом примере перегружен еще метод apply_nonuser_defaults(), о котором мы раньше не говорили. В этом методе рекомендуют устанавливать значения по умолчанию для всех переменных, которые нельзя изменить через параметры командной строки.

В файле pyproject.toml вернем явное указание пути до библиотеки:

[build-system] requires = ["sip >=5, <6"] build-backend = "sipbuild.api"  [tool.sip.metadata] name = "pyfoo" version = "0.4" license = "MIT"  [tool.sip.bindings.pyfoo] headers = ["foo.h"] libraries = ["foo"] include-dirs = ["foo"] library-dirs = ["foo/bin"] 

Чтобы проект успешно собрался, нужно войти в папку foo и собрать там библиотеку с помощью команды make. После этого вернуться в папку pyfoo_c_06 и запустить, например, команду sip-wheel. В результате, если отбросить предупреждения компилятора, будет выведен следующий текст:

get_options()
apply_nonuser_defaults()
get_options()
get_options()
apply_user_defaults()
get_options()
update()
These bindings will be built: pyfoo.
build_wheel()
Generating the pyfoo bindings…
Compiling the 'foo' module…
The wheel has been built.

Полужирным шрифтом выделены строки, которые выводятся из нашего файла project.py. Таким образом мы видим, что метод get_options вызывается несколько раз, и это надо учитывать, если вы собираетесь инициализировать какую-нибудь переменную-член в классе, производный от Project. Метод get_options для этого — не лучшее место.

Также полезно запомнить, что метод apply_nonuser_defaults вызывается до метода apply_user_defaults, т.е. в методе apply_user_defaults уже можно использовать переменные, значения которых установлены в методе apply_nonuser_defaults.

После этого вызывается метод update, а в самом конце метод, отвечающий непосредственно за сборку, в нашем случае — build_wheel.

Заключение к первой части

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

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

В файле *.sip описывается интерфейс Python-модуля с перечислением функций и классов, которые будут содержаться в модуле. Для описания интерфейса в файле *.sip используются директивы и аннотации.

Во второй части статьи мы создадим обвязку над объектно-ориентированной библиотекой, написанной на C++, и на ее примере изучим приемы, которые будут полезны при описании интерфейса классов C++, а заодно разберемся с новыми для нас директивами и аннотациями.

Продолжение следует.

Ссылки


Источник: habr.com

Комментарии: