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

有时,在使用Python处理项目时,希望使用不是用Python编写的库,而是使用C或C ++编写的库。原因可能有所不同:首先,Python是一种很棒的语言,但是在某些情况下它不够快。而且,如果您发现性能受到Python语言功能的限制,那么用另一种语言编写程序的一部分是有意义的(在本文中,我们将讨论C和C ++),将程序的这一部分安排为一个库,进行Python绑定(Python绑定)并使用由此获得的模块作为普通的Python库。其次,当您知道有一个库可以解决所需的问题时,通常会发生这种情况,但是不幸的是,该库不是用Python编写的,而是用同一C或C ++编写的。在这种情况下,我们还可以在该库上进行Python绑定并使用它,而无需考虑该库最初不是用Python编写的。

创建Python绑定的工具有很多种,从较低级别的工具(例如Python / C API)到较高级别的工具(如SWIGSIP)

我的目标不是比较创建Python绑定的不同方法,但我想谈谈使用一种工具SIP的基础知识。最初,SIP旨在创建围绕Qt库PyQt的绑定,并且还用于开发其他大型Python库,例如wxPython

在本文中,gcc将用作C的编译器,而g ++将用作C ++编译器。所有示例均在Arch Linux和Python 3.8下进行了测试。为了不使示例复杂化,本文的范围不包括针对不同操作系统和使用不同编译器(例如Visual Studio)的编译主题。

您可以从github上存储库下载本文的所有示例
具有SIP源的存储库位于https://www.riverbankcomputing.com/hg/sip/Mercurial用作SIP的版本控制系统。

在C中对库进行绑定


用C语言编写一个库


此示例位于源代码中的pyfoo_c_01文件夹中,但是在本文中,我们将假定我们从头开始进行所有操作。

让我们从一个简单的例子开始。首先,我们将创建一个简单的C库,然后将其从Python脚本运行。让我们的库成为唯一的函数

int foo(char*);

它将接受一个字符串,并返回其长度乘以2的长度。例如,

头文件foo.h可能看起来像这样:

#ifndef FOO_LIB
#define FOO_LIB

int foo(char* str);

#endif

并用foo.cpp执行该文件

#include <string.h>

#include "foo.h"

int foo(char* str) {
	return strlen(str) * 2;
}

为了测试库的功能,我们将编写一个简单的main.c程序

#include <stdio.h>

#include "foo.h"

int main(int argc, char* argv[]) {
	char* str = "0123456789";
	printf("%d\n", foo(str));
}

为了精确起见,创建一个Makefile

CC=gcc
CFLAGS=-c
DIR_OUT=bin

all: main

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

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

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

makedir:
	mkdir -p $(DIR_OUT)

clean:
	rm -rf $(DIR_OUT)/*

让库foo的所有源都位于源文件夹中foo的子文件夹中:

foo_c_01 /
└──foo
    ├──foo.c
    ├──foo.h
    ├──main.c
    └──Makefile


我们进入foo文件夹并使用以下命令编译源代码

make

在编译过程中,将显示文本。

mkdir -p bin
gcc -c main.c -o bin/main.o
gcc -c foo.c -o bin/foo.o
ar rcs bin/libfoo.a bin/foo.o
gcc bin/main.o -Lbin -lfoo -o bin/main

编译结果将放置在foo文件内的bin文件夹中

foo_c_01 /
└──foo
    ├──箱
    │├──foo.o
    │├──libfoo.a
    │├──主要
    │└──main.o
    ├──foo.c
    ├──foo.h
    ├──main.c
    └──Makefile


我们编译了一个用于静态链接的库,以及一个以main命名的程序编译后,您可以验证主程序是否启动。

让我们在foo库上进行Python绑定。

SIP基础


首先,您需要安装SIP。与使用pip的所有其他库一样,这是标准完成的操作:

pip install --user sip

当然,如果您在虚拟环境中工作,则不应指定--user参数,该参数指示SIP库应安装在用户文件夹中,而不是全局安装在系统中。

我们需要怎么做才能从Python代码中调用foo库?至少需要创建两个文件:一个是TOML格式的文件,并将其命名为pyproject.toml,另一个是扩展名为.sip的文件。让我们依次处理它们。

我们需要就来源的结构达成共识。pyfoo_c文件夹内部foo文件夹,其中包含该库的源代码。编译后,将foo文件夹中创建bin文件夹,其中将包含所有已编译的文件。稍后,我们将为用户提供通过命令行指定库的头文件和目标文件的路径的功能。

SIP所需的文件将与foo文件夹位于同一文件夹中

pyproject.toml


pyproject.toml 文件不是SIP开发人员的发明,而是Python项目描述格式,在PEP 517“源树的独立于构建系统的格式”PEP 518“指定Python项目的最低构建系统要求”中描述。这是一个TOML文件,可以认为是ini格式的更高级版本,其中参数以“键=值”的形式存储,并且参数不仅可以位于[foo]之类的区域,在TOML术语中称为表,但以及[foo.bar.spam]形式的小节中。参数不仅可以包含字符串,还可以包含列表,数字和布尔值。

该文件应该描述构建Python包所需的所有内容,而不必使用SIP。但是,正如我们稍后看到的那样,在某些情况下,该文件不够用,此外,还需要创建一个小的Python脚本。但是,让我们依次讨论所有内容。可以在SIP文档页面上找到pyproject.toml

文件与SIP相关的所有可能参数的完整说明 对于我们的示例,在与foo文件夹相同的级别上创建pyproject.toml文件



foo_c_01 /
├──富
│├──箱
││├──foo.o
││├──libfoo.a
││├──主要
││└──main.o
│├──foo.c
│├──foo.h
│├──main.c
│└──Makefile
└──pyproject.toml


pyproject.toml 的内容如下:

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

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

[tool.sip.bindings.pyfoo]
headers = ["foo.h"]
libraries = ["foo"]
include-dirs = ["foo"]
library-dirs = ["foo/bin"]

[build-system] 部分(TOML术语为“表”)是标准的,并在PEP 518中进行了描述它包含两个参数:

  • require-生成我们的软件包所需的软件包列表。软件包依赖项描述格式在PEP 508 Python软件包的依赖项规范中进行了描述在这种情况下,我们只需要Sip软件包版本5.x。
  • build-backend , . , Python-, . , , SIP, «sipbuild.api».

其他参数在[tool.sip。*]节中介绍[tool.sip.metadata]

部分包含有关程序包的常规信息:要组装的程序包的名称(我们的程序包将称为pyfoo,但不要将此名称与模块的名称混淆,我们稍后将其导入Python中),程序包的版本号(在本例中为版本号“ 0.1”)和许可证(例如“ MIT ”)。 从组装的角度来看,最重要的描述在[tool.sip.bindings中。pyfoo ]。 注意节标题中的软件包名称。我们在本节中添加了两个参数:





  • headers-使用foo库所需的头文件列表。
  • -为静态链接编译的目标文件列表。
  • include-dirs是除附加到C编译器的头文件之外的其他头文件的路径,在这种情况下,是foo.h文件的位置
  • library-dirs是查找除C编译器附带的对象文件之外的其他目标文件的路径,在这种情况下,这是创建编译的库文件foo的文件夹

因此,我们为SIP创建了第一个必需文件。现在,我们继续创建下一个文件,该文件将描述未来的Python模块的内容。

pyfoo.sip


在与pyproject.toml文件相同的文件夹中创建pyfoo.sip文件

foo_c_01 /
├──富
│├──箱
││├──foo.o
││├──libfoo.a
││├──主要
││└──main.o
│├──foo.c
│├──foo.h
│├──main.c
│└──Makefile
├──pyfoo.sip
└──pyproject.toml


扩展名为.sip的文件描述了源库的接口,该库将转换为Python中的模块。该文件具有自己的格式,我们现在将考虑它,它类似于带有附加标记的C / C ++头文件,这应该有助于SIP创建Python模块。

在我们的示例中,此文件应称为pyfoo.sip,因为在此之前我们在pyproject.toml文件中创建了[tool.sip.bindings。pyfoo]。在一般情况下,可以有多个这样的分区,因此,必须有多个* .sip文件。但是,如果我们有多个sip文件,那么从SIP的角度来看这是一种特殊情况,因此在本文中我们将不予考虑。请注意,通常情况下,.sip文件的名称(以及相应的部分的名称)可能与包的名称不一致,该包的名称[tool.sip.metadata]部分name参数指定

考虑以下示例中pyfoo.sip文件

%Module(name=foo, language="C")

int foo(char*);

以字符“%”开头的行称为指令。他们应该告诉SIP如何正确组装和样式化Python模块。本文档页面上描述了指令的完整列表。一些指令具有附加参数。可能不需要参数。

在此示例中,我们使用两个指令;在以下示例中,我们将了解其他一些指令。pyfoo.sip

文件%Module指令(名称= foo,语言=“ C”)开头。请注意,我们指定的第一个参数(名称)的值不带引号,而第二个参数的值(语言)加上引号,例如C / C ++中的字符串。%Module指令的文档中所述,这是该指令的要求

%Module指令中,仅需要name参数,该参数设置了将从中导入库函数的Python模块的名称。在这种情况下,该模块称为foo,它将包含函数foo,因此在组装和安装后,我们将使用以下代码导入该模块:

from foo import foo

我们可以通过替换以下行来使该模块嵌套在另一个模块中:

%Module(name=foo.bar, language="C")
...

然后导入函数foo将需要如下所示:

from foo.bar import foo

%Module指令language参数指示编写源库的语言。此参数的值可以是“ C”或“ C ++”。如果未指定此参数,则SIP将假定该库是用C ++编写的。 现在来看pyfoo.sip文件的最后一行



int foo(char*);

这是我们要放入Python模块中的库中函数接口的描述。sip根据此声明创建一个Python函数。我认为这里一切应该都清楚。

我们收集并检查


现在一切准备就绪,可以为C库建立具有绑定的Python包了,首先,您需要构建库本身。转到pyfoo_c_01 / foo /文件夹,并使用make命令开始构建

$ make

mkdir -p bin
gcc -c main.c -o bin/main.o
gcc -c foo.c -o bin/foo.o
ar rcs bin/libfoo.a bin/foo.o
gcc bin/main.o -Lbin -lfoo -o bin/main

如果一切顺利,则将在foo文件夹创建bin文件夹,在其中,除其他文件外,还将有一个已编译的libfoo.a让我提醒您,在这里,为了不偏离主要主题,我们仅在谈论使用gcc在Linux下进行构建。

返回到pyfoo_c_01文件夹现在是时候了解SIP团队了。安装SIP之后,以下命令行命令将变为可用(文档页面):

  • sip-build创建一个Python扩展对象文件。
  • sip-install创建一个Python扩展对象文件并安装。
  • sip-sdist. .tar.gz, pip.
  • sip-wheel. wheel ( .whl).
  • sip-module. , , SIP. , , . , standalone project, , , , .
  • sip-distinfo. .dist-info, wheel.

这些命令需要从pyproject.toml文件所在的文件夹中运行

首先,为了更好地理解SIP的操作,请运行sip-build命令(带有--verbose选项),以向控制台提供更详细的输出,并查看在构建过程中会发生什么。

$ sip-build --verbose

将建立以下绑定:pyfoo。
生成pyfoo绑定...
编译'foo'模块...
构建'foo'扩展名
创建build
创建build / temp.linux-x86_64-3.8
gcc -pthread -Wno-unused-result -Wsign-compare -DNDEBUG -g -fwrapv -O3 -Wall -march = x86-64 -mtune =通用-O3 -pipe -fno-plt -fno-semantic-interposition -march = x86-64 -mtune =普通-O3-管道-fno-plt -march = x86-64 -mtune =普通-O3-管道-fno-plt -fPIC -DSIP_PROTECTED_IS_PUBLIC -Dprotected =公用-I。 -I ../../ foo -I / usr / include / python3.8 -c sipfoocmodule.c -o build /temp.linux-x86_64-3.8/sipfoocmodule.o
sipfoocmodule.c:在func_foo函数中:
sipfoocmodule .c:29:22:警告:隐式函数声明“ foo” [-Wimplicit-function-declaration]
29 | sipRes = foo(a0);
| ^ ~~
gcc -pthread -Wno-unused-result -Wsign-compare -DNDEBUG -g -fwrapv -O3 -Wall -march = x86-64 -mtune =通用-O3 -pipe -fno-plt -fno-semantic-interposition -march = x86-64 -mtune =通用-O3-管道-fno-plt -march = x86-64 -mtune =通用-O3-管道-fno-plt -fPIC -DSIP_PROTECTED_IS_PUBLIC -Dprotected =公用-I。 -I ../../ foo -I / usr / include / python3.8 -c array.c -o build / temp.linux-x86_64-3.8 / array.o
gcc -pthread -Wno-unused-result -Wsign -compare -DNDEBUG -g -fwrapv -O3 -Wall -march = x86-64 -mtune =通用-O3 -pipe -fno-plt -fno-semantic-interposition -march = x86-64 -mtune =通用-O3 -pipe -fno-plt -march = x86-64 -mtune =通用-O3 -pipe -fno-plt -fPIC -DSIP_PROTECTED_IS_PUBLIC -Dprotected = public -I。 -I ../../ foo -I / usr / include / python3.8 -c bool.cpp -o build / temp.linux-x86_64-3.8 / bool.o
gcc -pthread -Wno-unused-result -Wsign-compare -DNDEBUG -g -fwrapv -O3 -Wall -march = x86-64 -mtune =通用-O3 -pipe -fno-plt -fno-semantic-interposition -march = x86-64 -mtune =通用-O3-管道-fno-plt -march = x86-64 -mtune =通用-O3-管道-fno-plt -fPIC -DSIP_PROTECTED_IS_PUBLIC -Dprotected =公用-I。 -I ../../ foo -I / usr / include / python3.8 -c objmap.c -o build / temp.linux-x86_64-3.8 / objmap.o
gcc -pthread -Wno-unused-result -Wsign -compare -DNDEBUG -g -fwrapv -O3 -Wall -march = x86-64 -mtune =通用-O3 -pipe -fno-plt -fno-semantic-interposition -march = x86-64 -mtune =通用-O3 -pipe -fno-plt -march = x86-64 -mtune =通用-O3 -pipe -fno-plt -fPIC -DSIP_PROTECTED_IS_PUBLIC -Dprotected = public -I。 -I ../../ foo -I / usr / include / python3.8 -c qtlib.c -o build / temp.linux-x86_64-3.8 / qtlib.o
gcc -pthread -Wno-unused-result -Wsign-compare -DNDEBUG -g -fwrapv -O3 -Wall -march = x86-64 -mtune =通用-O3 -pipe -fno-plt -fno-semantic-interposition -march = x86-64 -mtune =普通-O3-管道-fno-plt -march = x86-64 -mtune =普通-O3-管道-fno-plt -fPIC -DSIP_PROTECTED_IS_PUBLIC -Dprotected =公用-I。 -I ../../ foo -I / usr / include / python3.8 -c int_convertors.c -o build / temp.linux-x86_64-3.8 / int_convertors.o
gcc -pthread -Wno-unused-result -Wsign -compare -DNDEBUG -g -fwrapv -O3 -Wall -march = x86-64 -mtune =通用-O3 -pipe -fno-plt -fno-semantic-interposition -march = x86-64 -mtune =通用-O3 -pipe -fno-plt -march = x86-64 -mtune =通用-O3 -pipe -fno-plt -fPIC -DSIP_PROTECTED_IS_PUBLIC -Dprotected = public -I。 -I ../../ foo -I / usr / include / python3.8 -c voidptr.c -o build / temp.linux-x86_64-3.8 / voidptr.o
gcc -pthread -Wno-unused-result -Wsign-compare -DNDEBUG -g -fwrapv -O3 -Wall -march = x86-64 -mtune =通用-O3 -pipe -fno-plt -fno-semantic-interposition -march = x86-64 -mtune =普通-O3-管道-fno-plt -march = x86-64 -mtune =普通-O3-管道-fno-plt -fPIC -DSIP_PROTECTED_IS_PUBLIC -Dprotected =公用-I。 -I ../../ foo -I / usr / include / python3.8 -c apiversions.c -o build / temp.linux-x86_64-3.8 / apiversions.o
gcc -pthread -Wno-unused-result -Wsign -compare -DNDEBUG -g -fwrapv -O3 -Wall -march = x86-64 -mtune =通用-O3 -pipe -fno-plt -fno-semantic-interposition -march = x86-64 -mtune =通用-O3 -pipe -fno-plt -march = x86-64 -mtune =通用-O3 -pipe -fno-plt -fPIC -DSIP_PROTECTED_IS_PUBLIC -Dprotected = public -I。 -I ../../ foo -I / usr / include / python3.8 -c描述符.c -o build / temp.linux-x86_64-3.8 / descriptors.o
gcc -pthread -Wno-unused-result -Wsign-compare -DNDEBUG -g -fwrapv -O3 -Wall -march = x86-64 -mtune =通用-O3 -pipe -fno-plt -fno-semantic-interposition -march = x86-64 -mtune =通用-O3-管道-fno-plt -march = x86-64 -mtune =通用-O3-管道-fno-plt -fPIC -DSIP_PROTECTED_IS_PUBLIC -Dprotected =公用-I。 -I ../../ foo -I / usr / include / python3.8 -cthreads.c -o build / temp.linux-x86_64-3.8 /
threads.o gcc -pthread -Wno-unused-result -Wsign -compare -DNDEBUG -g -fwrapv -O3 -Wall -march = x86-64 -mtune =通用-O3 -pipe -fno-plt -fno-semantic-interposition -march = x86-64 -mtune =通用-O3 -pipe -fno-plt -march = x86-64 -mtune =通用-O3 -pipe -fno-plt -fPIC -DSIP_PROTECTED_IS_PUBLIC -Dprotected = public -I。 -I ../../ foo -I / usr / include / python3.8 -c siplib.c -o build / temp.linux-x86_64-3.8 / siplib.o
siplib.c:在函数“ slot_richcompare”中:
siplib.c:9536:16:警告:在此函数中,“ st”可能无需初始化即可使用[-Wmaybe-uninitialized]
9536 | slot = findSlotInClass(ctd,st);
| ^ ~~~~~~~~~~~~~~~~~~~~~~~
siplib.c:10671:19:备注:“ st”在此处声明为
10671 | sipPySlotType st;
| ^
〜siplib.c:在函数“ parsePass2”中:
siplib.c:5625:32:警告:在此函数中,可能没有初始化就使用“所有者” [-Wmaybe-uninitialized]
5625 | * owner = arg;
| ~~~~~~~ ^〜
g ++ -pthread -shared -Wl,-O1,-sort-common,-按需,-z,relro,-z,现在-fno语义插入-Wl,-O1,-sort-common, -按需,-z,relro,-z现在构建/ temp.linux-x86_64-3.8 / sipfoocmodule.o build / temp.linux-x86_64-3.8 / array.o build / temp.linux-x86_64-3.8 /bool.o build / temp.linux-x86_64-3.8 / objmap.o build / temp.linux-x86_64-3.8 / qtlib.o build / temp.linux-x86_64-3.8 / int_convertors.o build / temp.linux-x86_64 -3.8 / voidptr.o build / temp.linux-x86_64-3.8 / apiversions.o build / temp.linux-x86_64-3.8 /描述符.o build / temp.linux-x86_64-3.8 / thread.o build / temp.linux -x86_64-3.8 / siplib.o -L ../../ foo / bin -L / usr / lib -lfoo -o / home / jenyay / projects / soft / sip-examples / pypy_c_01 / build / foo / foo。 cpython-38-x86_64-linux-gnu.so
该项目已构建。

我们不会深入研究SIP的工作,但是从输出中可以看出,一些源正在编译。这些资源可以在此命令创建的build / foo /文件夹中看到

pyfoo_c_01
├──建筑
│└──富
│├──apiversions.c
│├──array.c
│├──array.h
│├──bool.cpp
│├──搭建
││└──temp.linux-x86_64-3.8
││├──apiversions.o
││├──array.o
││├──bool.o
││├──descriptors.o
││├──int_convertors.o
││├──objmap.o
││├──qtlib.o
││├──sipfoocmodule.o
││├──siplib.o
││├──threads.o
││└──voidptr.o
│├──descriptors.c
│├──foo.cpython-38-x86_64-linux-gnu.so
│├──int_convertors.c
│├──objmap.c
│├──qtlib.c
│├──sipAPIfoo.h
│├──sipfoocmodule.c
│├──sip.h
│├──sipint.h
│├──siplib.c
│├──threads.c
│└──voidptr.c
├──富
│├──箱
││├──foo.o
││├──libfoo.a
││├──主要
││└──main.o
│├──foo.c
│├──foo.h
│├──main.c
│└──Makefile
├──pyfoo.sip
└──pyproject.toml


辅助源出现build / foo文件夹中出于好奇,让我们看一下sipfoocmodule.c文件,因为它直接与将要创建foo模块有关:

/*
 * Module code.
 *
 * Generated by SIP 5.1.1
 */

#include "sipAPIfoo.h"

/* Define the strings used by this module. */
const char sipStrings_foo[] = {
    'f', 'o', 'o', 0,
};

PyDoc_STRVAR(doc_foo, "foo(str) -> int");

static PyObject *func_foo(PyObject *sipSelf,PyObject *sipArgs)
{
    PyObject *sipParseErr = SIP_NULLPTR;

    {
        char* a0;

        if (sipParseArgs(&sipParseErr, sipArgs, "s", &a0))
        {
            int sipRes;

            sipRes = foo(a0);

            return PyLong_FromLong(sipRes);
        }
    }

    /* Raise an exception if the arguments couldn't be parsed. */
    sipNoFunction(sipParseErr, sipName_foo, doc_foo);

    return SIP_NULLPTR;
}

/* This defines this module. */
sipExportedModuleDef sipModuleAPI_foo = {
    0,
    SIP_ABI_MINOR_VERSION,
    sipNameNr_foo,
    0,
    sipStrings_foo,
    SIP_NULLPTR,
    SIP_NULLPTR,
    0,
    SIP_NULLPTR,
    SIP_NULLPTR,
    0,
    SIP_NULLPTR,
    0,
    SIP_NULLPTR,
    SIP_NULLPTR,
    SIP_NULLPTR,
    {SIP_NULLPTR, SIP_NULLPTR, SIP_NULLPTR, SIP_NULLPTR, SIP_NULLPTR,
            SIP_NULLPTR, SIP_NULLPTR, SIP_NULLPTR, SIP_NULLPTR, SIP_NULLPTR},
    SIP_NULLPTR,
    SIP_NULLPTR,
    SIP_NULLPTR,
    SIP_NULLPTR,
    SIP_NULLPTR,
    SIP_NULLPTR,
    SIP_NULLPTR,
    SIP_NULLPTR
};

/* The SIP API and the APIs of any imported modules. */
const sipAPIDef *sipAPI_foo;

/* The Python module initialisation function. */
#if defined(SIP_STATIC_MODULE)
PyObject *PyInit_foo(void)
#else
PyMODINIT_FUNC PyInit_foo(void)
#endif
{
    static PyMethodDef sip_methods[] = {
        {sipName_foo, func_foo, METH_VARARGS, doc_foo},
        {SIP_NULLPTR, SIP_NULLPTR, 0, SIP_NULLPTR}
    };

    static PyModuleDef sip_module_def = {
        PyModuleDef_HEAD_INIT,
        "foo",
        SIP_NULLPTR,
        -1,
        sip_methods,
        SIP_NULLPTR,
        SIP_NULLPTR,
        SIP_NULLPTR,
        SIP_NULLPTR
    };

    PyObject *sipModule, *sipModuleDict;
    /* Initialise the module and get it's dictionary. */
    if ((sipModule = PyModule_Create(&sip_module_def)) == SIP_NULLPTR)
        return SIP_NULLPTR;

    sipModuleDict = PyModule_GetDict(sipModule);

    if ((sipAPI_foo = sip_init_library(sipModuleDict)) == SIP_NULLPTR)
        return SIP_NULLPTR;

    /* Export the module and publish it's API. */
    if (sipExportModule(&sipModuleAPI_foo, SIP_ABI_MAJOR_VERSION, SIP_ABI_MINOR_VERSION, 0) < 0)
    {
        Py_DECREF(sipModule);
        return SIP_NULLPTR;
    }
    /* Initialise the module now all its dependencies have been set up. */
    if (sipInitModule(&sipModuleAPI_foo,sipModuleDict) < 0)
    {
        Py_DECREF(sipModule);
        return SIP_NULLPTR;
    }

    return sipModule;
}

如果您使用Python / C API,则会看到熟悉的功能。请特别注意从第18行开始func_foo函数

编译这些源代码后,将创建文件build / foo / foo.cpython-38-x86_64-linux-gnu.so,其中包含Python扩展名,仍然需要正确安装。

为了编译扩展并立即安装,您可以使用sip-install命令,但我们不会使用它,因为默认情况下,它会尝试将创建的Python扩展全局安装到系统中。该命令具有参数--target-dir,您可以使用它指定要安装扩展的路径,但是我们最好使用其他创建软件包的工具,然后可以使用pip进行安装。

首先,使用sip-sdist命令使用它非常简单:

$ sip-sdist

The sdist has been built.

之后,将创建pyfoo-0.1.tar.gz文件,可以使用以下命令进行安装:

pip install --user pyfoo-0.1.tar.gz

结果,将显示以下信息并安装该软件包:

Processing ./pyfoo-0.1.tar.gz
  Installing build dependencies ... done
  Getting requirements to build wheel ... done
    Preparing wheel metadata ... done
Building wheels for collected packages: pyfoo
  Building wheel for pyfoo (PEP 517) ... done
  Created wheel for pyfoo: filename=pyfoo-0.1-cp38-cp38-manylinux1_x86_64.whl size=337289 sha256=762fc578...
  Stored in directory: /home/jenyay/.cache/pip/wheels/54/dc/d8/cc534fff...
Successfully built pyfoo
Installing collected packages: pyfoo
  Attempting uninstall: pyfoo
    Found existing installation: pyfoo 0.1
    Uninstalling pyfoo-0.1:
      Successfully uninstalled pyfoo-0.1
Successfully installed pyfoo-0.1

确保我们成功建立了Python绑定。我们启动Python并尝试调用该函数。让我提醒您,根据我们的设置,pyfoo软件包包含foo模块,该模块具有foo函数

>>> from foo import foo
>>> foo(b'123456')
12

请注意,作为函数的参数,我们不仅传递字符串,而且传递字节字符串b'123456'- char *与C 的直接相似。稍后,我们将char *的转换添加str,反之亦然。结果是预期的。让我提醒您,函数foo返回char *类型的数组的两倍大小,该数组作为参数传递给它。

让我们尝试将常规的Python字符串而不是字节列表传递给foo函数

>>> from foo import foo
>>> foo('123456')

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: foo(str): argument 1 has unexpected type 'str'

创建的绑定无法将字符串转换为char *;我们将在下一部分中讨论如何教它如何执行此操作。

恭喜,我们在用C编写的库上进行了第一个绑定。

退出Python解释器并以wheel格式组装程序集。如您最可能知道的那样,wheel是一种相对较新的软件包格式,最近已经普遍使用。该格式在PEP 427“ Wheel Binary Package Format 1.0”中进行了描述,但是对wheel格式功能的描述是值得单独撰写的大型文章的主题。对我们而言,重要的是,用户可以使用pip轻松安装轮式包装。

wheel格式的软件包并不比sdist格式的软件包复杂。为此,请在文件所在的文件夹中pyproject.toml需要执行命令

sip-wheel

运行此命令后,将显示构建过程,并且编译器可能会发出警告:

$ sip-wheel

将构建以下绑定:pyfoo。
生成pyfoo绑定...
编译'foo'模块...
sipfoocmodule.c:在func_foo函数中:
sipfoocmodule.c:29:22:警告:foo函数的隐式函数声明[-Wimplicit-function-declaration]
29 | sipRes = foo(a0);
| ^ ~~
siplib.c:在功能“slot_richcompare”:
siplib.c:9536:16:警告: “ST”可以在没有初始化在此函数中使用[-Wmaybe-未初始化]
9536 | slot = findSlotInClass(ctd,st);
| ^ ~~~~~~~~~~~~~~~~~~~~~~~~
siplib.c:10671:19:备注:“ st”在这里声明
10671 | sipPySlotType st;
| ^
〜siplib.c:在函数“ parsePass2”中:
siplib.c:5625:32:警告:在此函数中可能未经初始化就使用了“所有者” [-Wmaybe-uninitialized]
5625 | * owner = arg;
| ~~~~~~~ ^ ^ ~~
轮子已经盖好了。

组装完成后(我们的小型项目会快速编译),一个名为pyfoo-0.1-cp38-cp38-manylinux1_x86_64.whl或类似名称的文件将出现在项目文件夹中。生成文件的名称可能会因您的操作系统和Python版本而异。

现在我们可以使用pip安装此软件包:

pip install --user --upgrade pyfoo-0.1-cp38-cp38-manylinux1_x86_64.whl

这里使用--upgrade 选项以便pip替换之前安装的pyfoo模块。

此外,可以使用foo模块pyfoo,如上所述。

将转换规则添加到char *


在上一节中,我们遇到了一个问题,即foo函数只能接受一组字节,而不能接受字符串。现在,我们将修复此缺陷。为此,我们将使用另一个SIP工具- 注解。批注在.sip文件中使用,并应用于一些代码元素:函数,类,函数自变量,异常,变量等。批注写在正斜杠之间:/批注/

注释可以用作标志,可以处于已设置或未设置的状态,例如:/ ReleaseGIL /,或者某些注释需要分配一些值,例如:/ Encoding =“ UTF-8” /。如果需要将多个注释应用于某个对象,则将它们用斜杠内的逗号分隔:/批注_1,批注_2 /。

在以下示例(位于pyfoo_c_02文件夹中)中,添加文件pyfoo.sip注释参数函数foo

%Module(name=foo, language="C")

int foo(char* /Encoding="UTF-8"/);

Encoding 注释指示应以哪种编码方式编码要传递给函数的字符串。此注释的值可以是:ASCII,Latin-1,UTF-8或无。如果未指定Encoding批注或等于None,则该函数的参数将不进行任何编码,并按原样传递给该函数,但在这种情况下,Python代码中的参数必须为bytes类型,即如上例所示,它是字节数组。如果指定了编码,则此参数可以是字符串(Python中的类型为str)。编码注释只能应用于charconst charchar *const char *

让我们检查foo模块中foo函数现在如何工作为此,与以前一样,您必须首先通过foo文件夹内调用make命令来编译foo,然后从pyfoo_c_02 example文件夹中调用该命令,例如sip-wheel将创建文件pyfoo-0.2-cp38-cp38-manylinux1_x86_64.whl或具有类似名称的文件,可以使用以下命令进行设置:

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

如果一切顺利,请启动Python解释器,然后尝试使用字符串参数调用foo函数

>>> from foo import foo

>>> foo(b'qwerty')
12

>>> foo('qwerty')
12

>>> foo('')
24

首先,我们确保仍然可以使用字节。之后,我们确保现在也可以将字符串参数传递给foo函数。请注意,带有俄语字母的字符串参数foo函数返回的值是仅包含拉丁字母的字符串的两倍。这发生,因为该函数foo的不计算在字符字符串的长度(和双打它),但长度的char *阵列,并且由于以UTF-8编码俄语字母占据2个字节,所述的尺寸的char *阵列从转换后Python字符串的长度是原来的两倍。

精细!我们用函数参数解决了问题foo,但是如果我们的库中有数十个或数百个这样的函数,您将必须为每个函数指定参数编码吗?通常,程序中使用的编码是相同的,并且没有不同功能表示不同编码的目的。在这种情况下,可以在SIP中指定默认编码,如果某些功能需要其他编码,则可以使用Encoding批注重新定义

要默认设置功能参数的编码,请使用%DefaultEncoding指令。在pyfoo_c_03文件夹中的示例中显示了其用法

为了利用指令%DefaultEncoding,请更改文件pyfoo.sip,现在其内容如下:

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

int foo(char*);

现在,如果参数是charchar *类型的函数如果没有Encoding注释,则从%DefaultEncoding指令获取编码,如果没有,则不执行转换,并且对于所有参数char *等。有必要传输的不是字节而是字节

从一个示例pyfoo_c_03文件夹收集并以相同的方式验证为来自一个例子pyfoo_c_02文件夹

简要介绍project.py。自动化组装


到目前为止,我们已经使用了两个实用程序文件来创建Python绑定-pyproject.tomlpyfoo.sip。现在,我们将熟悉另一个这样的文件,该文件应该称为project.py。使用此脚本,我们可以影响程序包的构建过程。让我们进行构建自动化。为了从前面的部分中收集示例pyfoo_c_01 - pyfoo_c_03,您首先必须转到foo /文件夹,使用make命令在此处进行编译,返回到pyproject.toml文件所在的文件夹然后才可以使用以下方法之一开始构建软件包sip- *命令

现在我们的目标是确保在执行sip-buildsip-sdistsip-wheel 命令时,首先启动foo C-library的汇编,然后再启动命令本身。

在本节中创建的示例位于pyfoo_c_04文件夹中

要更改构建过程,我们可以在project.py文件中声明一个类(文件名应该就是该类),该类派生自sipbuild.Project此类具有一些我们可以自行覆盖的方法。当前,我们对以下方法感兴趣:

  • 建立在调用sip-build命令期间调用
  • build_sdist在调用sip-sdist命令时调用
  • build_wheel在调用sip-wheel命令时调用
  • 安装在调用sip-install命令时调用

也就是说,我们可以重新定义这些命令的行为。严格来说,列出的方法在抽象类sipbuild.AbstractProject中声明,从中创建派生类sipbuild.Project

创建一个具有以下内容project.py文件

import os
import subprocess

from sipbuild import Project

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

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

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

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

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

我们声明了sipbuild.Project派生的FooProject并在其中定义了buildbuild_sdistbuild_wheelinstall方法。在所有这些方法中,在调用_build_foo方法之前,我们从基类中调用相同名称的方法,该方法开始foo文件夹中执行make命令 请注意,build_sdistbuild_wheel方法应返回它们创建的文件的名称。这没有写在文档中,但是在SIP源中指出。 现在我们不需要运行make命令



手动构建foo,这将自动完成。

如果现在在pyfoo_c_04文件夹中运行sip-wheel命令则会根据您的操作系统和Python版本创建一个名为pyfoo-0.4-cp38-cp38-manylinux1_x86_64.whl的文件。

可以使用以下命令安装此软件包:

pip install --user --upgrade pyfoo-0.4-cp38-cp38-manylinux1_x86_64.whl

之后,您可以确保foo模块中的foo函数仍然有效。

添加命令行选项以进行构建


以下示例位于pyfoo_c_05文件夹中,并且该程序包的版本号为0.5(请参阅pyproject.toml文件中的设置)。本示例基于文档中示例并进行了一些更正。在此示例中,我们将重做我们的project.py文件,并为构建添加新的命令行选项。

在我们的示例中,我们正在构建一个非常简单的库foo,并且在实际项目中,该库可能很大,因此将其包含在Python绑定项目的源代码中是没有意义的。让我提醒您,SIP最初是为诸如Qt之类的庞大库创建绑定的。当然,您可以说git的子模块可以帮助组织源代码,但这不是重点。假设该库可能不在具有绑定源的文件夹中。在这种情况下,就会出现一个问题,SIP收集器应在哪里查找库头文件和目标文件?在这种情况下,不同的用户可能有自己的放置库的方式。

要解决此问题,请在构建系统中添加两个新的命令行选项,您可以使用它们指定文件foo.h的路径(参数--foo-include-dir)和库目标文件(参数--foo-library-dir)。另外,我们将假设,如果未指定这些参数,则foo仍与绑定源一起位于。

我们需要再次创建project.py文件,并在其中声明一个从sipbuild.Project派生的类。让我们先看看project.py文件的新版本,然后看看它是如何工作的。

import os

from sipbuild import Option, Project

class FooProject(Project):
    """        
       foo.
    """

    def get_options(self):
        """     . """

        tools = ['build', 'install', 'sdist', 'wheel']

        #   .
        options = super().get_options()

        #   
        inc_dir_option = Option('foo_include_dir',
                                help="the directory containing foo.h",
                                metavar="DIR",
                                default=os.path.abspath('foo'),
                                tools=tools)
        options.append(inc_dir_option)

        lib_dir_option = Option('foo_library_dir',
                                help="the directory containing the foo library",
                                metavar="DIR",
                                default=os.path.abspath('foo/bin'),
                                tools=tools)

        options.append(lib_dir_option)

        return options

    def apply_user_defaults(self, tool):
        """    . """

        #     
        super().apply_user_defaults(tool)

        #  ,         
        self.foo_include_dir = os.path.abspath(self.foo_include_dir)
        self.foo_library_dir = os.path.abspath(self.foo_library_dir)

    def update(self, tool):
        """   . """

        #   pyfoo
        # (  pyproject.toml  [tool.sip.bindings.pyfoo])
        foo_bindings = self.bindings['pyfoo']

        #   include_dirs  
        if self.foo_include_dir is not None:
            foo_bindings.include_dirs = [self.foo_include_dir]

        #   library_dirs  
        if self.foo_library_dir is not None:
            foo_bindings.library_dirs = [self.foo_library_dir]

        super().update(tool)

我们再次创建了sipbuild.Project派生FooProject。在此示例中,禁用了foo库的自动汇编,因为现在假定它可以在其他位置,并且在创建绑定时,头文件和目标文件应该已准备就绪。FooProject中重新定义三种方法:get_optionsapply_user_defaultsupdate。仔细考虑它们。 让我们从get_options方法开始。此方法应返回sipbuild.Option类的实例列表。



。每个列表项都是一个命令行选项。在覆盖的方法内部,我们通过调用相同名称的基类方法来获取默认选项的列表(options变量),然后创建两个新选项(--foo_include_dir--foo_library_dir)并将其添加到列表中,然后从函数中返回此列表。Option

类的构造函数接受一个必需的参数(选项名称)和足够多的可选参数,这些可选参数描述此参数的值的类型,默认值,参数描述等。本示例对Option构造函数使用以下选项

  • help , , sip-wheel -h
  • metavar — , , . metavar «DIR», , — .
  • default — . , , foo , ( ).
  • tools — , . sip-build, sip-install, sip-sdist sip-wheel, tools = ['build', 'install', 'sdist', 'wheel'].

以下重载的apply_user_defaults方法旨在设置用户可以通过命令行传递的参数值。所述apply_user_defaults方法从基类创建一个变量(类部件)用于在所创建的每个命令行参数get_options方法,因此,使用创建的变量,使得使用命令行参数创建的所有变量被创建并使用默认值初始化之前调用同一名称的基类的方法是很重要的。之后,在我们的示例中,将创建变量self.foo_include_dirself.foo_library_dir。如果用户尚未指定相应的命令行参数,则它们将根据Option类的构造函数的参数(默认参数采用默认值。如果未设置默认参数,则根据期望参数值的类型,将其初始化为None或一个空列表或0。

apply_user_defaults方法内部,我们确保变量self.foo_include_dirself.foo_library_dir中的路径始终是绝对的。这是必需的,因此它不依赖于程序集启动时工作文件夹的位置。

此类中的最后一个重载方法是update。。当有必要将之前所做的更改应用于项目时,将调用此方法。例如,更改或添加pyproject.toml文件中指定的参数。在前面的示例中,我们分别在[tool.sip.bindings.pyfoo]部分中使用include-dirslibrary-dirs参数设置头文件和目标文件的路径。现在,我们将从project.py脚本中设置这些参数,因此在pyproject.toml文件中,我们将删除这些参数:

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

[tool.sip.metadata]
name = "pyfoo"
version = "0.3"
license = "MIT"

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

在方法内部,从字典self.bindings键控pyfoo gat实例sipbuild.Bindings 更新我们。项名称对应于[tool.sip.bindings。pyfoo从] pyproject.toml文件,并将这样得到的类的实例描述了在本节中描述的设置。然后,为此类的成员include_dirslibrary_dirs(成员名称与参数include-dirslibrary-dirs对应,用下划线代替的下划线)被分配了包含存储在成员self.foo_include_dirself.foo_library_dir中的路径的列表。。在本示例中,出于准确性考虑,我们检查self.foo_include_dirself.foo_library_dir不等于None,但是在本示例中,由于创建的命令行参数具有默认值,因此始终满足此条件。

因此,我们准备了配置文件,以便在组装过程中可以指示头文件和目标文件的路径。让我们检查发生了什么。

首先,确保默认值有效。为此,请转到pyfoo_c_05 / foo文件夹并使用make命令构建库,因为在此示例中我们禁用了自动库构建。

之后,转到文件夹pyfoo_c_05并运行sip-wheel命令作为此命令的结果,将创建pyfoo-0.5-cp38-cp38-manylinux1_x86_64.whl文件或具有类似名称的文件

现在将foo文件夹移到pyfoo_c_05文件之外的某个位置,然后再次运行sip-wheel命令结果,我们得到预期的错误,通知我们没有对象库文件:

usr/bin/ld:   -lfoo
collect2: :  ld     1
sip-wheel: Unable to compile the 'foo' module: command 'g++' failed with exit status 1

之后,使用新的命令行选项运行sip-wheel

sip-wheel --foo-include-dir ".../foo" --foo-library-dir ".../foo/bin"

代替省略号,您需要指定将foo文件夹与组合库一起移动到的文件夹的路径因此,程序集应成功创建.whl文件。可以按照与前面各节相同的方式安装和测试创建的模块。

从project.py检查调用方法的顺序


我们将考虑的下一个示例非常简单;它将演示Project类的调用方法的顺序,我们在上一节中对其进行了重载。为了了解何时可以初始化变量,这可能很有用。该示例位于源存储库中的pyfoo_c_06文件夹中。

此示例的本质是重载我们之前project.py文件中的FooProject类中使用的所有方法,并向它们添加对print函数的调用,这将显示其所在方法的名称:

from sipbuild import Project

class FooProject(Project):
    def get_options(self):
        print('get_options()')
        options = super().get_options()
        return options

    def apply_user_defaults(self, tool):
        print('apply_user_defaults()')
        super().apply_user_defaults(tool)

    def apply_nonuser_defaults(self, tool):
        print('apply_nonuser_defaults()')
        super().apply_nonuser_defaults(tool)

    def update(self, tool):
        print('update()')
        super().update(tool)

    def build(self):
        print('build()')
        super().build()

    def build_sdist(self, sdist_directory):
        print('build_sdist()')
        return super().build_sdist(sdist_directory)

    def build_wheel(self, wheel_directory):
        print('build_wheel()')
        return super().build_wheel(wheel_directory)

    def install(self):
        print('install()')
        super().install()

细心的读者应该注意,除了以前使用的方法之外,在此示例中,我们还没有讨论apply_nonuser_defaults()方法。此方法建议为无法通过命令行参数更改的所有变量设置默认值。

pyproject.toml文件中返回该库的显式路径:

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

[tool.sip.metadata]
name = "pyfoo"
version = "0.4"
license = "MIT"

[tool.sip.bindings.pyfoo]
headers = ["foo.h"]
libraries = ["foo"]
include-dirs = ["foo"]
library-dirs = ["foo/bin"]

为了使项目成功构建,您需要进入foo文件夹并使用make命令在此处构建库。之后,返回到pyfoo_c_06文件夹并运行例如sip-wheel命令。结果,如果放弃编译器警告,将显示以下文本:

get_options()
apply_nonuser_defaults()
get_options()
get_options()
apply_user_defaults()
get_options()
update()
将创建以下绑定:pyfoo。
build_wheel()
生成pyfoo绑定...
编译'foo'模块...
轮子已经构建。

显示从我们project.py文件输出的粗线。因此,我们看到get_options方法已被调用多次,如果要初始化从Project派生的类中的任何成员变量,则必须考虑这一点get_options方法不是最佳选择。

这也是有用的记得apply_nonuser_defaults方法调用之前apply_user_defaults方法,即在apply_user_defaults方法中已经可以使用其值在apply_nonuser_defaults方法中设置的变量

之后,将调用update方法,最后是直接负责组装的方法,在我们的例子中是build_wheel

结语至第一部分


在本文中,我们开始研究SIP工具,该工具旨在为用C或C ++编写的库创建Python绑定。在本文的第一部分中,

我们以为用C编写的非常简单的库创建Python绑定的示例为例,研究了使用SIP的基础知识。我们确定了创建与SIP一起使用所需的文件。pyproject.toml文件包含有关软件包的信息(名称,版本号,许可证以及头文件和目标文件的路径)。使用project.py文件,您可以影响Python包的构建过程,例如,开始构建C库或让用户指定库的头文件和目标文件的位置。

* .sip文件中描述Python模块的接口,其中列出了模块中将包含的功能和类。指令和注释用于描述* .sip文件中的接口

在本文的第二部分中,我们将在用C ++编写的面向对象的库上创建绑定,并通过其示例,我们将研究对描述C ++类的接口有用的技术,同时我们还将为我们处理新的指令和注释。

未完待续。

参考文献



All Articles