在你的 C 程序中嵌入 Python

作者:William Nagel

在 Linux 中,大型、高性能应用程序的首选语言几乎总是 C,或者稍少一些情况是 C++。这两种语言都很强大,使你能够创建高性能的本地编译程序。然而,它们并非天生具有运行时灵活性的语言。一旦 C/C++ 应用程序被编译,其代码几乎就是静态的。有时,这可能会成为真正的障碍。例如,如果你想让程序用户轻松创建插件来扩展应用程序的功能,你就必须处理复杂的动态链接问题,这可能会导致无休止的麻烦。此外,你的用户必须懂 C/C++ 才能扩展应用程序,这严重限制了能够编写扩展程序的人数。

一个更好的解决方案是为你的用户提供一种脚本语言,他们可以使用这种语言来扩展你的应用程序。使用脚本语言,你往往会获得更大的运行时灵活性,以及更短的开发时间和更低的 learning curve,这将扩大能够创建扩展程序的用户群体。

不幸的是,创建一种脚本语言是非常重要的任务,很容易成为你程序的主要部分。幸运的是,你不需要创建脚本语言。使用 Python,你可以将解释器直接嵌入到你的应用程序中,并展现 Python 的全部功能和灵活性,而无需向你的应用程序添加太多代码。

在应用程序中包含 Python

在你的程序中包含 Python 解释器非常简单。Python 提供了一个单独的头文件,用于包含将解释器嵌入到你的应用程序中所需的所有定义,这个文件恰如其分地命名为 Python.h。它包含了很多内容,包括几个标准头文件。为了编译效率,如果你可以只包含你实际打算使用的接口部分可能会更好,但不幸的是 Python 并没有真正给你这个选项。如果你查看 Python.h 文件,你会看到它定义了几个重要的宏,并包含了一些通用头文件,这些头文件是文件中稍后包含的各个组件所必需的。

要在编译时将你的应用程序链接到 Python 解释器,你应该运行 python-config 程序来获取应该传递给编译器的链接选项列表。在我的系统上,这些选项是

-lpython2.3 -lm -L/usr/lib/python2.3/config
一个非常简单的嵌入式应用

那么,从 C 应用程序运行 Python 解释器需要多少代码呢?结果证明,非常少。实际上,如果你查看 Listing 1,你会看到只需三行代码即可完成,这三行代码初始化了解释器,向其发送要执行的 Python 代码字符串,然后关闭解释器。

清单 1. 用三行代码嵌入 Python

void exec_pycode(const char* code)
{
  Py_Initialize();
  PyRun_SimpleString(code);
  Py_Finalize();
}

或者,你可以通过调用 Py_Main() 在你的程序中嵌入一个交互式的 Python 终端,如清单 2 所示。这会启动解释器,就像你直接从命令行运行 Python 一样。在用户从解释器 shell 退出后,控制权将返回到你的应用程序。

清单 2. 嵌入交互式 Python

void exec_interactive_interpreter(int arg, char** argv)
{
  Py_Initialize();
  Py_Main(argc, argv);
  Py_Finalize();
}
Python 环境

用三行代码嵌入解释器足够容易,但让我们面对现实,仅仅在程序内部执行任意的 Python 代码字符串既不有趣,也不是很有用。幸运的是,这远非 Python 所允许的全部。在我深入探讨它能做什么之前,让我们先看看初始化 Python 在其中执行的环境。

当你运行 Python 解释器时,主环境上下文存储在 __main__ 模块的命名空间字典中。所有全局定义的函数、类和变量都可以在这个字典中找到。当以交互方式或在脚本文件中运行 Python 时,你很少需要关心这个全局命名空间。但是,当运行嵌入式解释器时,你通常需要访问这个字典以获取对函数或类的引用,以便调用或构造它们。你可能还会发现,你偶尔会想要复制全局字典,以便不同的代码位可以在不同的环境中运行。例如,你可能想要为你加载的每个插件创建一个新环境。

要访问 __main__ 模块的字典,你首先需要获取对该模块的引用。你可以通过调用 PyImport_AddModule() 函数来实现这一点,该函数会查找你提供的模块名称,并返回指向该对象的 PyObject 指针。为什么是 PyObject?所有 Python 数据类型都派生自 PyObject,这使其成为一个方便的最小公分母。因此,在与 Python 解释器交互时,你将处理的几乎所有函数都将接受或返回指向 PyObject 而不是其他更具体的 Python 数据类型的指针。

一旦你有了由 PyObject 引用的 __main__ 模块,你就可以使用 PyModule_GetDict() 函数来获取对主模块字典的引用,该引用再次作为 PyObject 指针返回。然后,你可以在执行其他 Python 命令时传递字典引用。例如,清单 3 显示了如何复制全局环境并在单独的环境中执行两个不同的 Python 文件。

清单 3. 复制环境

// Get a reference to the main module.
PyObject* main_module =
   PyImport_AddModule("__main__");

// Get the main module's dictionary
// and make a copy of it.
PyObject* main_dict =
   PyModule_GetDict(main_module);
PyObject* main_dict_copy =
   PyDict_Copy(main_dict);

// Execute two different files of
// Python code in separate environments
FILE* file_1 = fopen("file1.py", "r");
PyRun_File(file_1, "file1.py",
           Py_file_input,
           main_dict, main_dict);

FILE* file_2 = fopen("file2.py", "r");
PyRun_File(file_2, "file2.py",
           Py_file_input,
           main_dict_copy, main_dict_copy);

我稍后会详细介绍 PyRun_File() 的工作原理,但如果你仔细查看清单 3,你应该会注意到一些有趣的事情。当我调用 PyRun_File() 来执行文件时,字典被传递了两次。原因是 Python 代码在执行时实际上有两个环境上下文。第一个是全局上下文,我已经讨论过了。第二个上下文是局部上下文,其中包含任何局部定义的变量或函数。在这种情况下,它们是相同的,因为正在执行的代码是顶层代码。另一方面,如果你要使用多个 C 级调用动态执行函数,你可能想要创建一个局部上下文并使用它来代替全局字典。但在大多数情况下,通常可以安全地为全局和局部参数都传递全局环境。

在 C/C++ 中操作 Python 数据结构

此时,我相信你已经注意到在清单 3 示例中弹出的 Py_DECREF() 调用。这些有趣的小家伙在那里是为了内存管理的目的。在解释器内部,Python 通过跟踪对程序员透明的所有内存引用来自动处理内存管理。一旦它确定对给定内存块的所有引用都已释放,它就会释放不再需要的块。但是,当你开始在 C 端工作时,这可能会成为一个问题。由于 C 不是一种内存管理的语言,一旦 Python 数据结构最终从 C 引用,所有自动跟踪引用的能力都会丢失给 Python。C 应用程序可以根据需要制作尽可能多的引用副本,并无限期地持有它,而 Python 对此一无所知。

解决方案是让获取对 Python 对象引用的 C 代码手动处理所有引用计数。通常,当 Python 调用将对象传递给 C 程序时,它会将引用计数增加一。然后,C 代码可以随意处理该对象,而无需担心它会被意外删除。然后,当 C 程序完成对对象的操作时,它负责通过调用 Py_DECREF() 来释放其引用。

但是,重要的是要记住,当你在 C 程序中复制可能比你从中复制的指针寿命更长的指针时,你需要通过调用 Py_INCREF() 手动增加引用计数。例如,如果你制作 PyObject 指针的副本以存储在数组中,你可能需要调用 Py_INCREF() 以确保在原始 PyObject 引用递减后,指向的对象不会被垃圾回收。

从文件执行代码

现在让我们看一个稍微更有用的例子,看看如何将 Python 嵌入到实际程序中。如果你查看清单 4,你会看到一个小程序,允许用户在命令行上指定简短的表达式。然后,程序计算这些表达式的结果并在输出中显示它们。为了给混合添加一点情趣,程序还允许用户指定一个 Python 代码文件,该文件将在执行表达式之前加载。这样,用户可以定义可用于命令行表达式的函数。

清单 4. 一个简单的表达式计算器

#include <python2.3/Python.h>

void process_expression(char* filename,
                        int num,
                        char** exp)
{
    FILE*       exp_file;

    // Initialize a global variable for
    // display of expression results
    PyRun_SimpleString("x = 0");

    // Open and execute the file of
    // functions to be made available
    // to user expressions
    exp_file = fopen(filename, "r");
    PyRun_SimpleFile(exp_file, exp);

    // Iterate through the expressions
    // and execute them
    while(num--) {
        PyRun_SimpleString(*exp++);
        PyRun_SimpleString("print x");
    }
}

int main(int argc, char** argv)
{
    Py_Initialize();

    if(argc != 3) {
        printf("Usage: %s FILENAME EXPRESSION+\n");
        return 1;
    }
    process_expression(argv[1], argc - 1, argv + 2);
    return 0;
}

这个程序中使用了两个基本的 Python API 函数,PyRun_SimpleString() 和 PyRun_AnyFile()。你之前已经见过 PyRun_SimpleString()。它所做的只是在全局环境中执行给定的 Python 表达式。PyRun_SimpleFile() 类似于我之前讨论的 PyRun_File() 函数,但它默认在全局环境中运行。由于所有内容都在全局环境中运行,因此每个执行的表达式或表达式组的结果都将可用于稍后执行的表达式或表达式组。

获取可调用的函数对象

现在,假设你不想让我们的表达式计算器执行表达式列表,而是希望它从 Python 文件加载函数 f() 并执行多次以计算总计,基于命令行上提供的数字。你可以通过简单地运行来执行该函数PyRun_SimpleString("f()"),但这实际上效率不高,因为它要求解释器每次调用时都解析和评估字符串。如果我们能直接引用该函数来调用它,那就更好了。

如果你还记得,Python 将所有全局定义的函数存储在全局字典中。因此,如果你可以获得对全局字典的引用,你就可以提取对任何已定义函数的引用。幸运的是,Python API 提供了执行此操作的函数。你可以通过查看清单 5 来了解它的用法。

清单 5. 使用可调用的函数引用

#include <python2.3/Python.h>

void process_expression(int num, char* func_name)
{
    FILE*        exp_file;
    PyObject*    main_module, * global_dict, * expression;

    // Initialize a global variable for
    // display of expression results
    PyRun_SimpleString("x = 0");

    // Open and execute the Python file
    exp_file = fopen(exp, "r");
    PyRun_SimpleFile(exp_file, exp);

    // Get a reference to the main module
    // and global dictionary
    main_module = PyImport_AddModule("__main__");
    global_dict = PyModule_GetDict(main_module);

    // Extract a reference to the function "func_name"
    // from the global dictionary
    expression =
        PyDict_GetItemString(global_dict, func_name);

    while(num--) {
        // Make a call to the function referenced
        // by "expression"
        PyObject_CallObject(expression, NULL);
    }
    PyRun_SimpleString("print x");
}

为了获得函数引用,程序首先通过使用 PyImport_AddModule("__main__") 函数“导入”主模块来获取对主模块的引用。一旦它有了对主模块的引用,程序就使用 PyModule_GetDict() 函数来提取其字典。从那里,只需调用 PyDict_GetItemString(global_dict, "f") 即可从字典中提取函数。

现在程序有了对函数的引用,它可以使用 PyObject_CallObect() 函数来调用它。如你所见,这需要一个指向要调用的函数对象的指针。由于函数本身已经存在于 Python 环境中,因此它已经被编译。这意味着当你执行调用时,没有解析,几乎没有编译开销,这意味着函数可以非常快速地执行。

在函数调用中传递数据

此时,我相信你开始想,“哇,这很棒,但如果我真的可以向我正在调用的这些函数传递一些数据,那就更好了。” 好吧,你无需再疑惑了。事实证明,你完全可以做到这一点。一种方法是使用你在清单 5 中看到的传递给 PyObject_CallObject 的神秘 NULL 值。我稍后会讨论它的工作原理,但首先有一种更简单的方法可以使用 C/C++ 数据类型形式的参数调用函数,即 PyObject_CallFunction()。这个方便的函数不需要你执行 C 到 Python 的转换,而是接受一个格式字符串和可变数量的参数,很像 printf() 系列函数。

回顾我们的计算器程序,假设你想在一系列不连续的值上评估一个表达式。如果要评估的表达式是在加载的 Python 文件提供的函数中定义的,你可以像往常一样获取引用,然后迭代该范围。对于每个值,只需调用 PyObject_CallFunction(expression, "i", num)。“i”字符串告诉 Python 你将传递一个整数作为唯一的参数。如果你要调用的函数接受两个整数和一个字符串,你可以将函数调用写成 PyObject_CallFunction(expression, "iis", num1, num2, string)。如果该函数有返回值,它将作为 PyObject 指针在 PyObject_CallFunction() 的返回值中传递给你。

这是将参数传递给 Python 函数的最简单方法,但它实际上不是最灵活的。稍微思考一下。如果你动态选择要调用的函数会发生什么?你很可能希望能够灵活地调用各种接受不同数量和类型参数的函数。但是,使用 PyObject_CallFunction(),你必须在编译时选择参数的数量和类型,这与嵌入脚本语言固有的灵活性精神几乎不符。

解决方案是改用 PyObject_CallObject()。此函数允许你传递 Python 对象的一个元组,而不是可变长度的本机 C 数据项列表。这里的缺点是你需要首先将本机 C 值转换为 Python 对象,但你在执行速度方面损失的在灵活性方面得到了弥补。当然,在你将值作为 Python 元组传递给你的函数之前,你需要知道如何创建元组,这将我带到下一节。

在 Python 和 C 数据类型之间转换

Python 数据结构以 PyObject 的形式从 Python 解释器返回并传递给 Python 解释器。要获得特定类型,你需要将 PyObject 指针强制转换为正确的类型。例如,你可以通过强制转换 PyObject 指针来获得 PyIntObject 指针。但是,如果你不确定变量的类型,盲目地执行强制转换可能会导致灾难性的结果。在这种情况下,你可以调用许多 Check() 函数之一来查看对象是否确实是适当的类型,例如 PyFloat_Check() 函数,如果对象确实可以强制转换为浮点数,则该函数返回 true。换句话说,如果对象是浮点数或浮点数的子类型,则它返回 true。如果你想知道对象是否完全是浮点数,而不是子类,你可以使用 PyFloat_CheckExact()。

不透明的 PyObject 结构实际上对 C 程序没有用。为了在你的程序中访问 Python 数据,你需要使用各种转换函数,这些函数将返回本机 C 类型。例如,如果你想将 PyObject 转换为 long int,你可以运行 PyInt_AsLong()。PyInt_AsLong 是一个安全的函数,它会在提取 long int 值之前执行对 PyIntObject 的检查强制转换。如果你确定你要转换的值确实是 int,那么执行额外的检查可能会浪费资源,尤其是在它位于紧密的循环内部时。

通常,Python 函数要求或返回 Python 序列对象,例如元组或列表。这些对象在 C 中没有直接对应的类型,但 Python 提供了允许你从 C 数据类型构建它们的功能。作为一个例子,让我们看一下构建元组,因为你需要能够这样做才能使用 PyObject_CallObject() 调用函数。

创建新元组的第一步是使用 PyTuple_New() 构造一个空元组,它接受元组的长度并返回指向新元组的 PyObject 指针。然后,你可以使用 PyTuple_SetItem 设置元组项的值,并将每个值作为 PyObject 指针传递。

结论

现在你应该有足够的知识开始在你自己的应用程序中嵌入 Python 脚本了。有关更多信息,请查看 Python 文档。“扩展和嵌入 Python 解释器”更详细地介绍了反方向操作以及在 Python 中嵌入 C 函数。“Python/C API 参考手册”还包含有关所有可用于在程序中嵌入 Python 的函数的详细参考文档。《Linux Journal》档案还包含 Ivan Pulleyn 的一篇优秀文章,讨论了嵌入 Python 的多线程程序的问题。

本文资源: /article/8714.

William Nagel 是 Stage Logic, LLC 的首席软件工程师,这是一家小型软件开发公司,他在那里开发基于 Linux 的实时系统。他也是《Subversion 版本控制:在开发项目中使用 Subversion 版本控制系统》的作者。

加载 Disqus 评论