使用C语言调用Python脚本

本文提供了一个使用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的对象的位置属性,就能知道游戏是如何进行的了。

于是游戏开发者只需要放入素材资源以及编写逻辑脚本,引擎程序负责实际的解读脚本和运行,游戏程序的本质就变成了引擎程序、游戏资源及脚本两个部分,使得引擎开发与游戏开发相互独立了。

参考资料:

留下评论