嵌入 PyPy(已弃用)

PyPy 具有非常简陋且奇怪的嵌入接口,基于使用 cffi 以及 Python 比 C 更好的语言理念。它是在与 uwsgi 项目的 Roberto De Ioris 合作开发的。 PyPy uwsgi 插件 是使用嵌入 API 的一个很好的例子。

注意:您需要使用 --shared 选项编译的 PyPy,即具有 libpypy*-c.sopypy*-c.dll 文件。这是默认设置。

注意

此页面中描述的接口保留用于向后兼容性。从 PyPy 4.1 开始,建议改为使用 CFFI 的 原生嵌入支持,它提供了一种更简单的方法,适用于 CPython 和 PyPy。

生成的共享库导出很少的函数。这些函数在 PyPy.h 中定义,该文件在 v7.3.8 中已删除,但 仍然可用。只要遵循一些原则,它们就足以完成您需要的一切。API 是

void rpython_startup_code(void);

这是一个您必须在调用任何其他内容之前(一次)调用的函数。它初始化 RPython/PyPy GC 并执行大量必要的启动代码。此函数不会失败。

int pypy_setup_home(char* home, int verbose);

此函数从给定的“PyPy 主目录”开始搜索 PyPy 标准库。参数是

  • home:pypy 目录中可执行文件的路径(可以是 .so 名称,可以是虚构的)。用于查找标准库,并也设置为 sys.executable。从 PyPy 5.5 开始,只要 libpypy-c.so/dylib/dll 本身位于此目录中,您就可以在这里直接说 NULL。
  • verbose:如果非零,它将错误消息打印到 stderr

函数在成功时返回 0,在失败时返回 -1,可以多次调用,直到找到库。

void pypy_init_threads(void);

初始化线程。仅当涉及任何线程时才需要调用。必须在 pypy_setup_home() 之后调用

int pypy_execute_source(char* source);

执行 source 参数中给出的 Python 源代码。如果出现异常,它将 Python 追溯信息打印到 stderr 并返回 1,否则返回 0。您应该在源代码中真正进行自己的错误处理。它将获取 GIL。

注意:这仅用于一次或最多几次调用。请参见下面的 更完整的示例。在 PyPy <= 2.6.0 中,全局字典在多次调用中重复使用,这会导致潜在的奇怪结果(例如,对象过早死亡)。在 PyPy >= 2.6.1 中,每次调用都会获得一个新的全局字典(但随后,所有全局字典都将永远保存在 sys._pypy_execute_source 中)。

int pypy_execute_source_ptr(char* source, void* ptr);

注意

于 2014 年 6 月的 PyPy 2.3.1 版本中添加

与上面类似,但它在源代码作用域中注册了一个魔法参数,名为 c_argument,其中 void* 被编码为 Python 整数。

void pypy_thread_attach(void);

如果您的应用程序使用在 PyPy 之外初始化的线程,则需要调用此函数以告知 PyPy GC 跟踪此线程。请注意,此函数本身不是线程安全的,因此您需要用互斥锁保护它。

最小示例

请注意,此 API 比 CPython C API 简洁得多,因此一开始您可能会认为无法做太多事情。但是,诀窍是在 Python 中完成所有逻辑,并通过 cffi 回调公开它。我们编写了一个简单的 C 程序

#include "PyPy.h"
#include <stdio.h>
#include <stdlib.h>

static char source[] = "print 'hello from pypy'";

int main(void)
{
    int res;

    rpython_startup_code();
    /* Before PyPy 5.5, you may need to say e.g. "/opt/pypy/bin" instead
     * of NULL. */
    res = pypy_setup_home(NULL, 1);
    if (res) {
        printf("Error setting pypy home!\n");
        return 1;
    }

    res = pypy_execute_source((char*)source);
    if (res) {
        printf("Error calling pypy_execute_source!\n");
    }
    return res;
}

如果我们将其保存为 x.c,现在编译并运行它(在 Linux 上)使用

$ gcc -g -o x x.c -lpypy-c -L/opt/pypy/bin -I/opt/pypy/include
$ LD_LIBRARY_PATH=/opt/pypy/bin ./x
hello from pypy

在 OSX 上,如果要链接到它,则需要设置二进制文件的 rpath,使用类似以下的命令

gcc -o x x.c -lpypy-c -L. -Wl,-rpath -Wl,@executable_path
./x
hello from pypy

更完整的示例

注意

请注意,我们没有使用 extern "Python",这是 CFFI 1.4 中进行回调的新方法:这是因为这些示例使用 ABI 模式,而不是 API 模式,在 ABI 模式下,您仍然需要使用 ffi.callback()。将 extern "Python" 与嵌入的想法集成正在进行中(预计最终将导致比这里描述的更好的嵌入方式,并且在 CPython 和 PyPy 上都能很好地工作)。

通常,我们需要做的事情不仅仅是执行源代码。以下是一个完整的示例,有关详细信息,请参阅 cffi 文档。它有点长,但它概括了使用 PyPy 嵌入接口可以做些什么

# file "interface.py"

import cffi

ffi = cffi.FFI()
ffi.cdef('''
struct API {
    double (*add_numbers)(double x, double y);
};
''')

# Better define callbacks at module scope, it's important to
# keep this object alive.
@ffi.callback("double (double, double)")
def add_numbers(x, y):
    return x + y

def fill_api(ptr):
    global api
    api = ffi.cast("struct API*", ptr)
    api.add_numbers = add_numbers
/* C example */
#include "PyPy.h"
#include <stdio.h>
#include <stdlib.h>

struct API {
    double (*add_numbers)(double x, double y);
};

struct API api;   /* global var */

int initialize_api(void)
{
    static char source[] =
        "import sys; sys.path.insert(0, '.'); "
        "import interface; interface.fill_api(c_argument)";
    int res;

    rpython_startup_code();
    res = pypy_setup_home(NULL, 1);
    if (res) {
        fprintf(stderr, "Error setting pypy home!\n");
        return -1;
    }
    res = pypy_execute_source_ptr(source, &api);
    if (res) {
        fprintf(stderr, "Error calling pypy_execute_source_ptr!\n");
        return -1;
    }
    return 0;
}

int main(void)
{
    if (initialize_api() < 0)
        return 1;

    printf("sum: %f\n", api.add_numbers(12.3, 45.6));

    return 0;
}

您可以使用以下命令编译并运行它

$ gcc -g -o x x.c -lpypy-c -L/opt/pypy/bin -I/opt/pypy/include
$ LD_LIBRARY_PATH=/opt/pypy/bin ./x
sum: 57.900000

如您所见,我们所做的是创建一个 struct API,其中包含我们在此特定情况下所需的自定义 API。此结构由 Python 填充,以包含一个函数指针,然后从 C 端调用该函数指针。也可以使用其他函数指针,这些指针由 C 端填充并由 Python 端调用,甚至是非函数指针字段:基本上,两端通过定义 API 的单个 C 结构进行通信。

查找 pypy_home

如果您运行的是 PyPy >= 5.5(2016 年 10 月发布),通常可以跳过此部分

函数 pypy_setup_home() 以文件路径作为第一个参数,它可以从该路径推断出标准库的位置。更准确地说,它会尝试删除最终组件,直到找到 lib-pythonlib_pypy。目前没有“干净”的方法(pkg-config 想到)来查找此路径。您可以尝试以下(GNU 特定)技巧(不要忘记链接到 *dl*),它假设 libpypy-c.so 位于标准库目录中。(无论如何,这或多或少必须是这种情况,否则 pypy 程序本身将无法运行。)

#if !(_GNU_SOURCE)
#define _GNU_SOURCE
#endif

#include <dlfcn.h>
#include <limits.h>
#include <stdlib.h>

// caller should free returned pointer to avoid memleaks
// returns NULL on error
char* guess_pypyhome(void) {
    // glibc-only (dladdr is why we #define _GNU_SOURCE)
    Dl_info info;
    void *_rpython_startup_code = dlsym(0,"rpython_startup_code");
    if (_rpython_startup_code == 0) {
        return 0;
    }
    if (dladdr(_rpython_startup_code, &info) != 0) {
        const char* lib_path = info.dli_fname;
        char* lib_realpath = realpath(lib_path, 0);
        return lib_realpath;
    }
    return 0;
}

线程

如果您想使用 pthreads,您需要做的是从您创建的每个线程(但不是从主线程)调用 pypy_thread_attach,并从主线程调用 pypy_init_threads