Windows下Python与C++相互调用

Python调用DLL

Python下调用C/C++可以使用调用DLL的形式,Python可以借助ctypes包将数据组织成c语言的数据格式并作为DLL入口函数的参数。下面举一个简单的例子,我们通过Python向DLL中传递各种参数。首先使用C++编写一个简单的DLL程序。

extern "C" _declspec(dllexport) float testDLLEntence(int *Array, int sz_Array, int p1, double p2, float* outArray);
float testDLLEntence(int *Array, int sz_Array, int p1, double p2, float* outArray)
{
	for (int i = 0; i < sz_Array; i++)
	{
		outArray[i] = p2;
	}
	return p1;
}

下面是调用该DLL的Python程序:

from ctypes import *
import numpy as np

# 1 构建输入输出数组
size = 56 * 56 * 56
INPUT = c_int * size
input_Img = INPUT()
for i in range(size):
    input_Img[i] = 0
OUTPUT = c_float * size
output_Img = OUTPUT()
for i in range(size):
    output_Img[i] = 2
Arag2 = c_int(5)

# 2 调用DLL
dll = CDLL('DLLTest.dll')
Func = dll.testDLLEntence
Func.restype = c_float
res = Func(input_Img, c_int(size), Arag2, c_double(15.5), output_Img)
out_array = np.zeros(size)
for i in range(size):
    out_array[i] = output_Img[i]

在这个例子中我们从Python向DLL传递了两个数组,一个作为数据输入,另一个用来接收输出的数据。此外还传递了两个int型和一个double型参数,并设置了返回值类型为float。

C/C++中调用Python

Python为C/C++提供了API,在C/C++中可以直接运行Python解释器并执行.py文件。

环境配置

为了在C/C++中使用Python的API需要对C/C++的项目属性进行如下项目设置:

  • 在VC++目录→包含目录中添加“C:\ProgramData\Anaconda3\include”。
  • 在VC++目录→库目录中添加“C:\ProgramData\Anaconda3\libs”。
    (根据安装python的路径修改)

能够在C++工程中包含Python.h就说明成功了。如果使用CMake配置,在CMakeLists添加:

find_package(PythonLibs REQUIRED)

此外还需要注意的是:

  • 在调试过程中可能会遇到类似“error LNK2019: unresolved external symbol __imp__py_negativerefcount”的错误,这是由于我们没有Python37_d.lib造成的。解决方案是将“pyconfig.h”文件中的

    #ifdef _DEBUG
    # define Py_DEBUG
    #endif

    修改为:

    #ifdef _DEBUG
    //# define Py_DEBUG
    #endif

    参考:Lnk2019 error in pycaffe in debug mode for Caffe for Windows

  • 如果在工程中用到了QT,Python和QT都有slots关键字,会产生error。

    PyType_Slot *slots;
    object.h(445) : error C2059: 语法错误:“;”
    object .h(445) : error C2238: 意外的标记位于“;”之前

    解决办法是将报错的object.h中的“PyType_Slot *slots;”改为“PyType_Slot *_slots;”。
    参考:Windows下QT调用python脚本

  • 如果使用了多线程,“PyEval_InitThreads()”可能会报错:

    Fatal Python error: Py_Initialize: unable to load the file system codec

    在系统环境变量中添加一项,变量名为“PYTHONHOME”,变量值为“C:\ProgramData\Anaconda3”。根据Python路径自行修改。参考:Py_Initialize fails - unable to load the file system codec

如果需要将数组作为参数传递则还需要配置numpy。

  • 在VC++目录→包含目录中添加“C:\ProgramData\Anaconda3\pkgs\numpy-base-1.16.2-py37hc3f5095_0\Lib\site-packages\numpy\core\include”。
  • 在VC++目录→库目录中添加“C:\ProgramData\Anaconda3\pkgs\numpy-base-1.16.2-py37hc3f5095_0\Lib\site-packages\numpy\core\lib”。
    (根据安装python的路径修改)
    能够在C++工程中包含numpy/arrayobject.h就说明成功了。如果使用CMake配置,在CMakeLists添加:
# python
find_package(PythonLibs REQUIRED) # Python环境
if(NOT PYTHON_EXECUTABLE)
  if(NumPy_FIND_QUIETLY)
    find_package(PythonInterp QUIET)
  else()
    find_package(PythonInterp)
    set(__numpy_out 1)
  endif()
endif()

if (PYTHON_EXECUTABLE)
  # Find out the include path
  execute_process(
    COMMAND "${PYTHON_EXECUTABLE}" -c
            "from __future__ import print_function\ntry: import numpy; print(numpy.get_include(), end='')\nexcept:pass\n"
            OUTPUT_VARIABLE __numpy_path)
  # And the version
  execute_process(
    COMMAND "${PYTHON_EXECUTABLE}" -c
            "from __future__ import print_function\ntry: import numpy; print(numpy.__version__, end='')\nexcept:pass\n"
    OUTPUT_VARIABLE __numpy_version)
elseif(__numpy_out)
    message(STATUS "Python executable not found.")
endif(PYTHON_EXECUTABLE)

find_path(PYTHON_NUMPY_INCLUDE_DIR numpy/arrayobject.h
    HINTS "${__numpy_path}" "${PYTHON_INCLUDE_PATH}" NO_DEFAULT_PATH)

if(PYTHON_NUMPY_INCLUDE_DIR)
    set(PYTHON_NUMPY_FOUND 1 CACHE INTERNAL "Python numpy found")
endif(PYTHON_NUMPY_INCLUDE_DIR)

include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(NumPy REQUIRED_VARS PYTHON_NUMPY_INCLUDE_DIR
                                        VERSION_VAR __numpy_version)

一个简单的调用程序

编写add.py文件:

def add(a, b):
	return a + b

编写C/C++程序:

#include "stdafx.h"
#include <iostream>
#include <Python.h>

int main()
{
    // Python解释器初始化
    Py_Initialize();
    // 打开.py文件。注意参数是"Add"而不是"Add.py"
    PyObject* pModule = PyImport_ImportModule("Add");
    if (pModule)
    {
        // 读取文件中的函数,参数为函数名
        PyObject* pFunc = PyObject_GetAttrString(pModule, "add_func");
        if (pFunc)
        {
            // 构建参数,包含两个int
            PyObject* args = Py_BuildValue("(ii)", 1, 2);
            // 调用函数得到返回值
            PyObject* res = PyObject_CallObject(pFunc, args);
            if (!res)
            {
                // 打印错误信息
                PyErr_Print();
            }
            else
            {
                // 从PyObject中得到返回值
                int output = PyFloat_AsDouble(res);
                // 打印结果
                std::cout << "Result: " << output << std::endl;
            }
        }
    }
    // 关闭Python解释器
    Py_Finalize();

    system("pause");
    return 0;
}

在这个例子中我们调用了Python实现了最简单的加法功能,总共需要这几个步骤:

  • 初始化Python解释器,在一次程序运行中只可以初始化一次Python解释器。不可以初始化解释器,关闭解释器,第二次初始化解释器。所以如果需要多次调用Python程序时只需要第一次调用之前初始化解释器并在最后一次调用后关闭解释器。
  • 读取.py文件,参数为不带后缀的文件名,.py文件需要和exe程序位于相同路径。位于不同路径时需要将.py的路径添加到Python解释器中,可以使用下面的命令(在解释器初始化之后执行即可):
    PyRun_SimpleString("import sys");
    PyRun_SimpleString("sys.path.append('./Plugins')");
    
    其中“.”表示当前路径,此外“…”表示当前路径的上一级路径。也可以使用绝对路径。
  • 从.py文件中读取函数,如果.py中存在语法错误会返回nullptr。除了例子中的方法,还有很多别的方法。
  • 构建参数,多个参数时以元组的形式组织所有参数,在下一节数据交换中具体介绍。最终会得到一个Python元组作为输入的参数。
  • 调用函数,传递参数,得到返回值。如果有多个返回值时,则返回值是一个元组,否则只是一个普通的对象。
  • 如果返回值为空,可以调用“PyErr_Print()”打印错误信息。
  • 解析返回值,在下一节数据交换中具体介绍。

数据交换

在C/C++中,Python的各种类型(函数、元组、各种类型数据)均为PyObject*。在进行C/C++和Python的数据交换时就是在C的数据类型和PyObject*之间的转换,Python的API中提供了各种完成这种转换的函数。
在传入参数时,无论多么复杂的参数最终都需要组织成一个元组作为输入的参数。元组是Python的一种数据结构,相当于一个链表,每一个元素可以是不同的类型,甚至是一个子元组。“Py_BuildValue”可以将各种基本数据类型转换为PyObject*,下面举一个例子将不同数据类型封装成一个元组:

PyObject* arg0 = Py_BuildValue("i", 1); // int
PyObject* arg1 = Py_BuildValue("f", 2.); // float
PyObject* arg2 = Py_BuildValue("d", 3.); // double
PyObject* arg3 = Py_BuildValue("s", "hello"); // 字符串
// 这种方法也可以直接构建元组。如下面arg5则是一个元组,包含4个元素,int,int,float,字符串
PyObject* arg4 = Py_BuildValue("(i2fs)", 50, 100, 3.14, "hello");
//  还可以把不同的参数组成一个元组
PyObject* args = PyTuple_New(5);
PyTuple_SetItem(args, 0, arg0);
PyTuple_SetItem(args, 1, arg1);
PyTuple_SetItem(args, 2, arg2);
PyTuple_SetItem(args, 3, arg3);
PyTuple_SetItem(args, 4, arg4);
// 此时args是一个具有五个参数的元组,可以作为参数输入到Python的函数中。

如果需要将数组作为参数(如图像)则需要借助numpy包,首先是numpy的环境配置。下面的代码将一个数组转为PyObject*。

int dim[3] = {64, 64, 64};
npy_intp intpDim[3];
intpDim[0] = dim[0];
intpDim[1] = dim[1];
intpDim[2] = dim[2];
unsigned long int sz = dim[0] * dim[1] * dim[2];
float* pData = new float[sz];
import_array();
PyObject *PyArray = PyArray_SimpleNewFromData(3, intpDim, NPY_FLOAT, pData);

而调用Python的返回值同样是一个PyObject*,它有可能是一个简单的值,也有可能是一个list,也有可能是一个元组,我们来看下面的Python代码:

def func1():
    return 1 
    # 此时返回一个值

def func1():
    a = [1, 3, 5, 7, 8]
    return a
    # 此时返回一个list

def func3():
    return 1, 2.3, 'abc' 
    # 此时返回一个元组

如何解析返回值:

PyObject* res = PyObject_CallObject(pFunc, args); // res为函数返回值

// 如果res为一个值
int a = PyFloat_AsDouble(res);

// 如果res为一个list
int length = PyList_Size(res);
float* pArray = new float[length];
for (int i = 0; i < length; i++)
{
    pArray[i] = PyFloat_AsDouble(PyList_GetItem(res, i));
}

// 如果res为一个元组,则需要得到元组的元素
int length = PyTuple_Size(res);
PyObject* tu0 = PyTuple_GetItem(res, 0); // 得到第0个元素
// 得到的元素tu0仍然有可能是一个值,一个list,或一个元组。根据情况进行新一轮的操作即可。

下面我们来看一个完整的例子:
Seg_Threshold.py

# 这段程序实现了一幅图像的阈值分割,img为输入图像,dim为输入图像尺寸,
# Range_min和Range_max为两个阈值,返回值为一个元组,包括输出图像和其尺寸。
def inter_entrance(img, dim, Range_min, Range_max):
    import numpy as np
    array_img=np.array(img)
    array_result = np.zeros(dim, dtype = float, order = 'C')
    array_result[array_img>Range_min] = 2
    array_result[array_img>Range_max] = 0
    return (array_result.flatten().tolist(), dim)

main.cpp

#include "stdafx.h"
#include <iostream>
#include <Python.h>
#include <numpy/arrayobject.h>

int main()
{
    // 构建一个数组作为输入图像
    int Dim[3] = { 64, 64, 64 };
    unsigned long size = Dim[0] * Dim[1] * Dim[2];
    float* Img = new float[size];
    for (unsigned long i = 0; i < size; i++)
    {
        Img[i] = i;
    }
    int min = 100000;
    int max = 200000;
    // Python解释器初始化
    Py_Initialize();
    // 打开.py文件。注意参数是"Add"而不是"Add.py"
    PyObject* pModule = PyImport_ImportModule("Seg_Threshold");
    if (pModule)
    {
        // 读取文件中的函数,参数为函数名
        PyObject* pFunc = PyObject_GetAttrString(pModule, "inter_entrance");
        if (pFunc)
        {
            // 构建参数,包含两个int
            PyObject* args = PyTuple_New(4);
            npy_intp intpDim[3];
            intpDim[0] = Dim[0];
            intpDim[1] = Dim[1];
            intpDim[2] = Dim[2];
            import_array();
            PyObject* arg0 = PyArray_SimpleNewFromData(3, intpDim, NPY_FLOAT, Img);
            PyObject* arg1 = Py_BuildValue("iii", Dim[0], Dim[1], Dim[2]);
            PyObject* arg2 = Py_BuildValue("i", min);
            PyObject* arg3 = Py_BuildValue("i", max);
            PyTuple_SetItem(args, 0, arg0);
            PyTuple_SetItem(args, 1, arg1);
            PyTuple_SetItem(args, 2, arg2);
            PyTuple_SetItem(args, 3, arg3);
            // 调用函数得到返回值
            PyObject* res = PyObject_CallObject(pFunc, args);
            if (!res)
            {
                // 打印错误信息
                PyErr_Print();
            }
            else
            {
                // 从PyObject中得到返回值
                int size = PyTuple_Size(res);
                PyObject* tu0 = PyTuple_GetItem(res, 0); // 图像
                int length = PyList_Size(tu0);
                float* pImg = new float[length];
                for (int i = 0; i < length; i++)
                {
                    pImg[i] = PyFloat_AsDouble(PyList_GetItem(tu0, i));
                }
                PyObject* tu1 = PyTuple_GetItem(res, 1); // 图像尺寸
                Py_ssize_t resSize = PyTuple_Size(tu1);
                if (resSize != 3)
                {
                    return 0;
                }
                int resDim[3];
                resDim[0] = PyFloat_AsDouble(PyTuple_GetItem(tu1, 0));
                resDim[1] = PyFloat_AsDouble(PyTuple_GetItem(tu1, 1));
                resDim[2] = PyFloat_AsDouble(PyTuple_GetItem(tu1, 2));

                // 打印结果
                std::cout << "Result image dimension: " << resDim[0] << "  " <<
                    resDim[1] << "  " << resDim[2] << "  " << std::endl;

                Py_XDECREF(tu0); // 释放PyObject
                Py_XDECREF(tu1);
            }
            Py_XDECREF(args);
            Py_XDECREF(res);
        }
        Py_XDECREF(pFunc);
    }
    Py_XDECREF(pModule);
    // 关闭Python解释器
    Py_Finalize();

    system("pause");
    return 0;
}

多线程

Python并非是线程安全的,在多线程程序中调用Python需要使用API中提供的函数避免线程之间的冲突。对于多线程需要注意的有:

  • 初始化多线程:PyEval_InitThreads()
  • 线程锁的锁定与释放:PyEval_ReleaseLock() 和 PyEval_AcquireLock()
  • 线程状态的控制:PyThreadState_New()、PyThreadState_Swap()、PyThreadState_Clear()和PyThreadState_Delete()

下面是一个完整的多线程例子,例子中使用了Qt。在主线程中我们初始化了解释器并构建了参数,在另一个线程中调用Python函数进行计算并将运算结果又返回了主线程。

Seg_Threshold.py

def inter_entrance(img, dim, Range_min, Range_max):
    import numpy as np
    array_img=np.array(img)
    array_result = np.zeros(dim, dtype = float, order = 'C')
    array_result[array_img>Range_min] = 2
    array_result[array_img>Range_max] = 0
    return array_result.flatten().tolist(), dim

main.cpp

#include "QtGuiApplication1.h"
#include <QtWidgets/QApplication>

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    QtGuiApplication1 w;
    w.show();
    return a.exec();
}

QtGuiApplication1.h

#pragma once

#include <QtWidgets/QWidget>
#include <Python.h>
#include "ui_QtGuiApplication1.h"

class QtGuiApplication1 : public QWidget
{
    Q_OBJECT

public:
    QtGuiApplication1(QWidget *parent = Q_NULLPTR);

private:
    Ui::QtGuiApplication1Class ui;

    PyThreadState* m_mainThreadState = NULL;

private slots:

    void slotFinished(PyObject* res);
};

QtGuiApplication1.cpp

#include "QtGuiApplication1.h"
#include <iostream>
#include <Python.h>
#include <numpy/arrayobject.h>
#include "TestThread.h"

QtGuiApplication1::QtGuiApplication1(QWidget *parent)
	: QWidget(parent)
{
    ui.setupUi(this);

    // 构建一个数组作为输入图像
    int Dim[3] = { 64, 64, 64 };
    unsigned long size = Dim[0] * Dim[1] * Dim[2];
    float* Img = new float[size];
    for (unsigned long i = 0; i < size; i++)
    {
        Img[i] = i;
    }
    int min = 100000;
    int max = 200000;
    // Python解释器初始化
    Py_Initialize();
    PyEval_InitThreads();
    // 构建参数,包含两个int
    PyObject* args = PyTuple_New(4);
    npy_intp intpDim[3];
    intpDim[0] = Dim[0];
    intpDim[1] = Dim[1];
    intpDim[2] = Dim[2];
    import_array1(); // 无返回值
    PyObject* arg0 = PyArray_SimpleNewFromData(3, intpDim, NPY_FLOAT, Img);
    PyObject* arg1 = Py_BuildValue("iii", Dim[0], Dim[1], Dim[2]);
    PyObject* arg2 = Py_BuildValue("i", min);
    PyObject* arg3 = Py_BuildValue("i", max);
    PyTuple_SetItem(args, 0, arg0);
    PyTuple_SetItem(args, 1, arg1);
    PyTuple_SetItem(args, 2, arg2);
    PyTuple_SetItem(args, 3, arg3);
    // 新建线程
    TestThread* pTestThread = new TestThread(this);
    connect(pTestThread, SIGNAL(signalFinished(PyObject*)), this, SLOT(slotFinished(PyObject*)));
    pTestThread->SetPyArgs(args);
    // save a pointer to the main PyThreadState object
    m_mainThreadState = PyThreadState_Get();
    pTestThread->SetPyInterPreterState(m_mainThreadState->interp);
    // release the lock
    PyEval_ReleaseLock();
    pTestThread->start();
}

void QtGuiApplication1::slotFinished(PyObject* res)
{
    PyEval_RestoreThread(m_mainThreadState);
    if (res)
    {
        int size = PyTuple_Size(res);
        PyObject* tu0 = PyTuple_GetItem(res, 0); // 图像
        int length = PyList_Size(tu0);
        float* pImg = new float[length];
        for (int i = 0; i < length; i++)
        {
            pImg[i] = PyFloat_AsDouble(PyList_GetItem(tu0, i));
        }
        PyObject* tu1 = PyTuple_GetItem(res, 1); // 图像尺寸
        Py_ssize_t resSize = PyTuple_Size(tu1);
        if (resSize != 3)
        {
            return;
        }
        int resDim[3];
        resDim[0] = PyFloat_AsDouble(PyTuple_GetItem(tu1, 0));
        resDim[1] = PyFloat_AsDouble(PyTuple_GetItem(tu1, 1));
        resDim[2] = PyFloat_AsDouble(PyTuple_GetItem(tu1, 2));
        Py_XDECREF(tu0);
        Py_XDECREF(tu1);
    }
    Py_XDECREF(res);

    // 关闭Python解释器
    Py_Finalize();
}

TestThread.h

#pragma once

#include <QThread>
#include <Python.h>

class TestThread : public QThread
{
    Q_OBJECT

public:
    TestThread(QObject *parent);
    ~TestThread();

    void SetPyArgs(PyObject* args);
    void SetPyInterPreterState(PyInterpreterState* interp);

protected:
    void run();

private:
    PyObject* m_args;
    PyInterpreterState* m_interp;

signals:
    void signalFinished(PyObject* res);
};

TestThread.cpp

#include "TestThread.h"

TestThread::TestThread(QObject *parent)
    : QThread(parent)
{
}

TestThread::~TestThread()
{
}

void TestThread::SetPyArgs(PyObject* args)
{
    m_args = args;
}
void TestThread::SetPyInterPreterState(PyInterpreterState* interp)
{
    m_interp = interp;
}

void TestThread::run()
{
    // 该函数中的代码在第二个线程中运行。
    PyObject* res;
    // acquire the GIL
    PyEval_AcquireLock();
    // create a new thread state for the the sub interpreter interp
    PyThreadState* ts = PyThreadState_New(m_interp);
    // make ts the current thread state
    PyThreadState_Swap(ts);	
    // 打开.py文件。注意参数是"Add"而不是"Add.py"
    PyObject* pModule = PyImport_ImportModule("Seg_Threshold");
    if (pModule)
    {
        // 读取文件中的函数,参数为函数名
        PyObject* pFunc = PyObject_GetAttrString(pModule, "inter_entrance");
        if (pFunc)
        {

            // 调用函数得到返回值
            res = PyObject_CallObject(pFunc, m_args);
            if (!res)
            {
                // 打印错误信息
                PyErr_Print();
            }
            Py_XDECREF(m_args);
        }
        Py_XDECREF(pFunc);
    }
    Py_XDECREF(pModule);
    // clear out any cruft from thread state object
    PyThreadState_Clear(ts);
    // swap my thread state out of the interpreter
    PyThreadState_Swap(NULL);
    // delete my thread state object
    PyThreadState_Delete(ts);
    // release the lock
    PyEval_ReleaseLock();

    emit signalFinished(res);
}
  • 3
    点赞
  • 31
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值