当前位置: 移动技术网 > IT编程>脚本编程>Python > 记一次嵌入式Python+Swig入门开发

记一次嵌入式Python+Swig入门开发

2020年07月22日  | 移动技术网IT编程  | 我要评论

Python/C API开发

一、Python/C API简单用法

  Python 的应用编程接口(API)使得 C 和 C++ 程序员可以在多个层级上访问 Python 解释器。该 API 在 C++ 中同样可用,但为简化描述,通常将其称为 Python/C API。使用 Python/C API 有两个基本的理由。第一个理由是为了特定目的而编写扩展模块;它们是扩展 Python 解释器功能的 C 模块。这可能是最常见的使用场景。第二个理由是将 Python 用作更大规模应用的组件;这种技巧通常被称为在一个应用中 embedding Python。

1、初始化Python环境

  要调用 Python API 的函数,必须要包含Python头文件,该文件在用户 Python 安装目录的 include 文件夹下如:

C:\Users\YourName\AppData\Local\Programs\Python\PythonVersion\include

Python dll 库目录一般为:

C:\Users\YourName\AppData\Local\Programs\Python\PythonVersion

Python lib 库目录一般为:

C:\Users\YourName\AppData\Local\Programs\Python\PythonVersion\libs

.pro 文件中添加 Python 包含路径和库路径:

LibPath = C:\Users\YourName\AppData\Local\Programs\Python\PythonVersion
LIBS += -L$$LibPath -L$$LibPath/libs -lpythonVersion
INCLUDEPATH += $$LibPath/include

将上述目录加入程序头文件查找路径,然后在代码中添加头文件:

#include<Python.h>

这时如果进行编译的话,会发现程序报错,这是因为 Qt 与 Python 中的 object.h 文件中的 slots 发生冲突,因此我们需要修改 object.h 文件:

typedef struct{
    const char* name;
    int basicsize;
    int itemsize;
    unsigned int flags;
    #undef slots // 这里取消 slots 宏定义
    PyType_Slot *slots; /* terminated by slot==0. */
    #define slots Q_SLOTS // 这里恢复 slots 宏定义与 Qt 中 QObjectDefs.h 中一致
}PyType_Spec;
  • 初始化 Python 解释器

在调用 Python API 中的函数之前,必须要初始化 Python 解释器环境,且同一进程只能初始化一次:

if (!Py_IsInitialized())
{
    Py_Initialize();
    if (!Py_IsInitialized())
    {
        return false;
    }
}

同样,在程序结束之前,要取消对 Python 解释器的初始化以释放缓存:

if (Py_IsInitialized())
{
    Py_Finalize();
}
  • 初始化 Python 环境

  程序要调用 Python 首先要知道我们写的 Python 文件放在哪个目录下面程序才能找到。程序默认 Python 文件放在可执行文件的同级目录下,所以把写的 Python 文件放在程序可执行文件的同级目录下即可。
  但是有时候我们想要放在其他目录,这时候可以在初始化 Python 解释器后,用 Python 标准库里的 sys.path 来动态的去添加系统变量,让 Python 解释器找到我们的 Python 文件:

Py_Run_SimpleString("import sys")
PyRun_SimpleString("sys.path.append('YourPythonFilePath')");

注:YourPythonFilePath 如果是 Windows 路径,则路径中的 \ 需要进行两次转译,如 C:\Users 要写为 C:\\\\Users

2、调用Python流程

  经过上述的准备工作,接下来就可以去调用写的 Python 代码了。假如现在有一个 Python 文件,名为 Demo.py ,内容为:

class test():
    def __init__(self, num):
        print ('num : %d' % num)
        
    def function1(self):
        print('this is function1')
    
    def function2(self, param):
        print('this is function2, param: %s' % param)
    
    def function3(self):
        print('this is function3')
        return 'this is function3'

注:以下过程为调用 Python 模块中类中函数示例。

  • 导入模块

  就像 Python 一样,需要导入模块才能使用模块的功能,模块名就是 Python 文件名:

PyObject* pModule = PyImport_ImportModule("Demo");
  • 获取模块变量字典

  顾名思义就是获取模块中的变量,并存到一个字典中:

pDict = PyModule_GetDict(pModule);
  • 获取变量指针

  在字典中获取变量指针,示例代码中,模块变量只有一个类,名为 test

pClass = PyDict_GetItemString(pDict, "test");
  • 获取类构造函数

  只获取类指针还不能生成类实例对象,需要获取类的构造函数指针:

pConstructor = PyInstanceMethod_New(pClass);
  • 生成类实例对象

  示例代码中类有参数,则需要构建参数,然后在生成实例时传入,类参数不管有几个都是以元组(Tuple)传入的:

pArg= PyTuple_New(1);
PyTuple_SetItem(pArg, 0, Py_BuildValue("i", 2)); 
pInstance = PyObject_CallObject(pConstructor, pArg);

如果类没有参数,则 PyObject_CallObject 第二个参数填 NULL

  • 调用类函数

  调用没有参数的函数:

PyObject_CallMethod(pInstance, "function1", NULL);

  调用有参数的函数:

PyObject_CallMethod(pInstance, "function2", "s", "hello world");

  调用有返回值的函数:

PyObject *pResult = PyObject_CallMethod(pInstance, "function3",NULL);
char *retValue;
retValue = PyString_AsString(pResult);

3、构建参数

  这里主要介绍构建bytes类型参数,其他类型参数可以参考参考文档进行构建。

QByteArray msg = "this is a bytes test";
PyObject *bytes = PyBytes_FromStringAndSize(msg.data(), msg.size());

传参的时候可以这样写:

PyObject_CallMethod(pInstance, "function2", "S", bytes);

注:这里的 "S" 为大写。

4、捕获 Python 运行异常

  为了程序更好的运行,我们还需要捕获 Python 代码中的异常信息。以下是捕获异常的示例代码:

void fetchError()
{
    PyObject *type = NULL; // 错误类型
    PyObject *traceback = NULL; // 错误追溯
    PyObject *value = NULL; // 错误信息
    
    // 捕获异常
    PyErr_Fetch(&type, &value, &traceback);
    PyErr_NormalizeException(&type, &value, &traceback);
    
    // 解析异常信息
    QString errMsg = "Fetch Error: ";
    
    if (type)
    {
        errMsg += QString::fromUtf8(PyExceptionClass_Name(type)) + ":\n";
        Py_CLEAR(type);
    }
    
    if (traceback)
    {
        PyTracebackObject *tb = (PyTracebackObject *)traceback;
        for (; tb != NULL; tb = tb->tb_next)
        {
            PyObject *line = PyUnicode_FromFormat("File \"%U
            ", line %d, in %U\n", tb->tb_frame->f_code->co_filename, PyCode_Add2Line(tb->tb_frame->f_code, tb->tb_frame->f_lasti, tb->tb_frame->f_code->co_name);
            const char *err = PyUnicode_AsUTF8(line);
            errMsg += QString::fromUtf8(err);
            Py_CLEAR(line);
        }
        Py_CLEAR(traceback);
    }
    
    if (value)
    {
        PyObject *line = PyObject_Str(value);
        if (line && (PyUnicode_Check(line)))
        {
            const char *err = PyUnicode_AsUTF8(line);
            errMsg += QString::fromUtf8(err);
            Py_CLAER(line);
        }
        Py_CLEAR(value);
    }
}

在调用 PyObject_CallMethod() 之后,调用 fetchError() 就可以捕获 PyObject_CallMethod() 中的异常信息(如果发生异常话)。

5、多线程调用 Python

  有时因为需要我们要多线程调用 Python , 这时需要初始化 Python 的多线程机制,以启用多线程:

if (!Py_IsInitialized())
{
    Py_Initialize();
    if (!Py_IsInitialized())
    {
        return false;
    }
    PyEval_InitThreads();
    PyEval_ReleaseThread(PyThreadState_Get());
}

  如果需要 Python 在多线程中运行的话,你会发现按照上述示例运行会 crash,这是因为 Py_Initialize() 生成的是一个全局的 Python 解释器,多线程运行时 Python 需要去获得全局解释器使用权,这时候就需要去加锁,以防止资源访问冲突导致的崩溃。
Header 文件:

class PyThreadLocker
{
public:
    PyThreadLocker();
    ~PyThreadLocker();
private:
    PyGILState_SATE m_gState;
    PyThreadState *m_pSave;
    int m_nStatus;
}

Source 文件:

PyThreadLocker::PyThreadLocker() : m_pSave(NULL), m_nStatus(0)
{
    m_nStatus = PyGILState_Check(); // 检测当前线程是否拥有GIL
    if (!m_nStatus)
    {
        m_gState = PyGILState_Ensure(); // 获取GIL
        m_nStatus = 1;
    }
    m_pSave = PyEval_SaveThread();
    PyEval_RestoreThread(m_pSave);
}

PyThreadLocker::~PyThreadLocker()
{
    m_pSave = PyEval_SaveThread();
    PyEval_RestoreThread(m_pSave);
    if (m_nStatus)
    {
        PyGILState_Release(m_gState); // 释放当前线程的GIL
    }
}

上述代码写了一个类,在构造的时候去获取锁,析构的时候去释放锁,所以我们平时可以这样用:

void callPythonMethod()
{
    PyThreadLocker locker;
    PyObject_CallMethod(pInstance, "function1", NULL);
}

这样多线程调用 callPythonMethod() 函数就是线程安全的。
注:Python 的多线程并不是真正意义上的多线程,根据上述代码不难发现,多个线程必须加锁去获取全局解释器才能正常运行,性能也就相当于单线程。

二、嵌入式 Python 环境搭建

  此处的嵌入式 Python 指的是将 Python 环境嵌入到我们程序中。由于部分用户不想使用本地的 Python 环境或者本地 Python 环境存在问题等等原因不能使用本地 Python 环境,在这种情况下在程序中打包 Python 环境显得尤为必要。

1、安装嵌入式Python

   Python 官方提供了嵌入式 Python 环境,这是一个很小的 Python 环境包,其中只包含了 Python 运行的必要的一些标准库。

  而且这个嵌入式 Python 环境并没有安装 pip ,所以我们如果想要在嵌入式环境中安装第三方库,就要先安装 pip 。在这里我只介绍在没有外网的情况下,通过源码安装,安装需要的源码可以在 pypi官网 下载。

  • 修改 pythonVerison_.pth 文件
    安装 setuptoolspip 之前,需要修改 Embeddable Python 主目录下的 pythonVerison_.pth 文件:
pythonVersion.zip
.

# Uncomment to run site,main() automatically
import site

其实就是将注释的 import site 放开,即将 # import site 改为 import site

  • 安装 setuptools
    在官网中搜索 setuptools ,然后下载你需要的版本,一般下载最新版本即可。然后运行命令:
YourEmbeddablePythonPath\python setup.py install

一般会出现报错:找不到 setuptools 模块 ,这时将刚下载的源码包中的 setuptools 文件夹和 pkg_resources 文件夹复制到 embeddable python 主目录下再次执行上述命令即可(安装完成后,可把刚拷贝的两个文件夹在主目录删除)。

  • 安装 pip
    同样在官网中搜索 pip ,下载源码包然后运行命令:
YourEmbeddablePythonPath\python setup.py install

这时如果出现报错:没有 Lib\site-packages 目录 ,手动在 Embeddable Python 主目录下创建提示目录,然后再次执行上述命令即可。

  • 安装 pip
    安装好以后就可以使用 pip 安装第三方库里,但是这里要注意,运行的时候要通过 -m 指定 pip,即:
YourEmbeddablePythonPath\python -m pip install DstLib

2、嵌入式Python环境配置

使用嵌入式 Python 进行开发,除了跟普通 Python 一样需要在 .pro 文件中加载 Python 库和 include 路径(提示一下:Embeddable Python 环境包解压以后没有 include 文件夹,可以拷贝一个相同版本号的普通 Python 的 includes 文件夹到主目录,切记要按照上文步骤修改 object.h 文件)之外,还需要在代码中配置 Embeddable Python 环境变量。至于需要添加哪些环境变量,你可以在 Python 中查看当前用到了哪些环境:

YourEmbeddablePythonPath\python
>>> import sys
>>> import pprint
>>> pprint.pprint(says.path)

这时会打印出 Embeddable Python 用到的环境变量,然后在程序初始化 Python 解释器之前,即调用 Py_Initialize() 之前,添加环境变量:

static const wchar_t path[] = L”EmbeddablePythonEnvironmentPath”;
Py_SetPath(path)

三、SWIG简单用法

有时因为项目需求,我们写 Python 程序的时候需要调用 C++ 库的接口,这时就需要一个工具去帮助 C++ 接口嵌入到 Python 程序中,这种工具有很多,这里只介绍通过 SWIG 工具进行嵌入。

SWIG 是个帮助使用 C 或者 C++ 编写的软件能与其他各种高级编程语言进行嵌入连接的开发工具。SWIG能应用与各种不同类型的语言包括常用脚本编译语言例如 Perl, PHP, Tcl, Ruby 等。
这里我们只介绍 C++ 程序 嵌入到 Python 进行使用。

SWiG 是通过一个 .i 文件,声明一些接口供 Python 调用完成 C++ 库的嵌入。假如现在有一个 C++ 接口文件 YourFile.cpp

void interfaceFunc()
{
    qDebug() << “this is c++ interface!”;
}

YourFile.i 文件需要这样写:

%module DSTLibrary // 这是最后生成的 Python 库的名字

%{
    #include “YourFile.cpp” // 这是需要嵌入的 C++ 代码文件
%}

%include “YourFile.cpp”

然后通过命令:

swig -python -c++ YourFile.i

自动生成一个 YourFile_wrap.cxx 和一个 DSTLibrary.py 文件。其中 .cxx 文件需要添加到需要嵌入的 C++ 代码中进行编译,然后将编译生成的库文件改名为 .i 文件中的 DSTLibrary 并在名字最前面加上 _ ,即将编译生成的 YourFile.dll 改为 _DSTLibrary.pyd ,而 .py 文件作为 Python 使用的接口进行调用。所以还需要将上述的 _DSTLibrary.pydDSTLibrary.py 文件放到 Python 的环境目录下面,以便 Python 程序能够找到这个模块。这时 Python 程序调用 C++ 接口就可以直接通过 import 导入模块的方式调用:

import DSTLibrary as c

c.interfaceFunc()

以上只是最简单的 SWIG 应用,如需复杂的开发,可以参照 SWIG 文档

本文地址:https://blog.csdn.net/weixin_45493425/article/details/107457184

如对本文有疑问, 点击进行留言回复!!

相关文章:

验证码:
移动技术网