将 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)