CFFI - ABI模式與API模式

CFFI能夠在四種模式中使用:「ABI」和「API」級別,每種模式都有 in-line 或 out- line 準備(或編譯)python

ABI模式從二進制級別訪問庫,而更快的API模式經過C編譯器訪問庫windows

在 in-line 模式中,每次導入Python代碼時都會設置全部內容數組

在 out- line 模式中,有一個單獨的準備步驟(可能還有C編譯),它生成一個模塊,主程序能夠導入該模塊bash

| 版權聲明:itisyang,未經博主容許不得轉載。app

簡單例子(ABI, in-line)

>>> from cffi import FFI
>>> ffi = FFI()
>>> ffi.cdef(""" ... int printf(const char *format, ...); // copy-pasted from the man page ... """)
>>> C = ffi.dlopen(None)                     # loads the entire C namespace
>>> arg = ffi.new("char[]", "world")         # equivalent to C code: char arg[] = "world";
>>> C.printf("hi there, %s.\n", arg)         # call printf
hi there, world.
17                                           # this is the return value
>>>
複製代碼

注意,在Python 3中,須要將byte strings傳遞給char *參數,在上面的示例中,它將是b「world」和b「hi there, %s!\n」,一般也能夠這樣 somestring.encode(myencoding)函數

Windows上的Python 3: ffi.dlopen(None)沒法工做。這個問題很混亂,並且沒法真正解決。若是嘗試從系統上存在的特定DLL調用函數,則不會出現問題: 使用ffi.dlopen(「path.dll」)。佈局

這個例子不調用任何C編譯器。它在所謂的ABI模式下工做,這意味着若是您調用某個函數或訪問某個在cdef()中稍有錯誤聲明的結構的某些字段,它就會崩潰。性能

若是使用C編譯器安裝模塊是一個選項,那麼強烈建議使用API模式。(它也更快。)測試

結構/數組的例子(in-line)

from cffi import FFI
ffi = FFI()
ffi.cdef(""" typedef struct { unsigned char r, g, b; } pixel_t; """)
image = ffi.new("pixel_t[]", 800*600)

f = open('data', 'rb')     # binary mode -- important
f.readinto(ffi.buffer(image))
f.close()

image[100].r = 255
image[100].g = 192
image[100].b = 128

f = open('data', 'wb')
f.write(ffi.buffer(image))
f.close()
複製代碼

您能夠調用ffi.new(「pixel_t[600][800]」)得到一個二維數組。ui

API模式,調用C標準庫

# file "example_build.py"

# Note: we instantiate the same 'cffi.FFI' class as in the previous
# example, but call the result 'ffibuilder' now instead of 'ffi';
# this is to avoid confusion with the other 'ffi' object you get below

from cffi import FFI
ffibuilder = FFI()

ffibuilder.set_source("_example",
   r""" // passed to the real C compiler, // contains implementation of things declared in cdef() #include <sys/types.h> #include <pwd.h> // We can also define custom wrappers or other functions // here (this is an example only): static struct passwd *get_pw_for_root(void) { return getpwuid(0); } """,
    libraries=[])   # or a list of libraries to link with
    # (more arguments like setup.py's Extension class:
    # include_dirs=[..], extra_objects=[..], and so on)

ffibuilder.cdef(""" // declarations that are shared between Python and C struct passwd { char *pw_name; ...; // literally dot-dot-dot }; struct passwd *getpwuid(int uid); // defined in <pwd.h> struct passwd *get_pw_for_root(void); // defined in set_source() """)

if __name__ == "__main__":
    ffibuilder.compile(verbose=True)
複製代碼

您須要運行example_build.py腳本一次生成「source code」到文件_example.c中。並將其編譯爲一個常規的c擴展模塊。(CFFI根據set_source()的第二個參數是否爲None,選擇生成Python或C模塊。)

這一步須要一個C編譯器。它生成一個名爲 _example.so 或 _example.pyd 的文件。若是須要,它能夠像其餘擴展模塊同樣以預編譯的形式發佈。

在主程序中使用:

from _example import ffi, lib

p = lib.getpwuid(0)
assert ffi.string(p.pw_name) == b'root'
p = lib.get_pw_for_root()
assert ffi.string(p.pw_name) == b'root'
複製代碼

注意,passwd 結構體是獨立精確的C佈局結構(它是「API級別」,而不是「ABI級別」),須要一個C編譯器才能運行 example_build.py 。

還要注意,在運行時,API模式比ABI模式快。

把這個模塊使用Setuptools集成到setup.py中:

from setuptools import setup

setup(
    ...
    setup_requires=["cffi>=1.0.0"],
    cffi_modules=["example_build.py:ffibuilder"],
    install_requires=["cffi>=1.0.0"],
)
複製代碼

API模式,調用C源代碼而不是編譯庫

若是您想調用一些沒有預編譯的庫,可是有C源代碼的庫,那麼最簡單的解決方案是建立一個單獨的擴展模塊,該模塊由這個庫的C源代碼和額外的CFFI包裝器編譯而成。例如,假設您從文件 pi.c 和 pi.h 開始:

/* filename: pi.c*/
# include <stdlib.h>
# include <math.h>

/* Returns a very crude approximation of Pi
   given a int: a number of iteration */
float pi_approx(int n){

  double i,x,y,sum=0;

  for(i=0;i<n;i++){

    x=rand();
    y=rand();

    if (sqrt(x*x+y*y) < sqrt((double)RAND_MAX*RAND_MAX))
      sum++; }

  return 4*(float)sum/(float)n; }
複製代碼
/* filename: pi.h*/
float pi_approx(int n);
複製代碼

建立一個名爲 pi_extension_build.py 的腳本,構建C擴展:

from cffi import FFI
ffibuilder = FFI()

ffibuilder.cdef("float pi_approx(int n);")

ffibuilder.set_source("_pi",  # name of the output C extension
""" #include "pi.h"', """,
    sources=['pi.c'],   # includes pi.c as additional sources
    libraries=['m'])    # on Unix, link with the math library

if __name__ == "__main__":
    ffibuilder.compile(verbose=True)
複製代碼

構建擴展:

python pi_extension_build.py
複製代碼

能夠發現,在工做目錄中,生成的輸出文件: _pi.c, _pi.o 和編譯後的C擴展(例如Linux上生成 _pi.so )。它能夠從Python調用:

from _pi.lib import pi_approx

approx = pi_approx(10)
assert str(pi_approximation).startswith("3.")

approx = pi_approx(10000)
assert str(approx).startswith("3.1")
複製代碼

Out-of-line, ABI level

out-of-line ABI 模式是 常規(API)out-of-line模式和in-line ABI 的混合,優勢是不須要C編譯器),缺點是更容易崩潰。

這種混合模式能夠大大減小導入時間,由於解析大型C頭文件很慢。它還容許您在構建時進行更詳細的檢查,而沒必要擔憂性能。

# file "simple_example_build.py"

from cffi import FFI

ffibuilder = FFI()
ffibuilder.set_source("_simple_example", None)
ffibuilder.cdef(""" int printf(const char *format, ...); """)

if __name__ == "__main__":
    ffibuilder.compile(verbose=True)
複製代碼

運行一次會產生_simple_example.py,你的主程序只導入生成的模塊,而再也不須要 simple_example_build.py

from _simple_example import ffi

lib = ffi.dlopen(None)      # Unix: open the standard C library
#import ctypes.util # or, try this on Windows:
#lib = ffi.dlopen(ctypes.util.find_library("c"))

lib.printf(b"hi there, number %d\n", ffi.cast("int", 2))
複製代碼

注意,ffi.dlopen()不會調用任何額外的方法來定位庫,這意味着 ffi.dlopen(「libfoo.so」)是能夠的,可是ffi.dlopen(「foo」)不行。 在後一種狀況下,您能夠將其替換爲ffi.dlopen(ctypes.util.find_library(「foo」))。而且,None 只能在Unix打開C標準庫。

爲了便於分發,你能夠把它靜態地包含在你的項目的源文件中,使用Setuptools在setup.py這樣編寫:

from setuptools import setup

setup(
    ...
    setup_requires=["cffi>=1.0.0"],
    cffi_modules=["simple_example_build.py:ffibuilder"],
    install_requires=["cffi>=1.0.0"],
)
複製代碼

ABI 與 API

在二進制級別 (「ABI」) 訪問C庫充滿了問題,尤爲是在非 windows 平臺上。

ABI 級別最直接的缺點是,調用函數須要通過很是通用的 libffi 庫,它很慢(並且老是不能在非標準平臺上完美經過測試)。API模式編譯一個直接調用目標函數的 CPython C 包裝器。相對而言,它的速度要快得多(並且運行得比 libffi 更好)。

選擇API模式的更基本緣由是,C庫一般與C編譯器一塊兒使用。你不該該作諸如猜想結構中字段的位置之類的事情。上面的「真實示例」展現了CFFI如何在底層使用C編譯器:示例使用 set_source(..., "C source...") 而不是 dlopen()。當使用這種方法時,咱們有一個優點,咱們能夠在 cdef() 的不一樣地方使用字面上的 「……」 ,缺失的信息將在C編譯器的幫助下完成。CFFI 將把它轉換爲單個C源文件,其中包含未修改的 「C source」 部分,後面跟隨着一些特殊C代碼和 cdef() 派生的聲明。當編譯這個C文件時,生成的C擴展模塊將包含咱們須要的全部信息。就像往常同樣,若是咱們錯誤地聲明瞭某個函數的簽名,C編譯器將給出警告或錯誤。

注意,set_source()中的 「C source」 部分能夠包含任意的C代碼。您可使用它來聲明一些用c語言編寫的輔助函數。(你能夠在「C source」部分,使用static C關鍵字)

例如,這能夠用來將宏包裝成更標準的C函數。額外的C層在其餘方面也頗有用,好比調用函數,這些函數須要一些複雜的參數結構,您更喜歡在C中構建而不是在Python中。(另外一方面,若是您須要調用 「function-like」 宏,那麼您能夠直接在cdef()中聲明它們,就好像它們是函數同樣。)

相關文章
相關標籤/搜索