MicroPython .mpy 파일

MicroPython은 .mpy 파일이라는 개념을 정의합니다. 이는 미리 컴파일된 코드를 담는 바이너리 컨테이너 파일 형식으로, 일반적인 .py 모듈처럼 임포트할 수 있습니다. 파일 foo.mpy는 임포트 메커니즘이 일반적인 방식으로 foo.mpy를 찾을 수 있는 한 import foo를 통해 임포트할 수 있습니다. 보통 sys.path에 나열된 각 디렉터리가 순서대로 검색됩니다. 특정 디렉터리를 검색할 때는 먼저 foo.py를 찾고, 이를 찾지 못하면 foo.mpy를 찾으며, 둘 다 찾지 못하면 다음 디렉터리에서 검색을 계속합니다. 따라서 foo.pyfoo.mpy보다 우선합니다.

이러한 .mpy 파일은 bytecode를 포함할 수 있으며, 이는 보통 mpy-cross 프로그램을 통해 Python 소스 파일(.py 파일)로부터 생성됩니다. 일부 아키텍처의 경우 .mpy 파일에는 네이티브 머신 코드도 포함될 수 있으며, 이는 다양한 방법으로 생성할 수 있는데 가장 대표적으로 C 소스 코드로부터 생성됩니다.

mpy-cross 컴파일러

mpy-cross.py 소스 파일을 캠에서 임포트할 수 있는 .mpy 바이너리 컨테이너로 변환하는 크로스 컴파일러입니다. 이는 MicroPython 소스 트리(캠 펌웨어를 빌드하는 데 사용되는 것과 동일한 트리)의 일부이며, 전체 펌웨어 체크아웃 없이 호스트 측에서 사용할 수 있도록 pip 패키지로도 배포됩니다:

$ pip install --user mpy-cross

또는 pipx를 통해:

$ pipx install mpy-cross

설치한 후에는 단일 소스 파일에 대해 실행합니다:

$ mpy-cross foo.py

이렇게 하면 현재 디렉터리에 foo.mpy가 생성되어, 다른 모듈과 함께 캠의 파일 시스템에 복사하거나 ROMFS 이미지에 입력할 준비가 됩니다.

가장 유용한 명령줄 옵션:

  • -o <path> – 생성된 .mpy의 출력 경로(기본값은 확장자를 바꾼 입력 파일 이름이며, -o -는 stdout에 씁니다).

  • -O<n> – 최적화 수준 0부터 3까지. 기본값 0은 어설션과 전체 소스 위치를 유지합니다. 3은 어설션과 docstring을 제거하고 if __debug__ 블록을 재작성합니다. 이 수준은 런타임이 노출하는 것과 동일한 micropython.opt_level 인터페이스를 제어합니다.

  • -march=<arch>@native@viper 데코레이터가 적용된 함수에 대한 대상 네이티브 아키텍처입니다. 소스가 이러한 데코레이터를 사용할 때 필요합니다. 값은 캠의 MCU 클래스와 일치해야 합니다. mpy-cross --help가 출력하는 목록에서 선택하거나, 런타임에 sys.implementation._mpy로 캠에서 읽어내십시오.

  • -s <path>.mpy의 디버그 정보에 임베드되는 소스 경로 문자열입니다. 디스크상의 경로가 트레이스백에서 파일이 표시되어야 할 임포트 경로와 다를 때 유용합니다.

  • -X emit=bytecode|native|viper – 모듈 전체에 대한 기본 에미터를 선택합니다(함수별 @native / @viper 데코레이터의 대안).

  • --version – 이 바이너리가 생성하는 .mpy 형식 버전을 출력합니다. 이 번호는 캠 런타임이 지원하는 버전(아래 릴리스 표 참조)과 일치해야 하며, 그렇지 않으면 임포트 시 ValueError('incompatible .mpy file')가 발생합니다.

전체 플래그 목록은 mpy-cross --help를 실행하십시오.

pip 패키지는 또한 작은 Python 모듈 API를 노출하여, 빌드 스크립트가 직접 서브프로세스를 포크하는 대신 프로세스 내에서 컴파일러를 구동할 수 있게 합니다:

import mpy_cross

mpy_cross.compile('foo.py', dest='build/foo.mpy', opt=3,
                  march=mpy_cross.NATIVE_ARCH_ARMV7EMSP)

mpy_cross.compile, mpy_cross.run, mpy_cross.mpy_version이 세 가지 진입점입니다. mpy_cross.CrossCompileError는 문제가 발생했을 때 컴파일러의 stderr를 담습니다. 아키텍처 상수(NATIVE_ARCH_ARMV7EMSP, NATIVE_ARCH_ARMV7EMDP 등)는 -march 플래그가 허용하는 문자열과 일치합니다.

.mpy 파일의 버전 관리 및 호환성

특정 .mpy 파일은 특정 MicroPython 시스템과 호환될 수도 있고 그렇지 않을 수도 있습니다. 호환성은 다음을 기준으로 합니다:

  • .mpy 파일의 버전: 파일의 버전은 이를 로드하는 시스템이 지원하는 버전과 일치해야 합니다.

  • .mpy 파일의 하위 버전: .mpy 파일에 네이티브 머신 코드가 포함되어 있으면 파일의 하위 버전이 이를 로드하는 시스템이 지원하는 버전과 일치해야 합니다. 그렇지 않고 .mpy 파일에 네이티브 머신 코드가 없으면 로드 시 하위 버전은 무시됩니다.

  • Small integer 비트: .mpy 파일은 small integer에 최소한의 비트 수를 요구하며, 이를 로드하는 시스템은 최소한 이 비트 수를 지원해야 합니다.

  • 네이티브 아키텍처: .mpy 파일에 네이티브 머신 코드가 포함되어 있으면 해당 머신 코드의 아키텍처를 지정하며, 이를 로드하는 시스템은 해당 아키텍처 코드의 실행을 지원해야 합니다.

MicroPython 시스템이 .mpy 파일 임포트를 지원하는 경우 sys.implementation._mpy 필드가 존재하며, 버전(하위 8비트), 기능 및 네이티브 아키텍처를 인코딩한 정수를 반환합니다.

처음 네 가지 테스트 중 하나에 실패하는 .mpy 파일을 임포트하려고 하면 ValueError('incompatible .mpy file')가 발생합니다. (네이티브 머신 코드를 포함하는 경우) 네이티브 아키텍처 테스트에 실패하는 .mpy 파일을 임포트하려고 하면 ValueError('incompatible .mpy arch')가 발생합니다.

.mpy 파일 임포트에 실패하면 다음을 시도해 보십시오:

  • 다음을 실행하여 MicroPython 시스템이 지원하는 .mpy 버전과 플래그를 확인하십시오:

    import sys
    sys_mpy = sys.implementation._mpy
    arch = [None, 'x86', 'x64',
        'armv6', 'armv6m', 'armv7m', 'armv7em', 'armv7emsp', 'armv7emdp',
        'xtensa', 'xtensawin', 'rv32imc', 'rv64imc'][(sys_mpy >> 10) & 0x0F]
    print('mpy version:', sys_mpy & 0xff)
    print('mpy sub-version:', sys_mpy >> 8 & 3)
    print('mpy flags:', end='')
    if arch:
        print(' -march=' + arch, end='')
    if (sys_mpy >> 16) != 0:
        print(' -march-flags=' + (sys_mpy >> 16), end='')
    print()
    
  • 파일의 처음 두 바이트를 검사하여 .mpy 파일의 유효성을 확인하십시오. 첫 번째 바이트는 대문자 ‘M’이어야 하고 두 번째 바이트는 버전 번호로, 위에서 확인한 시스템 버전과 일치해야 합니다. 일치하지 않으면 .mpy 파일을 다시 빌드하십시오.

  • 시스템의 .mpy 버전이 해당 .mpy 파일을 빌드하는 데 사용된 mpy-cross가 생성하는 버전과 일치하는지 확인하십시오. 이는 mpy-cross --version으로 확인할 수 있습니다. 일치하지 않으면 mpy-cross --version이 보고하는 태그(또는 해시)에서 체크아웃한 Git 저장소로부터 mpy-cross를 다시 컴파일하십시오.

  • 위 코드로 확인하거나, 사용 중인 포트의 MPY_CROSS_FLAGS Makefile 변수를 검사하여 올바른 mpy-cross 플래그를 사용하고 있는지 확인하십시오.

  • .mpy 파일의 세 번째 바이트에 비트 #6이 설정되어 있으면, 인코딩된 아키텍처별 플래그 비트 vuint가 파일을 임포트하는 대상과 호환되는지 확인하십시오.

다음 표는 MicroPython 릴리스와 .mpy 버전 간의 대응 관계를 보여줍니다.

MicroPython 릴리스

.mpy 버전

v1.23.0 이상

6.3

v1.22.x

6.2

v1.20 - v1.21.0

6.1

v1.19.x

6

v1.12 - v1.18

5

v1.11

4

v1.9.3 - v1.10

3

v1.9 - v1.9.2

2

v1.5.1 - v1.8.7

0

완전성을 위해, 다음 표는 .mpy 버전이 변경된 메인 MicroPython 저장소의 Git 커밋을 보여줍니다.

.mpy 버전 변경

Git 커밋

6.2에서 6.3로

bdbc869f9ea200c0d28b2bc7bfb60acd9d884e1b

6.1에서 6.2로

6967ff3c581a66f73e9f3d78975f47528db39980

6에서 6.1로

d94141e1473aebae0d3c63aeaa8397651ad6fa01

5에서 6으로

f2040bfc7ee033e48acef9f289790f3b4e6b74e5

4에서 5로

5716c5cf65e9b2cb46c2906f40302401bdd27517

3에서 4로

9a5f92ea72754c01cc03e5efcdfe94021120531e

2에서 3으로

ff93fd4f50321c6190e1659b19e64fef3045a484

1에서 2로

dd11af209d226b7d18d5148b239662e30ed60bad

0에서 1로

6a11048af1d01c78bdacddadd1b72dc7ba7c6478

초기 버전 0

d8c834c95d506db979ec871417de90b7951edc30

.mpy 파일의 바이너리 인코딩

MicroPython .mpy 파일은 코드 객체(바이트코드 및 네이티브 머신 코드)가 중첩된 계층 구조로 내부에 저장되는 바이너리 컨테이너 형식입니다. 외부 모듈의 코드가 먼저 저장되고, 그다음 자식들이 따라옵니다. 각 자식은 추가 자식을 가질 수 있는데, 예를 들어 클래스가 메서드를 갖거나 함수가 람다 또는 컴프리헨션을 정의하는 경우가 그렇습니다. 가능한 값의 범위를 넓게 제공하면서도 파일을 작게 유지하기 위해, 많은 곳에서 가변 인코딩 부호 없는 정수(variably-encoded-unsigned-integer, vuint) 개념을 사용합니다. UTF-8 인코딩과 유사하게, 이 인코딩은 바이트당 7비트를 저장하며 하나 이상의 바이트가 뒤따르는 경우 8번째 비트(MSB)를 설정합니다. 부호 없는 정수의 비트는 vuint에 LSB 형식으로 저장됩니다.

.mpy 파일의 최상위 레벨은 세 부분으로 구성됩니다:

  • 헤더.

  • 전역 qstr 및 상수 테이블.

  • 모듈 외부 스코프의 raw-code. 이 외부 스코프는 .mpy 파일이 임포트될 때 실행됩니다.

예를 들어 mpy-tool.py를 사용하여 .mpy 파일의 내용을 검사할 수 있습니다(메인 MicroPython 저장소의 루트에서 실행):

$ ./tools/mpy-tool.py -xd myfile.mpy

헤더

.mpy 헤더는 다음과 같습니다:

크기

필드

바이트

값 0x4d (ASCII ‘M’)

바이트

.mpy 메이저 버전 번호

바이트

기능 플래그, 네이티브 아키텍처, 마이너 버전 번호(이전 버전에서는 기능 플래그였음)

바이트

small int의 비트 수

세 번째 바이트는 다음과 같이 나뉩니다(MSB 우선):

비트

의미

7

예약됨, 반드시 0이어야 함

6

헤더 다음에 아키텍처별 플래그 vuint가 따라옴

5..2

네이티브 아키텍처 번호

1..0

마이너 버전 번호

아키텍처별 플래그

헤더의 기능 플래그 바이트에서 비트 #6이 설정되어 있으면, 선택적 아키텍처별 정보를 담은 vuint가 헤더 다음에 따라옵니다. 이 정수의 내용은 파일이 대상으로 하는 네이티브 아키텍처에 따라 달라집니다.

현재 이는 MPY 파일이 I, M, C, Zicsr 외에 올바르게 작동하기 위해 필요한 RISC-V 프로세서 확장을 저장하는 데 사용됩니다. ArmV7의 여러 종류는 네이티브 아키텍처 번호로 식별되지만, 그 메커니즘을 재사용하면 RV32와 RV64에서는 일이 복잡해집니다.

특정 프로세서 확장이 필요하지 않은 RV32 또는 RV64 대상 MPY 파일은 플래그 정수를 제공할 필요가 없습니다(헤더에서 해당 비트를 설정하는 것과 함께). RV32 및 RV64 MPY 파일에 플래그 값이 없다는 것은 특정 확장이 필요하지 않음을 나타내는 데 사용되며, 최종 출력 바이너리에서 1바이트를 절약합니다.

MPY 파일을 만들 때 이 값을 설정하려면 mpy-tool.pympy-cross 양쪽의 -march-flags 명령줄 옵션, 그리고 mpy_ld.py--arch-flags 명령줄 옵션도 참조하십시오.

전역 qstr 및 상수 테이블

.mpy 파일은 하나의 qstr 테이블과 하나의 상수 객체 테이블을 포함합니다. 이들은 .mpy 파일에 대해 전역적이며, 모든 중첩된 raw-code 객체에 의해 참조됩니다. qstr 테이블은 내부 qstr 번호(.mpy 파일 내부)를 .mpy 파일이 임포트되는 런타임의 해석된 qstr 번호에 매핑합니다. 이는 .mpy 파일을 그것이 실행되는 시스템의 나머지 부분과 연결합니다. 상수 객체 테이블은 .mpy 파일이 필요로 하는 모든 상수 객체에 대한 참조로 채워집니다.

크기

필드

vuint

qstr의 수

vuint

상수 객체의 수

qstr 데이터

인코딩된 상수 객체

Raw code 요소

raw-code 요소는 바이트코드 또는 네이티브 머신 코드 중 하나의 코드를 포함합니다. 그 내용은 다음과 같습니다:

크기

필드

vuint

타입, 크기 및 하위 raw-code 요소의 존재 여부

코드(바이트코드 또는 머신 코드)

vuint

하위 raw-code 요소의 수(0이 아닌 경우에만)

하위 raw-code 요소

raw-code 요소의 첫 번째 vuint는 이 요소에 저장된 코드의 타입(최하위 두 비트), 이 raw-code에 자식이 있는지 여부(세 번째 최하위 비트), 그리고 뒤따르는 코드의 길이(이를 위해 할당할 RAM 양)를 인코딩합니다.

vuint 다음에는 코드 자체가 옵니다. 코드 타입이 재배치(relocation)가 있는 viper 코드가 아닌 한, 이 코드는 상수 데이터이며 수정할 필요가 없습니다.

이 raw-code에 자식이 있는 경우(첫 번째 vuint의 비트로 표시됨), 코드 다음에 하위 raw-code 요소의 수를 세는 vuint가 옵니다.

마지막으로 모든 하위 raw-code 요소가 재귀적으로 저장됩니다.