ctypes與numpy.ctypeslib的使用

巴蜀秀才發表於2021-12-11

numpy ctypeslib 與 ctypes介面使用說明


作者:elfin  


使用numpy.ctypelib或者ctypes庫可以實現python直接待用C++。numpy.ctypeslib後端是基於ctypes實現的!所以介面是類似的。

如果只想看如何呼叫外部介面可以直接檢視 1.4.3.1 動態連結庫使用成功案例 ,其他部分有很多試錯的過程!

Top --- Bottom


一、numpy.ctypeslib使用說明

numpy是python做計算非常基礎的一個庫,安裝就直接使用pip install numpy即可。

匯入python包import numpy.ctypeslib as ctl

1.1 準備好一個C++計算檔案

#include <iostream>
using namespace std;

int Paritition1(int A[], int low, int high) {
	int pivot = A[low];
    while (low < high) {
		while (low < high && A[high] >= pivot) {
			--high;
		}
		A[low] = A[high];
		while (low < high && A[low] <= pivot) {
			++low;
		}
		A[high] = A[low];
	}
	A[low] = pivot;
	return low;
}

void QuickSort(int A[], int low, int high) //快排母函式
{
	if (low < high) {
    int pivot = Paritition1(A, low, high);
    QuickSort(A, low, pivot - 1);
    QuickSort(A, pivot + 1, high);
    }
}

int main()
{
    int arr[5] = { 9, 2, 8, -6, 5 };
    QuickSort(arr, 0, 4);
    for (int i = 0; i < 4; i++)
	{
		cout << arr[i] << " ";
	}
	cout << arr[4] << endl;
}

上面是一段C++版的快排程式碼。假設我們已經有這個cpp檔案,那麼如何在python中使用?

需要獨立編譯時,Windows系統可以在標頭檔案中使用下面的語句

// __declspec(dllexport)  匯出到庫中(win系統環境下.dll檔案連結巨集)
extern "C" __declspec(dllexport) void QuickSort(int A[], int low, int high);

在Linux中需要使用extern進行宣告

1.2 ctypeslib主要的五個介面

ctypeslib給我們提供了五個介面使用:

  • load_library載入c++檔案;
  • ndpointer
  • c_intp
  • as_ctypes numpy資料型別轉為ctypes型別
  • as_arrayc++資料轉為numpy資料型別

Top --- Bottom

1.3 載入編譯後的檔案

我這裡是在Windows上面測試,生成exe檔案後可以直接讀入。

c_quick = ctl.load_library(
    libname="quick.exe",
    loader_path=r"H:\quick\x64\Debug"
)

檢視c_quick型別可以發現它是一個<class 'ctypes.CDLL'>型別。型別是沒錯的,但是_FuncPtr函式接收到QuickSort字串時,卻顯示找不到這個函式;如果我們直接使用cpp,或者dll等檔案,有顯示: 不是有效的win32應用程式。

按照1.4.3案例的寫法,可以實現exe軟體的載入使用,但是宣告部分要新增 __declspec(dllexport)

因為我們的執行環境是64位的,而visual studio是基於32位的,理論上有兩種辦法解決:

  • 將python程式改成32位版本,當然這種方法對我們來說一般難以接受;
  • 將外部連結編譯為64位的,我在studio裡面設定了x64,結果設定了一個寂寞。

為了不浪費太多時間,我將這部分搬到Ubuntu系統上進行測試。

1.4 Linux系統下載入編譯後的檔案

這裡我們記錄從檔案生成到最終呼叫的全過程。

1.4.1 書寫文件

$ sudo vim quik.c
# 將1.1的C++檔案內容書寫進文件並保持

1.4.2 編譯、打包原始檔

編譯連結為可執行檔案

$ sudo g++ -Wall quik.c -o quick
$ ./quick
-6 2 5 8 9

這裡對main函式的預設陣列進行了排序!

編譯為目標檔案

$ sudo g++ -Wall -c quik.c -o quick.o
$ ls
quick  quick.o  quik.c

生成靜態連結庫

$ sudo ar cr libquick.o quick.o
$ ls
libquick.o  quick  quick.o  quik.c

生成動態連結庫

$ sudo g++ -Wall -shared -fPIC quik.c -o libquick.so
$ ls
libquick.o  libquick.so  quick  quick.o  quik.c

Top --- Bottom

1.4.3 python載入外部連結庫

我們嘗試從不同的檔案進行載入,分別是c++原始檔、可執行檔案、.o目標檔案、外部靜態連結庫、外部動態連結庫。

首先說明結果:c++原始檔、可執行檔案都不能被載入。

載入c++原始檔報錯:OSError: quik.c: invalid ELF header

載入可執行檔案quick報錯:OSError: no file with expected extension

載入目標檔案報錯:OSError: quick.o: only ET_DYN and ET_EXEC can be loaded

載入外部靜態連結庫報錯:OSError: libquick.o: invalid ELF header

載入外部動態連結庫報錯:undefined symbol:“QuickSort”

現在我們只能使用main函式,其他函式呼叫會報未定義符號的錯誤,下面我們先基於so動態連結庫走通這個流程。

1.4.3.1 動態連結庫使用成功案例

首先我們模擬正常的專案,進行獨立編譯。我們將quik檔案拆分為quik.cmain.c

問題的解決方案可以參考linux動態庫so呼叫外部so,執行時出現undefined symbol

我們生成如下的quik.cmain.c文件:

#include <iostream>
using namespace std;

//extern "C" C++中編譯c格式的函式,如果利用c語言編譯不需要
extern "C" void QuickSort(int A[], int low, int high);

int Paritition1(int A[], int low, int high) {
    int pivot = A[low];
    while (low < high) {
        while (low < high && A[high] >= pivot) {
            --high;
        }
        A[low] = A[high];
        while (low < high && A[low] <= pivot) {
            ++low;
        }
        A[high] = A[low];
    }
    A[low] = pivot;
    return low;
}


void QuickSort(int A[], int low, int high) //快排母函式
{
    if (low < high) {
    int pivot = Paritition1(A, low, high);
    QuickSort(A, low, pivot - 1);
    QuickSort(A, pivot + 1, high);
    }
}
#include <iostream>
using namespace std;

// 宣告可以寫 在 標頭檔案中
extern "C" void QuickSort(int A[], int low, int high);

int main(){
    int arr[5] = { 9, 2, 8, -6, 5 };
    QuickSort(arr, 0, 4);
    for (int i = 0; i < 4; i++){
        cout << arr[i] << " ";
    }
    cout << arr[4] << endl;
}

上面我們使用extern進行了QuickSort函式的宣告,這樣就可以不使用quik.h標頭檔案,要注意的是宣告中使用的是“C”進行編譯防止函式名被優化!基於此項改動就可以得到如下效果

python呼叫so動態連結庫

$ sudo g++ -Wall -c main.c
$ sudo g++ -Wall -c quik.c -o quick.o
$ sudo g++ -Wall -shared -fPIC main.c quik.c  -o libquick.so
$ python
>>> import numpy.ctypeslib as ctl
>>> ctl_lib = ctl.load_library("libquick.so", "./")
>>> import numpy as np
>>> arr_1d = ctl.ndpointer(
...     dtype=np.int32,
...     ndim=1,
...     flags="CONTIGUOUS"
... )
>>> ctl_lib.QuickSort.restypes = None
>>> from ctypes import c_int, c_float
>>> ctl_lib.QuickSort.argtypes = [arr_1d, c_int, c_int]
>>> arr1 = np.array([5, -6, 8, 4, 7, 6, 3], dtype=np.int32)
>>> ctl_lib.QuickSort(arr1, 0, len(arr1)-1)
>>> print(arr1)
[-6  3  4  5  6  7  8]
>>> type(arr1)
<class 'numpy.ndarray'>

注: 關於最好還是寫在宣告標頭檔案中,main.c檔案引入標頭檔案;但是函式定義檔案中的宣告是一定要有的!

動態庫可以,靜態庫卻不可以?

直接載入quik.c原始檔生成的靜態連結庫確實是不行!即使我們已經宣告還是會有invalid ELF header錯誤,也不難理解,宣告是解決不了這個問題的。鑑於我們常使用動態連結庫,靜態庫該如何呼叫留作懸念吧。不過根據這個報錯,很可能我們呼叫不起來。

1.4.3.2 python呼叫外部連結庫總結

  • 呼叫外部C/C++介面,我們最好使用動態連結庫;

  • 動態連結庫生成的時候一定要注意進行宣告extern "C" void QuickSort(int A[], int low, int high);

    不然載入後會找不到函式名,這個名字會被優化成其他

  • 更多細節可以參考官方文件https://scipy-cookbook.readthedocs.io/items/Ctypes.html


Top --- Bottom

完!

相關文章