LinuxSir.cn,穿越时空的Linuxsir!

 找回密码
 注册
搜索
热搜: shell linux mysql
查看: 263|回复: 0

python在C++中编写扩展

[复制链接]
发表于 2024-1-17 17:16:27 | 显示全部楼层 |阅读模式

在C++中编写扩展
还可以在C++中编写扩展模块,只是有些限制。如果主程序(Python解释器)是使用C编译器来编译和链接的,全局或静态对象的构造器就不能使用。而如果是C++编译器来链接的就没有这个问题。函数会被Python解释器调用(通常就是模块初始化函数)必须声明为 extern "C" 。而是否在 extern "C" {...} 里包含Python头文件则不是那么重要,因为如果定义了符号 __cplusplus 则已经是这么声明的了(所有现代C++编译器都会定义这个符号)。

给扩展模块提供C API
很多扩展模块提供了新的函数和类型供Python使用,但有时扩展模块里的代码也可以被其他扩展模块使用。例如,一个扩展模块可以实现一个类型 "collection" 看起来是没有顺序的。就像是Python列表类型,拥有C API允许扩展模块来创建和维护列表,这个新的集合类型可以有一堆C函数用于给其他扩展模块直接使用。

开始看起来很简单:只需要编写函数(无需声明为 static ),提供恰当的头文件,以及C API的文档。实际上在所有扩展模块都是静态链接到Python解释器时也是可以正常工作的。当模块以共享库链接时,一个模块中的符号定义对另一个模块不可见。可见的细节依赖于操作系统,一些系统的Python解释器使用全局命名空间(例如Windows),有些则在链接时需要一个严格的已导入符号列表(一个例子是AIX),或者提供可选的不同策略(如Unix系列)。即便是符号是全局可见的,你要调用的模块也可能尚未加载。

可移植性需要不能对符号可见性做任何假设。这意味着扩展模块里的所有符号都应该声明为 static ,除了模块的初始化函数,来避免与其他扩展模块的命名冲突(在段落 模块方法表和初始化函数 中讨论) 。这意味着符号应该 必须 通过其他导出方式来供其他扩展模块访问。

Python 提供了一个特别的机制用来从一个扩展模块向另一个扩展模块传递 C 层级的信息 (指针): Capsule。 一个 Capsule 就是一个存储了指针 (void*) 的 Python 数据类型。 Capsule 只能通过其 C API 来创建和访问,但它们可以像任何其他 Python 对象一样被传递。 特别地,它们可以被赋值给扩展模块命名空间中的一个名称。 其他扩展模块将可以导入这个模块,获取该名称对应的值,然后从 Capsule 中获取指针。

Capsule可以用多种方式导出C API给扩展模块。每个函数可以用自己的Capsule,或者所有C API指针可以存储在一个数组里,数组地址再发布给Capsule。存储和获取指针也可以用多种方式,供客户端模块使用。

无论你选择哪个方法,为你的 Capsule 指定适当的名称都很重要。 函数 PyCapsule_New() 接受一个 name 形参 (const char*);允许你传入一个 NULL 作为名称,但我们强烈推荐你指定名称。 正确地命名的 Capsule 提供了一定的运行时类型安全性;没有可行的方式能区别两个未命名的 Capsule。

通常来说,Capsule用于暴露C API,其名字应该遵循如下规范:

modulename.attributename
便利函数 PyCapsule_Import() 可以方便的载入通过Capsule提供的C API,仅在Capsule的名字匹配时。这个行为为C API用户提供了高度的确定性来载入正确的C API。

下面的例子演示了一种将大部分负担交给导出模块编写者的处理方式,这对于常用的库模块来说是合适的。 它会将所有 C API 指针(在这个例子里只有一个!)储存到一个 void 指针数组,它将成为一个 Capsule 的值。 与模块对应的头文件提供了一个宏用来管理导入模块和获取其 C API 指针;客户端模块只需要在访问 C API 之前调用这个宏即可。

导出模块是对 一个简单的例子 部分的 spam 模块的修改。 函数 spam.system() 并不直接调用 C 库函数 system(),而是调用一个函数 PySpam_System(),这个函数在现实中当然会做一些更复杂的事情(比如在每条命令中添加“sapm”)。 该函数 PySpam_System() 也会导出给其他扩展模块。

函数 PySpam_System() 是一个纯 C 函数,像其他函数一样声明为 static:

static int
PySpam_System(const char *command)
{
    return system(command);
}
函数 spam_system() 已按如下方式修改:

static PyObject *
spam_system(PyObject *self, PyObject *args)
{
    const char *command;
    int sts;

    if (!PyArg_ParseTuple(args, "s", &command))
        return NULL;
    sts = PySpam_System(command);
    return PyLong_FromLong(sts);
}
在模块开头,在此行后:

#include <ython.h>
添加另外两行:

#define SPAM_MODULE
#include "spammodule.h"
#define 用于告知头文件需要包含给导出的模块,而不是客户端模块。最终,模块的初始化函数必须负责初始化C API指针数组:

PyMODINIT_FUNC
PyInit_spam(void)
{
    PyObject *m;
    static void *PySpam_API[PySpam_API_pointers];
    PyObject *c_api_object;

    m = PyModule_Create(&spammodule);
    if (m == NULL)
        return NULL;

    /* Initialize the C API pointer array */
    PySpam_API[PySpam_System_NUM] = (void *)PySpam_System;

    /* Create a Capsule containing the API pointer array's address */
    c_api_object = PyCapsule_New((void *)PySpam_API, "spam._C_API", NULL);

    if (PyModule_AddObject(m, "_C_API", c_api_object) < 0) {
        Py_XDECREF(c_api_object);
        Py_DECREF(m);
        return NULL;
    }

    return m;
}
请注意 PySpam_API 被声明为 static;否则指针数组会在 PyInit_spam() 终结时消失!

头文件 spammodule.h 里的一堆工作,看起来如下所示:

#ifndef Py_SPAMMODULE_H
#define Py_SPAMMODULE_H
#ifdef __cplusplus
extern "C" {
#endif

/* Header file for spammodule */

/* C API functions */
#define PySpam_System_NUM 0
#define PySpam_System_RETURN int
#define PySpam_System_PROTO (const char *command)

/* Total number of C API pointers */
#define PySpam_API_pointers 1


#ifdef SPAM_MODULE
/* This section is used when compiling spammodule.c */

static PySpam_System_RETURN PySpam_System PySpam_System_PROTO;

#else
/* This section is used in modules that use spammodule's API */

static void **PySpam_API;

#define PySpam_System \
(*(PySpam_System_RETURN (*)PySpam_System_PROTO) PySpam_API[PySpam_System_NUM])

/* Return -1 on error, 0 on success.
* PyCapsule_Import will set an exception if there's an error.
*/
static int
import_spam(void)
{
    PySpam_API = (void **)PyCapsule_Import("spam._C_API", 0);
    return (PySpam_API != NULL) ? 0 : -1;
}

#endif

#ifdef __cplusplus
}
#endif

#endif /* !defined(Py_SPAMMODULE_H) */
客户端模块要能够访问函数 PySpam_System() 所必须做的事情就是在其初始化函数中调用函数 (实际上是宏) import_spam():

PyMODINIT_FUNC
PyInit_client(void)
{
    PyObject *m;

    m = PyModule_Create(&clientmodule);
    if (m == NULL)
        return NULL;
    if (import_spam() < 0)
        return NULL;
    /* additional initialization can happen here */
    return m;
}
这种方法的主要缺点是,文件 spammodule.h 过于复杂。当然,对每个要导出的函数,基本结构是相似的,所以只需要学习一次。
您需要登录后才可以回帖 登录 | 注册

本版积分规则

快速回复 返回顶部 返回列表