將 DLL C 包裝到 Cython 到 Python
這演示了使用 Cython 包裝 C++ dll 的一個非平凡的例子。它將涵蓋以下主要步驟:
- 使用 Visual Studio 使用 C++建立示例 DLL。
- 用 Cython 包裝 DLL,以便可以在 Python 中呼叫它。
假設你已安裝 Cython 並可以在 Python 中成功匯入它
對於 DLL 步驟,還假設你熟悉在 Visual Studio 中建立 DLL。
完整示例包括建立以下檔案:
- complexFunLib.h:C++ DLL 原始碼的標頭檔案
- complexFunLib.cpp:C++ DLL 源的 CPP 檔案
- ccomplexFunLib.pxd:Cython- header檔案
- complexFunLib.pyx:Cython包裝檔案
- setup.py:使用 Cython 建立- complexFunLib.pyd的 Python 設定檔案
- run.py:匯入已編譯的 Cython 包裝 DLL 的示例 Python 檔案
C++ DLL 來源:complexFunLib.h 和 complexFunLib.cpp
**如果你已有 DLL 和標頭原始檔,請跳過此步驟。**首先,我們建立 C++原始碼,使用 Visual Studio 從中編譯 DLL。在這種情況下,我們希望使用復指數函式進行快速陣列計算。以下兩個函式在陣列 k 和 ee 上執行計算 k*exp(ee),其中結果儲存在 res 中。有兩個功能可以相容單精度和雙精度。請注意,這些示例函式使用 OpenMP,因此請確保在專案的 Visual Studio 選項中啟用了 OpenMP。
H 檔案
// Avoids C++ name mangling with extern "C"
#define EXTERN_DLL_EXPORT extern "C" __declspec(dllexport)  
#include <complex>
#include <stdlib.h>
// Handles 64 bit complex numbers, i.e. two 32 bit (4 byte) floating point numbers
EXTERN_DLL_EXPORT void mp_mlt_exp_c4(std::complex<float>* k, 
                                     std::complex<float>* ee,
                                     int sz, 
                                     std::complex<float>* res, 
                                     int threads);
// Handles 128 bit complex numbers, i.e. two 64 bit (8 byte) floating point numbers
EXTERN_DLL_EXPORT void mp_mlt_exp_c8(std::complex<double>* k,                                       std::complex<double>* ee,
                                     int sz, 
                                     std::complex<double>* res, 
                                     int threads);
CPP 檔案
#include "stdafx.h"
#include <stdio.h>
#include <omp.h>
#include "complexFunLib.h"
void mp_mlt_exp_c4(std::complex<float>* k,
                   std::complex<float>* ee,
                   int sz,
                   std::complex<float>* res,
                   int threads)
{
    // Use Open MP parallel directive for multiprocessing
    #pragma omp parallel num_threads(threads)
    {
        #pragma omp for
        for (int i = 0; i < sz; i++) res[i] = k[i] * exp(ee[i]);
    }
}
void mp_mlt_exp_c8(std::complex<double>* k,
                   std::complex<double>* ee,
                   int sz, std::complex<double>* res,
                   int threads)
{
    // Use Open MP parallel directive for multiprocessing
    #pragma omp parallel num_threads(threads)
    {
        #pragma omp for
        for (int i = 0; i < sz; i++) res[i] = k[i] * exp(ee[i]);
    }
}
Cython 來源:ccomplexFunLib.pxd 和 complexFunLib.pyx
接下來,我們建立包裝 C++ DLL 所必需的 Cython 原始檔。在這一步中,我們做出以下假設:
- 你已經安裝了 Cython
- 你擁有一個可用的 DLL,例如上面描述的 DLL
最終目標是建立將這些 Cython 原始檔與原始 DLL 結合使用來編譯 .pyd 檔案,該檔案可以作為 Python 模組匯入並公開用 C++編寫的函式。
PXD 檔案
該檔案對應 C++標頭檔案。在大多數情況下,你可以使用較小的 Cython 特定更改將標頭複製貼上到此檔案。在這種情況下,使用特定的 Cython 複雜型別。注意在 ccomplexFunLib.pxd 開頭新增 c。這不是必需的,但我們發現這樣的命名約定有助於維護組織。
cdef extern from "complexFunLib.h":
    void mp_mlt_exp_c4(float complex* k, float complex* ee, int sz,
                       float complex* res, int threads);
    void mp_mlt_exp_c8(double complex* k, double complex* ee, int sz,
                       double complex* res, int threads);
PYX 檔案
該檔案對應於 C++ cpp 原始檔。在這個例子中,我們將指向 Numpy ndarray 物件的指標傳遞給匯入 DLL 函式。也可以將內建的 Cython memoryview 物件用於陣列,但其效能可能不如 ndarray 物件(但語法非常清晰)。
cimport ccomplexFunLib  # Import the pxd "header"
# Note for Numpy imports, the C import most come AFTER the Python import
import numpy as np  # Import the Python Numpy
cimport numpy as np  # Import the C Numpy
# Import some functionality from Python and the C stdlib
from cpython.pycapsule cimport *
# Python wrapper functions.
# Note that types can be delcared in the signature
def mp_exp_c4(np.ndarray[np.complex64_t, ndim=1] k,
              np.ndarray[np.complex64_t, ndim=1] ee,
              int sz,
              np.ndarray[np.complex64_t, ndim=1] res,
              int threads):
    '''
    TODO: Python docstring
    '''
    # Call the imported DLL functions on the parameters.
    # Notice that we are passing a pointer to the first element in each array
    ccomplexFunLib.mp_mlt_exp_c4(&k[0], &ee[0], sz, &res[0], threads)
    
def mp_exp_c8(np.ndarray[np.complex128_t, ndim=1] k,
              np.ndarray[np.complex128_t, ndim=1] ee,
              int sz,
              np.ndarray[np.complex128_t, ndim=1] res,
              int threads):
    '''
    TODO: Python docstring
    '''
    ccomplexFunLib.mp_mlt_exp_c8(&k[0], &ee[0], sz, &res[0], threads)
Python 來源:setup.py 和 run.py
setup.py
該檔案是一個執行 Cython 編譯的 Python 檔案。其目的是生成已編譯的 .pyd 檔案,然後由 Python 模組匯入。在這個例子中,我們將所有必需的檔案(即 complexFunLib.h,complexFunLib.dll,ccomplexFunLib.pxd 和 complexFunLib.pyx)儲存在與 setup.py 相同的目錄中。
建立此檔案後,應從命令列執行引數:build_ext --inplace
執行此檔案後,它應生成 .pyd 檔案而不會引發任何錯誤。請注意,在某些情況下,如果出現錯誤,可能會建立 .pyd 但無效。在使用生成的 .pyd 之前,確保在執行 setup.py 時沒有丟擲任何錯誤。
from distutils.core import setup
from distutils.extension import Extension
from Cython.Distutils import build_ext
import numpy as np
ext_modules = [
    Extension('complexFunLib',
              ['complexFunLib.pyx'],
              # Note here that the C++ language was specified
              # The default language is C
              language="c++",  
              libraries=['complexFunLib'],
              library_dirs=['.'])
    ]
setup(
    name = 'complexFunLib',
    cmdclass = {'build_ext': build_ext},
    ext_modules = ext_modules,
    include_dirs=[np.get_include()]  # This gets all the required Numpy core files
)
run.py
現在 complexFunLib 可以直接匯入 Python 模組並呼叫包裝的 DLL 函式。
import complexFunLib
import numpy as np
# Create arrays of non-trivial complex numbers to be exponentiated,
# i.e. res = k*exp(ee)
k = np.ones(int(2.5e5), dtype='complex64')*1.1234 + np.complex64(1.1234j)
ee = np.ones(int(2.5e5), dtype='complex64')*1.1234 + np.complex64(1.1234j) 
sz = k.size  # Get size integer
res = np.zeros(int(2.5e5), dtype='complex64')  # Create array for results
# Call function
complexFunLib.mp_exp_c4(k, ee, sz, res, 8)  
# Print results
print(res)