10.2. 字节码和字节码库启动

10.2.1. 需求场景

在嵌入式应用中,使用 PikaPython 频繁地从文件系统加载和编译 Python 源文件,尤其是对于较大的文件,不仅会导致程序启动时间过长,还可能增加存储介质(如闪存)的磨损。这是因为每次编译都会写入新生成的文件,而闪存等存储介质的写入次数有限,过多的写入操作会缩短其寿命。因此,为了提高效率、减少启动时间,并减少对存储介质的磨损,引入字节码和字节码库(PIKA APP软件包)作为一种有效的解决方案。

使用字节码和字节码库可以减少频繁的编译过程,因为字节码文件和字节码库文件只需要在第一次编译时生成,后续只需加载这些已编译的文件。这种方法不仅提高了程序的运行效率,也显著减少了对存储介质的写入操作,从而延长了其使用寿命。

10.2.2. 概念解释:Python源码编译与解释执行

10.2.2.1. PikaPython 编译概念

在 Python中,源码的编译不同于传统编译型语言(如C或C++)中的概念。Python是一种被广泛认为是解释型的语言,但实际上,它在执行前将源码编译成字节码。

这一过程与传统编译型语言将代码编译成机器码的过程有所不同。Python 的编译过程主要涉及以下步骤:

  1. 解析: 解释器首先将 Python 源代码解析成一种称为抽象语法树(Abstract Syntax Tree,AST)的中间形式。

  2. 编译: 接着,将 AST 编译成字节码。这种字节码不是机器码,而是一种更高级的、与平台无关的代码。

  3. 执行: 最后,PikaPython 虚拟机(PikaVM)执行这些字节码。

10.2.2.2. PikaPython的执行流程

PikaPython 提供了在目标平台(如MCU)上直接编译和解释执行 Python 源码的能力。这意味着 PikaPython 可以完全在 MCU 上处理 Python 源代码的编译到字节码的过程,然后运行这些字节码。

这一特性使得 PikaPython 在嵌入式系统中尤为有用,因为它允许开发者在资源有限的环境中直接使用 Python 编程,而无需依赖外部的编译工具链。具体来说:

  • 在MCU中编译: PikaPython 允许在目标 MCU 平台上直接将 Python 源代码编译成字节码。

  • 解释执行: 解释执行本质上是指编译 Python 源码到字节码,然后由 Python 虚拟机运行这些字节码的完整流程。

但是,解释执行意味着每次启动 Python 脚本时都要完成完整的 编译->执行 的流程,如果可以跳过编译过程,直接执行,那么就能节省编译的耗时和对存储介质的磨损。

跳过编译过程的方式有:

  1. 直接启动目标 MCU 之前编译产生的字节码。

  2. 在 PC 上编译字节码,在目标 MCU 启动。

10.2.3. 字节码与字节码库

10.2.3.1. 字节码(.py.o)

  • 定义: PikaPython 的字节码是 Python 源代码编译后的中间表示形式,以 .py.o 格式存储。

  • 生成路径: 默认生成于pikascript-api/路径下,每个Python文件生成一个同名的.py.o文件。

  • 使用: 加快单个脚本的加载和执行速度。

10.2.3.2. 字节码库(PIKA APP软件包)

  • 定义: PikaPython 字节码库(亦称为PIKA APP软件包)是多个字节码文件的集合,格式为.py.a

  • 生成路径: 所有.py.o文件被打包为一个pikascript-api/pikaModules.py.a文件。

  • 使用: 适用于同时加载多个编译模块的复杂应用,便于整体管理和部署。

  • OTA更新优势: 在MCU应用中,通过OTA发送单个字节码库文件以更新整个PikaPython应用非常便利。这意味着无需逐一发送零散的Python源码文件,从而简化了更新流程,并减少了数据传输量。

  • 应用管理优势: 字节码库类似于Windows上的.exe文件,每个PIKA APP软件包都代表一个完整且可以独立启动的应用。在具有多种应用需求的场景中,可以在目标系统放置多个独立的APP软件包,从而方便地选择和切换运行的应用。这避免了由于零散的Python源码文件可能导致的命名冲突和项目管理问题。

10.2.4. 加载方式

10.2.4.1. 加载单个字节码(字节码数组)

将单个字节码文件 .py.o 中的数据,传入 pikaVM_runByteCode() API可以就直接运行单个字节码,可以参考g030中从字节码启动的用法。 https://gitee.com/Lyon1998/pikapython/blob/master/bsp/stm32g030c8/Booter/main.c

注意

  1. pikaVM_runByteCode() API 运行的字节码必须是 const 类型,如果字节码不是 const,则需要使用 pikaVM_runByteCodeInconstant()

  2. 如果已经对接了文件系统,可以使用 pikaVM_runByteCodeFile() API 直接运行 .py.o 文件。

  3. pikaVM_runByteCodeInconstant()pikaVM_runByteCodeFile() 需要内核版本 >=v1.11.7

10.2.4.2. 加载单个字节码文件

// 示例:加载单个字节码文件
PikaObj *pikaMain = newRootObj("pikaMain", New_PikaMain);
pikaVM_runBytecodeFile(pikaMain, "pikascript-api/example.py.o");

此示例展示了如何从pikascript-api/路径下加载特定的单个字节码文件。

10.2.4.3. 加载字节码库(PIKA APP软件包)并启动指定字节码

// 示例:加载字节码库并启动指定字节码
PikaObj *pikaMain = newRootObj("pikaMain", New_PikaMain);
obj_linkLibraryFile(pikaMain, "pikascript-api/pikaModules.py.a");

// 启动字节码库中的特定字节码
obj_runModule(pikaMain, "main");

在这个示例中,使用 obj_linkLibraryFile 函数来加载PIKA APP软件包,并通过obj_runModule 函数来启动字节码库中的指定模块,通常是从 main 模块启动(对应的 Python 源码文件是 main.py。

10.2.5. 在 PC 上将 Python 转为字节码:

编译字节码的过程也可以在 PC 完成,因为性能较弱的 MCU 在编译字节码时会需要花费很长的时间。

_images/1639281281608-011ffd89-5851-47d8-9dca-438ed963f5d4-164649975346225.png

预编译器 rust-msc-latest-win10.exe 集成了字节码生成器,在预编译时会将 main.py 和被 main.py import (包括间接 import) 的 .py 文件编译为字节码,生成的字节码文件在 pikascript-api 文件夹下。

.py 文件会被生成为 .py.o 字节码文件,例如 main.py 会生成 pikascript-api/main.py.o

同时,所有的 .py.o 文件会被自动打包成一个库文件 pikascript-api/pikaModules.py.a,库文件中包含了所有的字节码文件。

_images/image-20220823155644618-16612416630901-16612416855853.png

为了方便没有文件系统的 MCU 通过 C 语言编译的方式加载字节码库,预编译器还会把库文件自动转换为 C 的字节数组文件 pikascript-api/__asset_pikaModules_py_a.c

/* __asset_pikaModules_py_a.c */
#include "PikaPlatform.h"
/* warning: auto generated file, please do not modify */
PIKA_BYTECODE_ALIGN const unsigned char pikaModules_py_a[] = {
    0x7f, 0x70, 0x79, 0x61, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x6d, 0x61, 0x69, 0x6e, 
...