Python 調用 C 動態連接庫,包括結構體參數、回調函數等

項目中要對一個用 C 編寫的 .so 庫進行邏輯自測。這項工做,考慮到靈活性,我首先考慮用 Python 來完成。html

研究了一些資料,採用 python 的 ctypes 來完成這項工做。已經驗證經過,本文記錄一下適配流程。驗證採用 cpp 來設計,不過暫時尚未涉及類的內容。之後若是須要再補足。python

本文地址:http://www.javashuo.com/article/p-phcrhasr-mm.htmllinux

參考資料

ctypes

如下資料是關於 ctypes 的,也就是本文采用的資料:git

一些 Python 自己的資料

因爲研究 ctypes 時我用的是 Python 2.7,後來切換到 Python 3 的時候稍微遇到一點適配問題,所以也順便記錄一下我切換過程當中參考的一些資料:github

其餘 python 調用 C 的方法

Python 調用 C 還有其餘的幾個解決方案,好比 cythonSWIG 等等。可是查了很多資料沒能解決個人兩個關鍵訴求(結構體參數和回調函數):segmentfault

環境準備

ctypes 包準備

使用 ctypes,須要首先安裝 python-dev 包:centos

Ubuntu:
$ sudo apt-get install python-dev -y

CentOS:
$ sudo yum install python-devel -y

這裏主要包含了 ctypes 包。數組

.so 文件準備

將你的 C 代碼編譯成 .so 文件。這裏假設目標文件是 libtest.so,放在工做目錄下。多線程

基本參數函數調用

首先是最簡單的函數調用,而且函數參數爲基本數據類型。待調用的函數定義以下:函數

extern "C" int max(int a, int b)
{
    return (a > b) ? a : b;
}

這種狀況下,在 Python 中的調用就很簡單了。咱們須要使用 ctypes 包中的 cdll 模塊加載 .so 文件,而後就能夠調用庫中的函數了。

Python 代碼以下:

#!/usr/bin/python3
# -*- coding: UTF-8 -*-

from ctypes import *

so_file = cdll.LoadLibrary('./libtest.so')    # 若是前文使用的是 import ctypes,則這裏應該是 ctypes.cdll.LoadLobrary(...)
ret = so_file.max(22, 20)
print('so_file class:', type(so_file))
print('so_file.max =', ret)

輸出:

so_file class: <class 'ctypes.CDLL'>
so_file.max = 22

調用以結構體爲參數的函數

這就稍微複雜點了,由於 C 語言中的結構體在 Python 中並無直接一一對應。不過不用擔憂,簡單而言,解決方案就是:在 Python 代碼中調用 ctypes 的類進行 Python 化的封裝

網上的代碼進行了最簡化的演示,這裏我從這一小節開始,建議讀者把一個 .so 文件,封裝成 Python 模塊。這樣一來庫的包裝更加簡潔和清晰。


C 代碼

這裏是 C 代碼的部分,主要是結構體的聲明。用於示例的函數很簡單,只是一個 print 功能而已:

typedef struct _test_struct
{
    int     integer;
    char *  c_str;
    void *  ptr;
    int     array[8];
} TestStruct_st;


extern "C" const char *print_test_struct(TestStruct_st *pTestSt)
{
    if (NULL == pTestSt) {
        return "C -- parameter NULL";        # "C --" 打頭區分這是在 .so 裏面輸出的
    }

    printf("C -- {\n");
    printf("C -- \tinteger : %d\n", pTestSt->integer);
    printf("C -- \tcstr    : %s\n", pTestSt->c_str);
    printf("C -- \tptr     : %p\n", pTestSt->ptr);

    printf("C -- \tarray   : [");
    for (int tmp = 0; tmp < 7; tmp ++) {
        printf("%d, ", pTestSt->array[tmp]);
    }
    printf("%d]\n", pTestSt->array[7]);
    printf("C -- }\n");

    return "success";
}

Python 封裝

封裝結構體

首先,咱們要對結構體進行轉換:

from ctypes import *

INTARRAY8 = c_int * 8

class PyTestStruct(Structure):
    'TestStruct_st 的 Python 版本'
    _fields_ = [
        ("integer", c_int),
        ("c_str", c_char_p),
        ("ptr", c_void_p),
        ("array", INTARRAY8)
    ]

首先對結構體裏的 int 數組進行了重定義,也就是 INTARRAY8

接着,注意一下 _fields_ 的內容:這裏就是對 C 數據類型的轉換。左邊是 C 的結構成員名稱,右邊則是在 python 中聲明一下各個成員的類型。其餘的一些類型請參見官方文檔

此外還須要注意一下相似於 c_int, c_void_p 等等的定義是在 ctypes 中的,若是是用 impoer ctypes 的方式包含 ctypes 模塊,則應該寫成 ctypes.c_int, ctypes.c_void_p

第三個要注意的是:這個類必須定義爲 ctypes.Structure 的子類,不然在進行後續的函數傳遞時,ctypes 因爲不知道如何進行數據類型的對應,會拋出異常

封裝 .so 函數

class testdll:
    '用於 libtest.so 的加載,包含了 cdll 對象'

    def __init__(self):
        self.cdll = cdll.LoadLibrary('./libtest.so')    # 直接加載 .so 文件。感受更好的方式是寫成單例
        return

    def print_test_struct(self, test_struct):
        func = self.cdll.print_test_struct

        func.restype = c_char_p
        func.argtypes = [POINTER(PyTestStruct)]

        return func(byref(test_struct)).decode()

注意最後一句 func(byref(test_struct)) 中的 byref。這個函數能夠看成是 C 中的取地址符 & 的 Python 適配。由於函數參數是一個結構體指針(地址),所以咱們須要用上 byref 函數。

Python 調用

直接上 Python 代碼,很短的(import 語句就不用寫了吧,讀者自行發揮就好):

test_struct = PyTestStruct()
test_struct.integer = 1
test_struct.c_str = 'Hello, C'.encode()        # Python 2.x 則不須要寫 encode
test_struct.ptr = 0xFFFFFFFF
test_struct.array = INTARRAY8()

for i in range(0, len(test_struct.array)):
    j = i + 1
    test_struct.array[i] = j * 10 + j

so_file = testdll()
test_result = so_file.print_test_struct(test_struct)
print('test_result:', test_result)

執行結果:

C -- {
C --     integer : 1
C --     cstr    : Hello, C
C --     ptr     : 0xffffffff
C --     array   : [11, 22, 33, 44, 55, 66, 77, 88]
C -- }
test_result: success

這裏能夠看到,結構體參數的準備仍是很簡單的,就是將用 Python 適配過來以後的類中對應名字的成員進行賦值就行了。

注意一下在 Python 3.x 中,strbytes 類型是區分開的,而 char * 對應的是後者,所以須要進行 encode / decode 轉換。在 Python 2.x 則不須要。

調用以回調函數地址爲參數的函數

這個主題就稍微繞一些了,也就是說在 C 接口中,須要傳入回調函數做爲參數。這個問題在 Python 中也能夠解決,而且回調函數能夠用 Python 定義。

C 代碼

C 代碼很簡單:回調函數的傳入參數爲 int,返回參數也是 int。C 代碼獲取一個隨機數交給回調去處理。

extern "C" void print_given_num(int (*callback)(int))
{
    if (NULL == callback) {
        printf("C -- No number given\n");
    }

    static int s_isInit = 0;
    if (0 == s_isInit) {
        s_isInit = 1;
        srand(time(NULL));
    }
    int num = callback((int)rand());
    printf("C -- given num by callback: %d (0x%x)\n", num, num);

    return;
}

Python 封裝

這裏我仍是用前面的 testdll 類來封裝:

class testdll:
    '用於 libtest.so 的加載,包含了 cdll 對象'

    def __init__(self):
        self.cdll = cdll.LoadLibrary('./libtest.so')
        return

    def print_given_num(self, callback):
        self.cdll.print_given_num(callback)
        return

testCallbackType = CFUNCTYPE(None, c_int, c_int)

最後的 testCallbackType 經過 ctypes 定義了一個回調函數類型,這個在後面的調用中須要使用

CFUNCTYPE 後面的第一個參數爲 None,這表示回調函數的返回值類型爲 void

Python 調用

回調函數準備

回調函數用 Python 完成,注意接受的參數和返回數據類型都應該與 .so 中的定義一致。我這裏的回調函數中,將 .so 傳過來的參數取了一個最低字節返回:

def _callback(para):
    print('get callback req:', hex(para))
    print('return:', hex(para & 0xFF))
    return para & 0xFF

函數調用

so_file = testdll()
cb = testCallbackType(_callback)
so_file.print_given_num(cb)

執行結果:

get callback req: 0x4f770b3a
return: 0x3a
C -- given num by callback: 58 (0x3a)

怎麼樣,是否是以爲很簡單?

相關文章
相關標籤/搜索