VS2019编译python解释器源码及学习方法

Python源码编译

  Python是当下很火的一门编程语言,在人工智能、数据分析、后端开发等领域可谓是人人都会的语言,在用python实现各种应用服务的同时,估计很少有人去关注python的实现,python解释器、虚拟机是用c实现的,也称为CPython, 本篇博客介绍如何用vs2019来编译python解释器源码,如果你的电脑是vs2017/vs2022一样适用。
在这里插入图片描述
  python是动态语言,依赖于“虚拟机”的机制,将py代码转为字节码,再转成机器语言,供操作系统调用,python, lua, js都是这种机制。

1 python源码分析的资料

  • (1)python源码书籍,目前,关于python源码分析的书籍,只有一本2008陈儒编写的《python源码分析》,该书用的是python2.7, vs2003,作者也没有再更新新的版本。
  • (2)陈儒的博客:blog.csdn.net/balabalamerobert
  • (3)陈儒的python虚拟机开源项目:Cobra
      http://bv.csdn.net/resource/pythonympx.rar
  • (4)python架构:文档 The Architecture of Python
  • (5)python官方教程 https://docs.python.org/3/tutorial/

2 python源码下载

  python源码的下载,有两种途径
  (1)官网稳定版下载 https://www.python.org/downloads/source/
  (2)github alpha版 https://github.com/python/cpython
  我用的是官网的稳定版,当前是3.9.6,源码目录如下:
在这里插入图片描述
源码部分目录介绍

  • Lib ∶该目录包含了 Python 自带的所有标准库,Lib 中的库都是用 Python 语言编写的
  • Modules ∶ 该目录中包含了所有用C语言编写的模块,比如 random、cstringIo 等。 Modules 中的模块是那些对速度要求非常严格的模块,而有一些对速度没有太严格要求的模块,比如 os,就是用 Python 编写,并且放在 Lib 目录下的。
  • Parser ∶该目录中包含了 Python 解释器中的 Scanner 和 Parser 部分,即对 Python 源代码进行词法分析和语法分析的部分。除了这些,Parser 目录下还包含了一些有用的工具,这些工具能够根据 Python 语言的语法自动生成 Python 语言的词法和语法分析器,与YACC非常类似。
  • Objects ∶ 该目录中包含了所有 Python 的内建对象,包括整数、list、aict 等。同时,该目录还包括了Python 在运行时需要的所有的内部使用对象的实现。
  • Python ; 该目录下包含了 Python 解释器中的 Compiler 和执行引擎部分,是 Python运行的核心所在。
  • PCBuild ∶包含了Visual Studio C++ 的工程文件,当前源码对应的是vs2017以及更新的版本,用vs2019/vs2022也没问题。
  • externals:下载的第三方依赖库,网络不好,估计下载不了。

3 编译准备工作

(1)win10、64bit
(2)安装vs2019,

4 开始编译

(1)先阅读 Python-3.9.6/PCbuild/readme.txt,根据该文档可知,我们得先运行Python-3.9.6\PCbuild\get_externals.bat,在执行该脚本后,会下载依赖的.h .lib .dll,全部完成后在Python-3.9.6路径下创建externals,拉取的文件在该目录中,如果网络环境不允许,可能会导致依赖的库下载不全,编译时有问题。
在这里插入图片描述

(2) vs解决方案路径:Python-3.9.6/PCbuild/pcbuild.sln
默认是32位版本,vs2019,F7, 一共45个项目,无特殊情况可以全部编译ok
在这里插入图片描述

5 cpython各个子项目介绍

  • _asyncio :异步io, Python中专门用来支持并发编程的一种设计,从Python3.4发布开始到Python3.7,async IO得到了飞快的发展,甚至有可能会更好。
  • _bz2 : 该模块提供了一个使用 bzip2 压缩算法压缩和解压缩数据的综合接口
    该bz2模块包含:
    (1)用于读取和写入压缩文件的open()函数和BZ2File类。
    (2)用于增量(解)压缩的BZ2Compressor和BZ2Decompressor类。
    (3)用于一次性(解)压缩的compress()和decompress()函数。
    该模块中的所有类都可以安全地从多个线程访问。
  • _ctypes:c类型声明,封装了libffi, 实现不同语言抽象接口的调用
  • _decimal:python高精度浮点数
  • _elementree :python xml解析
  • _freeze_importlib : 生成安装包
  • _hashlib : 哈希算法相关
  • _multiprocessing:多进程并行模块
  • _ssl :加解密相关
  • _tkinter :python gui实现
  • _zoneinfo :时区模块
  • bdist_wininst:安装程序构建,bdist_wininst 自 Python 3.8 起已弃用,bdist_msi 自 Python 3.9 起已弃用
  • liblzma:liblzma 是一个压缩库,其 API 类似于 zlib。
    xz 是一个命令行工具,其语法类似于 gzip。
    xzdec 是一个仅解压的工具,比全功能的 xz 工具小。
    一组 shell 脚本(xzgrep、xzdiff 等)已经从 gzip 改编而来,以便于查看、grepping 和比较压缩文件。
    LZMA Utils 命令行工具的仿真简化了从 LZMA Utils 到 XZ Utils 的过渡。
    虽然 liblzma 具有类似 zlib 的 API,但 liblzma 不包含任何文件 I/O 函数。计划使用单独的 I/O 库,该库将使用易于使用的 API 抽象处理 .gz、.bz2 和 .xz 文件。
  • pyexpat :更快速的xml解析
  • pythoncore:python核心实现,最终编译为python39.dll
  • pythonw:启动器有2个版本,一个是控制台版本,一个是windows图形化版本。两个版本都是启动Python,他们被分别命名为’py.exe’和’pyw.exe’
  • pythonw_uwp:pw的uwp版本
  • select:select模块
  • winsound:winsound 模块提供了对Windows平台提供的基本声音播放机制的访问

python项目运行

pcbuild的启动程序是子项目"python", 运行后如下图:
在这里插入图片描述

pythoncore项目介绍

该项目时python的核心,最终编译后生成python39.dll, 在Objects下声明了python各种数据类型及对象。
在这里插入图片描述
CPython把所有的数据类型和对象都封装为结构体,主要有如下几种:

  • 整数 PyLongObject
  • 字符串:字符串分为三种PyASCIIObject、PyUnicodeObject、PyBytesObject
  • 集合:PySetObject
  • 列表:PyListObject
  • 元组:PyTupleObject
  • 字典:PyDictObject
  • 类:在abstract.c中实现了与类和对象的相关方法

python源码调试

main入口

源码调试,要定位到对应的位置,不如启动python.exe, 那么就需要去python项目,找到main函数,如下所示:

#ifdef MS_WINDOWS
int
wmain(int argc, wchar_t **argv)
{
    return Py_Main(argc, argv);
}

在该方法上打断点就可以进行调试了。

整数定义调试

比如在cmd窗口,定义一个整数a = 123, 那么会进入下面这个函数,在这个函数大断点即可。

/* Create a new int object from a C long int */

PyObject *
PyLong_FromLong(long ival)
{
    PyLongObject *v;
    unsigned long abs_ival;
    unsigned long t;  /* unsigned so >> doesn't propagate sign bit */
    int ndigits = 0;
    int sign;

    if (IS_SMALL_INT(ival)) {
        return get_small_int((sdigit)ival);
    }

    if (ival < 0) {
        /* negate: can't write this as abs_ival = -ival since that
           invokes undefined behaviour when ival is LONG_MIN */
        abs_ival = 0U-(unsigned long)ival;
        sign = -1;
    }
    else {
        abs_ival = (unsigned long)ival;
        sign = ival == 0 ? 0 : 1;
    }

    /* Fast path for single-digit ints */
    if (!(abs_ival >> PyLong_SHIFT)) {
        v = _PyLong_New(1);
        if (v) {
            Py_SET_SIZE(v, sign);
            v->ob_digit[0] = Py_SAFE_DOWNCAST(
                abs_ival, unsigned long, digit);
        }
        return (PyObject*)v;
    }

#if PyLong_SHIFT==15
    /* 2 digits */
    if (!(abs_ival >> 2*PyLong_SHIFT)) {
        v = _PyLong_New(2);
        if (v) {
            Py_SET_SIZE(v, 2 * sign);
            v->ob_digit[0] = Py_SAFE_DOWNCAST(
                abs_ival & PyLong_MASK, unsigned long, digit);
            v->ob_digit[1] = Py_SAFE_DOWNCAST(
                  abs_ival >> PyLong_SHIFT, unsigned long, digit);
        }
        return (PyObject*)v;
    }
#endif

    /* Larger numbers: loop to determine number of digits */
    t = abs_ival;
    while (t) {
        ++ndigits;
        t >>= PyLong_SHIFT;
    }
    v = _PyLong_New(ndigits);
    if (v != NULL) {
        digit *p = v->ob_digit;
        Py_SET_SIZE(v, ndigits * sign);
        t = abs_ival;
        while (t) {
            *p++ = Py_SAFE_DOWNCAST(
                t & PyLong_MASK, unsigned long, digit);
            t >>= PyLong_SHIFT;
        }
    }
    return (PyObject *)v;
}

注意,先启动python, 再打断点,python启动时会加载各个数据类型。例如,我的调试过程:
在这里插入图片描述
按下alt 7, 打开堆栈,就可以查看调用流程了
在这里插入图片描述
根据调用流程可知,主要如下:
(1)从stdin获取输入
(2)解析字符串,建立了语法树AST(abstract syntax tree)
(3)解析语法树中的节点,判断字符为number,将字符串转化为C long int
(4)由C long int创建Python的int对象
这些代码想看懂,得有深厚的编译原理,数据结构与算法功力。

python与libffi

lib官网:https://www.sourceware.org/libffi/

什么是libffi?

libffi是一个可移植的外部函数接口库,高级语言的编译器生成遵循某些约定的代码。这些约定在一定程度上对于单独编译工作是必要的。其中一种约定是“调用约定”。“调用约定”是编译器做出的一组假设,即在函数入口处可以找到函数参数的位置。“调用约定”还指定在哪里可以找到函数的返回值。
一些程序在编译时可能不知道将哪些参数传递给函数。例如,解释器可能会在运行时被告知用于调用给定函数的参数的数量和类型。Libffi 可用于此类程序,以提供从解释器程序到已编译代码的桥梁。
libffi 库为各种调用约定提供了可移植的高级编程接口。这允许程序员在运行时调用由调用接口描述指定的任何函数。
FFI 代表外部功能接口。外部函数接口是接口的流行名称,它允许用一种语言编写的代码调用用另一种语言编写的代码。libffi 库实际上仅提供功能齐全的外部函数接口的最低的机器相关层。libffi 之上必须存在一个层,用于处理两种语言之间传递的值的类型转换。

谁使用它?

libffi 库对于任何试图在解释型和本机编译型代码之间建立桥梁的人都很有用。一些著名的用户包括:

  • CPython - Python编程语言的默认、最广泛使用的实现使用标准ctypes 库中的 libffi。
  • OpenJDK - Java平台标准版的开源实现使用 libffi 在某些平台的解释器和本机代码之间架起桥梁。
  • js-ctypes -Mozilla 将在Firefox 3.6 中提供的javascript外部函数接口。
  • Dalvik - Dalvik 是在Android移动设备上运行 Java 平台的虚拟机。libffi 用于未编写自定义桥接代码的 Android 端口。
  • Java Native Access (JNA) - 从Java调用本机代码的无 JNI 方式。
  • Ruby的FFI -外国功能接口扩展红宝石。
  • fsbv - 按值的外部结构是Common Lisp的外部函数接口库, 它扩展了标准 CFFI 包以支持按值传递结构参数。
  • JSCocoa - 从Mac OSX 和 iPhone 上的javascript调用 Objective-C 代码(通过libffi-iphone端口)。
  • PyObjC -在 Mac OSX 上从Python调用 Objective-C 代码。
  • RubyCocoa -在 Mac OSX 上从Ruby调用 Objective-C 代码。
  • Glasgow Haskell 编译器- 从这个流行的Haskell实现中调用 C 代码。
  • Racket - 从这个流行的Scheme实现调用 C 代码。
  • gcj - Java 编程语言的 GNU 编译器的运行时库使用 libffi 来处理解释和本地编译代码之间的来回调用。gcj 是GCC(GNU 编译器集合)的一部分。

_ctypes

cpython在_ctypes子项目依赖了libffi, 如下图:
在这里插入图片描述

python与c++的相互调用

python调用c++ dll

c++ dll代码

extern "C" CMATHDLL_API int add(int a, int b);
extern "C" CMATHDLL_API int sub(int a, int b);

python代码

import ctypes
import os
 
#获取dll路径
current_path = os.path.dirname(__file__)
dllpath = os.path.join(current_path, "CMATHDLL.dll")
 
print(dllpath)
 
#加载C++ dll
pDll = ctypes.WinDLL(dllpath)
 
#打印dll地址
print(pDll)
 
#调用CMATHDLL.dll里的函数
pReault1 = pDll.add(1,2)
pReault2 = pDll.sub(1,2)
 
print(pReault1)
print(pReault2)

c++调用python

/*
C++ 程序如何调用Pyhton程序
*/
 
#include "stdafx.h"
#include <iostream>
#include "Python.h"
 
using namespace std;
 
 
//导入python静态库
#pragma comment(lib, "python37.lib")
 
int main()
{
	PyObject* pName = NULL;
	PyObject* pModule = NULL;
	PyObject* pDict = NULL;
	PyObject* pFunc = NULL;
	PyObject* pArgs = NULL;
	PyObject* pRet = NULL;
 
	// 1 初始化Python, 在使用Python系统前,必须使用Py_Initialize对其进行初始化;
	Py_Initialize();
 
	// 2 检查初始化是否成功, 返回0初始化失败
	int nRet = Py_IsInitialized();
	if (nRet == 0)
	{
		cout << "Python环境配置错误,初始化失败" << endl;
		return -1;
	}
 
	// 添加当前路径
	PyRun_SimpleString("import sys");
	PyRun_SimpleString("sys.path.append('./')");
 
	//3 加载python文件
	pModule = PyImport_ImportModule("Hello");   //在使用这个函数的时候,只需要写Hello.py文件的名称就可以了, 不用写后缀
 
	if (!pModule)
	{
		cout << "can't find Hello.py" << endl;
		return -1;
	}
 
	//4 导出Hello.py中的 Add方法
	pFunc = PyObject_GetAttrString(pModule, "Add");
 
	//5 传参,Add函数有两个参数,传入数量2
	pArgs = PyTuple_New(2);
 
	//0:表示序号,第一个参数; 1:表示第二个参数
	//i:表示传入的参数类型时int类型
	PyTuple_SetItem(pArgs, 0, Py_BuildValue("i", 2));  // 2 表示参数2
	PyTuple_SetItem(pArgs, 1, Py_BuildValue("i", 4));  // 4 表示参数4
 
	//6 正式执行Add函数
	PyObject *pReturn = PyEval_CallObject(pFunc, pArgs);
 
	int  nResult;
	PyArg_Parse(pReturn, "i", &nResult);  //i表示转换成int型变量
 
	cout << "2 + 4 = " << nResult << endl;
 
	//7 释放Python
	Py_Finalize();
 
	system("pause");
	return 0;
}

python解释器官方学习资料

例如python字节码与反编译:https://docs.python.org/3/library/dis.html
在这里插入图片描述
关于python源码的介绍,就以上这么多了,本人能力有限,有做不了太多的源码分析,有能力的大佬可以花时间去看看它的实现并分享,让众多码农在写代码之余也有点学习的乐趣。

  • 6
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

令狐掌门

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值