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

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

Введение

Иногда во время работы над проектом на языке 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/
└── foo
    ├── foo.c
    ├── foo.h
    ├── main.c
    └── Makefile

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

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

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, который будет создан:

  1. /*
  2.  * Module code.
  3.  *
  4.  * Generated by SIP 5.1.1
  5.  */
  6.  
  7. #include "sipAPIfoo.h"
  8.  
  9.  
  10. /* Define the strings used by this module. */
  11. const char sipStrings_foo[] = {
  12.     'f', 'o', 'o', 0,
  13. };
  14.  
  15.  
  16. PyDoc_STRVAR(doc_foo, "foo(str) -> int");
  17.  
  18. static PyObject *func_foo(PyObject *sipSelf,PyObject *sipArgs)
  19. {
  20.     PyObject *sipParseErr = SIP_NULLPTR;
  21.  
  22.     {
  23.         char* a0;
  24.  
  25.         if (sipParseArgs(&sipParseErr, sipArgs, "s", &a0))
  26.         {
  27.             int sipRes;
  28.  
  29.             sipRes = foo(a0);
  30.  
  31.             return PyLong_FromLong(sipRes);
  32.         }
  33.     }
  34.  
  35.     /* Raise an exception if the arguments couldn't be parsed. */
  36.     sipNoFunction(sipParseErr, sipName_foo, doc_foo);
  37.  
  38.     return SIP_NULLPTR;
  39. }
  40.  
  41.  
  42. /* This defines this module. */
  43. sipExportedModuleDef sipModuleAPI_foo = {
  44.     0,
  45.     SIP_ABI_MINOR_VERSION,
  46.     sipNameNr_foo,
  47.     0,
  48.     sipStrings_foo,
  49.     SIP_NULLPTR,
  50.     SIP_NULLPTR,
  51.     0,
  52.     SIP_NULLPTR,
  53.     SIP_NULLPTR,
  54.     0,
  55.     SIP_NULLPTR,
  56.     0,
  57.     SIP_NULLPTR,
  58.     SIP_NULLPTR,
  59.     SIP_NULLPTR,
  60.     {SIP_NULLPTR, SIP_NULLPTR, SIP_NULLPTR, SIP_NULLPTR, SIP_NULLPTR,
  61.             SIP_NULLPTR, SIP_NULLPTR, SIP_NULLPTR, SIP_NULLPTR, SIP_NULLPTR},
  62.     SIP_NULLPTR,
  63.     SIP_NULLPTR,
  64.     SIP_NULLPTR,
  65.     SIP_NULLPTR,
  66.     SIP_NULLPTR,
  67.     SIP_NULLPTR,
  68.     SIP_NULLPTR,
  69.     SIP_NULLPTR
  70. };
  71.  
  72.  
  73. /* The SIP API and the APIs of any imported modules. */
  74. const sipAPIDef *sipAPI_foo;
  75.  
  76.  
  77. /* The Python module initialisation function. */
  78. #if defined(SIP_STATIC_MODULE)
  79. PyObject *PyInit_foo(void)
  80. #else
  81. PyMODINIT_FUNC PyInit_foo(void)
  82. #endif
  83. {
  84.     static PyMethodDef sip_methods[] = {
  85.         {sipName_foo, func_foo, METH_VARARGS, doc_foo},
  86.         {SIP_NULLPTR, SIP_NULLPTR, 0, SIP_NULLPTR}
  87.     };
  88.  
  89.     static PyModuleDef sip_module_def = {
  90.         PyModuleDef_HEAD_INIT,
  91.         "foo",
  92.         SIP_NULLPTR,
  93.         -1,
  94.         sip_methods,
  95.         SIP_NULLPTR,
  96.         SIP_NULLPTR,
  97.         SIP_NULLPTR,
  98.         SIP_NULLPTR
  99.     };
  100.  
  101.     PyObject *sipModule, *sipModuleDict;
  102.     /* Initialise the module and get it's dictionary. */
  103.     if ((sipModule = PyModule_Create(&sip_module_def)) == SIP_NULLPTR)
  104.         return SIP_NULLPTR;
  105.  
  106.     sipModuleDict = PyModule_GetDict(sipModule);
  107.  
  108.     if ((sipAPI_foo = sip_init_library(sipModuleDict)) == SIP_NULLPTR)
  109.         return SIP_NULLPTR;
  110.  
  111.     /* Export the module and publish it's API. */
  112.     if (sipExportModule(&sipModuleAPI_foo, SIP_ABI_MAJOR_VERSION, SIP_ABI_MINOR_VERSION, 0) < 0)
  113.     {
  114.         Py_DECREF(sipModule);
  115.         return SIP_NULLPTR;
  116.     }
  117.     /* Initialise the module now all its dependencies have been set up. */
  118.     if (sipInitModule(&sipModuleAPI_foo,sipModuleDict) < 0)
  119.     {
  120.         Py_DECREF(sipModule);
  121.         return SIP_NULLPTR;
  122.     }
  123.  
  124.     return sipModule;
  125. }

Если вы работали с 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:

  1. %Module(name=foo, language="C")
  2.  
  3. 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 или с похожим названием, который можно установить с помощью команды

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

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

  1. >>> from foo import foo
  2.  
  3. >>> foo(b'qwerty')
  4. 12
  5.  
  6. >>> foo('qwerty')
  7. 12
  8.  
  9. >>> foo('йцукен')
  10. 24

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

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

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

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

  1. %Module(name=foo, language="C")
  2. %DefaultEncoding "UTF-8"
  3.  
  4. 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_01 - pyfoo_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 со следующим содержимым:

  1. import os
  2. import subprocess
  3.  
  4. from sipbuild import Project
  5.  
  6.  
  7. class FooProject(Project):
  8.     def _build_foo(self):
  9.         cwd = os.path.abspath('foo')
  10.         subprocess.run(['make'], cwd=cwd, capture_output=True, check=True)
  11.  
  12.     def build(self):
  13.         self._build_foo()
  14.         super().build()
  15.  
  16.     def build_sdist(self, sdist_directory):
  17.         self._build_foo()
  18.         return super().build_sdist(sdist_directory)
  19.  
  20.     def build_wheel(self, wheel_directory):
  21.         self._build_foo()
  22.         return super().build_wheel(wheel_directory)
  23.  
  24.     def install(self):
  25.         self._build_foo()
  26.         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.

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

  1. 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, а потом разберемся, как он работает.

  1. import os
  2.  
  3. from sipbuild import Option, Project
  4.  
  5.  
  6. class FooProject(Project):
  7.     """ Проект с дополнительными параметрами командной строки для задания
  8. путей до заголовочных и объектных файлов библиотеки foo.
  9.    """
  10.  
  11.     def get_options(self):
  12.         """ Возвращает список опций командной строки. """
  13.  
  14.         tools = ['build', 'install', 'sdist', 'wheel']
  15.  
  16.         # Получить стандартные опции.
  17.         options = super().get_options()
  18.  
  19.         # Добавить новые опции
  20.         inc_dir_option = Option('foo_include_dir',
  21.                                 help="the directory containing foo.h",
  22.                                 metavar="DIR",
  23.                                 default=os.path.abspath('foo'),
  24.                                 tools=tools)
  25.         options.append(inc_dir_option)
  26.  
  27.         lib_dir_option = Option('foo_library_dir',
  28.                                 help="the directory containing the foo library",
  29.                                 metavar="DIR",
  30.                                 default=os.path.abspath('foo/bin'),
  31.                                 tools=tools)
  32.  
  33.         options.append(lib_dir_option)
  34.  
  35.         return options
  36.  
  37.     def apply_user_defaults(self, tool):
  38.         """ Применить настройки по умолчанию. """
  39.  
  40.         # Применить стандартные настройки по умолчанию
  41.         super().apply_user_defaults(tool)
  42.  
  43.         # Чтобы гарантировать, что пути до заголовочных файлов и собранной библиотеки абсолютные
  44.         self.foo_include_dir = os.path.abspath(self.foo_include_dir)
  45.         self.foo_library_dir = os.path.abspath(self.foo_library_dir)
  46.  
  47.     def update(self, tool):
  48.         """ Обновить конфигурацию проекта. """
  49.  
  50.         # Получить обвязки pyfoo
  51.         # (в файле pyproject.toml раздел [tool.sip.bindings.pyfoo])
  52.         foo_bindings = self.bindings['pyfoo']
  53.  
  54.         # Установим параметр include_dirs для обвязки
  55.         if self.foo_include_dir is not None:
  56.             foo_bindings.include_dirs = [self.foo_include_dir]
  57.  
  58.         # Установим параметр library_dirs для обвязки
  59.         if self.foo_library_dir is not None:
  60.             foo_bindings.library_dirs = [self.foo_library_dir]
  61.  
  62.         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 эти параметры удалим:

  1. [build-system]
  2. requires = ["sip >=5, <6"]
  3. build-backend = "sipbuild.api"
  4.  
  5. [tool.sip.metadata]
  6. name = "pyfoo"
  7. version = "0.3"
  8. license = "MIT"
  9.  
  10. [tool.sip.bindings.pyfoo]
  11. headers = ["foo.h"]
  12. 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++, а заодно разберемся с новыми для нас директивами и аннотациями.

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

Ссылки

Вы можете подписаться на новости сайта через RSS, Группу Вконтакте или Канал в Telegram.
5 stars

Рейтинг 5.0/5. Всего 1 голос(а, ов)




Подписаться на комментарии
Автор:
Тема:
 Ваш комментарий
 
 
Введите код 699