Иногда во время работы над проектом на языке 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\n", 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/
L-- foo
    +-- foo.c
    +-- foo.h
    +-- main.c
    L-- 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/
L-- foo
    +-- bin
    ¦   +-- foo.o
    ¦   +-- libfoo.a
    ¦   +-- main
    ¦   L-- main.o
    +-- foo.c
    +-- foo.h
    +-- main.c
    L-- 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
¦   ¦   L-- main.o
¦   +-- foo.c
¦   +-- foo.h
¦   +-- main.c
¦   L-- Makefile
L-- 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
¦   ¦   L-- main.o
¦   +-- foo.c
¦   +-- foo.h
¦   +-- main.c
¦   L-- Makefile
+-- pyfoo.sip
L-- 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
¦   L-- foo
¦       +-- apiversions.c
¦       +-- array.c
¦       +-- array.h
¦       +-- bool.cpp
¦       +-- build
¦       ¦   L-- 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
¦       ¦       L-- 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
¦       L-- voidptr.c
+-- foo
¦   +-- bin
¦   ¦   +-- foo.o
¦   ¦   +-- libfoo.a
¦   ¦   +-- main
¦   ¦   L-- main.o
¦   +-- foo.c
¦   +-- foo.h
¦   +-- main.c
¦   L-- Makefile
+-- pyfoo.sip
L-- 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++, а заодно разберемся с новыми для нас директивами и аннотациями.

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

Ссылки