如何使用CFFI调用已有的C函数并给出源代码?

12
我有一个C源文件/头文件,它们是一个更大项目的一部分。我想将其作为一个单元进行测试,独立于实际项目。虽然可以通过创建具有不同main()的新项目在C中实现此目的,但我希望看看是否可以使用Python(3)及其框架(例如nose)加速测试构建,使用现有的报告框架等。
我认为可以使用CFFI来实现这一点。这里是一个示例C文件:
// magic.c
// Implementation of magic.
int add(int a, int b)
{
    return a;
}

标题:

// magic.h
// Add two numbers (where a + b is not greater than INT_MAX).
int add(int a, int b);

这里有一个脚本,它只是尝试编译它,以便我可以调用一些函数。
# cffi_test.py
import cffi

INCLUDE_DIRS = ('.',)

SOURCES = ('magic.c',)

ffi = cffi.FFI()

ffi.set_source(
    '_magic_tests',
    '#include "magic.h"',
    include_dirs = INCLUDE_DIRS,
    sources = SOURCES,
    libraries = [],
    )

ffi.compile()

最终,我计划将此作为一组单元测试设置的一部分。例如,一个纯Python函数test_add()将通过构建在测试设置中的ffi对象调用并检查C函数add()的结果。
上述脚本似乎有效;它可以正常运行,创建了一个_magic_tests.c文件,一个_magic_tests.cp35-win32.pyd文件和一个Release目录。我也可以无误地import _magic_tests
但我无法弄清如何通过CFFI实际调用C函数。我找不到set_source()函数的任何文档,而它似乎对整个过程非常重要。概述经常提到它,但参考中没有任何出现次数。文档中确实有一个关于调用函数的部分,但它引用了一些未显示如何创建的lib对象。如果我查看之前的示例,将从ffi.dlopen()创建一个lib对象,但我不知道如何将其应用于CFFI本身正在生成的内容。
我的主要问题(即我的X问题)是:
  • CFFI是一种在跨平台(Windows 7-10,Linux,OS X)中调用和测试C函数的合理工具吗?如果是,如何使用?

从我的当前方法(即我的Y问题)中产生的问题是:

  • set_source()的文档在哪里?我怎样才能找到它所需的参数?
  • 如何生成包含我想要调用的函数的lib对象?
  • 这是使用CFFI调用C函数的最简单方法吗?我不特别需要或想要生成共享库或可再分配软件包;如果必须发生,那很好,但这并非必要。我还可以尝试哪些其他方法?

我的当前设置是:

  • 操作系统:Windows 10
  • Python版本:CPython 3.5.1 32位
  • Pip版本:8.1.2
  • CFFI版本:1.6.0
  • C编译器:使用Visual C++ Build Tools 2015自带的编译器,详见此MSDN帖子

我正在使用来自Christoph Gohlke的仓库的CFFI和pycparser。

1个回答

14

我在我的一个项目中使用 cffi 来测试我的C代码。我认为 cffi 是一个很棒的工具,可以生成用于从python调用和测试C函数的绑定,并因此认为它是一个合理的工具。但是,由于您必须为每个平台编译绑定,因此您的代码将只跨越C代码所支持的平台。

下面是一些文档参考,可以回答您的问题。另外,我编写了一些示例代码来说明如何使用 cffi 。有一个更大的示例可以在我的项目中找到,https://github.com/ntruessel/qcgc/tree/master/test

对于您的示例,build_magic_tests.py 代码大致如下:

from cffi import FFI

ffibuilder = FFI()

# For every function that you want to have a python binding,
# specify its declaration here
ffibuilder.cdef("""
    int add(int a, int b);
                """)

# Here go the sources, most likely only includes and additional functions if necessary
ffibuilder.set_source("magic_tests",
    """
    #include "magic.h"
    """, sources=["magic.c"])

if __name__ == "__main__":
    ffibuilder.compile()

要生成magic_tests模块,您需要运行python build_magic_tests.py。生成的模块可以像这样导入和使用:

from magic_tests import ffi, lib

def run_add():
    assert 4 == lib.add(4, 5)

2
很棒的答案!我唯一的进一步问题是:似乎在头文件和ffibuilder.cdef()调用中存在一些重复;也就是说,我将声明该函数两次,并且有风险使其不同步或引入错误。您认为有减少重复的方法吗? - detly
2
不幸的是,我认为目前这是不可能的,至少我没有找到方法来做到这一点。在我看来,这是“cffi”的主要缺点之一。可以尝试使用另一个脚本自动生成整个“build_magic_tests.py”。但是,该脚本必须解决“ffibuilder.cdef()”方法的限制问题。 - Nicolas
那很烦人,但并不是致命问题。拥有访问Python的单元测试生态系统仍然是值得的。(而且它仍然比任何C单元测试框架都要少些样板代码。) - detly
我想到了另外一个问题:你如何进行清理呢?目前,你在编译后导入模块,在每个测试之前如果你想确保一个干净的状态,就必须取消导入。ffi.compile()返回.pyd文件的名称,这可以被删除,但是它生成了一堆其他编译相关的东西,这取决于编译器。你知道是否有办法通过CFFI调用直接获取到magic_tests.lib对象吗? - detly
啊,看起来我在谈论CFFI所称的“API, in-line”,即verify()函数。它已经被弃用了,所以我只能绕过它进行编程。 - detly
我觉得这个答案的质量值得更多的声望,而你在这样一个小众问题上得到的声望实在太少了。它真的帮了我很多,所以我加了一个赏金。 - detly

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接