在本文的第一部分,我们研究了使用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的简单类,用于设置和返回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文件夹还包含使用Foo类的main.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.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 ++编写的示例将打包在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 foocpp或from 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文件(我们将不在本文中详细讨论):
#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;
}
}
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);
}
}
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;
}
}
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);
}
}
sipNoMethod(sipParseErr, sipName_Foo, sipName_get_string_val, doc_Foo_get_string_val);
return SIP_NULLPTR;
}
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_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指令。本示例显示了使用此伪指令的几种方法。为了更好地理解此示例,让我们立即使用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_val和set_int_val功能,以及签名=“追加”的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)'
如您所见,使用%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指令中,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
|
| ----------------------------------------------------------------------
...
有效!我们设法重命名了类本身及其方法。有时,库会使用所有类的名称都以前缀开头的协议,例如,Qt中的字母“ Q”或wxWidgets中的“ wx”。如果要在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.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.h,foo.cpp和main.cpp位于foo文件夹中。生成文件和库的生成过程未更改。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
PyObject* newstring;
newstring = PyUnicode_FromWideChar(sipCpp->data(), -1);
return newstring;
%End
%ConvertToTypeCode
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
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
函数在出现错误的情况下返回指向PyObject或NULL的指针。PyObject是任何Python对象,在这种情况下,它将是str类。在Python / C API中,使用对象通常是通过PyObject *指针进行的,因此,在这种情况下,我们从%ConvertFromTypeCode指令返回PyObject *指针。%ConvertToTypeCode。将Python对象转换为C ++
%ConvertToTypeCode指令描述了
从Python对象(基本上是从PyObject *)到std :: wstring类的逆转换。在pyfoo_cpp_06示例中,转换如下:%ConvertToTypeCode
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指令添加文档行。基本类型,例如int,char,char *等等SIP会自动转换为类似的Python类,但是如果您需要执行更复杂的转换,则需要使用Python / C API 在%MappedType指令中自己编程。从Python类到C ++的转换必须在%ConvertToTypeCode嵌套指令中完成。从C ++类型转换为Python类必须在%ConvertFromTypeCode嵌套指令中完成。诸如%DefaultEncoding,%DefaultDocstringFormat和%DefaultDocstringSignature之类的某些指令是辅助指令,可用于在未显式设置某些注释参数的情况下设置默认值。在本文中,我们仅检查了基本和最简单的指令和注释,但其中许多被忽略了。例如,存在用于管理GIL,用于创建新的Python异常,用于手动管理内存和垃圾收集器,用于调整不同操作系统的类的指令,以及在创建复杂的C / C ++库绑定时可能有用的许多其他指令。我们也绕过了为不同的操作系统构建软件包的问题,将自己局限于使用gcc / g ++编译器在Linux下构建。参考文献