.mpy dosyalarındaki yerel makine kodu

Bu bölüm, Python dışında bir dilden gelen yerel makine kodu içeren .mpy dosyalarının nasıl oluşturulacağını ve bunlarla nasıl çalışılacağını açıklar. Bu, kodu C gibi bir dilde yazmanıza, onu derleyip bir .mpy dosyasına bağlamanıza ve ardından bu dosyayı normal bir Python modülü gibi içe aktarmanıza olanak tanır. Bu, performans açısından kritik olan işlevselliği uygulamak veya başka bir dilde yazılmış mevcut bir kütüphaneyi dahil etmek için kullanılabilir.

Yerel .mpy dosyaları kullanmanın başlıca avantajlarından biri, yerel makine kodunun, ana MicroPython aygıt yazılımını (firmware) yeniden oluşturmaya gerek kalmadan bir betik tarafından dinamik olarak içe aktarılabilmesidir. Bu, C’de özel modüller tanımlamaya da olanak tanıyan ancak bunların ana aygıt yazılımı görüntüsüne derlenmesi gereken MicroPython harici C modülleri ile zıtlık oluşturur.

Buradaki odak noktası yerel modüller oluşturmak için C kullanmaktır, ancak prensipte bağımsız makine koduna derlenebilen herhangi bir dil bir .mpy dosyasına konabilir.

Yerel bir .mpy modülü, projenin tools/ dizininde bulunan mpy_ld.py aracı kullanılarak oluşturulur. Bu araç bir dizi nesne dosyasını (.o dosyaları) alır ve bunları bir araya bağlayarak yerel bir .mpy dosyası oluşturur. CPython 3 ve pyelftools v0.25 veya üstü kütüphanesini gerektirir.

Desteklenen özellikler ve sınırlamalar

Bir .mpy dosyası MicroPython bayt kodu ve/veya yerel makine kodu içerebilir. Yerel makine kodu içeriyorsa, .mpy dosyasının kendisiyle ilişkilendirilmiş belirli bir mimarisi vardır. Şu anda desteklenen mimariler şunlardır (bunlar ARCH değişkeni için geçerli seçeneklerdir, aşağıya bakın):

  • x86 (32 bit)

  • x64 (64 bit x86)

  • armv6m (ARM Thumb, örn. Cortex-M0)

  • armv7m (ARM Thumb 2, örn. Cortex-M3)

  • armv7emsp (ARM Thumb 2, tek duyarlıklı float, örn. Cortex-M4F, Cortex-M7)

  • armv7emdp (ARM Thumb 2, çift duyarlıklı float, örn. Cortex-M7)

  • xtensa (pencereli olmayan, örn. ESP8266)

  • xtensawin (pencere boyutu 8 olan pencereli, örn. ESP32, ESP32S3)

  • rv32imc (sıkıştırılmış komutlu 32 bit RISC-V, örn. ESP32C3, ESP32C6)

  • rv64imc (sıkıştırılmış komutlu 64 bit RISC-V)

Seçilen platform açık mimari bayraklarını destekliyorsa ve çıktı .mpy dosyasının bu bayrakların değerini taşımasını istiyorsanız, .mpy dosyasını oluştururken bunları ARCH_FLAGS bayrakları değişkenine geçirmelisiniz.

Yerel .mpy dosyası derlenip bağlanırken mimari seçilmelidir ve karşılık gelen dosya yalnızca o mimaride içe aktarılabilir (ve mimari bayrakları mevcutsa, yalnızca hedefin yetenekleriyle eşleşmeleri durumunda). .mpy dosyaları hakkında daha fazla ayrıntı için MicroPython .mpy dosyaları bölümüne bakın.

Yerel kod, konumdan bağımsız kod (PIC) olarak derlenmeli ve genel bir ofset tablosu (GOT) kullanmalıdır, ancak bunun ayrıntıları mimariden mimariye değişir. Yerel kod içeren .mpy dosyaları içe aktarılırken, içe aktarma mekanizması yerel kodun bazı temel yer değiştirmelerini gerçekleştirebilir. Buna text, rodata ve BSS bölümlerinin yer değiştirmesi dahildir.

Bağlayıcının ve dinamik yükleyicinin desteklenen özellikleri şunlardır:

  • yürütülebilir kod (text)

  • salt okunur veri (rodata), dizeler ve sabit veriler (diziler, yapılar vb.) dahil

  • sıfırlanmış veri (BSS)

  • text içinden text, rodata ve BSS’ye işaretçiler

  • rodata içinden text, rodata ve BSS’ye işaretçiler

Bilinen sınırlamalar şunlardır:

  • data bölümleri desteklenmez; geçici çözüm: BSS verilerini kullanın ve veri değerlerini açıkça başlatın

  • statik BSS değişkenleri desteklenmez; geçici çözüm: genel BSS değişkenleri kullanın

  • iş parçacığı yerel depolama değişkenleri rv32imc üzerinde desteklenmez; geçici çözüm: genel BSS değişkenleri kullanın veya bunları depolamak için yığında biraz alan ayırın

Dolayısıyla, C kodunuzda yazılabilir veri varsa, verinin bir başlatıcı olmadan genel olarak tanımlandığından ve yalnızca işlevler içinde yazıldığından emin olun.

Yerel modül, libm.a ve libgcc.a gibi standart statik kütüphanelere otomatik olarak bağlanmaz, bu da undefined symbol hatalarına yol açabilir. Çalışma zamanı kütüphanelerini Makefile dosyanızda LINK_RUNTIME = 1 ayarlayarak bağlayabilirsiniz. Özel statik kütüphaneler de MPY_LD_FLAGS += -l path/to/library.a eklenerek bağlanabilir. Bunların yerel modüle bağlandığını ve diğer modüllerle veya sistemle paylaşılmayacağını unutmayın.

Bağlayıcı sınırlaması: yerel modül, MicroPython aygıt yazılımının tamamının sembol tablosuna karşı bağlanmaz. Bunun yerine, aygıt yazılımı oluşturma zamanında sabitlenen, mp_fun_table (py/nativeglue.h içinde) içinde bulunan açık bir dışa aktarılmış semboller tablosuna karşı bağlanır. Bu nedenle, sabit bir adreste bulunmadığı sürece, örneğin rastgele bir HAL/OS/RTOS/sistem işlevini basitçe çağırmak mümkün değildir. Bu durumda, bir dizi sembol adı ve bunların sabit adreslerini içeren bir bağlayıcı betiğinin yolu, --externs komut satırı argümanı aracılığıyla mpy_ld.py aracına geçirilebilir. Bu şekilde bağlayıcı betiğinde görünen semboller, nesne dosyalarından sağlananlara göre öncelik kazanır, ancak şu anda nesne dosyalarının uygulaması yine de son MPY dosyasında yer alacaktır. Bağlayıcı betiği ayrıştırıcısı yeteneklerinde sınırlıdır ve şu anda yalnızca ESP8266 bağlantı noktasının ROM semboller listesini ayrıştırmak için kullanılır (ports/esp8266/boards/eagle.rom.addr.v6.ld dosyasına bakın).

Tablonun sonuna yeni semboller eklenebilir ve aygıt yazılımı yeniden oluşturulabilir. Sembollerin ayrıca tools/mpy_ld.py dosyasının fun_table sözlüğüne aynı konuma eklenmesi gerekir. Bu, mpy_ld.py aracının yeni sembolleri alabilmesini ve mpy içe aktarıldığında bunlar için yer değiştirmeler sağlayabilmesini olanaklı kılar. Son olarak, sembol bir işlevse, işlevi çağırmayı kolaylaştırmak için py/dynruntime.h dosyasına bir makro veya iskelet eklenmelidir.

Yerel bir modül tanımlama

Yerel bir .mpy modülü, .mpy dosyasını oluşturmak için kullanılan bir dizi dosyayla tanımlanır. Dosya sistemi düzeni iki ana bölümden oluşur: kaynak dosyaları ve Makefile:

  • En basit durumda, .mpy modülüne derlenecek tüm kodu içeren tek bir C kaynak dosyası gerekir. Bu C kaynak kodu, MicroPython dinamik API’sine erişmek için py/dynruntime.h dosyasını dahil etmeli ve en azından mpy_init adlı bir işlev tanımlamalıdır. Bu işlev, modül içe aktarıldığında çağrılan modülün giriş noktası olacaktır.

    Modül istenirse birden çok C kaynak dosyasına bölünebilir. Modülün bazı bölümleri Python’da da uygulanabilir. Tüm kaynak dosyaları, SRC değişkenine eklenerek Makefile dosyasında listelenmelidir (aşağıya bakın). Buna hem C kaynak dosyaları hem de elde edilen .mpy dosyasına dahil edilecek herhangi bir Python dosyası dahildir.

  • Makefile modülün derleme yapılandırmasını içerir ve .mpy modülünü oluşturmak için kullanılan kaynak dosyaları listeler. MPY_DIR değişkenini MicroPython deposunun konumu olarak (başlık dosyalarını, ilgili Makefile parçasını ve mpy_ld.py aracını bulmak için), MOD değişkenini modülün adı olarak, SRC değişkenini kaynak dosyaların listesi olarak tanımlamalı, isteğe bağlı olarak makine mimarisini ARCH aracılığıyla, isteğe bağlı makine mimarisi bayraklarını ise ARCH_FLAGS aracılığıyla belirtmeli ve ardından py/dynruntime.mk dosyasını dahil etmelidir.

Asgari örnek

Bu bölüm, factorial adlı basit bir modülün tam çalışan bir örneğini sunar. Bu modül, girdinin faktöriyelini hesaplayan ve sonucu döndüren tek bir factorial.factorial(x) işlevi sağlar.

Dizin düzeni:

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

factorial.c dosyası şunları içerir:

// 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 dosyası şunları içerir:

# 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

Modülü derleme

Yerel bir .mpy dosyası oluşturmak için gereken ön koşul araçlar şunlardır:

  • MicroPython deposu (en azından py/ ve tools/ dizinleri).

  • CPython 3 ve pyelftools kütüphanesi (örn. pip install 'pyelftools>=0.25').

  • GNU make.

  • Hedef mimari için bir C derleyicisi (C kaynağı kullanılıyorsa).

  • İsteğe bağlı olarak MicroPython deposundan oluşturulan mpy-cross (.py kaynağı kullanılıyorsa).

Üzerinde çalıştıracağınız hedef için doğru ARCH değerini seçtiğinizden emin olun. Ardından şununla oluşturun:

$ make

Makefile dosyasını değiştirmeden hedef mimariyi şu şekilde belirtebilirsiniz:

$ make ARCH=armv7m

Aynı durum isteğe bağlı mimari bayrakları için de şu şekilde geçerlidir:

$ make ARCH=rv32imc ARCH_FLAGS=zba

MicroPython’da modül kullanımı

Modül oluşturulduktan sonra factorial.mpy adlı bir dosya olmalıdır. Bunu, MicroPython sisteminizin dosya sisteminde erişilebilir olacak ve içe aktarma yolunda bulunabilecek şekilde kopyalayın. Modüle artık Python’da diğer herhangi bir modül gibi erişilebilir, örneğin:

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

Modül oluştururken Picolibc kullanımı

C standart kütüphaneniz olarak Picolibc kullanmak yalnızca desteklenmekle kalmaz, aslında rv32imc ve rv64imc platformları için varsayılandır. Ancak, kod oluştururken daha sonra sorunlarla karşılaşmadığınızdan emin olmak için bahsetmeye değer birkaç husus vardır.

Bazı önceden oluşturulmuş Picolibc sürümleri (örneğin, Ubuntu Linux tarafından picolibc-arm-none-eabi, picolibc-riscv64-unknown-elf ve picolibc-xtensa-lx106-elf paketleri olarak sağlananlar), iş parçacığı yerel depolamanın (TLS) çalışma zamanında mevcut olduğunu varsayar, ancak ne yazık ki MicroPython modülleri bazı mimarilerde (yani rv32imc ve rv64imc) bunu desteklemez. Bu, Picolibc tarafından sağlanan bazı işlevselliklerin varsayılan olarak TLS kullanacağı ve derleme sırasında veya bağlama sırasında bir hata döndüreceği anlamına gelir.

Bunun sizi nasıl etkileyebileceğine dair bir örnek olarak, examples/natmod/btree örnek modülü, errno çalışmasını sağlamak için bir geçici çözüm içerir (Makefile dosyasında __PICOLIBC_ERRNO_FUNCTION ifadesine bakın ve oradan izi takip edin).

Daha fazla örnek

Yerel .mpy modüllerinin mevcut özelliklerinin çoğunu gösteren daha fazla örnek için examples/natmod/ dizinine bakın. Bu özellikler şunları içerir:

  • birden çok C kaynak dosyası kullanma

  • C koduyla birlikte Python kodu dahil etme

  • rodata ve BSS verileri

  • bellek tahsisi

  • kayan nokta kullanımı

  • istisna işleme

  • harici C kütüphanelerini dahil etme