本文提供了一个使用python的C语言API调用python脚本的示例,以及分享了此过程中可能遇到的一个问题及其解决方案。
首先编写C语言源文件与python语言源文件,我们将要编译此C语言源码,生成的可执行文件会去调用我们编写的python脚本,两份源文件如下:
C语言源文件run.c:
#include "include/Python.h"
int main()
{
Py_Initialize(); //初始化
if (!Py_IsInitialized())
{
return -1; // init python failed
}
//相当于在python中的import sys语句。这个函数是宏,相当于直接运行python语句
PyRun_SimpleString("import sys");
PyRun_SimpleString("sys.path.append('./')"); //是将搜索路径设置为当前目录。
PyObject *pmodule = PyImport_ImportModule("hello"); //导入hello.py
if (!pmodule)
{
printf("cannot find hello.py\n");
return -1;
}
else
{
printf("PyImport_ImportModule success\n");
}
PyObject *pfunc = PyObject_GetAttrString(pmodule, "func1"); //导入func1函数
if (!pfunc)
{
printf("cannot find func\n");
Py_XDECREF(pmodule);
return -1;
}
else
{
printf("PyObject_GetAttrString success\n");
}
// 向Python传参数是以元组(tuple)的方式传过去的,
// 因此我们实际上就是构造一个合适的Python元组就可以了
// 要用到PyTuple_New,Py_BuildValue,PyTuple_SetItem等几个函数
/*这个元组其实只是传参的载体
创建几个元素python函数就是几个参数,这里创建的元组是作为3个参数传递的*/
PyObject *pArgs = PyTuple_New(3);
PyObject *pVender = Py_BuildValue("i", 2); //构建参数1
PyObject *pDataID = Py_BuildValue("i", 10001); //构建参数2
PyObject *pyTupleList = PyTuple_New(2); //构建参数3,这里创建的元组是作为c的数组
float arr_f[2];
arr_f[0] = 78;
arr_f[1] = 3.41;
for (int i = 0; i < 2; i++)
{
//这里是把c数组构建成python的元组
PyTuple_SetItem(pyTupleList, i, Py_BuildValue("f", arr_f[i]));
}
//参数入栈
PyTuple_SetItem(pArgs, 0, pVender);
PyTuple_SetItem(pArgs, 1, pDataID);
PyTuple_SetItem(pArgs, 2, pyTupleList);
//调用python脚本函数
PyObject *pResult = PyObject_CallObject(pfunc, pArgs);
int a;
float b;
// PyArg_Parse(pResult, "i", &a);
PyArg_ParseTuple(pResult, "if", &a, &b);
printf("%d %f\n", a, b);
//释放资源
Py_XDECREF(pmodule);
Py_XDECREF(pfunc);
Py_XDECREF(pArgs);
Py_XDECREF(pResult);
Py_Finalize();
return 0;
}
python源文件hello.py:
import logging
LOG_FORMAT = "[%(asctime)s][%(levelname)s][%(module)s.py:%(lineno)d]---> %(message)s"
DATE_FORMAT = "%Y%m%d %T"
logging.basicConfig(
filename="hello.log", level=logging.DEBUG, format=LOG_FORMAT, datefmt=DATE_FORMAT
)
ch = logging.StreamHandler() # 输出到控制台
ch.setLevel(logging.DEBUG)
ch.setFormatter(logging.Formatter(LOG_FORMAT, DATE_FORMAT))
logging.getLogger().addHandler(ch)
def func1(vender, dataid, tuple):
logging.info("vender = %d, dataid = %d" % (vender, dataid))
logging.info("type tuple = [%s] tuple = [%s]" % (str(type(tuple)), str(tuple)))
return (15, 5.6)
编译的命令为:(要针对自己电脑的python路径进行调整)
gcc -I D:\Development\Miniconda3\envs\python -L D:\Development\Miniconda3\envs\python\libs run.c -l python310
解释一下上面编译命令的各个参数:
-I为include预处理命令中所包含头文件所在的位置,我们要包含的include/Python.h位于这个目录下,当然也可以直接在源文件中include命令的引号中填写这个绝对路径,那么编译命令中就可以省略此选项。
-L为链接器搜索库文件的额外目录,后面的-l则指定了我们要链接哪个库文件,在我的-L指定的目录下,有一个叫python310.lib的文件,这就是我要链接的库,而将此文件名传给-l选项时,不需要传入.lib的部分。注意run.c必须在-l选项之前。
编译成功则会生成a.exe,可以尝试运行,如果顺利执行,那么命令行将会打印以下结果:
PyImport_ImportModule success
PyObject_GetAttrString success
[20221027 15:42:11][INFO][hello.py:16]---> vender = 2, dataid = 10001
[20221027 15:42:11][INFO][hello.py:17]---> type tuple = [<class 'tuple'>] tuple = [(78.0, 3.4100000858306885)]
并且当前目录也会生成一个hello.log文件记录上述结果。
而我在实际运行时遇到了下面的错误:
Python path configuration:
PYTHONHOME = (not set)
PYTHONPATH = (not set)
program name = 'python'
isolated = 0
environment = 1
user site = 1
import site = 1
sys._base_executable = 'D:\\Workspace\\test\\a.exe'
sys.base_prefix = 'D:\\Development\\Miniconda3\\envs\\python'
sys.base_exec_prefix = 'D:\\Development\\Miniconda3\\envs\\python'
sys.platlibdir = 'lib'
sys.executable = 'D:\\Workspace\\test\\a.exe'
sys.prefix = 'D:\\Development\\Miniconda3\\envs\\python'
sys.exec_prefix = 'D:\\Development\\Miniconda3\\envs\\python'
sys.path = [
'D:\\Development\\Miniconda3\\envs\\python\\python310.zip',
'.\\DLLs',
'.\\lib',
'D:\\Workspace\\test',
]
Fatal Python error: init_fs_encoding: failed to get the Python codec of the filesystem encoding
Python runtime state: core initialized
ModuleNotFoundError: No module named 'encodings'
Current thread 0x00004490 (most recent call first):
<no Python frame>
根据查阅的资料,这是环境变量未正确设置导致的,线索就位于上面信息的第二和第三行,提示PYTHONHOME和PYTHONPATH未设置,这是最后报出Fatal Python error的原因,生成程序调用的python解释器不知道去哪个路径调用python脚本库。
因此需要设置这两个环境变量,我经过尝试发现,这两个环境变量只需有一个正确设置即可使a.exe按预期工作,可以在powershell命令行中执行下面的命令来设置临时的环境变量:
$env:PYTHONHOME="D:\Development\Miniconda3\envs\python"
$env:PYTHONPATH="D:\Development\Miniconda3\envs\python\DLLs;D:\Development\Miniconda3\envs\python\Lib;D:\Development\Miniconda3\envs\python\Lib\site-packages"
以上两行环境变量设置命令只要有一个设置了即可,设置了环境变量以后,再调用a.exe应该就能看到预期效果了。
额外的想法
其实之所以写这篇文章,是因为好奇游戏引擎的工作原理,就拿Unity为例,引擎程序本体使用C++实现,而游戏脚本使用C#来编写,我很好奇这二者要如何交互。从这篇文章可以大致猜测下,本文的C语言就对应Unity引擎的本体语言C++,然后本文中的Python就对应游戏脚本语言C#。
C#的运行时,也即.NET虚拟机应该也提供了类似Python这样的其他编程语言接口(C++接口),允许使用其他编程语言和虚拟机的内存进行交互,那么Unity的引擎主体就由C++实现,并且可以利用这个接口来要求虚拟机执行游戏开发者写的C#脚本中的函数,并取得返回值,或者游戏开发者修改了什么全局变量,游戏引擎也可以去读取这个全局变量。总之,这样Unity引擎就能知道游戏开发者想执行什么样的游戏逻辑了。
引擎可以用C#提供很多预设好的类和函数,来表达各种预设的游戏功能,比如表示物体的类O,使得物体移动的函数m,那么游戏开发者就可以直接导入类O并实例化一个对象(引擎开发者应该会加入实例化的同时追踪实例的代码),然后调用m来使其移动,引擎直接取回类O的对象的位置属性,就能知道游戏是如何进行的了。
于是游戏开发者只需要放入素材资源以及编写逻辑脚本,引擎程序负责实际的解读脚本和运行,游戏程序的本质就变成了引擎程序、游戏资源及脚本两个部分,使得引擎开发与游戏开发相互独立了。
参考资料: