將 DLL C 包裝到 Cython 到 Python

這演示了使用 Cython 包裝 C++ dll 的一個非平凡的例子。它將涵蓋以下主要步驟:

  • 使用 Visual Studio 使用 C++建立示例 DLL。
  • 用 Cython 包裝 DLL,以便可以在 Python 中呼叫它。

假設你已安裝 Cython 並可以在 Python 中成功匯入它

對於 DLL 步驟,還假設你熟悉在 Visual Studio 中建立 DLL。

完整示例包括建立以下檔案:

  1. complexFunLib.h:C++ DLL 原始碼的標頭檔案
  2. complexFunLib.cpp:C++ DLL 源的 CPP 檔案
  3. ccomplexFunLib.pxd:Cythonheader 檔案
  4. complexFunLib.pyx:Cython包裝檔案
  5. setup.py:使用 Cython 建立 complexFunLib.pyd 的 Python 設定檔案
  6. run.py:匯入已編譯的 Cython 包裝 DLL 的示例 Python 檔案

C++ DLL 來源:complexFunLib.hcomplexFunLib.cpp

**如果你已有 DLL 和標頭原始檔,請跳過此步驟。**首先,我們建立 C++原始碼,使用 Visual Studio 從中編譯 DLL。在這種情況下,我們希望使用復指數函式進行快速陣列計算。以下兩個函式在陣列 kee 上執行計算 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.pxdcomplexFunLib.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.pyrun.py

setup.py

該檔案是一個執行 Cython 編譯的 Python 檔案。其目的是生成已編譯的 .pyd 檔案,然後由 Python 模組匯入。在這個例子中,我們將所有必需的檔案(即 complexFunLib.hcomplexFunLib.dllccomplexFunLib.pxdcomplexFunLib.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)