Python 调用 C++ DLL

January 28th, 2018 by JasonLe's Tech Leave a reply »

update 2018-2-4

早些时候用Pyqt写了一个app,最近需要加入一个License的认证模块,然而这个认证模块是拿C++写的,并只给出了dll、lib和header。。。网上查找了一些资料,一种方法是采用ctypes 去加载dll;另外一种就是编写一个PyObject 然后编译成pyd模块供python调用。

这里我采用第一种方式,他们提供给我的模块是_stdll的方式,因为然后我又把这个dll包了一层,以__cdecl方式导出。

#ifdef LICENSEDLL_EXPORTS
#define LICENSEDLL_API extern "C" __declspec(dllexport)
#else
#define LICENSEDLL_API __declspec(dllimport)
#endif

LICENSEDLL_API bool  verify(char *module, char *errorMsg);
.....

然后我将这个工程编译生成LicenseDLL.dll。下面就是python方面的任务了,我引入ctypes,然后利用CDLL加载LicenseDLL.dll,利用dll.verify.argtypes,我声明verify函数具有两个参数,其中pinfo为100个字节的数组,并将其传入verify函数。该函数返回后,写入ErrorInfo,使用QString(str(ErrorInfo.raw))将其转换成QString对象,完成对于dll的调用。

def CheckLicenseInfo():
    from ctypes import *
    dll = CDLL(app.g_pwd + os.sep + 'LicenseDLL.dll')
    dll.verify.argtypes = [c_char_p,c_char_p]
    pinfo = c_char * 100
    ErrorInfo = pinfo()

    if not dll.verify('DESIGNAPP', ErrorInfo):
        QMessageBox.critical(None, 'Error', QString(str(ErrorInfo.raw)))
        exit()

但是这种方式有个最大的问题:由于直接读dll,可能会存在dll依赖链的问题,比如我操作的时候就遇到Windows Error 126和127的问题,126应该就是dll依赖缺失,需要使用depandency的问题,127有可能是dll本身的问题,没有把里面的函数符号导出。127比较复杂,有时候遇到挺懵逼的….. 

编写python扩展也是一种可行的方式,可以编译成pyd的形式,在python下直接import。因为python要调用C++的模块,那么必然涉及到python对象向C++传值和传地址的问题,也涉及到C++返回值向python传值的问题,直接上源码来一步一步说明吧。

编写代码首先要引入Python.h Python.lib 默认用安装包安装的是没有python_d.lib的,当然也要引入需要wapper的头文件,我根据官方提供的spamError例子代码修改为我的代码,总的框架代码有PyMODINIT_FUNC initXXXXX(void),static PyMethodDef LicenseMethods[]={…},还有常规的warpper代码static PyObject *
xxxxxxxxxx(PyObject *self, PyObject *args)。PyMODINIT_FUNC initXXXXX(void)为固定格式,把XXXXX换成我们编写的模块即可。

#include "Python.h"
#include "LicenseProve.h"

static PyObject *LicenseError;

static PyObject *isHavingLicense(PyObject *self, PyObject *args)
{
    const char *name;
	char *err;
    if (!PyArg_ParseTuple(args, "s|s", &name,&err))
        return NULL;

    std::string modelName(name);
    std::string errorMsg(err);

    bool ret = Allone::License::CLicenseProve::getInstance()->isHavingLicense(modelName,errorMsg);

	//if(ret == false)
	//	return Py_False;
    //return Py_True;
    return Py_BuildValue("(iss)",(int)ret,modelName.c_str(),errorMsg.c_str());
}

static PyObject *getLicenseType(PyObject *self, PyObject *args)
{
    size_t ret = Allone::License::CLicenseProve::getInstance()->getLicenseType();
    return PyInt_FromSize_t(ret);
}

static PyObject *getLicenseInfo(PyObject *self, PyObject *args){...}
static PyObject *getRegisterInfo(PyObject *self, PyObject *args){...}
static PyObject *getAppendInfo(PyObject *self, PyObject *args){...}

static PyMethodDef LicenseMethods[] = {
    {"isHavingLicense",  isHavingLicense, METH_VARARGS,"Having License in this Computer?"},
	{"getLicenseType", getLicenseType, METH_NOARGS,"Get License type"},
	{"getLicenseInfo", getLicenseInfo, METH_NOARGS,"Get License Info"},
	{"getRegisterInfo", getRegisterInfo, METH_NOARGS,"Get License Register Info"},
	{"getAppendInfo", getAppendInfo, METH_NOARGS,"Get License Append Info"},
    {NULL, NULL, 0, NULL}        /* Sentinel */
};

PyMODINIT_FUNC initLicense(void)
{
    PyObject *m;

    m = Py_InitModule("License", LicenseMethods);
    if (m == NULL)
        return;

    LicenseError = PyErr_NewException("License.error", NULL, NULL);
    Py_INCREF(LicenseError);
    PyModule_AddObject(m, "error", LicenseError);
}

在LicenseMethods需要加入我们编写的函数,双引号里面的是python调用的函数名,第二个参数是c++函数里面的函数名,第三个是声明传入参数类型,METH_VARARGS为可变参数,METH_NOARGS是没有参数。这个实质上就是一个参数表,连接python和c++端的实现。c++端的实现都写成static PyObject *
xxxxxxxxxx(PyObject *self, PyObject *args),我们需要的参数都是通过args以Tuple传入,调用PyArg_ParseTuple(args, “s|s”, &name,&err),可以拿到传入的值,”s|s”表示元祖里每个参数的类型,常用类型可以在官方wiki中找到,s就是代表string,i就是代表integer等等。后面就是根据指定的类型,传入到name和error中,C++中可以使用参数传值,但是python的参数传入的都是值,因此我将修改的的值都放置到一个tuple中返回给python,也就是使用Py_BuildValue返回,参数和PyArg_ParseTuple类似,第一个是每个参数的类型,后面是参数值。如果返回值简单,我们也可以直接返回Py_False等。

在PyMODINIT_FUNC initLicense(void)中,大致也分为Py_InitModule初始化,PyErr_NewException注册,在异常处理这一块,我没有细看,但是这个应该是处理各种C++发生的异常,由于python不用管理内存,但是在C++中我们就必须处理,因此Py_INCREF用来管理引用计数,最后还需要将error和注册的python模块连接起来。

在python中,只需要使用import License,直接引入即可,看下面代码:

def CheckLicenseInfo2():
    error = ""
    # print(License.isHavingLicense('DESIGNAPP',error))
    ret = License.isHavingLicense('DESIGNAPP',error)
    err = ret[2]
    if not ret[0]:
        QMessageBox.critical(None, 'Error', QString(str(err)))
        exit()

    print(License.getLicenseType())
    print(License.getLicenseInfo())
    print(License.getRegisterInfo())
    print(License.getAppendInfo())

由于isHavingLicense返回的是tuple,因此我们只要指明索引,就可以找到需要的内容。更多的参数类型可以查看C:\Python27\include,下面都是各种的传入传出函数和宏,满足我们的需要。

这种pyd的方式实质上也是dll,但是比直接调用ctype读取dll有更好的兼容性,在实际生产中推荐使用pyd调用C++函数函数。

 

参考:

http://www.cnblogs.com/night-ride-depart/p/4907613.html

https://docs.python.org/2/library/ctypes.html

http://wolfprojects.altervista.org/dllforpyinc.php

http://icejoywoo.github.io/2015/10/30/intro-of-extending-cpython.html

https://docs.python.org/2/c-api/arg.html

https://docs.python.org/2/extending/extending.html?highlight=meth_varargs

http://blog.csdn.net/mkc1989/article/details/38943927