65.9K
CodeProject 正在变化。 阅读更多。
Home

在 C/C++ 代码中嵌入 Python 程序

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.78/5 (40投票s)

2014 年 9 月 22 日

CPOL

12分钟阅读

viewsIcon

320963

downloadIcon

5747

如何将 Python 解释器嵌入到 C/C++ 代码中,并动态更改编译后的原生代码的代码路径。

引言

在本文中,我们将讨论以下主题

  • 获取用于工作的 Python C/C++ API。
  • 初始化和销毁 Python 环境。
  • 从 C/C++ 运行简单的内联 Python 代码。
  • 从 C/C++ 程序运行简单的 Python 文件。
  • 从 C/C++ 调用 Python 方法。
  • 从 Python 代码调用 C/C++ 函数。
  • 我们为什么要这样做???(要点)

 

背景

在我们继续之前,读者必须精通 C/C++ 编程。并具备 Python 的基础编程知识。

开始之前:我们需要什么??

 

  • 创建一个控制台应用程序。
  • 假设 Python 3 已完全安装在系统中,位置为“D:\Python34”。
  • 现在将“D:\Python34\include”的路径添加到 C/C++ 项目的包含路径中。
  • 将“D:\Python34\libs”的路径添加到项目的库路径中。
  • 尝试编译项目。如果构建成功,我们就准备好开始工作了!!!

 

初始化和清理 Python 环境

最简单的嵌入式程序是

#include <stdio.h>
#include <conio.h>
#include <Python.h>

int main()
{
	PyObject* pInt;

	Py_Initialize();

	PyRun_SimpleString("print('Hello World from Embedded Python!!!')");
	
	Py_Finalize();

	printf("\nPress any key to exit...\n");
	if(!_getch()) _getch();
	return 0;
}
  • Python 头文件是“Python.h”,它包含了我们所需的所有代码。链接器库文件是 Python34.lib(通常是 PythonXX.lib,XX=34)。
  • 通过调用 Py_Initialize() 初始化 Python 环境。
  • 通过调用 Py_Finalize() 销毁并清理环境。
  • 就是这样,在我们的 C 代码中启动并运行 Python 解释器。


正如您所见,我们的程序中嵌入了 Python 代码。这段代码是

print('Hello World from Embedded Python!!!')

这段 Python 代码将在控制台屏幕上打印“Hello World from Embedded Python!!!”这一行。我们的代码使用 PyRun_SimpleString(char* our_python_code_to_run) 来执行此程序。但调用者必须确保该函数在 Python 解释器初始化后、销毁前调用。
这样,我们就知道了如何在 C 中从字符串执行简单的 Python 代码。接下来,我们将研究如何从外部文本文件执行 Python 代码。

 

从文件执行 Python 程序

考虑要执行的简单 Python 程序,它存储在文件“pyemb7.py”中。

print('External Python program running...')
print('Hello World from Python program')

我们可以使用以下程序从 C/C++ 代码运行上述程序。

#include <stdio.h>
#include <conio.h>
#include <Python.h>

int main()
{
	char filename[] = "pyemb7.py";
	FILE* fp;

	Py_Initialize();

	fp = _Py_fopen(filename, "r");
	PyRun_SimpleFile(fp, filename);

	Py_Finalize();
	return 0;
}
  • 如前所述初始化 Python 环境。
  • 声明一个 FILE* 来存储我们的程序文件对象。
  • 现在使用 _Py_fopen(char* program_filename_with_py_extension, char* file_open_mode) 打开 Python 程序文件。此函数类似于标准 C/C++ 的 fopen 函数。此处我们将 pyemb7.py 文件以 r(读取)模式打开。
  • 检查返回的 FILE* 对象。如果为 NULL,则无法打开文件,因此无法继续。报告错误并中止。
  • 现在我们已经打开了文件。我们必须使用 PyRun_SimpleFile(opened_python_program_file_pointer, char* program_filename_which_becomes_argv_0) 来执行它。program_filename_which_becomes_argv_0 参数是作为 Python 程序传递给 argv[0] 的文件名。
  • 如前所示,销毁 Python 环境。


现在,我们已经完成了从原生代码内部从外部文件执行 Python 代码。接下来,我们将讨论一段从 Python 文件调用特定 Python 函数的代码。

 

Python 的 C++ 助手

C++ 头文件 pyhelper.hpp 的内容

#ifndef PYHELPER_HPP
#define PYHELPER_HPP
#pragma once

#include <Python.h>

class CPyInstance
{
public:
	CPyInstance()
	{
		Py_Initialize();
	}

	~CPyInstance()
	{
		Py_Finalize();
	}
};


class CPyObject
{
private:
	PyObject *p;
public:
	CPyObject() : p(NULL)
	{}

	CPyObject(PyObject* _p) : p(_p)
	{}

	
	~CPyObject()
	{
		Release();
	}

	PyObject* getObject()
	{
		return p;
	}

	PyObject* setObject(PyObject* _p)
	{
		return (p=_p);
	}

	PyObject* AddRef()
	{
		if(p)
		{
			Py_INCREF(p);
		}
		return p;
	}

	void Release()
	{
		if(p)
		{
			Py_DECREF(p);
		}

		p= NULL;
	}

	PyObject* operator ->()
	{
		return p;
	}

	bool is()
	{
		return p ? true : false;
	}

	operator PyObject*()
	{
		return p;
	}

	PyObject* operator = (PyObject* pp)
	{
		p = pp;
		return p;
	}

	operator bool()
	{
		return p ? true : false;
	}
};


#endif

从现在开始,我们将使用 C++ 项目和上述文件。
为了自动初始化 Python 实例,我们只需声明一个 CPyInstance 对象。当该对象超出范围时,Python 环境将被销毁。
现在我们可以这样写上面的第一个程序:

#include <stdio.h>
#include <conio.h>
#include <pyhelper.hpp>

int main()
{
    CPyInstance pyInstance;

	PyRun_SimpleString("print('Hello World from Embedded Python!!!')");
	

	printf("\nPress any key to exit...\n");
	if(!_getch()) _getch();
	return 0;
}

看,我们是如何移除这两个函数的:Py_Initialize()Py_Finalize()CPyInstance 类的优点是,Python 会自动反初始化,程序员可以专注于其余的代码。
在下一节中,我们将讨论 Python 对象 PyObject 的使用以及相应的助手类 CPyObject

 

PyObject 和 CPyObject 助手

PyObject 是一个 Python 对象,用于访问 Python 解释器使用或返回的 Python 对象。Python 使用引用来跟踪返回的对象。当对象不再需要时,必须使用 DECREFXDECREF 来解除引用。请看下面的代码示例:

    PyObject* pObj = ...;
    .... the pObj is used in this part of code ....

    DECREF(pObj);

在上面的代码中,假设 pObj 是由 Python 函数返回的,并在后续的代码路径中使用。一旦对象不再需要,我们就调用 DECREF 宏并将 pObj 指针传递给它,我们就完成了。在此之后,pObj 代码将变得无用,调用者不应再使用它。XDECREF 宏怎么样?调用 DECREF 要求传递的 PyObject 指针永远不能为 null。但 XDECREF 是安全的。XDECREF 会检查 NULL 指针,然后在内部调用 DECREF(如果传递的 PyObject 指针 **不** 为 NULL)。

如果我们想进一步重新引用 PyObject,或者将其传递给另一个函数,那么我们必须增加它的引用。因此,使用 INCREFXINCREF,并将 PyObject 指针传递给它。XINCREF 是安全的,您也可以传递 NULL 指针。但 INCREF 不能处理 NULL 指针。

为了安全起见,我们将使用 C++ 类 CPyObject。请看代码示例:

    CPyObject pObj = ...;
    ... use the pObj in further code ...
    
    ... forget about pObj it will be deferenced when pObj goes out of scope ...

因此,在上面的代码中,使用 CPyObject 代替 PyObject*。这个助手类将 PyObject* 封装在 CPyObject 类中。一旦类超出作用域,对象就会自动解除引用。此外,NULL 对象会被自动处理,并且可以自动类型转换为 PyObject*。但是,要增加引用指针,请调用 CPyObject 类的 AddReff() 方法。

#include <pyhelper.hpp>

int main()
{
	CPyInstance pInstance;

	CPyObject p;
	p = PyLong_FromLong(50);
	printf_s("Value =  %ld\n", PyLong_AsLong(p));

	return 0;
}
  • 通过声明 CPyInstance 对象来初始化 Python 环境。
  • 现在我们编写一个名为 pCPyObject 对象。
  • 现在我们通过调用 Python_FromLong(...) 来创建一个 Python 对象(来自 long 值)。此函数签名是 PyObject* PyLong_FromLong(long v)。该函数接收一个 long 值并返回一个 PyObject
  • 接下来,我们想将 PyObject* 转换为 long 值。我们知道 p 对象代表一个 long 值。所以我们调用 PyLong_AsLong(...) 来获取 long 值。函数签名是 long PyLong_AsLong(PyObject *pylong)。此函数接收一个 PyObject* 并返回一个 long 值。在上面的程序中,我们打印了这个 Python 对象的值。
  • 完成后,我们只需忘记该对象,它就会自动解除引用。

接下来,我们将从 C++ 调用 Python 函数。

 

从 C++ 调用 Python 函数(方法)

考虑以下 Python 程序,存储在 pyemb3.py 中:

def getInteger():
    print('Python function getInteger() called')
    c = 100*50/30
    return c

现在,我们想从以下 C++ 代码调用 getInteger() 函数并打印该函数返回的值。这是客户端 C++ 代码:

#include <stdio.h>
#include <Python.h>
#include <pyhelper.hpp>

int main()
{
	CPyInstance hInstance;

	CPyObject pName = PyUnicode_FromString("pyemb3");
	CPyObject pModule = PyImport_Import(pName);

	if(pModule)
	{
		CPyObject pFunc = PyObject_GetAttrString(pModule, "getInteger");
		if(pFunc && PyCallable_Check(pFunc))
		{
			CPyObject pValue = PyObject_CallObject(pFunc, NULL);

			printf_s("C: getInteger() = %ld\n", PyLong_AsLong(pValue));
		}
		else
		{
			printf("ERROR: function getInteger()\n");
		}

	}
	else
	{
		printf_s("ERROR: Module not imported\n");
	}

	return 0;
}

因此,我们将遵循以下规则来调用 Python 函数:

  • 初始化 Python 环境。
  • 导入 Python 模块。
  • 获取要调用的 Python 函数的引用。
  • 检查函数是否可以调用,并进行调用。
  • 然后处理函数执行后返回的 Python 对象。
  • 将返回的 Python 对象转换为 C++ 对象,并在屏幕上打印。

现在,我们来解释程序代码:

  • 首先,我们使用 CPyInstance hInstance 初始化 Python 环境。
  • 接下来,我们获取一个代表要调用的模块的 Python 对象。我们的模块名是 pyemb3.py,所以我们通过函数 PyObject *PyUnicode_FromString(const char *u) 来获取 Python 对象。该函数返回 CPyObject 中的 PyObject*,如 CPyObject pName = PyUnicode_FromString("pyemb3");
  • 接下来,使用 PyObject* PyImport_Import(PyObject *name),我们将导入模块并返回一个代表已导入模块的对象句柄。我们使用此函数作为 CPyObject pModule = PyImport_Import(pName);,如果模块已加载,则会返回一个有效的 Python 对象。此处未进行空值检查,程序员可以根据需要进行检查
  • 接下来,我们使用函数 PyObject* PyObject_GetAttrString(PyObject *o, const char *attr_name) 获取函数属性对象。因此,我们将其用作 CPyObject pFunc = PyObject_GetAttrString(pModule, "getInteger");。第一个参数是要使用的模块的句柄,第二个参数是要调用的函数的名称。
  • 接下来,我们检查返回的函数对象是否为 NULL。如果不为 NULL,则检查是否可以调用该函数。为此,我们使用 int PyCallable_Check(PyObject *o)。我们将函数对象指针传递给此函数。如果返回 1,则表示可以调用该函数。只有在可以调用函数时,我们才能继续。
  • 接下来,我们使用 PyObject* PyObject_CallObject(PyObject *callable_object, PyObject *args) 调用 Python 函数。这里的第一个参数是我们之前获得的函数对象。第二个参数是要传递给 Python 方法的参数。但我们不传递任何参数,所以传递 NULL。函数执行后,将返回 Python 方法返回的对象,即 Python 函数的结果。
  • 由于我们知道该函数返回一个 long 值,因此我们使用程序行 printf_s("C: getInteger() = %ld\n", PyLong_AsLong(pValue)); 来打印该值。

因此,我们看到上述程序的输出是:

Python function getInteger() called
C: getInteger() = 166

接下来,我们将讨论如何从嵌入的 Python 程序调用 C/C++ 函数。

 

从 Python 代码调用 C/C++ 函数

考虑以下 Python 代码 pyemb6.py

import arnav
print('in python: ')

val = arnav.foo()
print('in python:arnav.foo() returned ', val)

arnav.show(val*20+80)

现在,考虑下面的 C++ 程序,它调用上面的代码:

#include <stdio.h>
#include <Python.h>
#include <pyhelper.hpp>

static PyObject* arnav_foo(PyObject* self, PyObject* args)
{
	printf_s("... in C++...: foo() method\n");
	return PyLong_FromLong(51);
}

static PyObject* arnav_show(PyObject* self, PyObject* args)
{
	PyObject *a;
	if(PyArg_UnpackTuple(args, "", 1, 1, &a))
	{
		printf_s("C++: show(%ld)\n", PyLong_AsLong(a));
	}

	return PyLong_FromLong(0);
}

static struct PyMethodDef methods[] = {
	{ "foo", arnav_foo, METH_VARARGS, "Returns the number"},
	{ "show", arnav_show, METH_VARARGS, "Show a number" },
	{ NULL, NULL, 0, NULL }
};

static struct PyModuleDef modDef = {
	PyModuleDef_HEAD_INIT, "arnav", NULL, -1, methods, 
	NULL, NULL, NULL, NULL
};

static PyObject* PyInit_arnav(void)
{
	return PyModule_Create(&modDef);
}

int main()
{
	PyImport_AppendInittab("arnav", &PyInit_arnav);

	CPyInstance hInstance;

	const char pFile[] = "pyemb6.py";
	FILE* fp = _Py_fopen(pFile, "r");
	PyRun_AnyFile(fp, pFile);

	return 0;
}

首先,关注 main()。代码中熟悉的 bagian 是:初始化 Python 环境,然后加载 Python 程序并运行程序。但现在我们将重点介绍如何向 Python 环境添加新模块。

首先,必须在 Python 实例初始化之前添加导入的模块。现在我们如何做到这一点?

  • C 函数必须符合以下格式:
    • PyObject* self 作为第一个对象,代表函数本身。
    • PyObject* args 作为第二个对象,代表 Python 程序传递的参数。
    • 函数本身返回的 PyObject*。这代表要返回给调用此函数的 Python 程序的数据。
    • 函数名遵循 moduleNamespace_functionName 的格式。
    所以,我们在模块命名空间 arnav 中有一个函数 foo,它的 C 类型是 long arnav.foo(void),代码是:
    static PyObject* arnav_foo(PyObject* self, PyObject* args)
    {
    	printf_s("... in C++...: foo() method\n");
    	return PyLong_FromLong(51);
    }
    
    接下来,我们在 arnav 中声明另一个函数 show,它的类型是 long arnav.show(long value)
    static PyObject* arnav_show(PyObject* self, PyObject* args)
    {
    	PyObject *a;
    	if(PyArg_UnpackTuple(args, "", 1, 1, &a))
    	{
    		printf_s("C++: show(%ld)\n", PyLong_AsLong(a));
    	}
    
    	return PyLong_FromLong(0);
    }
    
    注意使用了 PyArg_UnpackTuple 函数,该函数用于解析参数。其签名是 int PyArg_UnpackTuple(PyObject *args, const char *name, Py_ssize_t min, Py_ssize_t max, ...)。第一个参数是 args 指针的引用。参数 name 未使用,可能指访问模式。min 表示 Python 代码传递的参数(元组)的最小数量,max 表示可以传递的参数的最大数量。我们的函数只需要 1 个参数,所以我们将 min = max = 1
  • 接下来,我们声明一个方法表:
    static struct PyMethodDef methods[] = {
    	{ "foo", arnav_foo, METH_VARARGS, "Returns the number"},
    	{ "show", arnav_show, METH_VARARGS, "Show a number" },
    	{ NULL, NULL, 0, NULL }
    };
    
    这是使用 PyMethodDef 结构体数组完成的。它的字段是:
    • 第一个字段表示 Python 代码用来调用我们原生代码的方法名。
    • 当 Python 调用时将被调用的 C/C++ 函数指针。
    • 方法可以传递可变数量的参数。这是必须的。
    • 最后一个字段表示帮助字符串。指定我们的函数做什么。可以看作是我们函数在 Python 环境中的帮助。
  • 在 **Python 3** 中开发导入模块的一个新方法是声明 PyModuleDef
        static struct PyModuleDef modDef = {
    	PyModuleDef_HEAD_INIT, "arnav", NULL, -1, methods, 
    	NULL, NULL, NULL, NULL
    };
    
    最简单的情况下,我们将只使用 PyModuleDef 结构的少数几个字段。
    • 第一个参数 - 指定必须指定 PyModuleDef_HEAD_INIT
    • 第二个参数 - Python 代码中要使用的模块/命名空间的名称。在我们的例子中,它是 arnav
    • 第三个参数 - 我们不使用它。指定 NULL
    • 第四个参数 - 指定 -1
    • 第五个参数 - 我们传递指向我们方法声明结构 PyMethodDef 的指针。在我们的例子中,我们传递我们的静态变量 method
    • 最后 4 个参数我们不使用。因此,我们为这些参数指定 NULL
    注意:我们使用的是最小声明,这足以解决我们的大部分问题。因此,在未来的文章中,我们将使用此结构的更多字段,并详细介绍每个字段及其影响。
  • 接下来,我们声明以下代码:
    static PyObject* PyInit_arnav(void)
    {
    	return PyModule_Create(&modDef);
    }
    
    每当我们需要加载我们的模块时,都会调用此函数。这里我们声明一个格式为 PyInit_moduleName(void) 的函数。在我们的例子中,对于命名空间 arnav,我们使用函数名 PyInit_arnav(void)。此函数不接受任何参数,但返回一个 PyObject*。注意使用 PyObject* PyModule_Create(PyModuleDef *module)。在这里,我们传递我们的 PyModuleDef 结构。此函数将创建一个新模块。在我们的例子中,我们传递我们 modDef 变量的地址。当模块加载时,该函数返回一个 PyObject*,函数 PyInit_arnav(void) 返回此创建的对象。
  • 现在,在 main() 函数中,在 Python 环境初始化之前。这是通过使用函数 PyImport_AppendInittab 完成的。该函数签名是 int PyImport_AppendInittab(const char *name, PyObject* (*initfunc)(void))。我们将我们的初始化函数 PyInit_arnav 指针传递给此函数。如果成功(函数不会返回 -1),新模块将被添加到 Python 环境中。
  • 最后,模块将可以从 Python 模块中调用。

现在我们将研究 Python 源如何利用这个新的命名空间 arnav

  • 首先,导入新的命名空间。
        import arnav
    
  • 现在我们可以使用 moduleName.functionName(...) 的格式调用 arnav 模块中的方法:
    val = arnav.foo()
    print('in python:arnav.foo() returned ', val)
    
    arnav.show(val*20+80)
    

程序输出是:

in python:
... in C++...: foo() method
in python:arnav.foo() returned  51
C++: show(1100)

 

我们为什么要这样做?(需要记住的点)

Python 是一种非常流行的语言,拥有庞大的代码库。此外,Python API 非常详尽且易于使用。此外,将解释器嵌入到原生语言中将使我们能够在程序设计完成后更改其进程,从而实现动态编程方法。
未来,我们将更深入地研究嵌入式 Python,并详细检查上述结构。
 

  • 下载附带的 ZIP 文件。它捆绑了 Python3 SDK 和本文讨论的一些演示应用程序。您可以直接使用它。
  • 如果您安装 Python3,请使用 x86 版本来构建基于 x86 的应用程序。
  • Boost 和其他 API 替代方案可用于执行相同的工作,但由于我从未尝试过,所以我坚持使用 Python 官方 SDK。
  • 始终将 Python34.dll 与您的 C/C++ 应用程序一起分发。您的原生应用程序依赖于它。否则,客户端系统必须正确安装 Python3。

 

© . All rights reserved.