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

Все примеры для статьи доступны в репозитории на github по ссылке: https://github.com/Jenyay/sip-examples.

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


Следующий пример, который мы будем рассматривать, находится в папке pyfoo_cpp_01.

Для начала создадим библиотеку, для которой мы будем делать обвязку. Библиотека будет по-прежнему располагаться в папке foo и содержать один класс — Foo. Заголовочный файл foo.h с объявлением этого класса выглядит следующим образом:

#ifndef FOO_LIB
#define FOO_LIB

class Foo {
    private:
        int _int_val;
        char* _string_val;
    public:
        Foo(int int_val, const char* string_val);
        virtual ~Foo();

        void set_int_val(int val);
        int get_int_val();

        void set_string_val(const char* val);
        char* get_string_val();
};

#endif

Это простой класс с двумя геттерами и сеттерами, устанавливающие и возвращающие значения типа int и char*. Реализация класса выглядит следующим образом:

#include <string.h>

#include "foo.h"

Foo::Foo(int int_val, const char* string_val): _int_val(int_val) {
    _string_val = nullptr;
    set_string_val(string_val);
}

Foo::~Foo(){
    delete[] _string_val;
    _string_val = nullptr;
}

void Foo::set_int_val(int val) {
    _int_val = val;
}

int Foo::get_int_val() {
    return _int_val;
}

void Foo::set_string_val(const char* val) {
    if (_string_val != nullptr) {
        delete[] _string_val;
    }

    auto count = strlen(val) + 1;
    _string_val = new char[count];
    strcpy(_string_val, val);
}

char* Foo::get_string_val() {
    return _string_val;
}

Для проверки работоспособности библиотеки в папке foo также содержится файл main.cpp, использующий класс Foo:

#include <iostream>

#include "foo.h"

using std::cout;
using std::endl;

int main(int argc, char* argv[]) {
    auto foo = Foo(10, "Hello");
    cout << "int_val: " << foo.get_int_val() << endl;
    cout << "string_val: " << foo.get_string_val() << endl;

    foo.set_int_val(0);
    foo.set_string_val("Hello world!");

    cout << "int_val: " << foo.get_int_val() << endl;
    cout << "string_val: " << foo.get_string_val() << endl;
}

Для сборки библиотеки foo используется следующий Makefile:

CC=g++
CFLAGS=-c -fPIC
DIR_OUT=bin

all: main

main: main.o libfoo.a
    $(CC) $(DIR_OUT)/main.o -L$(DIR_OUT) -lfoo -o $(DIR_OUT)/main

main.o: makedir main.cpp
    $(CC) $(CFLAGS) main.cpp -o $(DIR_OUT)/main.o

libfoo.a: makedir foo.cpp
    $(CC) $(CFLAGS) foo.cpp -o $(DIR_OUT)/foo.o
    ar rcs $(DIR_OUT)/libfoo.a $(DIR_OUT)/foo.o

makedir:
    mkdir -p $(DIR_OUT)

clean:
    rm -rf $(DIR_OUT)/*

Отличие от Makefile в предыдущих примерах, помимо изменение компилятора с gcc на g++, заключается в том, что для компиляции был добавлен еще один параметр -fPIC, который указывает компилятору размещать код в библиотеке определенным образом (так называемый «позиционно-независимый код»). Поскольку эта статья не про компиляторы, то не будем более подробно разбираться с тем, что этот параметр делает и зачем он нужен.

Начнем делать обвязку для этой библиотеки. Файлы pyproject.toml и project.py почти не изменятся по сравнению с предыдущими примерами. Вот как теперь выглядит файл pyproject.toml:

[build-system]
requires = ["sip >=5, <6"]
build-backend = "sipbuild.api"

[tool.sip.metadata]
name = "pyfoocpp"
version = "0.1"
license = "MIT"

[tool.sip.bindings.pyfoocpp]
headers = ["foo.h"]
libraries = ["foo"]

Теперь наши примеры, написанные на языке C++ будут упаковываться в Python-пакет pyfoocpp, это, пожалуй, единственное заметное изменение в этом файле.

Файл project.py остался такой же, как и в примере pyfoo_c_04:

import os
import subprocess

from sipbuild import Project

class FooProject(Project):
    def _build_foo(self):
        cwd = os.path.abspath('foo')
        subprocess.run(['make'], cwd=cwd, capture_output=True, check=True)

    def build(self):
        self._build_foo()
        super().build()

    def build_sdist(self, sdist_directory):
        self._build_foo()
        return super().build_sdist(sdist_directory)

    def build_wheel(self, wheel_directory):
        self._build_foo()
        return super().build_wheel(wheel_directory)

    def install(self):
        self._build_foo()
        super().install()

А вот файл pyfoocpp.sip мы рассмотрим более подробно. Напомню, что этот файл описывает интерфейс для будущего Python-модуля: что он должен в себя включать, как должен выглядеть интерфейс классов и т.д. Файл .sip не обязан повторять заголовочный файл библиотеки, хоть у них и будет много общего. Внутри этого класса могут добавляться новые методы, которых не было в исходном классе. Т.е. интерфейс, описанный в файле .sip может подстраивать классы библиотеки под принципы, принятые в языке Python, если это необходимо. В файле pyfoocpp.sip мы увидим новые для нас директивы.

Для начала посмотрим, что этот файл содержит:

%Module(name=foocpp, language="C++")
%DefaultEncoding "UTF-8"

class Foo {
    %TypeHeaderCode
    #include <foo.h>
    %End

    public:
        Foo(int, const char*);

        void set_int_val(int);
        int get_int_val();

        void set_string_val(const char*);
        char* get_string_val();
};

Первые строки нам уже должны быть понятны по предыдущим примерам. В директиве %Module мы указываем имя Python-модуля, который будет создан (т.е. для использования этого модуля мы должны будем использовать команды import foocpp или from foocpp import .... В этой же директиве мы указываем, что язык у нас теперь — C++. Директива %DefaultEncoding задает кодировку, которая будет использоваться для преобразования строки Python в типы char, const char, char* и const char*.

Затем следует объявление интерфейса класса Foo. Сразу после объявления класса Foo встречается не используемая до сих пор директива %TypeHeaderCode, которая заканчивается директивой %End. Директива %TypeHeaderCode должна содержать код, объявляющий интерфейс класса C++, для которого создается обертка. Как правило, в этой директиве достаточно подключить заголовочный файл с объявлением класса.

После этого перечислены методы класса, которые будут преобразованы в методы класса Foo для языка Python. Важно отметить, что в этом месте мы объявляем только публичные методы, которые будут доступны из класса Foo в Python (поскольку в Python нет приватных и защищенных членов). Поскольку мы в самом начале использовали директиву %DefaultEncoding, то в методах, принимающих аргументы типа const char*, можно не использовать аннотацию Encoding для указания кодировки для преобразования этих параметров в строки Python и обратно.

Теперь нам остается собрать Python-пакет pyfoocpp и проверить его. Но прежде чем собирать полноценный wheel-пакет, давайте воспользуемся командой sip-build и посмотрим, какие исходные файлы для последующей компиляции создаст SIP, и попытаемся найти в них что-то похожее на тот класс, который будет создаваться в коде на языке Python. Для этого вышеуказанную команду sip-build нужно вызвать в папке pyfoo_cpp_01. В результате будет создана папка build со следующим содержимым:

build
L-- foocpp
    +-- 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
    ¦       +-- sipfoocppcmodule.o
    ¦       +-- sipfoocppFoo.o
    ¦       +-- siplib.o
    ¦       +-- threads.o
    ¦       L-- voidptr.o
    +-- descriptors.c
    +-- foocpp.cpython-38-x86_64-linux-gnu.so
    +-- int_convertors.c
    +-- objmap.c
    +-- qtlib.c
    +-- sipAPIfoocpp.h
    +-- sipfoocppcmodule.cpp
    +-- sipfoocppFoo.cpp
    +-- sip.h
    +-- sipint.h
    +-- siplib.c
    +-- threads.c
    L-- voidptr.c


В качестве дополнительного задания рассмотрите внимательнее файл sipfoocppFoo.cpp (мы его не будем подробно обсуждать в этой статье):

/*
 * Interface wrapper code.
 *
 * Generated by SIP 5.1.1
 */

#include "sipAPIfoocpp.h"

#line 6 "/home/jenyay/temp/2/pyfoocpp.sip"
    #include <foo.h>
#line 12 "/home/jenyay/temp/2/build/foocpp/sipfoocppFoo.cpp"

PyDoc_STRVAR(doc_Foo_set_int_val, "set_int_val(self, int)");

extern "C" {static PyObject *meth_Foo_set_int_val(PyObject *, PyObject *);}
static PyObject *meth_Foo_set_int_val(PyObject *sipSelf, PyObject *sipArgs)
{
    PyObject *sipParseErr = SIP_NULLPTR;

    {
        int a0;
         ::Foo *sipCpp;

        if (sipParseArgs(&sipParseErr, sipArgs, "Bi", &sipSelf, sipType_Foo, &sipCpp, &a0))
        {
            sipCpp->set_int_val(a0);

            Py_INCREF(Py_None);
            return Py_None;
        }
    }

    /* Raise an exception if the arguments couldn't be parsed. */
    sipNoMethod(sipParseErr, sipName_Foo, sipName_set_int_val, doc_Foo_set_int_val);

    return SIP_NULLPTR;
}

PyDoc_STRVAR(doc_Foo_get_int_val, "get_int_val(self) -> int");

extern "C" {static PyObject *meth_Foo_get_int_val(PyObject *, PyObject *);}
static PyObject *meth_Foo_get_int_val(PyObject *sipSelf, PyObject *sipArgs)
{
    PyObject *sipParseErr = SIP_NULLPTR;

    {
         ::Foo *sipCpp;

        if (sipParseArgs(&sipParseErr, sipArgs, "B", &sipSelf, sipType_Foo, &sipCpp))
        {
            int sipRes;

            sipRes = sipCpp->get_int_val();

            return PyLong_FromLong(sipRes);
        }
    }

    /* Raise an exception if the arguments couldn't be parsed. */
    sipNoMethod(sipParseErr, sipName_Foo, sipName_get_int_val, doc_Foo_get_int_val);

    return SIP_NULLPTR;
}

PyDoc_STRVAR(doc_Foo_set_string_val, "set_string_val(self, str)");

extern "C" {static PyObject *meth_Foo_set_string_val(PyObject *, PyObject *);}
static PyObject *meth_Foo_set_string_val(PyObject *sipSelf, PyObject *sipArgs)
{
    PyObject *sipParseErr = SIP_NULLPTR;

    {
        const char* a0;
        PyObject *a0Keep;
         ::Foo *sipCpp;

        if (sipParseArgs(&sipParseErr, sipArgs, "BA8", &sipSelf, sipType_Foo, &sipCpp, &a0Keep, &a0))
        {
            sipCpp->set_string_val(a0);
            Py_DECREF(a0Keep);

            Py_INCREF(Py_None);
            return Py_None;
        }
    }

    /* Raise an exception if the arguments couldn't be parsed. */
    sipNoMethod(sipParseErr, sipName_Foo, sipName_set_string_val, doc_Foo_set_string_val);

    return SIP_NULLPTR;
}

PyDoc_STRVAR(doc_Foo_get_string_val, "get_string_val(self) -> str");

extern "C" {static PyObject *meth_Foo_get_string_val(PyObject *, PyObject *);}
static PyObject *meth_Foo_get_string_val(PyObject *sipSelf, PyObject *sipArgs)
{
    PyObject *sipParseErr = SIP_NULLPTR;

    {
         ::Foo *sipCpp;

        if (sipParseArgs(&sipParseErr, sipArgs, "B", &sipSelf, sipType_Foo, &sipCpp))
        {
            char*sipRes;

            sipRes = sipCpp->get_string_val();

            if (sipRes == SIP_NULLPTR)
            {
                Py_INCREF(Py_None);
                return Py_None;
            }

            return PyUnicode_FromString(sipRes);
        }
    }

    /* Raise an exception if the arguments couldn't be parsed. */
    sipNoMethod(sipParseErr, sipName_Foo, sipName_get_string_val, doc_Foo_get_string_val);

    return SIP_NULLPTR;
}

/* Call the instance's destructor. */
extern "C" {static void release_Foo(void *, int);}
static void release_Foo(void *sipCppV, int)
{
    delete reinterpret_cast< ::Foo *>(sipCppV);
}

extern "C" {static void dealloc_Foo(sipSimpleWrapper *);}
static void dealloc_Foo(sipSimpleWrapper *sipSelf)
{
    if (sipIsOwnedByPython(sipSelf))
    {
        release_Foo(sipGetAddress(sipSelf), 0);
    }
}

extern "C" {static void *init_type_Foo(sipSimpleWrapper *, PyObject *, 
                 PyObject *, PyObject **, PyObject **, PyObject **);}
static void *init_type_Foo(sipSimpleWrapper *, PyObject *sipArgs, PyObject *sipKwds,
                                   PyObject **sipUnused, PyObject **, PyObject **sipParseErr)
{
     ::Foo *sipCpp = SIP_NULLPTR;

    {
        int a0;
        const char* a1;
        PyObject *a1Keep;

        if (sipParseKwdArgs(sipParseErr, sipArgs, sipKwds, SIP_NULLPTR, sipUnused, "iA8", &a0, &a1Keep, &a1))
        {
            sipCpp = new  ::Foo(a0,a1);
            Py_DECREF(a1Keep);

            return sipCpp;
        }
    }

    {
        const  ::Foo* a0;

        if (sipParseKwdArgs(sipParseErr, sipArgs, sipKwds, SIP_NULLPTR, sipUnused, "J9", sipType_Foo, &a0))
        {
            sipCpp = new  ::Foo(*a0);

            return sipCpp;
        }
    }

    return SIP_NULLPTR;
}

static PyMethodDef methods_Foo[] = {
    {sipName_get_int_val, meth_Foo_get_int_val, METH_VARARGS, doc_Foo_get_int_val},
    {sipName_get_string_val, meth_Foo_get_string_val, METH_VARARGS, doc_Foo_get_string_val},
    {sipName_set_int_val, meth_Foo_set_int_val, METH_VARARGS, doc_Foo_set_int_val},
    {sipName_set_string_val, meth_Foo_set_string_val, METH_VARARGS, doc_Foo_set_string_val}
};

PyDoc_STRVAR(doc_Foo, "\1Foo(int, str)\n"
"Foo(Foo)");

sipClassTypeDef sipTypeDef_foocpp_Foo = {
    {
        -1,
        SIP_NULLPTR,
        SIP_NULLPTR,
        SIP_TYPE_CLASS,
        sipNameNr_Foo,
        SIP_NULLPTR,
        SIP_NULLPTR
    },
    {
        sipNameNr_Foo,
        {0, 0, 1},
        4, methods_Foo,
        0, SIP_NULLPTR,
        0, SIP_NULLPTR,
        {SIP_NULLPTR, SIP_NULLPTR, SIP_NULLPTR, SIP_NULLPTR, SIP_NULLPTR,
         SIP_NULLPTR, SIP_NULLPTR, SIP_NULLPTR, SIP_NULLPTR, SIP_NULLPTR},
    },
    doc_Foo,
    -1,
    -1,
    SIP_NULLPTR,
    SIP_NULLPTR,
    init_type_Foo,
    SIP_NULLPTR,
    SIP_NULLPTR,
    SIP_NULLPTR,
    SIP_NULLPTR,
    dealloc_Foo,
    SIP_NULLPTR,
    SIP_NULLPTR,
    SIP_NULLPTR,
    release_Foo,
    SIP_NULLPTR,
    SIP_NULLPTR,
    SIP_NULLPTR,
    SIP_NULLPTR,
    SIP_NULLPTR,
    SIP_NULLPTR,
    SIP_NULLPTR
};

Теперь соберем пакет с помощью команды sip-wheel. После выполнения этой команды, если все пройдет успешно, будет создан файл pyfoocpp-0.1-cp38-cp38-manylinux1_x86_64.whl или с похожим именем. Установим его с помощью команды pip install --user pyfoocpp-0.1-cp38-cp38-manylinux1_x86_64.whl и запустим интерпретатор Python для проверки:

>>> from foocpp import Foo
>>> x = Foo(10, 'Hello')

>>> x.get_int_val()
10

>>> x.get_string_val()
'Hello'

>>> x.set_int_val(50)
>>> x.set_string_val('Привет')

>>> x.get_int_val()
50

>>> x.get_string_val()
'Привет'

Работает! Таким образом, мы с вами только что сделали Python-модуль с обвязкой для класса на C++. Дальше будем наводить в этом классе красоту и добавлять разные удобства.

Добавляем свойства


Классы, созданные с помощью SIP не обязаны в точности повторять интерфейс классов C++. Например, в нашем классе Foo имеется два геттера и два сеттера, которые явно можно объединить в свойство, чтобы класс стал более «питоновским». Добавить свойства с помощью сип достаточно легко, как это делается, показывает пример в папке pyfoo_cpp_02.

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

%Module(name=foocpp, language="C++")
%DefaultEncoding "UTF-8"

class Foo {
    %TypeHeaderCode
    #include <foo.h>
    %End

    public:
        Foo(int, const char*);

        void set_int_val(int);
        int get_int_val();
        %Property(name=int_val, get=get_int_val, set=set_int_val)

        void set_string_val(const char*);
        char* get_string_val();
        %Property(name=string_val, get=get_string_val, set=set_string_val)
};

Как видите, все достаточно просто. Чтобы добавить свойство, предназначена директива %Property, у которой имеется два обязательных параметра: name для задания имени свойства, а также get для указания метода, который возвращает какое-либо значение (геттер). Сеттера может не быть, но если свойству нужно также присваивать значения, то метод-сеттер указывается в качестве значения параметра set. В нашем примере свойства создаются достаточно прямолинейно, поскольку уже имеются функции, работающие как геттеры и сеттеры.

Нам остается только лишь собрать пакет с помощью команды sip-wheel, установить его, после этого проверим работу свойств в командном режиме интерпретатора python:

>>> from foocpp import Foo
>>> x = Foo(10, "Hello")
>>> x.int_val
10

>>> x.string_val
'Hello'

>>> x.int_val = 50
>>> x.string_val = 'Привет'

>>> x.get_int_val()
50

>>> x.get_string_val()
'Привет'

Как видно из примера использования класса Foo, свойства int_val и string_val работают и на чтение, и на запись.

Добавляем строки документации


Продолжим улучшать наш класс Foo. Следующий пример, который расположен в папке pyfoo_cpp_03 показывает, как добавлять к различным элементам класса строки документации (docstring). Этот пример сделан на основе предыдущего, и главное изменение в нем касается файла pyfoocpp.sip. Вот его содержимое:

%Module(name=foocpp, language="C++")
%DefaultEncoding "UTF-8"

class Foo {
%Docstring
Class example from C++ library
%End

    %TypeHeaderCode
    #include <foo.h>
    %End

    public:
        Foo(int, const char*);

        void set_int_val(int);
        %Docstring(format="deindented", signature="prepended")
            Set integer value
        %End

        int get_int_val();
        %Docstring(format="deindented", signature="prepended")
            Return integer value
        %End

        %Property(name=int_val, get=get_int_val, set=set_int_val)
        {
            %Docstring "deindented"
                The property for integer value
            %End
        };

        void set_string_val(const char*);
        %Docstring(format="deindented", signature="appended")
            Set string value
        %End

        char* get_string_val();
        %Docstring(format="deindented", signature="appended")
            Return string value
        %End

        %Property(name=string_val, get=get_string_val, set=set_string_val)
        {
            %Docstring "deindented"
                The property for string value
            %End
        };
};

Как вы уже поняли, для того, чтобы добавить строки документации к какому-либо элементу класса, нужно воспользоваться директивой %Docstring. В этом примере показано несколько способов использования этой директивы. Для лучшего понимания этого примера давайте сразу скомпилируем пакет pyfoocpp с помощью команды sip-wheel, установим его и будем последовательно разбираться с тем, какой параметр этой директивы на что влияет, рассматривая получившиеся строки документации в командном режиме Python. Напомню, что строки документации сохраняются в члены __doc__ объектов, к которым относятся эти строки.

Первая строка документации относится к классу Foo. Как вы видите, все строки документации расположены между директивами %Docstring и %End. В строках 5-7 этого примера не используются никакие дополнительные параметры директивы %Docstring, поэтому строка документации будет записана в класс Foo как есть. Именно поэтому в строках 5-7 нет отступов, иначе отступы перед строкой документации тоже попали бы в Foo.__doc__. Убедимся в том, что класс Foo действительно содержит ту строку документации, которую мы ввели:

>>> from foocpp import Foo
>>> Foo.__doc__
'Class example from C++ library'

Следующая директива %Docstring, расположенная на 17-19 строках, использует сразу два параметра. Параметр format может принимать одно из двух значений: «raw» или «deindented». В первом случае строки документации сохраняются в том виде, как они записаны, а во втором — удаляются начальные символы пробелов (но не табуляции). Значение по умолчанию для случая, если параметр format не указан, можно задать с помощью директивы %DefaultDocstringFormat (мы ее рассмотрим чуть позже), а если она не указана, то считается, что format=«raw».

Помимо заданных строк документации, SIP добавляет к строкам документации функций описание ее сигнатуры (какие типы переменных ожидаются на входе и какой тип функция возвращает). Параметр signature указывает, куда помещать такую сигнатуру: до указанной строки документации (signature=«prepended»), после нее (signature=«appended») или не добавлять сигнатуру (signature=«discarded»).

Наш пример устанавливает параметр signature=«prepended» для функций get_int_val и set_int_val, а также signature=«appended» для функций get_string_val и set_string_val. Также был добавлен параметр format=«deindented» для того, чтобы удалить пробелы в начале строки документации. Проверим работу этих параметров в Python:

>>> Foo.get_int_val.__doc__
'get_int_val(self) -> int\nReturn integer value'

>>> Foo.set_int_val.__doc__
'set_int_val(self, int)\nSet integer value'

>>> Foo.get_string_val.__doc__
'Return string value\nget_string_val(self) -> str'

>>> Foo.set_string_val.__doc__
'Set string value\nset_string_val(self, str)'

Как видим, с помощью параметра signature директивы %Docstring можно менять положение описания сигнатуры функции в строке документации.

Теперь рассмотрим добавление строки документации в свойства. Обратите внимание, что в этом случае директивы %Docstring...%End заключены в фигурные скобки после директивы %Property. Такой формат записи описан в документации к директиве %Property.

Также обратите внимание, как мы указываем параметр директивы %Docstring. Такой формат записи директив возможен, если мы устанавливаем только первый параметр директивы (в данном случае параметр format). Таким образом, в этом примере используются сразу три способа использования директив.

Убедимся, что строка документации для свойств установлена:

>>> Foo.int_val.__doc__
'The property for integer value'

>>> Foo.string_val.__doc__
'The property for string value'

>>> help(Foo)
Help on class Foo in module foocpp:

class Foo(sip.wrapper)
 |  Class example from C++ library
 |  
 |  Method resolution order:
 |      Foo
 |      sip.wrapper
 |      sip.simplewrapper
 |      builtins.object
 |  
 |  Methods defined here:
 |  
 |  get_int_val(...)
 |      get_int_val(self) -> int
 |      Return integer value
 |  
 |  get_string_val(...)
 |      Return string value
 |      get_string_val(self) -> str
 |  
 |  set_int_val(...)
 |      set_int_val(self, int)
 |      Set integer value
 |  
 |  set_string_val(...)
 |      Set string value
 |      set_string_val(self, str)
...


Давайте упростим этот пример, установив значения по умолчанию для параметров format и signature с помощью директив %DefaultDocstringFormat и %DefaultDocstringSignature. Использование этих директив показано в примере из папки pyfoo_cpp_04. Файл pyfoocpp.sip в этом примере содержит следующий код:

%Module(name=foocpp, language="C++")
%DefaultEncoding "UTF-8"
%DefaultDocstringFormat "deindented"
%DefaultDocstringSignature "prepended"

class Foo {
    %Docstring
    Class example from C++ library
    %End

    %TypeHeaderCode
    #include <foo.h>
    %End

    public:
        Foo(int, const char*);

        void set_int_val(int);
        %Docstring
            Set integer value
        %End

        int get_int_val();
        %Docstring
            Return integer value
        %End

        %Property(name=int_val, get=get_int_val, set=set_int_val)
        {
            %Docstring
                The property for integer value
            %End
        };

        void set_string_val(const char*);
        %Docstring
            Set string value
        %End

        char* get_string_val();
        %Docstring
            Return string value
        %End

        %Property(name=string_val, get=get_string_val, set=set_string_val)
        {
            %Docstring
                The property for string value
            %End
        };
};

В начале файла добавлены строки %DefaultDocstringFormat «deindented» и %DefaultDocstringSignature «prepended», а далее все параметры из директивы %Docstring были убраны.

После сборки и установки этого примера можем посмотреть, как теперь выглядит описание класса Foo, которое выводит команда help(Foo):

>>> from foocpp import Foo
>>> help(Foo)

class Foo(sip.wrapper)
 |  Class example from C++ library
 |  
 |  Method resolution order:
 |      Foo
 |      sip.wrapper
 |      sip.simplewrapper
 |      builtins.object
 |  
 |  Methods defined here:
 |  
 |  get_int_val(...)
 |      get_int_val(self) -> int
 |      Return integer value
 |  
 |  get_string_val(...)
 |      get_string_val(self) -> str
 |      Return string value
 |  
 |  set_int_val(...)
 |      set_int_val(self, int)
 |      Set integer value
 |  
 |  set_string_val(...)
 |      set_string_val(self, str)
 |      Set string value
...

Все выглядит достаточно аккуратно и однотипно.

Переименовываем классы и методы


Как мы уже говорили, интерфейс, предоставляемый обвязкой на языке Python не обязательно должен совпадать с тем интерфейсом, который предоставляет библиотека на языке C/C++. Выше мы добавляли свойства в классы, а сейчас рассмотрим еще один прием, который может быть полезен, если возникают конфликты имен классов или функций, например, если имя функции совпадает с каким-нибудь ключевым словом языка Python. Для этого предусмотрена возможность переименования классов, функций, исключений и других сущностей.

Для переименования сущности используется аннотация PyName, значению которой нужно присвоить новое имя сущности. Работа с аннотацией PyName показана в примере из папки pyfoo_cpp_05. Этот пример создан на основе предыдущего примера pyfoo_cpp_04 и отличается от него файлом pyfoocpp.sip, содержимое которого теперь выглядит следующим образом:

%Module(name=foocpp, language="C++")
%DefaultEncoding "UTF-8"
%DefaultDocstringFormat "deindented"
%DefaultDocstringSignature "prepended"

class Foo /PyName=Bar/ {
    %Docstring
    Class example from C++ library
    %End

    %TypeHeaderCode
    #include <foo.h>
    %End

    public:
        Foo(int, const char*);

        void set_int_val(int) /PyName=set_integer_value/;
        %Docstring
            Set integer value
        %End

        int get_int_val() /PyName=get_integer_value/;
        %Docstring
            Return integer value
        %End

        %Property(name=int_val, get=get_integer_value, set=set_integer_value)
        {
            %Docstring
                The property for integer value
            %End
        };

        void set_string_val(const char*) /PyName=set_string_value/;
        %Docstring
            Set string value
        %End

        char* get_string_val() /PyName=get_string_value/;
        %Docstring
            Return string value
        %End

        %Property(name=string_val, get=get_string_value, set=set_string_value)
        {
            %Docstring
                The property for string value
            %End
        };
};

В этом примере мы переименовали класс Foo в класс Bar, а также присвоили другие имена всем методам с помощью аннотации PyName. Думаю, что все здесь достаточно просто и понятно, единственное, на что стоит обратить внимание — это создание свойств. В директиве %Property в качестве параметров get и set нужно указывать имена методов, как они будут называться в Python-классе, а не те имена, как они назывались изначально к коде на C++.

Скомпилируем пример, установим его и посмотрим, как этот класс будет выглядеть в языке Python:

>>> from foocpp import Bar
>>> help(Bar)

Help on class Bar in module foocpp:

class Bar(sip.wrapper)
 |  Class example from C++ library
 |  
 |  Method resolution order:
 |      Bar
 |      sip.wrapper
 |      sip.simplewrapper
 |      builtins.object
 |  
 |  Methods defined here:
 |  
 |  get_integer_value(...)
 |      get_integer_value(self) -> int
 |      Return integer value
 |  
 |  get_string_value(...)
 |      get_string_value(self) -> str
 |      Return string value
 |  
 |  set_integer_value(...)
 |      set_integer_value(self, int)
 |      Set integer value
 |  
 |  set_string_value(...)
 |      set_string_value(self, str)
 |      Set string value
 |  
 |  ----------------------------------------------------------------------
 |  Data descriptors defined here:
 |  
 |  __weakref__
 |      list of weak references to the object (if defined)
 |  
 |  int_val
 |      The property for integer value
 |  
 |  string_val
 |      The property for string value
 |  
 |  ----------------------------------------------------------------------
...

Сработало! Нам удалось переименовать сам класс и его методы.

Иногда в библиотеках используется договоренность, что имена всех классов начинаются с какого-либо префикса, например, с буквы «Q» в Qt или «wx» в wxWidgets. Если в своей Python-обвязке вы хотите переименовать все классы, избавившись от таких префиксов, то для того, чтобы не задавать аннотацию PyName для каждого класса, можно воспользоваться директивой %AutoPyName. Мы не будем рассматривать эту директиву в данной статье, скажем только, что директива %AutoPyName должна располагаться внутри директивы %Module и ограничимся примером из документации:

%Module PyQt5.QtCore
{
    %AutoPyName(remove_leading="Q")
}

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


Пример с использованием класса std::wstring


До сих пор мы рассматривали функции и классы, которые работали с простейшими типами вроде int и char*. Для таких типов SIP автоматически создавал конвертер в классы Python и обратно. В следующем примере, который расположен в папке pyfoo_cpp_06, мы рассмотрим случай, когда методы класса принимают и возвращают более сложные объекты, например, строки из STL. Чтобы упростить пример и не усложнять преобразование байтов в Unicode и обратно, в этом примере будет использоваться класс строк std::wstring. Идея этого примера — показать, как можно вручную задавать правила преобразования классов C++ в классы Python и обратно.

Для этого примера мы изменим класс Foo из библиотеки foo. Теперь определение класса будет выглядеть следующим образом (файл foo.h):

#ifndef FOO_LIB
#define FOO_LIB

#include <string>

using std::wstring;

class Foo {
    private:
        int _int_val;
        wstring _string_val;
    public:
        Foo(int int_val, wstring string_val);

        void set_int_val(int val);
        int get_int_val();

        void set_string_val(wstring val);
        wstring get_string_val();
};

#endif

Реализация класса Foo в файле foo.cpp:

#include <string>

#include "foo.h"

using std::wstring;

Foo::Foo(int int_val, wstring string_val):
    _int_val(int_val), _string_val(string_val) {}

void Foo::set_int_val(int val) {
    _int_val = val;
}

int Foo::get_int_val() {
    return _int_val;
}

void Foo::set_string_val(wstring val) {
    _string_val = val;
}

wstring Foo::get_string_val() {
    return _string_val;
}

И файл main.cpp для проверки работоспособности библиотеки:

#include <iostream>

#include "foo.h"

using std::cout;
using std::endl;

int main(int argc, char* argv[]) {
    auto foo = Foo(10, L"Hello");
    cout << L"int_val: " << foo.get_int_val() << endl;
    cout << L"string_val: " << foo.get_string_val().c_str() << endl;

    foo.set_int_val(0);
    foo.set_string_val(L"Hello world!");

    cout << L"int_val: " << foo.get_int_val() << endl;
    cout << L"string_val: " << foo.get_string_val().c_str() << endl;
}

Файлы foo.h, foo.cpp и main.cpp, как и раньше, располагаются в папке foo. Makefile и процесс сборки библиотеки не изменился. Также нет существенных изменений в файлах pyproject.toml и project.py.

А вот файл pyfoocpp.sip стал заметно сложнее:

%Module(name=foocpp, language="C++")
%DefaultEncoding "UTF-8"

class Foo {
    %TypeHeaderCode
    #include <foo.h>
    %End

    public:
        Foo(int, std::wstring);

        void set_int_val(int);
        int get_int_val();
        %Property(name=int_val, get=get_int_val, set=set_int_val)

        void set_string_val(std::wstring);
        std::wstring get_string_val();
        %Property(name=string_val, get=get_string_val, set=set_string_val)
};

%MappedType std::wstring
{
%TypeHeaderCode
#include <string>
%End

%ConvertFromTypeCode
    // Convert an std::wstring to a Python (Unicode) string
    PyObject* newstring;
    newstring = PyUnicode_FromWideChar(sipCpp->data(), -1);
    return newstring;
%End

%ConvertToTypeCode
    // Convert a Python (Unicode) string to an std::wstring
    if (sipIsErr == NULL) {
        return PyUnicode_Check(sipPy);
    }
    if (PyUnicode_Check(sipPy)) {
        *sipCppPtr = new std::wstring(PyUnicode_AS_UNICODE(sipPy));
        return 1;
    }
    return 0;
%End
};

Для наглядности файл pyfoocpp.sip не добавляет строки документации. Если бы мы в файле pyfoocpp.sip оставили только объявление класса Foo без последующей директивы %MappedType, то с процессе сборки получили бы следующую ошибку:

$ sip-wheel

These bindings will be built: pyfoocpp.
Generating the pyfoocpp bindings...
sip-wheel: std::wstring is undefined

Нам нужно явно описать, как объект типа std::wstring будет преобразовываться в какой-либо Python-объект, а также описать обратное преобразование. Для описания преобразования нам нужно будет работать на достаточно низком уровне на языке C и использовать Python/C API. Поскольку Python/C API — это большая тема, достойная даже не отдельной статьи, а книги, то в этом разделе мы рассмотрим только те функции, которые используются в примере, не особо углубляясь в подробности.

Для объявления преобразований из объектов C++ в Python и наоборот предназначена директива %MappedType, внутри которой могут находиться три другие директивы: %TypeHeaderCode, %ConvertToTypeCode и %ConvertFromTypeCode. После выражения %MappedType нужно указать тип, для которого будут создаваться конвертеры. В нашем случае директива начинается с выражения %MappedType std::wstring.

С директивой %TypeHeaderCode мы уже встречались в разделе Делаем обвязку для библиотеки на языке C++. Напомню, что эта директива предназначена для того, чтобы объявить используемые типы или подключить заголовочные файлы, в которых они объявлены. В данном примере внутри директивы %TypeHeaderCode подключается заголовочный файл string, где объявлен класс std::string.

Теперь нам нужно описать преобразования

%ConvertFromTypeCode. Преобразование объектов C++ в Python


Начнем с преобразования объектов std::wstring в класс str языка Python. Данное преобразование в примере выглядит следующим образом:

%ConvertFromTypeCode
    // Convert an std::wstring to a Python (Unicode) string
    PyObject* newstring;
    newstring = PyUnicode_FromWideChar(sipCpp->data(), -1);
    return newstring;
%End

Внутри этой директивы у нас имеется переменная sipCpp — указатель на объект из кода на C++, по которому нужно создать Python-объект и вернуть созданный объект из директивы с помощью оператора return. В данном случае переменная sipCpp имеет тип std::wstring*. Чтобы создать класс str, используется функция PyUnicode_FromWideChar из Python/C API. Эта функция в качестве первого параметра принимает массив (указатель) типа const wchar_t *w, а в качестве второго параметра — размер этого массива. Если в качестве второго параметра передать значение -1, то функция PyUnicode_FromWideChar сама рассчитает длину с помощью функции wcslen.

Чтобы получить массив wchar_t* используется метод data из класса std::wstring.

Функция PyUnicode_FromWideChar возвращает указатель на PyObject или NULL в случае ошибки. PyObject представляет собой любой Python-объект, в данном случае это будет класс str. В Python/C API работа с объектами происходит обычно через указатели PyObject*, поэтому и в данном случае из директивы %ConvertFromTypeCode мы возвращаем указатель PyObject*.

%ConvertToTypeCode. Преобразование объектов Python в C++


Обратное преобразование из объекта Python (по сути из PyObject*) в класс std::wstring описывается в директиве %ConvertToTypeCode. В примере pyfoo_cpp_06 преобразование выглядит следующим образом:

%ConvertToTypeCode
    // Convert a Python (Unicode) string to an std::wstring
    if (sipIsErr == NULL) {
        return PyUnicode_Check(sipPy);
    }
    if (PyUnicode_Check(sipPy)) {
        *sipCppPtr = new std::wstring(PyUnicode_AS_UNICODE(sipPy));
        return 1;
    }
    return 0;
%End

Код директивы %ConvertToTypeCode выглядит более сложно, потому что в процессе преобразования он вызывается несколько раз с разными целями. Внутри директивы %ConvertToTypeCode SIP создает несколько переменных, которые мы можем (или должны) использовать.

Одна из таких переменных PyObject *sipPy представляет собой Python-объект, по которому нужно создать в данном случае экземпляр класса std::wstring. Результат нужно будет записать в другую переменную — sipCppPtr — это двойной указатель на создаваемый объект, т.е. в нашем случае эта переменная будет иметь тип std::wstring**.

Еще одна создаваемая внутри директивы %ConvertToTypeCode переменная — int *sipIsErr. Если значение этой переменной равно NULL, значит директива %ConvertToTypeCode вызывается только с целью проверки, возможно ли преобразование типа. В этом случае мы не обязаны выполнять преобразование, а должны только проверить, возможно ли оно в принципе. Если возможно, то из директивы должны вернуть не нулевое значение, в противном случае, если преобразование невозможно, должны вернуть 0. Если этот указатель не равен NULL, значит нужно выполнить преобразование, а в случае возникновения ошибки в процессе преобразования, целочисленный код ошибки можно сохранить в эту переменную (с учетом того, что эта переменная является указателем на int*).

В данном примере для проверки того, что sipPy представляет собой юникодную строку (класс str) используется макрос PyUnicode_Check, который принимает в качестве параметра аргумент типа PyObject*, если переданный аргумент представляет собой юникодную строку или класс, производный от нее.

Преобразование в объект C++ осуществляется с помощью строки *sipCppPtr = new std::wstring(PyUnicode_AS_UNICODE(sipPy));. Здесь вызывается макрос PyUnicode_AS_UNICODE из Python/C API, который возвращает массив типа Py_UNICODE*, что эквивалентно wchar_t*. Этот массив передается в конструктор класса std::wstring. Как уже было сказано выше, результат сохраняется в переменной sipCppPtr.

В данный момент директива PyUnicode_AS_UNICODE объявлена устаревшей и рекомендуется использовать другие макросы, но для упрощения примера используется именно этот макрос.

Если преобразование прошло успешно, директива %ConvertToTypeCode должна вернуть не нулевое значение (в данном случае 1), а в случае ошибки должна вернуть 0.

Проверка


Мы описали преобразование типа std::wstring в str и обратно, теперь можем убедиться, что пакет успешно собирается и обвязка работает, как надо. Для сборки вызываем sip-wheel, затем устанавливаем пакет с помощью pip и проверяем работоспособность в командном режиме Python:

>>> from foocpp import Foo
>>> x = Foo(10, 'Hello')

>>> x.string_val
'Hello'

>>> x.string_val = 'Привет'
>>> x.string_val
'Привет'

>>> x.get_string_val()
'Привет'

Как видим, все работает, с русским языком тоже проблем нет, т.е. преобразования юникодных строк выполнено корректно.

Заключение


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

В файле *.sip описывается интерфейс Python-модуля с перечислением функций и классов, которые будут содержаться в модуле. Для описания интерфейса в файле *.sip используются директивы и аннотации. Интерфейс классов Python не обязательно должен совпадать с интерфейсом классов C++. Например, в классы можно добавлять свойства с помощью директивы %Property, переименовывать сущности с помощью аннотации /PyName/, добавлять строки документации с помощью директивы %Docstring.

Элементарные типы вроде int, char, char* и т.п. SIP автоматически преобразует в аналогичные классы Python, но если нужно выполнять более сложное преобразование, то его нужно запрограммировать самостоятельно внутри директивы %MappedType, используя Python/C API. Преобразование из класса Python в C++ должно осуществляться во вложенной директиве %ConvertToTypeCode. Преобразование из типа C++ в класс Python должно осуществляться во вложенной директиве %ConvertFromTypeCode.

Некоторые директивы вроде %DefaultEncoding, %DefaultDocstringFormat и %DefaultDocstringSignature являются вспомогательными и позволяют устанавливать значения по умолчанию для случаев, когда какие-то параметры аннотаций не установлены явно.

В этой статье мы рассмотрели только лишь основные и самые простые директивы и аннотации, но многие из них обошли вниманием. Например, существуют директивы для управления GIL, для создания новых Python-исключений, для ручного управления памятью и сборщиком мусора, для подстройки классов под разные операционные системы и многие другие, которые могут быть полезны при создании обвязок сложных C/C++-библиотек. Также мы обошли вопрос сборки пакетов под разные операционные системы, ограничившись сборкой под Linux с помощью компиляторов gcc/g++.

Ссылки