使用SIP为C / C ++库创建Python绑定。第2部分

在本文的第一部分,我们研究了使用SIP实用程序的基础知识,该SIP实用程序旨在为用C和C ++编写的库创建Python绑定。我们研究了使用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

这是一个具有两个getter和setter的简单类,用于设置和返回intchar *类型的值该类的实现如下:

#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文件夹还包含使用Foomain.cpp文件

#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)/*

除了将编译器从gcc更改g ++之外,与之前示例中Makefile的不同之处在于,添加了另一个选项-fPIC进行编译,该选项告诉编译器以某种方式将代码放入库中(所谓的“位置无关代码”)。由于本文与编译器无关,因此我们将不更详细地研究此参数的作用以及为什么需要此参数。 让我们开始绑定这个库。pyproject.tomlproject.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 ++编写的示例将打包在pyfoocpp Python包中,这也许是此文件中唯一值得注意的更改。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 foocppfrom foocpp import ...命令。在同一指令中,我们指示我们现在拥有该语言- C ++的%DefaultEncoding指令集,将用于在Python字符串转换的编码字符常量字符字符*常量字符*类型

然后,的接口声明的Foo类如下所述的声明之后紧随。美孚仍然没有使用%TypeHeaderCode指令,该指令%End指令结尾。所述%TypeHeaderCode指令必须包含声明为正被创建的包装的C ++类的接口代码。通常,在此伪指令中将头文件包含在类声明中就足够了。

之后,列出了将转换为Python语言Foo类的方法的类方法。重要的是要注意,此时我们只声明可从Python中Foo类访问的公共方法(因为Python中没有私有和受保护的成员)。由于我们从一开始就使用%DefaultEncoding指令,然后在采用const char *类型参数的方法中,不能使用Encoding批注指定将这些参数转换为Python字符串的编码,反之亦然。

现在我们只需要编译pyfoocpp Python软件包并对其进行测试。但是,在组装完整的轮包之前,让我们使用sip-build命令,查看SIP将创建哪些源文件以用于以后的编译,并尝试在其中找到与将在Python代码中创建的类类似的东西。为此,必须在pyfoo_cpp_01文件夹中调用上述sip-build命令结果,将创建构建文件夹。 具有以下内容:

建立
└──foocpp
    ├── apiversions.c
    ├── array.c
    ├── array.h
    ├── bool.cpp
    ├── build
    │   └── temp.linux-x86_64-3.8
    │       ├── apiversions.o
    │       ├── array.o
    │       ├── bool.o
    │       ├── descriptors.o
    │       ├── int_convertors.o
    │       ├── objmap.o
    │       ├── qtlib.o
    │       ├── sipfoocppcmodule.o
    │       ├── sipfoocppFoo.o
    │       ├── siplib.o
    │       ├── threads.o
    │       └── voidptr.o
    ├── descriptors.c
    ├── foocpp.cpython-38-x86_64-linux-gnu.so
    ├── int_convertors.c
    ├── objmap.c
    ├── qtlib.c
    ├── sipAPIfoocpp.h
    ├── sipfoocppcmodule.cpp
    ├── sipfoocppFoo.cpp
    ├── sip.h
    ├── sipint.h
    ├── siplib.c
    ├── threads.c
    └── voidptr.c


作为一项附加任务,请仔细考虑sipfoocppFoo.cpp文件(我们将不在本文中详细讨论):

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

#include "sipAPIfoocpp.h"

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

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

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

    {
        int a0;
         ::Foo *sipCpp;

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

            Py_INCREF(Py_None);
            return Py_None;
        }
    }

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

    return SIP_NULLPTR;
}

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

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

    {
         ::Foo *sipCpp;

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

            sipRes = sipCpp->get_int_val();

            return PyLong_FromLong(sipRes);
        }
    }

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

    return SIP_NULLPTR;
}

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

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

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

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

            Py_INCREF(Py_None);
            return Py_None;
        }
    }

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

    return SIP_NULLPTR;
}

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

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

    {
         ::Foo *sipCpp;

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

            sipRes = sipCpp->get_string_val();

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

            return PyUnicode_FromString(sipRes);
        }
    }

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

    return SIP_NULLPTR;
}

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

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

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

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

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

            return sipCpp;
        }
    }

    {
        const  ::Foo* a0;

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

            return sipCpp;
        }
    }

    return SIP_NULLPTR;
}

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

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

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

现在,使用sip-wheel命令构建软件包执行此命令后,如果一切顺利,将创建一个pyfoocpp-0.1-cp38-cp38-manylinux1_x86_64.whl文件或具有类似名称的文件使用pip install --user pyfoocpp-0.1-cp38-cp38-manylinux1_x86_64.whl命令进行安装,然后运行Python解释器进行验证:

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

>>> x.get_int_val()
10

>>> x.get_string_val()
'Hello'

>>> x.set_int_val(50)
>>> x.set_string_val('')

>>> x.get_int_val()
50

>>> x.get_string_val()
''

作品!因此,我们刚刚制作了一个带有C ++类绑定的Python模块。此外,我们将把美丽带到这一堂课,并增加各种便利设施。

添加属性


使用SIP创建的类不需要完全重复C ++类接口。例如,在我们的Foo类中,有两个getter和两个setter,可以将它们清楚地组合为一个属性,以使该类更像“ Python”。使用sip添加属性非常容易,因为这样做很容易,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)
};

如您所见,一切都很简单。若要添加属性,则%物业指令的目的是,它有两个必需的参数:名称指定属性的名称,并获得指定的方法将返回一个值(吸气)。可能没有设置器,但是如果还需要为属性分配值,则将设置器方法指定为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_valstring_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指令。本示例显示了使用此伪指令的几种方法。为了更好地理解此示例,让我们立即使用sip-wheel命令编译pyfoocpp软件包安装它,然后考虑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'

位于第17-19行 的以下%Docstring指令一次使用两个参数。format参数可以采用两个值之一:“ raw”或“ deindented”。在第一种情况下,文档行将在编写时保存,在第二种情况下,将删除初始空格字符(但不包括制表符)。如果未指定format参数,则可以使用%DefaultDocstringFormat伪指令设置默认值(稍后再考虑),如果未指定,则假定format =“ raw”

除了指定的文档行之外,SIP在函数文档行中还添加了其签名的描述(输入中应包含什么类型的变量以及函数返回什么类型)。signature参数指示将这样的签名放置在何处:在文档的指定行之前(signature =“ prepended”),在文档之后(signature =“ appended”)或不添加签名(signature =“ discarded”)。

我们的例子中设置了签名=“前缀”参数get_int_valset_int_val功能,以及签名=“追加”get_string_valset_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)'

如您所见,使用%Docstring指令signature参数您可以在文档行中更改函数签名描述的位置。 现在考虑向属性添加文档行。请注意,在这种情况下,%Docstring ... %End指令用%Property指令括在大括号中。在%Property指令的文档中描述了这种记录格式 还请注意我们如何指定%Docstring指令参数。如果我们仅设置指令的第一个参数(在这种情况下,格式参数



因此,在此示例中,一次使用了三种使用指令的方法。

确保设置了属性的文档行:

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


让我们通过使用%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“前缀”中添加,然后将所有从参数%文档字符串指令被拆除。

组装并安装完此示例后,我们可以看到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批注,需要为其赋予其值一个新的实体名称。pyfoo_cpp_05文件夹的示例中显示了使用PyName批注的工作。本示例基于上一个示例。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指令中,getset参数必须指定方法的名称,因为它们将在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
 |  
 |  ----------------------------------------------------------------------
...

有效!我们设法重命名了类本身及其方法。

有时,库会使用所有类的名称都以前缀开头的协议,例如,Qt中的字母“ Q”或wxWidgets中的“ wx”。如果要在Python绑定中重命名所有类,以摆脱此类前缀,则为了不为每个类设置PyName批注,可以使用%AutoPyName指令我们不会在本文中考虑此伪指令,我们只会说%AutoPyName伪指令应该位于%Module伪指令内部,并将自己限制为文档中的示例:

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

添加类型转换


使用std :: wstring类的示例


到目前为止,我们已经研究了适用于简单类型(例如intchar *)的函数和类。对于这些类型,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.cpp文件中Foo实现

#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.hfoo.cppmain.cpp位于foo文件夹中生成文件和库的生成过程未更改。pyproject.tomlproject.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开头在C ++中为库建立绑定一节中,我们已经满足%TypeHeaderCode

指令让我提醒您,此指令旨在声明使用的类型或包括在其中声明它们的头文件。在此示例中,头文件字符串(在其中声明了类std :: string连接%TypeHeaderCode指令内部 现在我们需要描述转换



%ConvertFromTypeCode。将C ++对象转换为Python


我们首先将std :: wstring对象转换为Python str。示例中的此转换如下:

%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,请使用Python / C API中PyUnicode_FromWideChar函数。此函数接受类型为const wchar_t * w的数组(指针)作为第一个参数,并将此数组的大小作为第二个参数。如果您将值-1作为第二个参数传递,则PyUnicode_FromWideChar函数本身将使用该函数计算长度wcslen

要获取wchar_t *数组,请使用std :: wstring类中data方法PyUnicode_FromWideChar 函数在出现错误的情况下返回指向PyObjectNULL的指针PyObject是任何Python对象,在这种情况下,它将是str在Python / C API中,使用对象通常是通过PyObject *指针进行的,因此,在这种情况下,我们%ConvertFromTypeCode指令返回PyObject *指针



%ConvertToTypeCode。将Python对象转换为C ++


%ConvertToTypeCode指令描述了 从Python对象(基本上是从PyObject *)到std :: wstring的逆转换。在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是一个unicode字符串(str),使用PyUnicode_Check如果传递的参数是unicode字符串或从其派生的,则该宏采用PyObject *类型的参数。

使用字符串* sipCppPtr = new std :: wstring(PyUnicode_AS_UNICODE(sipPy))可以转换为C ++对象。在这里,从Python / C API 调用PyUnicode_AS_UNICODE宏,该宏返回一个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()
''

如您所见,一切正常,俄语也没有问题,即 Unicode字符串转换正确执行。

结论


在本文中,我们介绍了使用SIP为用C和C ++编写的库创建Python绑定的基础知识。首先(在第一部分中),我们使用C语言创建了一个简单的库,并弄清楚了需要创建以使用SIP的文件。pyproject.toml文件包含有关软件包的信息(名称,版本号,许可证以及头文件和目标文件的路径)。使用project.py文件,您可以影响构建Python包的过程,例如,开始构建C / C ++库或允许用户指定库的头文件和目标文件的位置。

* .sip文件中描述Python模块的接口,其中列出了模块中将包含的功能和类。指令和注释用于描述* .sip文件中的接口。 Python类接口不必与C ++类接口匹配。例如,您可以使用%Property指令向类添加属性,使用/ PyName / Annotation重命名实体,并使用%Docstring指令添加文档行

基本类型,例如intcharchar *等等SIP会自动转换为类似的Python类,但是如果您需要执行更复杂的转换,则需要使用Python / C API %MappedType指令中自己编程。从Python类到C ++的转换必须在%ConvertToTypeCode嵌套指令中完成。从C ++类型转换为Python类必须在%ConvertFromTypeCode嵌套指令中完成

诸如%DefaultEncoding%DefaultDocstringFormat%DefaultDocstringSignature之类的某些指令是辅助指令,可用于在未显式设置某些注释参数的情况下设置默认值。

在本文中,我们仅检查了基本和最简单的指令和注释,但其中许多被忽略了。例如,存在用于管理GIL,用于创建新的Python异常,用于手动管理内存和垃圾收集器,用于调整不同操作系统的类的指令,以及在创建复杂的C / C ++库绑定时可能有用的许多其他指令。我们也绕过了为不同的操作系统构建软件包的问题,​​将自己局限于使用gcc / g ++编译器在Linux下构建。

参考文献



All Articles