c++调用python原理_C++回调Python的技术手段

C++回调Python的技术手段

上一篇文章《C++和Python混合编程的利器》中着重介绍了如何利用Boost Python库来实现C++和Python交互过程中的参数传递。本文将深入到Python源码并结合Python的运行机制,来分析C++回调Python的基本原理。如果对实现原理不感兴趣的同学,可以直接跳至下文中“Boost Python实现C++同步/异步回调Python的Demo” 章节,通过Demo来学习如何实现C++回调Python的方法。

Python解释器运行原理

我们都知道,使用C/C++之类的编译性语言编写的程序,是需要从源文件转换成计算机使用的机器语言,经过链接器链接之后形成了二进制的可执行文件。运行该程序的时候,就可以把二进制程序从硬盘载入到内存中运行。但是对于Python而言,Python源码不需要编译成二进制代码,它可以直接从源代码运行程序。当运行Python程序的时候,Python解释器将源代码转换为字节码,然后再由Python解释器来一条一条执行字节码指令,从而完成程序的执行。

Python编译器和字节码文件

字节码是Python编译器编译 (.py文件)生成的,生成后字节码文件格式为:.pyc,它在Python解释器中对应的是PyCodeObject对象。Python源码中对此对象定义如下:

1. /* Bytecode object */

2. typedef struct {

3.     PyObject_HEAD

4.     int co_argcount;        /* #arguments, except *args */

5.     int co_kwonlyargcount;    /* #keyword only arguments */

6.     int co_nlocals;        /* #local variables */

7.     int co_stacksize;        /* #entries needed for evaluation stack */

8.     int co_flags;        /* CO_..., see below */

9.     PyObject *co_code;        /* instruction opcodes */

10.     PyObject *co_consts;    /* list (constants used) */

11.     PyObject *co_names;        /* list of strings (names used) */

12.     PyObject *co_varnames;    /* tuple of strings (local variable names) */

13.     PyObject *co_freevars;    /* tuple of strings (free variable names) */

14.     PyObject *co_cellvars;      /* tuple of strings (cell variable names) */

15.     unsigned char *co_cell2arg; /* Maps cell vars which are arguments. */

16.     PyObject *co_filename;    /* unicode (where it was loaded from) */

17.     PyObject *co_name;        /* unicode (name, for reference) */

18.     int co_firstlineno;        /* first source line number */

19.     PyObject *co_lnotab;    /* string (encoding addrlineno mapping) See

20.                    Objects/lnotab_notes.txt for details. */

21.     void *co_zombieframe;    /* for optimization only (see frameobject.c) */

22.     PyObject *co_weakreflist;   /* to support weakrefs to code objects */

23. } PyCodeObject;

要了解PyCodeObject的作用,首先必须要清楚PyCodeObject结构体定义中的每一个域都表示什么含义,表1列出了PyCodeObject对象中各个域的具体意义。

字段

类型

说明

PyObject_HEAD

PyObject

PyObject_HEAD宏 定义了每个 PyObject 的初始段

co_argcount

int

代码块的位置参数的个数

co_kwonlyargcount

int

代码块的关键参数的个数

co_nlocals

int

代码块中局部变量的个数

co_stacksize

int

执行该代码块需要的栈空间

co_flags

int

co_code

PyObject *

代码块编译后的字节码指令序列,PyStringObject的形式存在

co_consts

PyObject *

代码块中所有的常量

co_names

PyObject *

代码块中所有的符号

co_varnames

PyObject *

代码块中所有的局部变量名

co_freevars

PyObject *

实现闭包使用的变量

co_cellvars

PyObject *

代码块中嵌套函数所引用的所有变量名

co_cell2arg

char *

映射单元的参数

co_filename

PyObject *

代码块对应py文件的完整路径

co_name

PyObject *

代码块的名字,通常是函数名或类名

co_firstlineno

int

代码块在对应py文件中的起始行

co_lnotab

PyObject *

字节码指令与py文件中代码行的对应关系

co_zombieframe

void *

优化选项,编译优化时使用

co_weakreflist

PyObject *

支持weakrefs模块的代码对象

PyCodeObject对象包含了py文件中的代码经过编译后得到的字节码序列。Python会将这些字节码序列和PyCodeObject对象一起存储在pyc文件中。

可能细心的读者会有疑问:执行脚本时,并不一定会生成pyc文件,按照本文前面的解释,py脚本运行之前必须经过编译器进行编译,既然编译了,就会产生pyc文件。那为什么我在执行脚本时候,有时候并不会产生pyc文件呢?

Python在通过import对moudle进行动态加载时,如果没有找到对应的pyc和dll文件,就会在py文件的

基础上自动创建pyc文件,也可以显示调用compile把py文件编译成pyc文件。由于篇幅限制,pyc文件的创建和更新机制的细节问题就不再赘述。只需要明白pyc的自动生成的机制就行了。

解析Python字节码(pyc)文件

介绍了PyCodeObject和pyc以及它们之间的关系,下面我们利用Python dis库来解析pyc文件,通过更底层的指令集来分析Python处理类/函数/变量的实现机制:

首先,创建test.py,脚本中只有简单的print函数。

1. #test.py

2. print("Hello, world")

然后,我们创建demo.py,先把test脚本编译成pyc文件,然后利用dis库解析生成的pyc文件。

1. #demo.py

2. s=open('test.py').read()

3. co=compile(s,'test.py','exec')

4. import dis

5. dis.dis(co)

最后,我们执行demo.py,会在控制台输出如下信息:

第一列表示以下几个指令在py文件中的行号;

第二列是该指令在指令序列(PyCodeObject结构体)中的偏移量;

第三列是指令opcode的名称,分为有操作数和无操作数两种,opcode在指令序列中是一个字节的整数;这里对上面用到的指令序列做个简单的说明:

LOAD_NAME

将关联的值co_names[namei]压栈

LOAD_CONST

将关联的值co_consts[consti]压栈。

CALL_FUNCTION

调用一个函数。

POP_TOP

删除顶部堆栈(TOS)项目。

RETURN_VALUE

将TOS返回给函数的调用者。

“字节码”,是指令以字节为单位,1个字节最多只能表示256个不同的字节码指令。实际上Python只用了101条字节码指令,我们这里只列举了其中的5条,如果对Python指令集感兴趣,可以访问网站https://docs.python.org/3/library/dis.html,里面详细罗列了Python所有指令。

第四列是操作数oparg,在指令序列中占两个字节,基本都是co_consts或者co_names的下标;

第五列带括号的是操作数说明。

Python解释器中的函数机制

了解了Python编译的过程,并通过对产生的pyc文件进行解析,明白了pyc文件都包含了哪些内容。但Python的解释器才是Python的核心,在py文件被编译器编译为字节码指令序列后,Python解释器就接管了整个工作。Python解释器从编译PyCodeObject对象中依次读入每一条字节码指令,并在上下文环境中执行这条字节码指令。Python的解释器实际上是在模拟X86的系统中运行可执行文件的过程。这就必须了解X86下函数调用及栈帧原理,当子函数调用时,调用者与被调用者的栈帧结构如下图所示:

在子函数调用时,执行的操作有:

1) 父函数将调用参数从后向前压栈

2) 将返回地址压栈保存

3) 跳转到子函数起始地址执行

4) 子函数将父函数栈帧起始地址(%rpb) 压栈

5) 将%rbp 的值设置为当前 %rsp 的值,即将 %rbp 指向子函数栈帧的起始地址。

上述过程中,保存返回地址和跳转到子函数处执行由call 一条指令完成,在call 指令执行完成时,已经进入了子程序中,因而将上一栈帧%rbp 压栈的操作,需要由子程序来完成。函数调用时在汇编层面的指令序列如下:

1. ...   # 参数压栈

2. call FUNC  # 将返回地址压栈,并跳转到子函数FUNC处执行

3. ...  # 函数调用的返回位置

4. FUNC:  # 子函数入口

5. pushq %rbp  # 保存旧的帧指针,相当于创建新的栈帧

6. movq  %rsp, %rbp  # 让%rbp指向新栈帧的起始位置

7. subq  $N, %rsp  # 在新栈帧中预留一些空位,供子程序使用,用(%rsp+K)或(%rbp-K)的形式引用空位

保存返回地址和保存上一栈帧的%rbp 都是为了函数返回时,恢复父函数的栈帧结构。在使用高级语言进行函数调用时,由编译器自动完成上述整个流程。需要注意的是,父函数中进行参数压栈时,顺序是从后向前进行的。但是,这一行为并不是固定的,是依赖于编译器的具体实现的。栈帧间通过esp指针和ebp指针建立了关系,使新的栈帧在结束之后能顺利回到旧的栈帧中。

Python解释器中处理函数调用正是模拟上述X86下的处理过程。当Python在执行环境中,执行函数调用的字节码指令时,会在当前的执行环境之外创建新的执行环境。新的执行环境就对应上图中一个新的栈帧。Python真正执行的时候,解释器实际上执行的并不是一个PyCodeObject对象,而是PyFrameObject。它在Python源码中的定义是:

1. typedef struct _frame {

2.     PyObject_VAR_HEAD

3.     struct _frame *f_back;      /* 执行环境上一个栈帧*/

4.     PyCodeObject *f_code;       /* PyCodeObject对象*/

5.     PyObject *f_builtins;       /* builtin名字空间*/

6.     PyObject *f_globals;        /* global名字空间*/

7.     PyObject *f_locals;         /* local名字空间*/

8.     PyObject **f_valuestack;    /* 运行时栈底*/

9.     PyObject **f_stacktop;      /* 运行时栈顶*/

10.     PyObject *f_trace;          /* 异常处理对象*/

11.     PyObject *f_exc_type, *f_exc_value, *f_exc_traceback;/* 异常信息*/

12.     PyObject *f_gen;            /* 生成器对象*/

13.     int f_lasti;                /* 上一个字节码指令偏移*/

14.     int f_lineno;               /* 源码行号*/

15.     PyObject *f_localsplus[1];  /* 动态内存,维护运行环境所需要的空间*/

16.     ...

17. } PyFrameObject;

通过f_back域以及X86的函数处理的堆栈原理,我们可以得出如下结论:Python的实际执行过程中,会产生很多堆栈(PyFrameObject)对象,而这些对象会被Python解释器管理起来,形成一条执行环境栈链,而Python解释器正是通过执行栈链的内容来实现函数调用。

Python中万物皆对象,函数也不例外,在Python源码中,函数对象的定义是PyFunctionObject,它与我们上面分析Python编译过程中产生的PyCodeObject对象又是什么关系呢?它们之间又有什么千丝万缕的联系呢?带着这个问题,我们来一起研究下Python中函数对象的实现,以及在函数调用的过程中,Python是如何进行处理的。

函数对象PyCodeObject在Python源码中的定义:

1. typedef struct {

2.     PyObject_HEAD

3.     PyObject *func_code;    /* 对应函数编译后的PyCodeObject对象*/

4.     PyObject *func_globals; /* 函数运行时的globals名字空间*/

5.     PyObject *func_defaults;    /* 默认参数*/

6.     PyObject *func_kwdefaults;  /* 关键参数*/

7.     PyObject *func_closure; /* 用户实现函数闭包*/

8.     PyObject *func_doc;     /* 函数的文档对象*/

9.     PyObject *func_name;    /* 函数的名称*/

10.     PyObject *func_dict;    /* 函数的dict属性*/

11.     PyObject *func_weakreflist; /* 弱引用处理对象*/

12.     PyObject *func_module;  /* 函数的moudle属性*/

13.     PyObject *func_annotations; /* 函数注释*/

14.     PyObject *func_qualname;    /* 限定名*/

15. } PyFunctionObject;

PyCodeObject对象是对py文件的静态编译结果。它包含了这个py文件的静态信息,例如test.py中的print(“Hello World”),print和字符串”Hello World”就是一种静态信息,它们分别存储在PyCodeObject的co_consts,co_names和co_code中。

而PyFunctionObject则不同,它是Python代码在运行时动态产生的,更准确的说,是在执行def语句的时候创建的。PyFunctionObject函数中当然会包括这个函数的静态信息,它是通过func_code进行映射的。除此之外,PyFunctionObject对象还包括了函数执行过程中需要的上下文信息。

PyCodeObject和PyFunctionObject是1:N的关系,比如一个函数多次调用,Python在执行的过程中会创建多个PyFunctionObject对象,每个对象的func_code域都会关联到这个PyCodeObject。

我们通过一个实例,来一起分析下Python处理函数调用的过程。

1) 修改test.py,把print语句使用函数封装起来,代码如下:

1)

1. #test.py

2. def func():

3.     print("Hello, world")

4. func()

2) 运行上面示例中的demo.py

1. E:\svn>python demo.py

2.   2           0 LOAD_CONST               0 ()

3.               3 LOAD_CONST               1 ('func')

4.               6 MAKE_FUNCTION            0

5.               9 STORE_NAME               0 (func)

6.   5          12 LOAD_NAME                0 (func)

7.              15 CALL_FUNCTION            0 (0 positional, 0 keyword pair)

8.              18 POP_TOP

9.              19 LOAD_CONST               2 (None)

10.              22 RETURN_VALUE

3) Python执行函数指令的主要过程:

a) 将test.py静态编译结果PyCodeObject压栈

b) 将co_consts[1]的字节码对象func压栈

c) 执行def语句,动态的创建PyFunctionObject对象并压栈

d) 取出co_names,函数对象出栈,并且f_locals[‘func’]=函数对象

e) 将函数对象压栈,实际对应的是f_locals[‘func’]

f) 调用函数,创建新的栈帧,并在新的栈帧上执行

g) 函数返回并出栈

h) 将None加载到堆栈

i) 返回结果

了解到了Python函数机制的实现过程,通过解析Python中如何实现函数的定义和调用,我们不难发现,C++和Python的函数定义和调用的机制并无区别:都是通过创建新栈帧,在新的栈帧上执行代码来实现的。

本文主题:通过Boost Python库来实现C++回调Python的技术,以上原理的介绍,可能有些人看的比较晕,如果能搞清楚我们在Python解释器运行原理中给出的时序图的执行过程,可能会更加容易接受,不过原理型知识并不影响我们学习如何实现C++回调Python的技术。

接下来,让我们转换下风格。先介绍同步/异步回调的技术实现代码,再来分析其实现过程。

Boost Python实现C++同步回调Python的Demo

所谓同步,就是发出一个功能调用时,在没有得到结果之前,该调用不返回或继续执行后续操作。简单来说,同步就是必须一件一件做,等前一件做完了才能做下一件事。

C++ 模块代码:

1. //Test.h

2. #pragma once

3. #define BOOST_PYTHON_STATIC_LIB

4. #include

5. #include

6.

7. using namespace std;

8. using namespace boost::python;

9.

10. //GIL全局锁简化获取用,

11. //用于帮助C++线程获得GIL锁,从而防止python崩溃

12. class PyLock

13. {

14. private:

15.     PyGILState_STATE gil_state;

16. public:

17.     //在某个函数方法中创建该对象时,获得GIL锁

18.     PyLock()

19.     {

20.         gil_state = PyGILState_Ensure();

21.     }

22.     //在某个函数完成后销毁该对象时,解放GIL锁

23.     ~PyLock()

24.     {

25.         PyGILState_Release(gil_state);

26.     }

27. };

28.

29. int TestCallBack(const string& szParam, object pyCallBack);

1. //Test.cpp

2. #include "Test.h"

3. #include

4.

5. int TestCallBack(const string& szParam, object pyCallBack)

6. {

7.     //Python中传递的参数,C++中可以直接使用

8.     std::cout << szParam << std::endl;

9.     {

10.         PyLock _pylock;

11.         pyCallBack("Hello Python, I am is C++");

12.     }

13.     return 0;

14. }

15.

16. //导出的模块名

17. BOOST_PYTHON_MODULE(Test)

18. {

19.     //导入时运行,保证先创建GIL

20.     PyEval_InitThreads();

21.

22.     def("TestCallBack", TestCallBack);

23. }

Python模块代码:

1. from Test import TestCallBack

2.

3. def testCallBack(szParam):

4.     print(szParam)

5.

6. TestCallBack("Hello C++, I am is Python", testCallBack)

在Python环境中执行上述代码,结果如下图:

我们对上述代码的几个重要步骤做下分析:

1) C++定义了导出给Python 接口TestCallBack,参数一个是字符串,一个是Python的函数

2) 执行任何Python代码前,都需要先初始化Python解释器的运行环境(PyEval_InitThreads函数)

3) 执行任何Python代码前,都需要先获得Python解释器GIL锁的使用权限,PyLock封装了C API接口并巧妙的利用了C++类的构造/析构的机制来实现GIL锁的获取和释放

Boost Python实现C++异步回调Python的Demo

异步与同步相对,当一个异步过程调用发出后,调用者在没有得到结果之前,就可以继续执行后续操作。当这个调用完成后,一般通过状态、通知和回调来通知调用者。对于异步调用,调用的返回并不受调用者控制。

1. //Test.h

2. #pragma once

3. #define BOOST_PYTHON_STATIC_LIB

4. #include

5. #include

6. #include

7.

8. using namespace std;

9. using namespace boost::python;

10.

11. //GIL全局锁简化获取用,

12. //用于帮助C++线程获得GIL锁,从而防止python崩溃

13. class PyLock

14. {

15. private:

16.     PyGILState_STATE gil_state;

17. public:

18.     //在某个函数方法中创建该对象时,获得GIL锁

19.     PyLock()

20.     {

21.         gil_state = PyGILState_Ensure();

22.     }

23.     //在某个函数完成后销毁该对象时,解放GIL锁

24.     ~PyLock()

25.     {

26.         PyGILState_Release(gil_state);

27.     }

28. };

29. void RegisterPyCallBack(object pyCallBack);

30. int TestCallBack(const string& szParam);

31. static void TestThread();

32.

33. object g_pyCallBack;    //存储Python回调对象

1. #include "Test.h"

2. #include

3.

4. boost::mutex the_mutex;

5. boost::function0 funcPyCallback = boost::bind(&TestThread);

6. boost::thread tRevAsync(funcPyCallback);

7. bool g_bStart = false;

8.

9. void RegisterPyCallBack(object pyCallBack)

10. {

11.     boost::mutex::scoped_lock lock(the_mutex);

12.     g_pyCallBack = pyCallBack;

13.     g_bStart = true;

14. }

15.

16. void TestThread()

17. {

18.     while (true)

19.     {

20.         if (g_bStart)

21.         {

22.             PyLock _pylock;

23.             boost::mutex::scoped_lock lock(the_mutex);

24.             g_pyCallBack("Hello Python, I am is Async C++");

25.             break;

26.         }

27.     }

28. }

29.

30. int TestCallBack(const string& szParam)

31. {

32.     std::cout << szParam << std::endl;

33.     return 0;

34. }

35.

36. //导出的模块名

37. BOOST_PYTHON_MODULE(Test)

38. {

39.     //导入时运行,保证先创建GIL

40.     PyEval_InitThreads();

41.

42.     def("RegisterPyCallBack", RegisterPyCallBack);

43.     def("TestCallBack", TestCallBack);

44. }

Python代码:

1. from Test import TestCallBack, RegisterPyCallBack

2.

3. def testCallBack(szParam):

4.     print(szParam)

5. RegisterPyCallBack(testCallBack)

6. TestCallBack("Hello C++, I am is Python")

在Python环境中执行上述代码,结果如下图:

通过对比同步和异步回调的代码发现,从Python调用者的角度来看,就只增加了一个注册回调函数的接口。对于C++的开发者来说,回调也没什么大的区别,无非是取回调的地址方式发生了变化而已。

Boost Python库实现C++同步/异步回调Python的过程

以上代码执行整个过程如下:

利用Boost Python实现C++回调Python接口的主要内容就是这些了。我们从Python解释器的运行原理开始,到Python文件的编译和节码(pyc)文件的创建和生成,再到Python解释器中的函数机制,最后通过C++同步/异步回调Python实例来逐步学习。本文希望读者在学习C++回调Python的技术的同时,能够对Python的实现原理能有所了解,希望能对大家的学习和工作能有所帮助。

你可能感兴趣的:(c++调用python原理)