.mpy 파일의 네이티브 머신 코드

이 절에서는 Python이 아닌 다른 언어로 작성된 네이티브 머신 코드를 포함하는 .mpy 파일을 빌드하고 다루는 방법을 설명합니다. 이를 통해 C와 같은 언어로 코드를 작성하고, 이를 컴파일 및 링크하여 .mpy 파일로 만든 다음, 일반 Python 모듈처럼 이 파일을 import할 수 있습니다. 이는 성능이 중요한 기능을 구현하거나, 다른 언어로 작성된 기존 라이브러리를 포함하는 데 사용할 수 있습니다.

네이티브 .mpy 파일을 사용하는 주요 이점 중 하나는 메인 MicroPython 펌웨어를 다시 빌드할 필요 없이 스크립트가 네이티브 머신 코드를 동적으로 import할 수 있다는 점입니다. 이는 C로 사용자 정의 모듈을 정의할 수 있지만 메인 펌웨어 이미지에 컴파일되어야 하는 MicroPython 외부 C 모듈 와는 대조적입니다.

여기서는 C를 사용하여 네이티브 모듈을 빌드하는 데 초점을 맞추지만, 원칙적으로 독립 실행형 머신 코드로 컴파일할 수 있는 모든 언어를 .mpy 파일에 넣을 수 있습니다.

네이티브 .mpy 모듈은 프로젝트의 tools/ 디렉터리에 있는 mpy_ld.py 도구를 사용하여 빌드됩니다. 이 도구는 일련의 오브젝트 파일(.o 파일)을 받아 함께 링크하여 네이티브 .mpy 파일을 만듭니다. CPython 3과 pyelftools v0.25 이상의 라이브러리가 필요합니다.

지원되는 기능과 제한 사항

.mpy 파일은 MicroPython 바이트코드 및/또는 네이티브 머신 코드를 포함할 수 있습니다. 네이티브 머신 코드를 포함하는 경우 .mpy 파일에는 특정 아키텍처가 연결됩니다. 현재 지원되는 아키텍처는 다음과 같습니다(이는 ARCH 변수에 사용할 수 있는 유효한 옵션입니다. 아래 참조):

  • x86 (32비트)

  • x64 (64비트 x86)

  • armv6m (ARM Thumb, 예: Cortex-M0)

  • armv7m (ARM Thumb 2, 예: Cortex-M3)

  • armv7emsp (ARM Thumb 2, 단정밀도 부동소수점, 예: Cortex-M4F, Cortex-M7)

  • armv7emdp (ARM Thumb 2, 배정밀도 부동소수점, 예: Cortex-M7)

  • xtensa (비윈도우, 예: ESP8266)

  • xtensawin (윈도우 크기 8의 윈도우 방식, 예: ESP32, ESP32S3)

  • rv32imc (압축 명령어를 사용하는 32비트 RISC-V, 예: ESP32C3, ESP32C6)

  • rv64imc (압축 명령어를 사용하는 64비트 RISC-V)

선택한 플랫폼이 명시적 아키텍처 플래그를 지원하고 출력 .mpy 파일이 해당 플래그 값을 갖도록 하려면, .mpy 파일을 빌드할 때 ARCH_FLAGS 플래그 변수에 이를 전달해야 합니다.

네이티브 .mpy 파일을 컴파일하고 링크할 때 아키텍처를 선택해야 하며, 해당 파일은 그 아키텍처에서만(그리고 아키텍처 플래그가 있는 경우 대상의 기능과 일치하는 경우에만) import할 수 있습니다. .mpy 파일에 대한 자세한 내용은 MicroPython .mpy 파일 를 참조하십시오.

네이티브 코드는 위치 독립 코드(PIC)로 컴파일되어야 하고 전역 오프셋 테이블(GOT)을 사용해야 하지만, 그 세부 사항은 아키텍처마다 다릅니다. 네이티브 코드가 포함된 .mpy 파일을 import할 때 import 메커니즘은 네이티브 코드에 대한 기본적인 재배치를 수행할 수 있습니다. 여기에는 text, rodata, BSS 섹션의 재배치가 포함됩니다.

링커와 동적 로더가 지원하는 기능은 다음과 같습니다:

  • 실행 가능한 코드(text)

  • 읽기 전용 데이터(rodata), 문자열 및 상수 데이터(배열, 구조체 등) 포함

  • 0으로 초기화된 데이터(BSS)

  • text에서 text, rodata, BSS를 가리키는 포인터

  • rodata에서 text, rodata, BSS를 가리키는 포인터

알려진 제한 사항은 다음과 같습니다:

  • data 섹션은 지원되지 않습니다. 해결 방법: BSS 데이터를 사용하고 데이터 값을 명시적으로 초기화하십시오

  • 정적 BSS 변수는 지원되지 않습니다. 해결 방법: 전역 BSS 변수를 사용하십시오

  • 스레드 로컬 저장소 변수는 rv32imc에서 지원되지 않습니다. 해결 방법: 전역 BSS 변수를 사용하거나 힙에 일부 공간을 할당하여 저장하십시오

따라서 C 코드에 쓰기 가능한 데이터가 있는 경우, 해당 데이터가 초기화자 없이 전역적으로 정의되고 함수 내에서만 기록되도록 하십시오.

네이티브 모듈은 libm.alibgcc.a 와 같은 표준 정적 라이브러리에 자동으로 링크되지 않으므로 undefined symbol 오류가 발생할 수 있습니다. Makefile에서 LINK_RUNTIME = 1 을 설정하여 런타임 라이브러리를 링크할 수 있습니다. MPY_LD_FLAGS += -l path/to/library.a 를 추가하여 사용자 정의 정적 라이브러리도 링크할 수 있습니다. 이들은 네이티브 모듈에 링크되며 다른 모듈이나 시스템과 공유되지 않는다는 점에 유의하십시오.

링커 제한 사항: 네이티브 모듈은 전체 MicroPython 펌웨어의 심볼 테이블에 링크되지 않습니다. 대신, mp_fun_table (py/nativeglue.h 내)에 있는 익스포트된 심볼의 명시적 테이블에 링크되며, 이는 펌웨어 빌드 시점에 고정됩니다. 따라서 예를 들어 고정된 주소에 있지 않는 한 임의의 HAL/OS/RTOS/시스템 함수를 단순히 호출하는 것은 불가능합니다. 그러한 경우, 일련의 심볼 이름과 그 고정 주소를 포함하는 링커 스크립트의 경로를 --externs 명령줄 인수를 통해 mpy_ld.py 에 전달할 수 있습니다. 그러면 링커 스크립트에 나타나는 심볼이 오브젝트 파일에서 제공되는 것보다 우선하지만, 현재로서는 오브젝트 파일의 구현이 여전히 최종 MPY 파일에 남아 있습니다. 링커 스크립트 파서는 기능이 제한되어 있으며, 현재 ESP8266 포트 ROM 심볼 목록을 파싱하는 데만 사용됩니다(ports/esp8266/boards/eagle.rom.addr.v6.ld 참조).

새 심볼을 테이블 끝에 추가하고 펌웨어를 다시 빌드할 수 있습니다. 심볼은 동일한 위치에서 tools/mpy_ld.pyfun_table dict에도 추가해야 합니다. 이를 통해 mpy_ld.py 가 새 심볼을 인식하고 mpy를 import할 때 그에 대한 재배치를 제공할 수 있습니다. 마지막으로, 심볼이 함수인 경우 함수를 쉽게 호출할 수 있도록 py/dynruntime.h 에 매크로나 스텁을 추가해야 합니다.

네이티브 모듈 정의하기

네이티브 .mpy 모듈은 .mpy를 빌드하는 데 사용되는 일련의 파일로 정의됩니다. 파일시스템 레이아웃은 소스 파일과 Makefile의 두 가지 주요 부분으로 구성됩니다:

  • 가장 단순한 경우에는 단일 C 소스 파일 하나만 필요하며, 이 파일에는 .mpy 모듈로 컴파일될 모든 코드가 포함됩니다. 이 C 소스 코드는 MicroPython 동적 API에 접근하기 위해 py/dynruntime.h 파일을 include해야 하며, 최소한 mpy_init 이라는 함수를 정의해야 합니다. 이 함수는 모듈의 진입점이 되며, 모듈이 import될 때 호출됩니다.

    원하는 경우 모듈을 여러 C 소스 파일로 분할할 수 있습니다. 모듈의 일부를 Python으로 구현할 수도 있습니다. 모든 소스 파일은 SRC 변수에 추가하여 Makefile에 나열해야 합니다(아래 참조). 여기에는 C 소스 파일뿐만 아니라 결과 .mpy 파일에 포함될 모든 Python 파일도 포함됩니다.

  • Makefile 에는 모듈의 빌드 구성이 포함되어 있으며 .mpy 모듈을 빌드하는 데 사용되는 소스 파일이 나열됩니다. MPY_DIR 을 MicroPython 저장소의 위치로(헤더 파일, 관련 Makefile 조각, mpy_ld.py 도구를 찾기 위해), MOD 를 모듈 이름으로, SRC 를 소스 파일 목록으로 정의해야 하며, 선택적으로 ARCH 를 통해 머신 아키텍처를 지정하고 ARCH_FLAGS 를 통해 선택적 머신 아키텍처 플래그를 지정한 다음, py/dynruntime.mk 를 include해야 합니다.

최소 예제

이 절에서는 factorial 이라는 간단한 모듈의 완전히 동작하는 예제를 제공합니다. 이 모듈은 입력값의 팩토리얼을 계산하여 결과를 반환하는 단일 함수 factorial.factorial(x) 를 제공합니다.

디렉터리 레이아웃:

factorial/
├── factorial.c
└── Makefile

factorial.c 파일의 내용은 다음과 같습니다:

// Include the header file to get access to the MicroPython API
#include "py/dynruntime.h"

// Helper function to compute factorial
static mp_int_t factorial_helper(mp_int_t x) {
    if (x == 0) {
        return 1;
    }
    return x * factorial_helper(x - 1);
}

// This is the function which will be called from Python, as factorial(x)
static mp_obj_t factorial(mp_obj_t x_obj) {
    // Extract the integer from the MicroPython input object
    mp_int_t x = mp_obj_get_int(x_obj);
    // Calculate the factorial
    mp_int_t result = factorial_helper(x);
    // Convert the result to a MicroPython integer object and return it
    return mp_obj_new_int(result);
}
// Define a Python reference to the function above
static MP_DEFINE_CONST_FUN_OBJ_1(factorial_obj, factorial);

// This is the entry point and is called when the module is imported
mp_obj_t mpy_init(mp_obj_fun_bc_t *self, size_t n_args, size_t n_kw, mp_obj_t *args) {
    // This must be first, it sets up the globals dict and other things
    MP_DYNRUNTIME_INIT_ENTRY

    // Make the function available in the module's namespace
    mp_store_global(MP_QSTR_factorial, MP_OBJ_FROM_PTR(&factorial_obj));

    // This must be last, it restores the globals dict
    MP_DYNRUNTIME_INIT_EXIT
}

Makefile 파일의 내용은 다음과 같습니다:

# Location of top-level MicroPython directory
MPY_DIR = ../../..

# Name of module
MOD = factorial

# Source files (.c or .py)
SRC = factorial.c

# Architecture to build for (x86, x64, armv6m, armv7m, xtensa, xtensawin, rv32imc, rv64imc)
ARCH = x64

# Include to get the rules for compiling and linking the module
include $(MPY_DIR)/py/dynruntime.mk

모듈 컴파일하기

네이티브 .mpy 파일을 빌드하는 데 필요한 사전 도구는 다음과 같습니다:

  • MicroPython 저장소(최소한 py/tools/ 디렉터리).

  • CPython 3 및 pyelftools 라이브러리(예: pip install 'pyelftools>=0.25').

  • GNU make.

  • 대상 아키텍처용 C 컴파일러(C 소스를 사용하는 경우).

  • 선택적으로 MicroPython 저장소에서 빌드한 mpy-cross (.py 소스를 사용하는 경우).

실행할 대상에 맞는 올바른 ARCH 를 선택했는지 확인하십시오. 그런 다음 다음과 같이 빌드하십시오:

$ make

Makefile을 수정하지 않고도 다음과 같이 대상 아키텍처를 지정할 수 있습니다:

$ make ARCH=armv7m

선택적 아키텍처 플래그에도 다음과 같이 동일하게 적용됩니다:

$ make ARCH=rv32imc ARCH_FLAGS=zba

MicroPython에서 모듈 사용하기

모듈을 빌드하고 나면 factorial.mpy 라는 파일이 생성됩니다. 이를 복사하여 MicroPython 시스템의 파일시스템에서 접근할 수 있고 import 경로에서 찾을 수 있도록 하십시오. 이제 다른 모듈과 마찬가지로 Python에서 이 모듈에 접근할 수 있습니다. 예를 들면:

import factorial
print(factorial.factorial(10))
# should display 3628800

모듈을 빌드할 때 Picolibc 사용하기

Picolibc 를 C 표준 라이브러리로 사용하는 것은 지원될 뿐만 아니라, 실제로 rv32imc 및 rv64imc 플랫폼에서는 기본값입니다. 그러나 나중에 코드를 빌드할 때 문제가 발생하지 않도록 몇 가지 언급할 만한 사항이 있습니다.

일부 사전 빌드된 Picolibc 버전(예: Ubuntu Linux에서 picolibc-arm-none-eabi, picolibc-riscv64-unknown-elf, picolibc-xtensa-lx106-elf 패키지로 제공되는 것들)은 런타임에 스레드 로컬 저장소(TLS)를 사용할 수 있다고 가정하지만, 안타깝게도 MicroPython 모듈은 일부 아키텍처(즉, rv32imcrv64imc)에서 이를 지원하지 않습니다. 이는 Picolibc가 제공하는 일부 기능이 기본적으로 TLS를 사용하게 되어 컴파일 중 또는 링크 중에 오류를 반환함을 의미합니다.

이것이 어떻게 영향을 미칠 수 있는지에 대한 예로, examples/natmod/btree 예제 모듈에는 errno 가 동작하도록 하는 해결 방법이 포함되어 있습니다(Makefile에서 __PICOLIBC_ERRNO_FUNCTION 을 찾고 거기서부터 따라가십시오).

추가 예제

네이티브 .mpy 모듈의 사용 가능한 많은 기능을 보여주는 추가 예제는 examples/natmod/ 를 참조하십시오. 그러한 기능에는 다음이 포함됩니다:

  • 여러 C 소스 파일 사용하기

  • C 코드와 함께 Python 코드 포함하기

  • rodata 및 BSS 데이터

  • 메모리 할당

  • 부동소수점 사용

  • 예외 처리

  • 외부 C 라이브러리 포함하기