1. 在其它应用程序嵌入 Python
2026/9/24 17:58:00 网站建设 项目流程

1. 把它放到了其它的应用程序当中, 嵌入进去。

前几章讨论了如何对 进行扩展,也就是如何用 C 函数库 扩展 的功能。反过来也是可以的:将 嵌入到 C/C++ 应用程序中丰富其功能。这种嵌入可以让应用程序用 来实现某些功能,而不是用 C 或 C++ 。

用途会有很多;比如允许用户用 编写一些脚本,以便定制应用程序满足需求。假设说, 有一些特定的功能模块, 通过用某种特定的方式进行编写的话, 会让人觉得更加轻松、简单, 那么相关的开发人员基于这样的情况, 他们自己也是能够采取这种做法。

它的嵌入操作, 跟扩展的操作, 有相似的地方, 但是这中间还是有区别, 并不是完全一样的。

它们的不同点主要在于, 当进行扩展到某个环境时, 那个应用程序里的主导程序, 依然还是解释器在运行;而如果是把代码嵌入到应用里时, 那个主导的程序可能跟解释器本身完全没有关系, 它只是让应用程序中的某些部分, 偶尔去调用一下解释器, 从而执行一段特定的代码而已。

所以, 要是想要进行嵌入的话, 就必须要去准备一个属于自己的主程序。这个主程序需要执行的操作之一就是去初始化那个解释器。这一步至少得调用某一个函数才行。

还有一些可选的调用操作, 可以通过这些操作把命令行参数传递进去给到这个解释器。等到把这些事情都做完之后, 就可以直接从应用程序里面的任何地方去调用这个解释器了。

调用解释器的方法存在多种选择, 其一可以是向特定输入传入一个包含代码语句的字符串;其二则是可以向其指定一个标准输入输出文件指针以及一个文件名, 其中该文件名仅在发生错误信息的展示时起到识别作用。除此之外, 我们还可以使用之前介绍过的底层操作手段来构建对象并使其投入使用。

参见

这里所提供的信息是充分的, 也是非常重要的。

1.1. 高层次的嵌入

采用非常高层的接口, 这种嵌入形式是最简单的。使用这种接口的目标, 是完全不需要和应用程序进行直接交互, 而是只执行一段脚本。通过以下方式中的代码示例, 就可以对指定的文件完成相应的操作处理。

#define PY_SSIZE_T_CLEAN

#include

int

main(int argc, char *argv[])

{

PyStatus status;

PyConfig config;

PyConfig_InitPythonConfig(&config);

/* 以下是可选的但推荐使用 */

status = PyConfig_SetBytesString(&config, &config.program_name, argv[0]);

if (PyStatus_Exception(status)) {

goto exception;

}

status = Py_InitializeFromConfig(&config);

if (PyStatus_Exception(status)) {

goto exception;

}

PyConfig_Clear(&config);

PyRun_SimpleString("from time import time,ctime\n"

"print('Today is', ctime(time()))\n");

if (Py_FinalizeEx() < 0) {

exit(120);

}

return 0;

exception:

PyConfig_Clear(&config);

Py_ExitStatusException(status);

}

备注

# 它被用来指明, 应当在某些 API 中代替 int 来使用这个符号。从 3.13 开始, 它已经不再需要了, 但是我们出于向下兼容的考虑, 仍然保留了这个宏。请查阅有关文档以获取该宏的详细描述。

设置 这个动作应当在 之前被调用才比较好, 这样做的目的是为了告诉解释器关于运行时库所在路径的信息。然后在接下来这一步 , 解释器将会使用 来做初始化工作, 接着去执行那个已经被硬编码进去的 脚本, 而这个脚本的主要作用就是打印出当前的日期以及时间的内容。

在过了这一段之后 , 还会接着调用 来关闭已经运行中的解释器, 随着它的关闭, 整个程序也就随之结束掉了。

但是在真实存在的程序编写情境之中, 你可能确实需要从其他的一些来源渠道处去获取 脚本的内容, 比如说有可能源自于文本编辑器里面提供的例程功能或者来自某一个个具体的文件又或者是从某个数据库里面提取出来的内容也都是可以的。

通过调用该函数, 你能够比较妥善地去实现自文件当中获取目标代码这一操作, 此举可以有效避免你需要去自行分配存储空间以及执行读取文件内部内容这类较为繁琐的任务程序。

1.2. 突破高层次嵌入的限制, 概述。

高级接口能够让应用程序来执行各种各样的代码, 但是, 如果要进行数据的交换的话, 那情况就得说是变得非常麻烦的了。因此, 如果存在需要交换数据这样的需求, 那么就应当选择去使用那些比较更低层级的调用方式。它几乎能够实现所有想要的功能, 然而, 这么做的代价就是需要编写多得多的 C 语言相关的代码了。

需要大家注意的一个事实是, 虽然具体的意图存在差异, 但是在展开处理以及嵌入处理的整个过程里, 两边的情况是非常相似的。在前面章节当中所探讨的那些主要议题, 如今大部分仍然处于有效状态之中。

为了让大家能够把这个问题看得更加清楚一点, 我们可以试着去观察一下从那个特定的起点直到代码变为C语言格式这一系列操作的执行细节到底包含哪些内容:

把相关的数据信息做进一步的调整, 然后最终转换成为所谓的 C 这种具体的数据格式。

将转换之后的数据拿来, 去执行C语言的程序里的函数调用。

我们需要将那些由调用方法之后所返回出来的数据, 从C这样一种形式进行转变, 最终把它们调整到另外一个格式上面去。

当它被嵌入进去的时候, 那段处理接口的程序代码就会按照下述的方式进行操作。

把C里面的那一些数据, 进行转换的这样一个操作, 然后把它变成相应的格式。

先把那些数据处理转换一番, 接着再用转好后的数据, 去调用关于那个接口对应的函数。

把调用程序返回来的那些数据,进行一系列的操作, 从一种格式转换为C语言的格式。

从表面上看, 这只是数据转换步骤之间的顺序发生了调换, 其主要目的是为了适应跨语言传输的具体方向。这里面真正存在的一个唯一的区别在于: 在两次执行数据转换这个动作的中间环节, 所调用的函数是不一样的。

具体情况是这样的, 当执行扩展这个操作的时候, 所调用的是一个 C 函数, 然而, 当执行嵌入这个操作的时候, 所调用的就是一个 PHP 函数。

本文不涉及如何把数据从 C 转换为其它内容, 也不涉及反过来怎么弄。另外, 还假设读者会使用引用, 并且能处理好错误的情况。因为这些事情和扩展解释器的区别不大, 大家可以去看看前面的章节来获取所需要的信息。

1.第三步, 仅仅进行嵌入这个操作。

第一个程序的目标是执行脚本中的某个函数, 这就像高层次接口那样, 解释器并不会直接与应用程序直接地进行交互, 但是下一节的内容将会改变这种状况。

为了运行在该脚本里所定义的函数, 具体的操作代码如下图所示。

#define PY_SSIZE_T_CLEAN

#include

int

main(int argc, char *argv[])

{

PyObject *pName, *pModule, *pFunc;

PyObject *pArgs, *pValue;

int i;

if (argc < 3) {

fprintf(stderr,"Usage: call pythonfile funcname [args]\n");

return 1;

}

Py_Initialize();

pName = PyUnicode_DecodeFSDefault(argv[1]);

/* 略去 pName 的错误检测 */

pModule = PyImport_Import(pName);

Py_DECREF(pName);

if (pModule !

= NULL) {

pFunc = PyObject_GetAttrString(pModule, argv[2]);

/* pFunc 是一个新引用 */

if (pFunc && PyCallable_Check(pFunc)) {

pArgs = PyTuple_New(argc - 3);

for (i = 0; i < argc - 3; ++i) {

pValue = PyLong_FromLong(atoi(argv[i + 3]));

if (!

pValue) {

Py_DECREF(pArgs);

Py_DECREF(pModule);

fprintf(stderr, "Cannot convert argument\n");

return 1;

}

/* 这里偷取了 pValue 引用: */

PyTuple_SetItem(pArgs, i, pValue);

}

pValue = PyObject_CallObject(pFunc, pArgs);

Py_DECREF(pArgs);

if (pValue !

= NULL) {

printf("Result of call: %ld\n", PyLong_AsLong(pValue));

Py_DECREF(pValue);

}

else {

Py_DECREF(pFunc);

Py_DECREF(pModule);

PyErr_Print();

fprintf(stderr,"Call failed\n");

return 1;

}

}

else {

if (PyErr_Occurred())

PyErr_Print();

fprintf(stderr, "Cannot find function \"%s\"\n", argv[2]);

}

Py_XDECREF(pFunc);

Py_DECREF(pModule);

}

else {

PyErr_Print();

fprintf(stderr, "Failed to load \"%s\"\n", argv[1]);

return 1;

}

if (Py_FinalizeEx() < 0) {

return 120;

}

return 0;

}

上述代码先利用 argv 加载脚本, 然后再调用由 argv 所指定的那个函数, 并且把 argv 数组里面剩下的其他值当成整数参数传递给这个函数, 如果把这个最终形成的可执行程序叫做 call , 接着就用它去执行某个脚本, 举一个例子来说的话。

def multiply(a,b):

print("Will compute", a, "times", b)

c = 0

for i in range(0, a):

c = c + b

return c

然后, 最后得到的结果应当是。

$ call multiply multiply 3 2

Will compute 3 times 2

Result of call: 6

虽然该程序的体积相对于它所实现的功能来说, 是非常大的。但是大部分代码是用来处理数据和C语言之间的格式转换的, 同时也是用来报告错误的情况。从这一点来看啊, 嵌入部分就出现了比较有意思的阶段了。

Py_Initialize();

pName = PyUnicode_DecodeFSDefault(argv[1]);

/* 略去 pName 的错误检测 */

pModule = PyImport_Import(pName);

在初始化解析器之后, 然后使用它去加载脚本。这个例程需要用一个字符串当作它的参数, 这个字符串是使用数据转换例程来构造的。

pFunc = PyObject_GetAttrString(pModule, argv[2]);

/* pFunc 是一个新引用 */

if (pFunc && PyCallable_Check(pFunc)) {

...

}

Py_XDECREF(pFunc);

一旦这个脚本加载完毕, 就会去查找那个属性名称。如果名称存在, 并且返回的是可调用对象, 这样就可以安全地把它视为函数。然后, 程序会继续执行操作。接着, 照常去构建由参数组成的元组。最后, 用以下方式来调用该函数:

pValue = PyObject_CallObject(pFunc, pArgs);

当函数返回的时候, 要么为NULL, 要么包含对函数返回值的引用。请确保用完后释放该引用。

1.4. 对嵌入 功能进行扩展

一直到这个时点, 那个嵌入式的解释器都是没法去碰应用程序自己的那些功能的。API 是通过一种方式来搞定的, 那就是去拓展那个嵌入式的解释器。说得更直白一点, 就是用应用程序里拿出来的一些函数, 给这个嵌入式的解释器加上这些能力。这件事要是认真想想, 会觉得有点复杂, 但实际上情况也没那么糟糕。

现在大家先把那些事放一边, 就暂时忘掉是谁启动了那个解释器吧, 反正记住是哪个应用程序启动它就行了, 就这么个意思。把应用程序看做是一堆子程序, 然后编写一些胶水代码来访问这些子程序, 这就像是编写普通的扩展程序一样, 具体的例子如下:

static int numargs=0;

/* 返回应用程序命令行的参数数量 */

static PyObject*

emb_numargs(PyObject *self, PyObject *args)

{

if(!

PyArg_ParseTuple(args, ":numargs"))

return NULL;

return PyLong_FromLong(numargs);

}

static PyMethodDef emb_module_methods[] = {

{"numargs", emb_numargs, METH_VARARGS,

"Return the number of arguments received by the process."},

{NULL, NULL, 0, NULL}

};

static struct PyModuleDef emb_module = {

.m_base = PyModuleDef_HEAD_INIT,

.m_name = "emb",

.m_size = 0,

.m_methods = emb_module_methods,

};

static PyObject*

PyInit_emb(void)

{

return PyModuleDef_Init(&emb_module);

}

在main()函数的前面位置插入上面所说的代码, 并且在调用的前面位置插入下面写成的这两条语句。

numargs = argc;

PyImport_AppendInittab("emb", &PyInit_emb);

这两行代码完成了变量的初始化工作, 使得嵌入式的解释器能够访问名为emb的这个函数对象或者叫方法。通过这些所做的扩展配置, 脚本具备了执行以下操作的能力。

import emb

print("Number of arguments", emb.numargs())

在那些真实存在的应用程序里, 这种方法会使得应用的API被展现出来, 从而让外界能够使用。

1.5. 在 C++ 中嵌入

另外, 还可以将这个内容嵌入到 C++ 程序里面去;确切地讲, 具体的实现方式会根据 C++ 系统的实际细节而有所不同;通常情况下, 需要使用 C++ 语言来编写主程序代码, 并且要使用 C++ 编译器对程序进行编译和链接操作;在这个过程中, 并不需要针对该内容本身重新进行编译步骤。

1.就是去那些长得有点像 Unix 的系统里头, 把代码先编译好, 再把它们链接起来。

为了将解释器嵌入到应用程序里面去, 要找到能够传送给编译器以及链接器的正确编译参数, 并不是件容易的事情, 尤其在加载库模块是通过 C 动态扩展也就是用 .so 文件形式来实现的背景下, 这一点尤其明显。

为了能够获得需要的编译器和链接器参数, 可以执行一个 Y 脚本。这个脚本是在安装时候生成的, 也有可能还会存在其他相关脚本。该脚本有不少参数, 其中有以下几个参数对实际操作会有直接的作用:

备注

为了避免多个安装版本引发混乱, 尤其是在系统安装版本和自己编译版本之间的时候, 建议用.Y-指定绝对路径, 如上例所述。

要是这个办法没有效果, 因为不能保证在所有的类 Unix 平台上都能起作用, 这时候大家如果有啥建议欢迎提出来。那你就得去翻翻系统里面关于动态链接的文档, 然后查清楚相关的位置在哪, 并且还得看看编译参数都写的是什么。

在这个阶段, 某个模块会变成一个挺好用的工具, 它能通过编程的形式, 把那些需要组合在一起的配置数值给提取出来。举个例子来说:

>>> import sysconfig

>>> sysconfig.get_config_var('LIBS')

'-lpthread -ldl -lutil'

>>> sysconfig.get_config_var('LINKFORSHARED')

'-Xlinker -export-dynamic'

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询