From 625758495134698baaa9db3db571effc85e0f5ab Mon Sep 17 00:00:00 2001 From: Jan Pochyla Date: Tue, 23 Mar 2021 13:14:33 +0100 Subject: [PATCH] feat(core): Add Rust bindings to MicroPython and trezorhal core: Remove dangling module decls core: Use new Cargo feature resolver, use external MacOS debug info core: Rust docs improvements core: Upgrade bindgen core: Add test target to Rust ci: build rust sources build(core): .ARM.exidx.text.__aeabi_ui2f in t1 firmware size It's an unwind table for softfloat function inserted by rustc, probably can be removed to save 8 bytes: https://github.com/rust-embedded/cortex-m-rt/blob/599c58db70c5dd4eb1dfb92e1dad7c80ed848937/link.x.in#L175-L182 scons: Remove dead code core: Move Rust target to build/rust core: Replace extern with a FFI version core: Add some explanatory Rust comments core: Use correct path for the Rust lib core: Remove Buffer::as_mut() Mutable buffer access needs MP_BUFFER_WRITE flag. TBD in the Protobuf PR. core: Improve docs for micropython::Buffer core: Minor Rust docs changes core: Rewrite trezor_obj_get_ll_checked core: Fix incorrect doc comment core: Remove cc from deps fixup! core: Rewrite trezor_obj_get_ll_checked core: update safety comments --- ci/shell.nix | 35 +- core/.changelog.d/1540.added | 1 + core/Makefile | 5 +- core/SConscript.firmware | 36 +- core/SConscript.unix | 32 ++ core/embed/extmod/trezorobj.c | 64 ++++ core/embed/extmod/trezorobj.h | 2 + core/embed/firmware/memory_1.ld | 3 +- core/embed/firmware/memory_1_min.ld | 3 +- core/embed/rust/.cargo/config.toml | 2 + core/embed/rust/Cargo.lock | 385 +++++++++++++++++++++ core/embed/rust/Cargo.toml | 34 ++ core/embed/rust/build.rs | 166 +++++++++ core/embed/rust/micropython.h | 5 + core/embed/rust/qstr.h | 5 + core/embed/rust/rust-toolchain | 2 + core/embed/rust/rustfmt.toml | 2 + core/embed/rust/src/error.rs | 49 +++ core/embed/rust/src/lib.rs | 37 ++ core/embed/rust/src/micropython/buffer.rs | 66 ++++ core/embed/rust/src/micropython/dict.rs | 70 ++++ core/embed/rust/src/micropython/ffi.rs | 5 + core/embed/rust/src/micropython/func.rs | 18 + core/embed/rust/src/micropython/gc.rs | 81 +++++ core/embed/rust/src/micropython/iter.rs | 69 ++++ core/embed/rust/src/micropython/list.rs | 50 +++ core/embed/rust/src/micropython/macros.rs | 132 +++++++ core/embed/rust/src/micropython/map.rs | 193 +++++++++++ core/embed/rust/src/micropython/mod.rs | 15 + core/embed/rust/src/micropython/obj.rs | 327 +++++++++++++++++ core/embed/rust/src/micropython/qstr.rs | 59 ++++ core/embed/rust/src/micropython/runtime.rs | 10 + core/embed/rust/src/micropython/typ.rs | 28 ++ core/embed/rust/src/trezorhal/common.rs | 24 ++ core/embed/rust/src/trezorhal/display.rs | 69 ++++ core/embed/rust/src/trezorhal/mod.rs | 3 + core/embed/rust/src/trezorhal/random.rs | 16 + core/embed/rust/src/util.rs | 63 ++++ 38 files changed, 2151 insertions(+), 15 deletions(-) create mode 100644 core/.changelog.d/1540.added create mode 100644 core/embed/extmod/trezorobj.c create mode 100644 core/embed/rust/.cargo/config.toml create mode 100644 core/embed/rust/Cargo.lock create mode 100644 core/embed/rust/Cargo.toml create mode 100644 core/embed/rust/build.rs create mode 100644 core/embed/rust/micropython.h create mode 100644 core/embed/rust/qstr.h create mode 100644 core/embed/rust/rust-toolchain create mode 100644 core/embed/rust/rustfmt.toml create mode 100644 core/embed/rust/src/error.rs create mode 100644 core/embed/rust/src/lib.rs create mode 100644 core/embed/rust/src/micropython/buffer.rs create mode 100644 core/embed/rust/src/micropython/dict.rs create mode 100644 core/embed/rust/src/micropython/ffi.rs create mode 100644 core/embed/rust/src/micropython/func.rs create mode 100644 core/embed/rust/src/micropython/gc.rs create mode 100644 core/embed/rust/src/micropython/iter.rs create mode 100644 core/embed/rust/src/micropython/list.rs create mode 100644 core/embed/rust/src/micropython/macros.rs create mode 100644 core/embed/rust/src/micropython/map.rs create mode 100644 core/embed/rust/src/micropython/mod.rs create mode 100644 core/embed/rust/src/micropython/obj.rs create mode 100644 core/embed/rust/src/micropython/qstr.rs create mode 100644 core/embed/rust/src/micropython/runtime.rs create mode 100644 core/embed/rust/src/micropython/typ.rs create mode 100644 core/embed/rust/src/trezorhal/common.rs create mode 100644 core/embed/rust/src/trezorhal/display.rs create mode 100644 core/embed/rust/src/trezorhal/mod.rs create mode 100644 core/embed/rust/src/trezorhal/random.rs create mode 100644 core/embed/rust/src/util.rs diff --git a/ci/shell.nix b/ci/shell.nix index c2648b61ce..693dc5e1e0 100644 --- a/ci/shell.nix +++ b/ci/shell.nix @@ -2,25 +2,35 @@ , hardwareTest ? false }: -# the last successful build of nixpkgs-unstable as of 2021-03-25 -with import (builtins.fetchTarball { - url = "https://github.com/NixOS/nixpkgs/archive/c0e881852006b132236cbf0301bd1939bb50867e.tar.gz"; - sha256 = "0fy7z7yxk5n7yslsvx5cyc6h21qwi4bhxf3awhirniszlbvaazy2"; -}) -{ }; - let - moneroTests = fetchurl { + mozillaOverlay = import (builtins.fetchTarball { + url = "https://github.com/mozilla/nixpkgs-mozilla/archive/8c007b60731c07dd7a052cce508de3bb1ae849b4.tar.gz"; + sha256 = "1zybp62zz0h077zm2zmqs2wcg3whg6jqaah9hcl1gv4x8af4zhs6"; + }); + # the last successful build of nixpkgs-unstable as of 2021-03-25 + nixpkgs = import (builtins.fetchTarball { + url = "https://github.com/NixOS/nixpkgs/archive/c0e881852006b132236cbf0301bd1939bb50867e.tar.gz"; + sha256 = "0fy7z7yxk5n7yslsvx5cyc6h21qwi4bhxf3awhirniszlbvaazy2"; + }) { overlays = [ mozillaOverlay ]; }; + moneroTests = nixpkgs.fetchurl { url = "https://github.com/ph4r05/monero/releases/download/v0.17.1.9-tests/trezor_tests"; sha256 = "410bc4ff2ff1edc65e17f15b549bd1bf8a3776cf67abdea86aed52cf4bce8d9d"; }; - moneroTestsPatched = runCommandCC "monero_trezor_tests" {} '' + moneroTestsPatched = nixpkgs.runCommandCC "monero_trezor_tests" {} '' cp ${moneroTests} $out chmod +wx $out - ${patchelf}/bin/patchelf --set-interpreter "$(cat $NIX_CC/nix-support/dynamic-linker)" "$out" + ${nixpkgs.patchelf}/bin/patchelf --set-interpreter "$(cat $NIX_CC/nix-support/dynamic-linker)" "$out" chmod -w $out ''; + rustNightly = (nixpkgs.rustChannelOf { date = "2021-03-29"; channel = "nightly"; }).rust.override { + targets = [ + "x86_64-unknown-linux-gnu" # emulator + "thumbv7em-none-eabihf" # TT + "thumbv7em-none-eabi" # T1 + ]; + }; in +with nixpkgs; stdenv.mkDerivation ({ name = "trezor-firmware-env"; buildInputs = lib.optionals fullDeps [ @@ -38,6 +48,7 @@ stdenv.mkDerivation ({ bash check clang-tools + clang editorconfig-checker gcc gcc-arm-embedded @@ -52,6 +63,8 @@ stdenv.mkDerivation ({ pkgconfig poetry protobuf3_6 + rustfmt + rustNightly wget zlib ] ++ lib.optionals (!stdenv.isDarwin) [ @@ -80,6 +93,8 @@ stdenv.mkDerivation ({ # Fix bdist-wheel problem by setting source date epoch to a more recent date SOURCE_DATE_EPOCH = 1600000000; + # Used by rust bindgen + LIBCLANG_PATH = "${llvmPackages.libclang}/lib"; } // (lib.optionalAttrs fullDeps) { TREZOR_MONERO_TESTS_PATH = moneroTestsPatched; }) diff --git a/core/.changelog.d/1540.added b/core/.changelog.d/1540.added new file mode 100644 index 0000000000..736131c0bf --- /dev/null +++ b/core/.changelog.d/1540.added @@ -0,0 +1 @@ +Rust FFI for MicroPython. diff --git a/core/Makefile b/core/Makefile index 8790fbbc75..e78fdba4c4 100644 --- a/core/Makefile +++ b/core/Makefile @@ -12,6 +12,7 @@ PRODTEST_BUILD_DIR = $(BUILD_DIR)/prodtest REFLASH_BUILD_DIR = $(BUILD_DIR)/reflash FIRMWARE_BUILD_DIR = $(BUILD_DIR)/firmware UNIX_BUILD_DIR = $(BUILD_DIR)/unix +RUST_BUILD_DIR = $(BUILD_DIR)/rust UNAME_S := $(shell uname -s) UNIX_PORT_OPTS ?= @@ -179,10 +180,10 @@ clean_reflash: ## clean reflash build rm -rf $(REFLASH_BUILD_DIR) clean_firmware: ## clean firmware build - rm -rf $(FIRMWARE_BUILD_DIR) + rm -rf $(FIRMWARE_BUILD_DIR) $(RUST_BUILD_DIR) clean_unix: ## clean unix build - rm -rf $(UNIX_BUILD_DIR) + rm -rf $(UNIX_BUILD_DIR) $(RUST_BUILD_DIR) clean_cross: ## clean mpy-cross build $(MAKE) -C vendor/micropython/mpy-cross clean $(CROSS_PORT_OPTS) diff --git a/core/SConscript.firmware b/core/SConscript.firmware index 0aab59ceee..2ff590f5d1 100644 --- a/core/SConscript.firmware +++ b/core/SConscript.firmware @@ -1,4 +1,5 @@ # pylint: disable=E0602 +# fmt: off import os @@ -49,6 +50,7 @@ CPPDEFINES_MOD += [ ('USE_EOS', '1' if EVERYTHING else '0'), ] SOURCE_MOD += [ + 'embed/extmod/trezorobj.c', 'embed/extmod/modtrezorcrypto/crc.c', 'embed/extmod/modtrezorcrypto/modtrezorcrypto.c', 'embed/extmod/modtrezorcrypto/rand.c', @@ -394,11 +396,13 @@ if TREZOR_MODEL == 'T': CPU_CCFLAGS = '-mthumb -mcpu=cortex-m4 -mfloat-abi=hard -mfpu=fpv4-sp-d16 -mtune=cortex-m4 ' CPU_MODEL = 'STM32F427xx' LD_VARIANT = '_zkp' if FEATURE_FLAGS["SECP256K1_ZKP"] else '' + RUST_TARGET = 'thumbv7em-none-eabihf' elif TREZOR_MODEL == '1': CPU_ASFLAGS = '-mthumb -mcpu=cortex-m3 -mfloat-abi=soft' CPU_CCFLAGS = '-mthumb -mtune=cortex-m3 -mcpu=cortex-m3 -mfloat-abi=soft ' CPU_MODEL = 'STM32F405xx' LD_VARIANT = '' if EVERYTHING else '_min' + RUST_TARGET = 'thumbv7em-none-eabi' else: raise ValueError('Unknown Trezor model') @@ -416,6 +420,7 @@ env.Replace( LINKFLAGS='-T embed/firmware/memory_${TREZOR_MODEL}%s.ld -Wl,--gc-sections -Wl,-Map=build/firmware/firmware.map -Wl,--warn-common' % LD_VARIANT, CPPPATH=[ '.', + 'embed/rust', 'embed/firmware', 'embed/trezorhal', 'embed/extmod/modtrezorui', @@ -616,6 +621,33 @@ if FEATURE_FLAGS["SECP256K1_ZKP"]: action='cd ${SOURCE.dir}; ./gen_context', ) +# +# Rust library +# + +RUST_PROFILE = 'release' +RUST_LIB = 'trezor_lib' +RUST_LIBDIR = f'build/rust/{RUST_TARGET}/{RUST_PROFILE}' +RUST_LIBPATH = f'{RUST_LIBDIR}/lib{RUST_LIB}.a' + +def cargo_build(): + # Determine the profile build flags. + if RUST_PROFILE == 'release': + profile = '--release' + else: + profile = '' + return f'cd embed/rust; cargo build {profile} --target={RUST_TARGET}' + +rust = env.Command( + target=RUST_LIBPATH, + source='', + action=cargo_build(), ) + +env.Depends(rust, qstr_generated) + +env.Append(LINKFLAGS=f' -L{RUST_LIBDIR}') +env.Append(LINKFLAGS=f' -l{RUST_LIB}') + # # Program objects # @@ -658,9 +690,11 @@ program_elf = env.Command( target='firmware.elf', source=obj_program, action= - '$LINK -o $TARGET $CCFLAGS $CFLAGS $LINKFLAGS $SOURCES -lc_nano -lm -lgcc', + '$LINK -o $TARGET $CCFLAGS $CFLAGS $SOURCES $LINKFLAGS -lc_nano -lm -lgcc', ) +env.Depends(program_elf, rust) + if TREZOR_MODEL == 'T': action_bin=[ '$OBJCOPY -O binary -j .vendorheader -j .header -j .flash -j .data --pad-to 0x08100000 $SOURCE ${TARGET}.p1', diff --git a/core/SConscript.unix b/core/SConscript.unix index 2dbe27d058..5f232631df 100644 --- a/core/SConscript.unix +++ b/core/SConscript.unix @@ -1,4 +1,5 @@ # pylint: disable=E0602 +# fmt: off import os @@ -48,6 +49,7 @@ CPPDEFINES_MOD += [ ('USE_EOS', '1' if EVERYTHING else '0'), ] SOURCE_MOD += [ + 'embed/extmod/trezorobj.c', 'embed/extmod/modtrezorcrypto/modtrezorcrypto.c', 'embed/extmod/modtrezorcrypto/crc.c', 'vendor/trezor-crypto/address.c', @@ -367,6 +369,7 @@ env.Replace( LIBS=['m'], CPPPATH=[ '.', + 'embed/rust', 'embed/unix', 'embed/extmod/modtrezorui', 'vendor/micropython', @@ -567,6 +570,33 @@ if FEATURE_FLAGS["SECP256K1_ZKP"]: action='cd ${SOURCE.dir}; ./gen_context', ) +# +# Rust library +# + +RUST_PROFILE = 'release' +RUST_LIB = 'trezor_lib' +RUST_LIBDIR = f'build/rust/{RUST_PROFILE}' +RUST_LIBPATH = f'{RUST_LIBDIR}/lib{RUST_LIB}.a' + +def cargo_build(): + # Determine the profile build flags. + if RUST_PROFILE == 'release': + profile = '--release' + else: + profile = '' + return f'cd embed/rust; cargo build {profile}' + +rust = env.Command( + target=RUST_LIBPATH, + source='', + action=cargo_build(), ) + +env.Depends(rust, qstr_generated) + +env.Append(LINKFLAGS=f'-L{RUST_LIBDIR}') +env.Append(LINKFLAGS=f'-l{RUST_LIB}') + # # Program objects # @@ -588,3 +618,5 @@ program = env.Command( target='trezor-emu-core', source=obj_program, action='$CC -o $TARGET $SOURCES $_LIBDIRFLAGS $_LIBFLAGS $LINKFLAGS', ) + +env.Depends(program, rust) diff --git a/core/embed/extmod/trezorobj.c b/core/embed/extmod/trezorobj.c new file mode 100644 index 0000000000..518d44db2a --- /dev/null +++ b/core/embed/extmod/trezorobj.c @@ -0,0 +1,64 @@ +/* + * This file is part of the Trezor project, https://trezor.io/ + * + * Copyright (c) SatoshiLabs + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include + +#include "memzero.h" +#include "py/objint.h" + +static bool mpz_as_ll_checked(const mpz_t *i, long long *value) { + // Analogue of `mpz_as_int_checked` from mpz.c + + unsigned long long val = 0; + mpz_dig_t *d = i->dig + i->len; + + while (d-- > i->dig) { + if (val > (~0x8000000000000000 >> MPZ_DIG_SIZE)) { + // will overflow + *value = 0; + return false; + } + val = (val << MPZ_DIG_SIZE) | *d; + } + + if (i->neg != 0) { + val = -val; + } + + *value = val; + return true; +} + +bool trezor_obj_get_ll_checked(mp_obj_t obj, long long *value) { + if (mp_obj_is_small_int(obj)) { + // Value is fitting in a small int range. Return it directly. + *value = MP_OBJ_SMALL_INT_VALUE(obj); + return true; + + } else if (mp_obj_is_type(obj, &mp_type_int)) { + // Value is not fitting into small int range, but is an integer. + mp_obj_int_t *self = MP_OBJ_TO_PTR(obj); + // Try to get the long long value out of the MPZ struct. + return mpz_as_ll_checked(&self->mpz, value); + } else { + // Value is not integer. + *value = 0; + return false; + } +} diff --git a/core/embed/extmod/trezorobj.h b/core/embed/extmod/trezorobj.h index 1c984ec1d2..98a53703bc 100644 --- a/core/embed/extmod/trezorobj.h +++ b/core/embed/extmod/trezorobj.h @@ -75,4 +75,6 @@ static inline uint8_t trezor_obj_get_uint8(mp_obj_t obj) { return u; } +bool trezor_obj_get_ll_checked(mp_obj_t obj, long long *value); + #endif diff --git a/core/embed/firmware/memory_1.ld b/core/embed/firmware/memory_1.ld index a6669973c1..abd745fd90 100644 --- a/core/embed/firmware/memory_1.ld +++ b/core/embed/firmware/memory_1.ld @@ -26,7 +26,8 @@ sram_end = ORIGIN(SRAM) + LENGTH(SRAM); _ram_start = sram_start; _ram_end = sram_end; -_codelen = SIZEOF(.flash) + SIZEOF(.data) + SIZEOF(.ARM.exidx); +/* .ARM.exidx.text.__aeabi_ui2f is probably not needed as long as we are using panic = "abort" */ +_codelen = SIZEOF(.flash) + SIZEOF(.data) + SIZEOF(.ARM.exidx) + SIZEOF(.ARM.exidx.text.__aeabi_ui2f); _flash_start = ORIGIN(FLASH); _flash_end = ORIGIN(FLASH) + LENGTH(FLASH); _heap_start = ADDR(.heap); diff --git a/core/embed/firmware/memory_1_min.ld b/core/embed/firmware/memory_1_min.ld index 34bbbfb8f2..0a8ec38ac8 100644 --- a/core/embed/firmware/memory_1_min.ld +++ b/core/embed/firmware/memory_1_min.ld @@ -26,7 +26,8 @@ sram_end = ORIGIN(SRAM) + LENGTH(SRAM); _ram_start = sram_start; _ram_end = sram_end; -_codelen = SIZEOF(.flash) + SIZEOF(.data); +/* .ARM.exidx.text.__aeabi_ui2f is probably not needed as long as we are using panic = "abort" */ +_codelen = SIZEOF(.flash) + SIZEOF(.data) + SIZEOF(.ARM.exidx.text.__aeabi_ui2f); _flash_start = ORIGIN(FLASH); _flash_end = ORIGIN(FLASH) + LENGTH(FLASH); _heap_start = ADDR(.heap); diff --git a/core/embed/rust/.cargo/config.toml b/core/embed/rust/.cargo/config.toml new file mode 100644 index 0000000000..e0a2aaa116 --- /dev/null +++ b/core/embed/rust/.cargo/config.toml @@ -0,0 +1,2 @@ +[build] +target-dir = "../../build/rust" diff --git a/core/embed/rust/Cargo.lock b/core/embed/rust/Cargo.lock new file mode 100644 index 0000000000..0e46534906 --- /dev/null +++ b/core/embed/rust/Cargo.lock @@ -0,0 +1,385 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "aho-corasick" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7404febffaa47dac81aa44dba71523c9d069b1bdc50a77db41195149e17f68e5" +dependencies = [ + "memchr", +] + +[[package]] +name = "ansi_term" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" +dependencies = [ + "winapi", +] + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi", +] + +[[package]] +name = "bindgen" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5d2549a1ca679efa833b9d88a424955109722c9fd95418b95414c19fd6f4bda" +dependencies = [ + "bitflags", + "cexpr", + "clang-sys", + "clap", + "env_logger", + "lazy_static", + "lazycell", + "log", + "peeking_take_while", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "which", +] + +[[package]] +name = "bitflags" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" + +[[package]] +name = "cc" +version = "1.0.67" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c69b077ad434294d3ce9f1f6143a2a4b89a8a2d54ef813d85003a4fd1137fd" + +[[package]] +name = "cexpr" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4aedb84272dbe89af497cf81375129abda4fc0a9e7c5d317498c15cc30c0d27" +dependencies = [ + "nom", +] + +[[package]] +name = "cfg-if" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "clang-sys" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0659001ab56b791be01d4b729c44376edc6718cf389a502e579b77b758f3296c" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "clap" +version = "2.33.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002" +dependencies = [ + "ansi_term", + "atty", + "bitflags", + "strsim", + "textwrap", + "unicode-width", + "vec_map", +] + +[[package]] +name = "cstr_core" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26bdf52fc09b421407bd990b4662a380420b6b6f61708bcba417731dca1b65ef" +dependencies = [ + "cty", + "memchr", +] + +[[package]] +name = "cty" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7313c0d620d0cb4dbd9d019e461a4beb501071ff46ec0ab933efb4daa76d73e3" + +[[package]] +name = "env_logger" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26ecb66b4bdca6c1409b40fb255eefc2bd4f6d135dab3c3124f80ffa2a9661e" +dependencies = [ + "atty", + "humantime", + "log", + "regex", + "termcolor", +] + +[[package]] +name = "glob" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" + +[[package]] +name = "hermit-abi" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "322f4de77956e22ed0e5032c359a0f1273f1f7f0d79bfa3b8ffbc730d7fbcc5c" +dependencies = [ + "libc", +] + +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + +[[package]] +name = "libc" +version = "0.2.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89203f3fba0a3795506acaad8ebce3c80c0af93f994d5a1d7a0b1eeb23271929" + +[[package]] +name = "libloading" +version = "0.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "351a32417a12d5f7e82c368a66781e307834dae04c6ce0cd4456d52989229883" +dependencies = [ + "cfg-if 1.0.0", + "winapi", +] + +[[package]] +name = "log" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcf3805d4480bb5b86070dcfeb9e2cb2ebc148adb753c5cca5f884d1d65a42b2" +dependencies = [ + "cfg-if 0.1.10", +] + +[[package]] +name = "memchr" +version = "2.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ee1c47aaa256ecabcaea351eae4a9b01ef39ed810004e298d2511ed284b1525" + +[[package]] +name = "nom" +version = "5.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffb4262d26ed83a1c0a33a38fe2bb15797329c85770da05e6b828ddb782627af" +dependencies = [ + "memchr", + "version_check", +] + +[[package]] +name = "peeking_take_while" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" + +[[package]] +name = "proc-macro2" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0704ee1a7e00d7bb417d0770ea303c1bccbabf0ef1667dae92b5967f5f8a71" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "quote" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "991431c3519a3f36861882da93630ce66b52918dcf1b8e2fd66b397fc96f28df" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "regex" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9251239e129e16308e70d853559389de218ac275b515068abc96829d05b948a" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", + "thread_local", +] + +[[package]] +name = "regex-syntax" +version = "0.6.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5eb417147ba9860a96cfe72a0b93bf88fee1744b5636ec99ab20c1aa9376581" + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "shlex" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42a568c8f2cd051a4d283bd6eb0343ac214c1b0f1ac19f93e1175b2dee38c73d" + +[[package]] +name = "staticvec" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c87f4be0fd89694157f3814ca88715ad8ba6010c453b1e89ca264aee04d70b9" + +[[package]] +name = "strsim" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" + +[[package]] +name = "termcolor" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "textwrap" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" +dependencies = [ + "unicode-width", +] + +[[package]] +name = "thread_local" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb9bc092d0d51e76b2b19d9d85534ffc9ec2db959a2523cdae0697e2972cd447" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "trezor_lib" +version = "0.1.0" +dependencies = [ + "bindgen", + "cc", + "cstr_core", + "cty", + "staticvec", +] + +[[package]] +name = "unicode-width" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3" + +[[package]] +name = "unicode-xid" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" + +[[package]] +name = "vec_map" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" + +[[package]] +name = "version_check" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5a972e5669d67ba988ce3dc826706fb0a8b01471c088cb0b6110b805cc36aed" + +[[package]] +name = "which" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d011071ae14a2f6671d0b74080ae0cd8ebf3a6f8c9589a2cd45f23126fe29724" +dependencies = [ + "libc", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/core/embed/rust/Cargo.toml b/core/embed/rust/Cargo.toml new file mode 100644 index 0000000000..f2c7f11cc1 --- /dev/null +++ b/core/embed/rust/Cargo.toml @@ -0,0 +1,34 @@ +[package] +name = "trezor_lib" +version = "0.1.0" +authors = ["SatoshiLabs "] +edition = "2018" +resolver = "2" +build = "build.rs" + +[lib] +crate-type = ["staticlib"] + +[profile.dev] +panic = "abort" +split-debuginfo = "unpacked" + +[profile.release] +panic = "abort" +opt-level = "z" +lto = true +codegen-units = 1 + +[dependencies] +cty = "0.2.1" + +[dependencies.staticvec] +version = "0.10.5" +default_features = false + +[dependencies.cstr_core] +version = "0.2.2" +default_features = false + +[build-dependencies] +bindgen = "0.58.0" diff --git a/core/embed/rust/build.rs b/core/embed/rust/build.rs new file mode 100644 index 0000000000..c675e45927 --- /dev/null +++ b/core/embed/rust/build.rs @@ -0,0 +1,166 @@ +use std::{env, path::PathBuf, process::Command}; + +fn main() { + generate_qstr_bindings(); + generate_micropython_bindings(); +} + +/// Generates Rust module that exports QSTR constants used in firmware. +fn generate_qstr_bindings() { + let out_path = env::var("OUT_DIR").unwrap(); + let target = env::var("TARGET").unwrap(); + + // Tell cargo to invalidate the built crate whenever the header changes. + println!("cargo:rerun-if-changed=qstr.h"); + + bindgen::Builder::default() + .header("qstr.h") + // Build the Qstr enum as a newtype so we can define method on it. + .default_enum_style(bindgen::EnumVariation::NewType { is_bitfield: false }) + // Pass in correct include paths. + .clang_args(&[ + "-I", + if target.starts_with("thumbv7em-none-eabi") { + "../../build/firmware" + } else { + "../../build/unix" + }, + ]) + // Customize the standard types. + .use_core() + .ctypes_prefix("cty") + .size_t_is_usize(true) + // Tell cargo to invalidate the built crate whenever any of the + // included header files change. + .parse_callbacks(Box::new(bindgen::CargoCallbacks)) + .generate() + .expect("Unable to generate Rust QSTR bindings") + .write_to_file(PathBuf::from(out_path).join("qstr.rs")) + .unwrap(); +} + +fn generate_micropython_bindings() { + let out_path = env::var("OUT_DIR").unwrap(); + let target = env::var("TARGET").unwrap(); + + // Tell cargo to invalidate the built crate whenever the header changes. + println!("cargo:rerun-if-changed=micropython.h"); + + let mut bindings = bindgen::Builder::default() + .header("micropython.h") + // obj + .new_type_alias("mp_obj_t") + .allowlist_type("mp_obj_type_t") + .allowlist_type("mp_obj_base_t") + .allowlist_function("mp_obj_new_int") + .allowlist_function("mp_obj_new_int_from_ll") + .allowlist_function("mp_obj_new_int_from_ull") + .allowlist_function("mp_obj_new_int_from_uint") + .allowlist_function("mp_obj_new_bytes") + .allowlist_function("mp_obj_new_str") + .allowlist_function("mp_obj_get_int_maybe") + .allowlist_function("mp_obj_is_true") + .allowlist_function("mp_call_function_n_kw") + .allowlist_function("trezor_obj_get_ll_checked") + .allowlist_function("trezor_obj_get_ull_checked") + // buffer + .allowlist_function("mp_get_buffer") + .allowlist_var("MP_BUFFER_READ") + .allowlist_var("MP_BUFFER_WRITE") + .allowlist_var("MP_BUFFER_RW") + // dict + .allowlist_type("mp_obj_dict_t") + .allowlist_function("mp_obj_new_dict") + .allowlist_function("mp_obj_dict_store") + .allowlist_var("mp_type_dict") + // fun + .allowlist_type("mp_obj_fun_builtin_fixed_t") + .allowlist_var("mp_type_fun_builtin_1") + .allowlist_var("mp_type_fun_builtin_2") + .allowlist_var("mp_type_fun_builtin_3") + // gc + .allowlist_function("gc_alloc") + // iter + .allowlist_type("mp_obj_iter_buf_t") + .allowlist_function("mp_getiter") + .allowlist_function("mp_iternext") + // list + .allowlist_type("mp_obj_list_t") + .allowlist_function("mp_obj_new_list") + .allowlist_function("mp_obj_list_append") + .allowlist_var("mp_type_list") + // map + .allowlist_type("mp_map_elem_t") + .allowlist_type("mp_map_lookup_kind_t") + .allowlist_function("mp_map_init") + .allowlist_function("mp_map_init_fixed_table") + .allowlist_function("mp_map_lookup") + // runtime + .allowlist_function("mp_raise_ValueError") + // typ + .allowlist_var("mp_type_type"); + + // `ffi::mp_map_t` type is not allowed to be `Clone` or `Copy` because we tie it + // to the data lifetimes with the `MapRef` type, see `src/micropython/map.rs`. + // TODO: We should disable `Clone` and `Copy` for all types and only allow-list + // the specific cases we require. + bindings = bindings.no_copy("_mp_map_t"); + + // Pass in correct include paths and defines. + if target.starts_with("thumbv7em-none-eabi") { + bindings = bindings.clang_args(&[ + "-nostdinc", + "-I../firmware", + "-I../trezorhal", + "-I../../build/firmware", + "-I../../vendor/micropython", + "-I../../vendor/micropython/lib/stm32lib/STM32F4xx_HAL_Driver/Inc", + "-I../../vendor/micropython/lib/stm32lib/CMSIS/STM32F4xx/Include", + "-I../../vendor/micropython/lib/cmsis/inc", + "-DTREZOR_MODEL=T", + "-DSTM32F405xx", + "-DUSE_HAL_DRIVER", + "-DSTM32_HAL_H=", + ]); + // Append gcc-arm-none-eabi's include paths. + let cc_output = Command::new("arm-none-eabi-gcc") + .arg("-E") + .arg("-Wp,-v") + .arg("-") + .output() + .expect("arm-none-eabi-gcc failed to execute"); + if !cc_output.status.success() { + panic!("arm-none-eabi-gcc failed"); + } + let include_paths = + String::from_utf8(cc_output.stderr).expect("arm-none-eabi-gcc returned invalid output"); + let include_args = include_paths + .lines() + .skip_while(|s| !s.contains("search starts here:")) + .take_while(|s| !s.contains("End of search list.")) + .filter(|s| s.starts_with(" ")) + .map(|s| format!("-I{}", s.trim())); + + bindings = bindings.clang_args(include_args); + } else { + bindings = bindings.clang_args(&[ + "-I../unix", + "-I../../build/unix", + "-I../../vendor/micropython", + ]); + } + + bindings + // Customize the standard types. + .use_core() + .ctypes_prefix("cty") + .size_t_is_usize(true) + // Tell cargo to invalidate the built crate whenever any of the + // included header files change. + .parse_callbacks(Box::new(bindgen::CargoCallbacks)) + // Write the bindings to a file in the OUR_DIR. + .generate() + .expect("Unable to generate Rust Micropython bindings") + .write_to_file(PathBuf::from(out_path).join("micropython.rs")) + .unwrap(); +} diff --git a/core/embed/rust/micropython.h b/core/embed/rust/micropython.h new file mode 100644 index 0000000000..9838a9cd30 --- /dev/null +++ b/core/embed/rust/micropython.h @@ -0,0 +1,5 @@ +#include "py/gc.h" +#include "py/obj.h" +#include "py/runtime.h" + +#include "../extmod/trezorobj.h" diff --git a/core/embed/rust/qstr.h b/core/embed/rust/qstr.h new file mode 100644 index 0000000000..70348fb529 --- /dev/null +++ b/core/embed/rust/qstr.h @@ -0,0 +1,5 @@ +enum Qstr { +#define QDEF(id, str) id, +#include "genhdr/qstrdefs.generated.h" +#undef QDEF +}; diff --git a/core/embed/rust/rust-toolchain b/core/embed/rust/rust-toolchain new file mode 100644 index 0000000000..c1a4c6bb41 --- /dev/null +++ b/core/embed/rust/rust-toolchain @@ -0,0 +1,2 @@ +nightly + diff --git a/core/embed/rust/rustfmt.toml b/core/embed/rust/rustfmt.toml new file mode 100644 index 0000000000..859f26e7c1 --- /dev/null +++ b/core/embed/rust/rustfmt.toml @@ -0,0 +1,2 @@ +wrap_comments = true + diff --git a/core/embed/rust/src/error.rs b/core/embed/rust/src/error.rs new file mode 100644 index 0000000000..8cb2622afc --- /dev/null +++ b/core/embed/rust/src/error.rs @@ -0,0 +1,49 @@ +use core::convert::Infallible; +use core::fmt; + +use cstr_core::CStr; + +pub enum Error { + Missing, + OutOfRange, + InvalidType, + NotBuffer, + NotInt, +} + +impl Error { + pub fn as_cstr(&self) -> &'static CStr { + // SAFETY: Safe because we are passing in \0-terminated literals. + unsafe { + let cstr = |s: &'static str| CStr::from_bytes_with_nul_unchecked(s.as_bytes()); + match self { + Error::Missing => cstr("Missing\0"), + Error::OutOfRange => cstr("OutOfRange\0"), + Error::InvalidType => cstr("InvalidType\0"), + Error::NotBuffer => cstr("NotBuffer\0"), + Error::NotInt => cstr("NotInt\0"), + } + } + } +} + +impl From for &'static CStr { + fn from(val: Error) -> Self { + val.as_cstr() + } +} + +impl fmt::Debug for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.as_cstr().fmt(f) + } +} + +// Implements a conversion from `core::convert::Infallible` to `Error` to so +// that code generic over `TryFrom` can work with values covered by the blanket +// impl for `Into`: `https://doc.rust-lang.org/std/convert/enum.Infallible.html` +impl From for Error { + fn from(_: Infallible) -> Self { + unreachable!() + } +} diff --git a/core/embed/rust/src/lib.rs b/core/embed/rust/src/lib.rs new file mode 100644 index 0000000000..ae28ac1b6f --- /dev/null +++ b/core/embed/rust/src/lib.rs @@ -0,0 +1,37 @@ +#![cfg_attr(not(test), no_std)] +#![feature(never_type)] +#![feature(unsize)] +#![feature(coerce_unsized)] +#![feature(dispatch_from_dyn)] +#![deny(clippy::all)] +#![deny(unsafe_op_in_unsafe_fn)] +#![allow(dead_code)] + +mod error; +#[macro_use] +mod micropython; +mod trezorhal; +mod util; + +#[cfg(not(test))] +use core::panic::PanicInfo; +#[cfg(not(test))] +use cstr_core::CStr; + +#[cfg(not(test))] +#[panic_handler] +fn panic(_info: &PanicInfo) -> ! { + // Although it would be ideal to use the original error message, ignoring it + // lets us avoid the `fmt` machinery and its code size and is also important for + // security reasons, as we do not always controls the message contents. We + // should also avoid printing "panic" or "rust" on the user screen to avoid any + // confusion. + + // SAFETY: Safe because we are passing in \0-terminated literals. + let empty = unsafe { CStr::from_bytes_with_nul_unchecked("\0".as_bytes()) }; + let msg = unsafe { CStr::from_bytes_with_nul_unchecked("rs\0".as_bytes()) }; + + // TODO: Ideally we would take the file and line info out of + // `PanicInfo::location()`. + trezorhal::common::fatal_error(&empty, &msg, &empty, 0, &empty); +} diff --git a/core/embed/rust/src/micropython/buffer.rs b/core/embed/rust/src/micropython/buffer.rs new file mode 100644 index 0000000000..49930cbf1e --- /dev/null +++ b/core/embed/rust/src/micropython/buffer.rs @@ -0,0 +1,66 @@ +use core::{convert::TryFrom, ops::Deref, ptr, slice}; + +use crate::{error::Error, micropython::obj::Obj}; + +use super::ffi; + +/// Represents an immutable slice of bytes stored on the MicroPython heap and +/// owned by values that obey the buffer protocol, such as `bytes`, `str`, +/// `bytearray` or `memoryview`. +/// +/// # Safety +/// +/// In most cases, it is unsound to store `Buffer` values in a GC-unreachable +/// location, such as static data. +pub struct Buffer { + ptr: *const u8, + len: usize, +} + +impl TryFrom for Buffer { + type Error = Error; + + fn try_from(obj: Obj) -> Result { + let mut bufinfo = ffi::mp_buffer_info_t { + buf: ptr::null_mut(), + len: 0, + typecode: 0, + }; + // SAFETY: We assume that if `ffi::mp_get_buffer` returns successfully, + // `bufinfo.buf` contains a pointer to data of `bufinfo.len` bytes. Here + // we consider this data either GC-allocated or effectively 'static, and + // store the pointer directly in `Buffer`. It is unsound to store + // `Buffer` values in a GC-unreachable location, such as static data. + if unsafe { ffi::mp_get_buffer(obj, &mut bufinfo, ffi::MP_BUFFER_READ as _) } { + Ok(Self { + ptr: bufinfo.buf as _, + len: bufinfo.len as _, + }) + } else { + Err(Error::NotBuffer) + } + } +} + +impl Deref for Buffer { + type Target = [u8]; + + fn deref(&self) -> &Self::Target { + self.as_ref() + } +} + +impl AsRef<[u8]> for Buffer { + fn as_ref(&self) -> &[u8] { + if self.ptr.is_null() { + // `self.ptr` can be null if len == 0. + &[] + } else { + // SAFETY: We assume that `self.ptr` is pointing to memory: + // - without any mutable references, + // - immutable for the whole lifetime of `&self`, + // - with at least `self.len` bytes. + unsafe { slice::from_raw_parts(self.ptr, self.len) } + } + } +} diff --git a/core/embed/rust/src/micropython/dict.rs b/core/embed/rust/src/micropython/dict.rs new file mode 100644 index 0000000000..2d4b9dd20a --- /dev/null +++ b/core/embed/rust/src/micropython/dict.rs @@ -0,0 +1,70 @@ +use core::convert::TryFrom; + +use crate::error::Error; + +use super::{ffi, gc::Gc, map::Map, obj::Obj}; + +/// Insides of the MicroPython `dict` object. +pub type Dict = ffi::mp_obj_dict_t; + +impl Dict { + /// Allocate a new dictionary on the GC heap, empty, but with a capacity of + /// `capacity` items. + pub fn alloc_with_capacity(capacity: usize) -> Gc { + unsafe { + // SAFETY: We expect that `ffi::mp_obj_new_dict` either returns a GC-allocated + // pointer to `ffi::mp_obj_dict_t` or raises (i.e. on allocation failure). + let ptr = ffi::mp_obj_new_dict(capacity); + Gc::from_raw(ptr.as_ptr().cast()) + } + } + + /// Constructs a dictionary definition by taking ownership of given [`Map`]. + pub fn with_map(map: Map) -> Self { + unsafe { + Self { + base: ffi::mp_obj_base_t { + type_: &ffi::mp_type_dict, + }, + map, + } + } + } + + /// Returns a reference to the contained [`Map`]. + pub fn map(&self) -> &Map { + &self.map + } + + /// Returns a mutable reference to the contained [`Map`]. + pub fn map_mut(&mut self) -> &mut Map { + &mut self.map + } +} + +impl From> for Obj { + fn from(value: Gc) -> Self { + // SAFETY: + // - `value` is an object struct with a base and a type. + // - `value` is GC-allocated. + unsafe { Obj::from_ptr(Gc::into_raw(value).cast()) } + } +} + +impl TryFrom for Gc { + type Error = Error; + + fn try_from(value: Obj) -> Result { + if unsafe { ffi::mp_type_dict.is_type_of(value) } { + // SAFETY: We assume that if `value` is an object pointer with the correct type, + // it is managed by MicroPython GC (see `Gc::from_raw` for details). + let this = unsafe { Gc::from_raw(value.as_ptr().cast()) }; + Ok(this) + } else { + Err(Error::InvalidType) + } + } +} + +// SAFETY: We are in a single-threaded environment. +unsafe impl Sync for Dict {} diff --git a/core/embed/rust/src/micropython/ffi.rs b/core/embed/rust/src/micropython/ffi.rs new file mode 100644 index 0000000000..d268afd305 --- /dev/null +++ b/core/embed/rust/src/micropython/ffi.rs @@ -0,0 +1,5 @@ +#![allow(non_camel_case_types)] +#![allow(non_upper_case_globals)] +#![allow(dead_code)] + +include!(concat!(env!("OUT_DIR"), "/micropython.rs")); diff --git a/core/embed/rust/src/micropython/func.rs b/core/embed/rust/src/micropython/func.rs new file mode 100644 index 0000000000..038157555a --- /dev/null +++ b/core/embed/rust/src/micropython/func.rs @@ -0,0 +1,18 @@ +use super::{ffi, obj::Obj}; + +pub type Func = ffi::mp_obj_fun_builtin_fixed_t; + +impl Func { + /// Convert a "static const" function to a MicroPython object. + pub const fn to_obj(&'static self) -> Obj { + // SAFETY: + // - We are an object struct with a base and a type. + // - 'static lifetime holds us in place. + // - MicroPython is smart enough not to mutate `mp_obj_fun_builtin_fixed_t` + // objects. + unsafe { Obj::from_ptr(self as *const _ as *mut _) } + } +} + +// SAFETY: We are in a single-threaded environment. +unsafe impl Sync for Func {} diff --git a/core/embed/rust/src/micropython/gc.rs b/core/embed/rust/src/micropython/gc.rs new file mode 100644 index 0000000000..e4f6ece9d4 --- /dev/null +++ b/core/embed/rust/src/micropython/gc.rs @@ -0,0 +1,81 @@ +use core::{ + alloc::Layout, + marker::Unsize, + ops::{CoerceUnsized, Deref, DispatchFromDyn}, + ptr::{self, NonNull}, +}; + +use super::ffi; + +/// A pointer type for values on the garbage-collected heap. +/// +/// Although a garbage-collected pointer type technically should implement +/// `Copy` and `Clone`, we avoid doing this until proven necessary. +pub struct Gc(NonNull); + +impl, U: ?Sized> CoerceUnsized> for Gc {} +impl, U: ?Sized> DispatchFromDyn> for Gc {} + +impl Gc { + /// Allocate memory on the heap managed by the MicroPython garbage collector + /// and then place `v` into it. `v` will _not_ get its destructor called. + pub fn new(v: T) -> Self { + let layout = Layout::for_value(&v); + // TODO: Assert that `layout.align()` is the same as the GC alignment. + // SAFETY: + // - Unfortunately we cannot respect `layout.align()` as MicroPython GC does + // not support custom alignment. + // - `ptr` is guaranteed to stay valid as long as it's reachable from the stack + // or the MicroPython heap. + unsafe { + let raw = ffi::gc_alloc(layout.size(), 0).cast(); + ptr::write(raw, v); + Self::from_raw(raw) + } + } + + /// Return a mutable reference to the value. + /// + /// # Safety + /// + /// `Gc` values can originate in the MicroPython interpreter, and these can + /// be both shared and mutable. Before calling this function, you have to + /// ensure that `this` is unique for the whole lifetime of the + /// returned mutable reference. + pub unsafe fn as_mut(this: &mut Self) -> &mut T { + // SAFETY: The caller must guarantee that `this` meets all the requirements for + // a mutable reference. + unsafe { this.0.as_mut() } + } +} + +impl Gc { + /// Construct a `Gc` from a raw pointer. + /// + /// # Safety + /// + /// This function is unsafe because the caller has to guarantee that `ptr` + /// is pointing to a memory understood by the MicroPython GC, that is: + /// - previously allocated through `Gc::new()` or `gc_alloc()`, or + /// - through the MicroPython interpreter, or + /// - one of the GC roots (sys.argv, sys.modules, etc.). + pub unsafe fn from_raw(ptr: *mut T) -> Self { + // SAFETY: The caller must guarantee that `ptr` is something the MicroPython GC + // can reason about. + unsafe { Self(NonNull::new_unchecked(ptr)) } + } + + /// Convert `this` into a raw pointer. This will _not_ drop the contained + /// value. + pub fn into_raw(this: Self) -> *mut T { + this.0.as_ptr() + } +} + +impl Deref for Gc { + type Target = T; + + fn deref(&self) -> &Self::Target { + unsafe { self.0.as_ref() } + } +} diff --git a/core/embed/rust/src/micropython/iter.rs b/core/embed/rust/src/micropython/iter.rs new file mode 100644 index 0000000000..90dbb5cbf3 --- /dev/null +++ b/core/embed/rust/src/micropython/iter.rs @@ -0,0 +1,69 @@ +use core::ptr; + +use crate::error::Error; +use crate::micropython::obj::Obj; + +use super::ffi; + +pub struct IterBuf { + iter_buf: ffi::mp_obj_iter_buf_t, +} + +impl IterBuf { + pub fn new() -> Self { + Self { + iter_buf: ffi::mp_obj_iter_buf_t { + base: ffi::mp_obj_base_t { + type_: ptr::null_mut(), + }, + buf: [Obj::const_null(), Obj::const_null(), Obj::const_null()], + }, + } + } +} + +pub struct Iter<'a> { + iter: Obj, + iter_buf: &'a mut IterBuf, + finished: bool, +} + +impl<'a> Iter<'a> { + pub fn try_from_obj_with_buf(o: Obj, iter_buf: &'a mut IterBuf) -> Result { + // SAFETY: + // - In the common case, `ffi::mp_getiter` does not heap-allocate, but instead + // uses memory from the passed `iter_buf`. We maintain this invariant by + // taking a mut ref to `IterBuf` and tying it to the lifetime of returned + // `Iter`. + // - Raises if `o` is not iterable and in other cases as well. + // - Returned obj is referencing into `iter_buf`. + // TODO: Use a fn that doesn't raise on non-iterable object. + let iter = unsafe { ffi::mp_getiter(o, &mut iter_buf.iter_buf) }; + Ok(Self { + iter, + iter_buf, + finished: false, + }) + } +} + +impl<'a> Iterator for Iter<'a> { + type Item = Obj; + + fn next(&mut self) -> Option { + if self.finished { + return None; + } + // SAFETY: + // - We assume that `mp_iternext` returns objects without any lifetime + // invariants, i.e. heap-allocated, unlike `mp_getiter`. + // - Can raise. + let item = unsafe { ffi::mp_iternext(self.iter) }; + if item == Obj::const_stop_iteration() { + self.finished = true; + None + } else { + Some(item) + } + } +} diff --git a/core/embed/rust/src/micropython/list.rs b/core/embed/rust/src/micropython/list.rs new file mode 100644 index 0000000000..28120bf894 --- /dev/null +++ b/core/embed/rust/src/micropython/list.rs @@ -0,0 +1,50 @@ +use core::convert::TryFrom; + +use crate::error::Error; + +use super::{ffi, gc::Gc, obj::Obj}; + +pub type List = ffi::mp_obj_list_t; + +impl List { + pub fn alloc(values: &[Obj]) -> Gc { + // SAFETY: Altough `values` are copied into the new list and not mutated, + // `mp_obj_new_list` is taking them through a mut pointer. + unsafe { + let list = ffi::mp_obj_new_list(values.len(), values.as_ptr() as *mut Obj); + Gc::from_raw(list.as_ptr().cast()) + } + } + + pub fn append(&mut self, value: Obj) { + unsafe { + let ptr = self as *mut Self; + let list = Obj::from_ptr(ptr.cast()); + ffi::mp_obj_list_append(list, value); + } + } +} + +impl From> for Obj { + fn from(value: Gc) -> Self { + // SAFETY: + // - `value` is an object struct with a base and a type. + // - `value` is GC-allocated. + unsafe { Obj::from_ptr(Gc::into_raw(value).cast()) } + } +} + +impl TryFrom for Gc { + type Error = Error; + + fn try_from(value: Obj) -> Result { + if unsafe { ffi::mp_type_list.is_type_of(value) } { + // SAFETY: We assume that if `value` is an object pointer with the correct type, + // it is managed by MicroPython GC (see `Gc::from_raw` for details). + let this = unsafe { Gc::from_raw(value.as_ptr().cast()) }; + Ok(this) + } else { + Err(Error::InvalidType) + } + } +} diff --git a/core/embed/rust/src/micropython/macros.rs b/core/embed/rust/src/micropython/macros.rs new file mode 100644 index 0000000000..e60526f7a0 --- /dev/null +++ b/core/embed/rust/src/micropython/macros.rs @@ -0,0 +1,132 @@ +macro_rules! obj_fn_1 { + ($f:expr) => {{ + #[allow(unused_unsafe)] + unsafe { + use $crate::micropython::ffi; + + ffi::mp_obj_fun_builtin_fixed_t { + base: ffi::mp_obj_base_t { + type_: &ffi::mp_type_fun_builtin_1, + }, + fun: ffi::_mp_obj_fun_builtin_fixed_t__bindgen_ty_1 { _1: Some($f) }, + } + } + }}; +} + +macro_rules! obj_fn_2 { + ($f:expr) => {{ + #[allow(unused_unsafe)] + unsafe { + use $crate::micropython::ffi; + + ffi::mp_obj_fun_builtin_fixed_t { + base: ffi::mp_obj_base_t { + type_: &ffi::mp_type_fun_builtin_2, + }, + fun: ffi::_mp_obj_fun_builtin_fixed_t__bindgen_ty_1 { _2: Some($f) }, + } + } + }}; +} + +macro_rules! obj_fn_3 { + ($f:expr) => {{ + #[allow(unused_unsafe)] + unsafe { + use $crate::micropython::ffi; + + ffi::mp_obj_fun_builtin_fixed_t { + base: ffi::mp_obj_base_t { + type_: &ffi::mp_type_fun_builtin_3, + }, + fun: ffi::_mp_obj_fun_builtin_fixed_t__bindgen_ty_1 { _3: Some($f) }, + } + } + }}; +} + +/// Construct fixed static const `Map` from `key` => `val` pairs. +macro_rules! obj_map { + ($($key:expr => $val:expr),*) => ({ + Map::from_fixed_static(&[ + $( + Map::at($key, $val), + )* + ]) + }); + ($($key:expr => $val:expr),* ,) => ({ + obj_map!($($key => $val),*) + }); +} + +/// Construct a `Dict` from the backing `Map`. See `obj_map` above. +macro_rules! obj_dict { + ($map:expr) => {{ + #[allow(unused_unsafe)] + unsafe { + use $crate::micropython::ffi; + + ffi::mp_obj_dict_t { + base: ffi::mp_obj_base_t { + type_: &ffi::mp_type_dict, + }, + map: $map, + } + } + }}; +} + +macro_rules! obj_type { + (name: $name:expr, + $(locals: $locals:expr,)? + $(attr_fn: $attr_fn:ident,)? + $(call_fn: $call_fn:ident,)? + ) => {{ + #[allow(unused_unsafe)] + unsafe { + use $crate::micropython::ffi; + + let name = $name.to_u16(); + + #[allow(unused_mut)] + #[allow(unused_assignments)] + let mut attr: ffi::mp_attr_fun_t = None; + $(attr = Some($attr_fn);)? + + #[allow(unused_mut)] + #[allow(unused_assignments)] + let mut call: ffi::mp_call_fun_t = None; + $(call = Some($call_fn);)? + + // TODO: This is safe only if we pass in `Dict` with fixed `Map` (created by + // `Map::fixed()`), because only then will Micropython treat `locals_dict` as + // immutable, and make the mutable cast safe. + #[allow(unused_mut)] + #[allow(unused_assignments)] + let mut locals_dict = ::core::ptr::null_mut(); + $(locals_dict = $locals as *const _ as *mut _;)? + + ffi::mp_obj_type_t { + base: ffi::mp_obj_base_t { + type_: &ffi::mp_type_type, + }, + flags: 0, + name, + print: None, + make_new: None, + call, + unary_op: None, + binary_op: None, + attr, + subscr: None, + getiter: None, + iternext: None, + buffer_p: ffi::mp_buffer_p_t { get_buffer: None }, + protocol: ::core::ptr::null(), + parent: ::core::ptr::null(), + locals_dict, + } + } + }}; +} diff --git a/core/embed/rust/src/micropython/map.rs b/core/embed/rust/src/micropython/map.rs new file mode 100644 index 0000000000..de032878dd --- /dev/null +++ b/core/embed/rust/src/micropython/map.rs @@ -0,0 +1,193 @@ +use core::{marker::PhantomData, mem::MaybeUninit, ops::Deref, ptr, slice}; + +use crate::{ + error::Error, + micropython::{obj::Obj, qstr::Qstr}, +}; + +use super::ffi; + +pub type Map = ffi::mp_map_t; +pub type MapElem = ffi::mp_map_elem_t; + +impl Map { + pub const fn from_fixed_static(table: &'static [MapElem; N]) -> Self { + // We can't use the regular accessors generated by bindgen for us, as they are + // not `const`. Instead, we build the bitfield manually. + + // micropython/py/obj.h MP_DEFINE_CONST_DICT + // .all_keys_are_qstrs = 1, + // .is_fixed = 1, + // .is_ordered = 1, + // .used = MP_ARRAY_SIZE(table_name), + // .alloc = MP_ARRAY_SIZE(table_name), + let bits = 0b111 | N << 3; + let bitfield = ffi::__BindgenBitfieldUnit::new(bits.to_ne_bytes()); + Self { + _bitfield_align_1: [], + _bitfield_1: bitfield, + alloc: N, + table: table.as_ptr() as *mut MapElem, + } + } + + pub const fn at(key: Qstr, value: Obj) -> MapElem { + MapElem { + key: key.to_obj(), + value, + } + } +} + +impl Map { + pub fn from_fixed<'a>(table: &'a [MapElem]) -> MapRef<'a> { + let mut map = MaybeUninit::uninit(); + // SAFETY: `mp_map_init` completely initializes all fields of `map`. + unsafe { + ffi::mp_map_init_fixed_table(map.as_mut_ptr(), table.len(), table.as_ptr().cast()); + MapRef::new(map.assume_init()) + } + } + + pub fn with_capacity(capacity: usize) -> Self { + let mut map = MaybeUninit::uninit(); + // SAFETY: `mp_map_init` completely initializes all fields of `map`, allocating + // the backing storage for `capacity` items on the heap. + unsafe { + ffi::mp_map_init(map.as_mut_ptr(), capacity); + map.assume_init() + } + } + + pub fn len(&self) -> usize { + self.used() + } + + pub fn elems(&self) -> &[MapElem] { + // SAFETY: `self.table` should always point to an array of `MapElem` of + // `self.len()` items valid at least for the lifetime of `self`. + unsafe { slice::from_raw_parts(self.table, self.len()) } + } + + pub fn contains_key(&self, index: impl Into) -> bool { + self.get_obj(index.into()).is_ok() + } + + pub fn get(&self, index: impl Into) -> Result { + self.get_obj(index.into()) + } + + pub fn get_obj(&self, index: Obj) -> Result { + // SAFETY: + // - `mp_map_lookup` returns either NULL or a pointer to a `mp_map_elem_t` + // value with a lifetime valid for the whole lifetime of the passed immutable + // ref of `map`. + // - with `_mp_map_lookup_kind_t_MP_MAP_LOOKUP`, `map` stays unmodified and the + // cast to mut ptr is therefore safe. + unsafe { + let map = self as *const Self as *mut Self; + let elem = ffi::mp_map_lookup(map, index, ffi::_mp_map_lookup_kind_t_MP_MAP_LOOKUP) + .as_ref() + .ok_or(Error::Missing)?; + Ok(elem.value) + } + } + + pub fn set(&mut self, index: impl Into, value: impl Into) { + self.set_obj(index.into(), value.into()) + } + + pub fn set_obj(&mut self, index: Obj, value: Obj) { + // SAFETY: + // - `mp_map_lookup` with `_mp_map_lookup_kind_t_MP_MAP_LOOKUP_ADD_IF_NOT_FOUND + // returns a pointer to a `mp_map_elem_t` value with a lifetime valid for the + // whole lifetime of `&mut self`. + // - adding an element is an allocation, so it might raise. + // - the original `elem.value` might be an `Obj` that will get GCd when we + // replace it. + unsafe { + let map = self as *mut Self; + let elem = ffi::mp_map_lookup( + map, + index, + ffi::_mp_map_lookup_kind_t_MP_MAP_LOOKUP_ADD_IF_NOT_FOUND, + ) + .as_mut() + .unwrap(); + elem.value = value; + } + } + + pub fn delete(&mut self, index: impl Into) { + self.delete_obj(index.into()) + } + + pub fn delete_obj(&mut self, index: Obj) { + unsafe { + let map = self as *mut Self; + ffi::mp_map_lookup( + map, + index, + ffi::_mp_map_lookup_kind_t_MP_MAP_LOOKUP_REMOVE_IF_FOUND, + ); + } + } +} + +impl Clone for Map { + fn clone(&self) -> Self { + let mut map = Self::with_capacity(self.len()); + unsafe { + ptr::copy_nonoverlapping(self.table, map.table, self.len()); + } + map.set_used(self.used()); + map.set_all_keys_are_qstrs(self.all_keys_are_qstrs()); + map.set_is_ordered(self.is_ordered()); + map.set_is_fixed(0); + map + } +} + +impl Default for Map { + fn default() -> Self { + Self::with_capacity(0) + } +} + +// SAFETY: We are in a single-threaded environment. +unsafe impl Sync for Map {} +unsafe impl Sync for MapElem {} + +/// Wrapper over a private `Map` value with element lifetime `'a`. +/// +/// `Map` itself does not have any lifetime parameter, so it can effectively +/// only reference element tables with `'static` lifetime. `MapRef` is tying a +/// private `Map` value to a lifetime `'a`, exposing it only by-ref within the +/// `'a` lifetime. It's not possible to move the value out from the reference, +/// and because `Map` is not `Clone`, it is also impossible to clone it. All +/// this only holds for immutable ref, therefore we only impl `Deref`, not +/// `DerefMut`. +// +// TODO: Better solution would be to make Map a wrapper type instead of a type +// alias and add a lifetime parameter. +pub struct MapRef<'a> { + map: Map, + _phantom: PhantomData<&'a Map>, +} + +impl<'a> MapRef<'a> { + fn new(map: Map) -> Self { + Self { + map, + _phantom: PhantomData, + } + } +} + +impl<'a> Deref for MapRef<'a> { + type Target = Map; + + fn deref(&self) -> &Self::Target { + &self.map + } +} diff --git a/core/embed/rust/src/micropython/mod.rs b/core/embed/rust/src/micropython/mod.rs new file mode 100644 index 0000000000..3b45216195 --- /dev/null +++ b/core/embed/rust/src/micropython/mod.rs @@ -0,0 +1,15 @@ +#[macro_use] +pub mod macros; + +pub mod buffer; +pub mod dict; +pub mod ffi; +pub mod func; +pub mod gc; +pub mod iter; +pub mod list; +pub mod map; +pub mod obj; +pub mod qstr; +pub mod runtime; +pub mod typ; diff --git a/core/embed/rust/src/micropython/obj.rs b/core/embed/rust/src/micropython/obj.rs new file mode 100644 index 0000000000..df4c4bbdce --- /dev/null +++ b/core/embed/rust/src/micropython/obj.rs @@ -0,0 +1,327 @@ +use core::convert::{TryFrom, TryInto}; +use core::num::TryFromIntError; + +use crate::error::Error; + +use super::ffi; + +pub type Obj = ffi::mp_obj_t; +pub type ObjBase = ffi::mp_obj_base_t; + +impl PartialEq for Obj { + fn eq(&self, other: &Self) -> bool { + self.as_bits() == other.as_bits() + } +} + +impl Eq for Obj {} + +impl Obj { + pub const unsafe fn from_ptr(ptr: *mut cty::c_void) -> Self { + Self(ptr) + } + + pub const fn as_ptr(self) -> *mut cty::c_void { + self.0 + } + + pub const unsafe fn from_bits(bits: cty::uintptr_t) -> Self { + Self(bits as _) + } + + pub fn as_bits(self) -> cty::uintptr_t { + self.0 as _ + } +} + +impl Obj { + pub fn is_small_int(self) -> bool { + // micropython/py/obj.h mp_obj_is_small_int + self.as_bits() & 1 != 0 + } + + pub fn is_qstr(self) -> bool { + // micropython/py/obj.h mp_obj_is_qstr + self.as_bits() & 7 == 2 + } + + pub fn is_immediate(self) -> bool { + // micropython/py/obj.h mp_obj_is_immediate_obj + self.as_bits() & 7 == 6 + } + + pub fn is_ptr(self) -> bool { + // micropython/py/obj.h mp_obj_is_obj + self.as_bits() & 3 == 0 + } +} + +impl Obj { + pub const fn const_null() -> Self { + // micropython/py/obj.h + // #define MP_OBJ_NULL (MP_OBJ_FROM_PTR((void *)0)) + unsafe { Self::from_bits(0) } + } + + pub const fn const_stop_iteration() -> Self { + // micropython/py/obj.h + // #define MP_OBJ_STOP_ITERATION (MP_OBJ_FROM_PTR((void *)0)) + unsafe { Self::from_bits(0) } + } + + pub const fn const_none() -> Self { + // micropython/py/obj.h + // #define mp_const_none MP_OBJ_NEW_IMMEDIATE_OBJ(0) + unsafe { Self::immediate(0) } + } + + pub const fn const_false() -> Self { + // micropython/py/obj.h + // #define mp_const_false MP_OBJ_NEW_IMMEDIATE_OBJ(1) + unsafe { Self::immediate(1) } + } + + pub const fn const_true() -> Self { + // micropython/py/obj.h + // #define mp_const_true MP_OBJ_NEW_IMMEDIATE_OBJ(3) + unsafe { Self::immediate(3) } + } + + const unsafe fn immediate(val: usize) -> Self { + // SAFETY: + // - `val` is in `0..=3` range. + // - MicroPython compiled with `MICROPY_OBJ_REPR == MICROPY_OBJ_REPR_A`. + // - MicroPython compiled with `MICROPY_OBJ_IMMEDIATE_OBJS`. + // micropython/py/obj.h #define MP_OBJ_NEW_IMMEDIATE_OBJ(val) + // ((mp_obj_t)(((val) << 3) | 6)) + unsafe { Self::from_bits((val << 3) | 6) } + } +} + +impl Obj { + pub fn call_with_n_args(self, args: &[Obj]) -> Obj { + // SAFETY: + // - Can call python code and therefore raise. + // - Each of `args` has no lifetime bounds. + unsafe { ffi::mp_call_function_n_kw(self, args.len(), 0, args.as_ptr()) } + } +} + +// +// # Converting `Obj` into plain data. +// + +impl TryFrom for bool { + type Error = Error; + + fn try_from(obj: Obj) -> Result { + // TODO: Avoid type casts on the Python side. + // SAFETY: + // - Can call python code and therefore raise. + if unsafe { ffi::mp_obj_is_true(obj) } { + Ok(true) + } else { + Ok(false) + } + } +} + +impl TryFrom for i32 { + type Error = Error; + + fn try_from(obj: Obj) -> Result { + let mut int: ffi::mp_int_t = 0; + + // TODO: Avoid type casts on the Python side. + // SAFETY: + // - Can raise if `obj` is int but cannot fit into `cty::mp_int_t`. + if unsafe { ffi::mp_obj_get_int_maybe(obj.as_ptr(), &mut int) } { + let int = int.try_into()?; + Ok(int) + } else { + Err(Error::NotInt) + } + } +} + +impl TryFrom for i64 { + type Error = Error; + + fn try_from(obj: Obj) -> Result { + let mut ll: cty::c_longlong = 0; + + if unsafe { ffi::trezor_obj_get_ll_checked(obj, &mut ll) } { + Ok(ll) + } else { + Err(Error::NotInt) + } + } +} + +// +// # Converting plain data into `Obj`. +// + +impl From for Obj { + fn from(val: bool) -> Self { + if val { + Obj::const_true() + } else { + Obj::const_false() + } + } +} + +impl From for Obj { + fn from(val: i32) -> Self { + // `mp_obj_new_int` accepts a `mp_int_t` argument, which is word-sized. We + // primarily target 32-bit architecture, and therefore keep the primary signed + // conversion type as `i32`, but convert through `into()` if the types differ. + + // SAFETY: + // - Can raise if allocation fails. + unsafe { ffi::mp_obj_new_int(val.into()) } + } +} + +impl From for Obj { + fn from(val: i64) -> Self { + // Because `mp_obj_new_int_from_ll` allocates even if `val` fits into small-int, + // we try to go through `mp_obj_new_int` first. + match i32::try_from(val) { + Ok(smaller_val) => smaller_val.into(), + // SAFETY: + // - Can raise if allocation fails. + Err(_) => unsafe { ffi::mp_obj_new_int_from_ll(val) }, + } + } +} + +impl From for Obj { + fn from(val: u32) -> Self { + // `mp_obj_new_int_from_uint` accepts a `mp_uint_t` argument, which is + // word-sized. We primarily target 32-bit architecture, and therefore keep + // the primary unsigned conversion type as `u32`, but convert through `into()` + // if the types differ. + + // SAFETY: + // - Can raise if allocation fails. + unsafe { ffi::mp_obj_new_int_from_uint(val.into()) } + } +} + +impl From for Obj { + fn from(val: u64) -> Self { + // Because `mp_obj_new_int_from_ull` allocates even if `val` fits into + // small-int, we try to go through `mp_obj_new_int_from_uint` first. + match u32::try_from(val) { + Ok(smaller_val) => smaller_val.into(), + // SAFETY: + // - Can raise if allocation fails. + Err(_) => unsafe { ffi::mp_obj_new_int_from_ull(val) }, + } + } +} + +/// Byte slices are converted into `bytes` MicroPython objects, by allocating +/// new space on the heap and copying. +impl From<&[u8]> for Obj { + fn from(val: &[u8]) -> Self { + // SAFETY: + // - Can raise if allocation fails. + unsafe { ffi::mp_obj_new_bytes(val.as_ptr(), val.len()) } + } +} + +/// String slices are converted into `str` MicroPython objects. Strings that are +/// already interned will turn up as QSTRs, strings not found in the QSTR pool +/// will be allocated on the heap and copied. +impl From<&str> for Obj { + fn from(val: &str) -> Self { + // SAFETY: + // - Can raise if allocation fails. + // - `str` is guaranteed to be UTF-8. + unsafe { ffi::mp_obj_new_str(val.as_ptr().cast(), val.len()) } + } +} + +// +// # Additional conversions based on the methods above. +// + +impl From for Obj { + fn from(val: u8) -> Self { + u32::from(val).into() + } +} + +impl From for Obj { + fn from(val: u16) -> Self { + u32::from(val).into() + } +} + +impl From for Obj { + fn from(val: usize) -> Self { + // Willingly truncate the bits on 128-bit architectures. + (val as u64).into() + } +} + +impl TryFrom for u8 { + type Error = Error; + + fn try_from(obj: Obj) -> Result { + let val = i32::try_from(obj)?; + let this = Self::try_from(val)?; + Ok(this) + } +} + +impl TryFrom for u16 { + type Error = Error; + + fn try_from(obj: Obj) -> Result { + let val = i32::try_from(obj)?; + let this = Self::try_from(val)?; + Ok(this) + } +} + +impl TryFrom for u32 { + type Error = Error; + + fn try_from(obj: Obj) -> Result { + let val = i64::try_from(obj)?; + let this = Self::try_from(val)?; + Ok(this) + } +} + +impl TryFrom for u64 { + type Error = Error; + + fn try_from(obj: Obj) -> Result { + // TODO: Support full range. + let val = i64::try_from(obj)?; + let this = Self::try_from(val)?; + Ok(this) + } +} + +impl TryFrom for usize { + type Error = Error; + + fn try_from(obj: Obj) -> Result { + // TODO: Support full range. + let val = i64::try_from(obj)?; + let this = Self::try_from(val)?; + Ok(this) + } +} + +impl From for Error { + fn from(_: TryFromIntError) -> Self { + Self::OutOfRange + } +} diff --git a/core/embed/rust/src/micropython/qstr.rs b/core/embed/rust/src/micropython/qstr.rs new file mode 100644 index 0000000000..1a6425a492 --- /dev/null +++ b/core/embed/rust/src/micropython/qstr.rs @@ -0,0 +1,59 @@ +#![allow(non_camel_case_types)] +#![allow(non_upper_case_globals)] +#![allow(dead_code)] + +use core::convert::TryFrom; + +use crate::{error::Error, micropython::obj::Obj}; + +impl Qstr { + pub const fn to_obj(self) -> Obj { + // SAFETY: + // - Micropython compiled with `MICROPY_OBJ_REPR == MICROPY_OBJ_REPR_A`. + // micropython/py/obj.h #define MP_OBJ_NEW_QSTR(qst) + // ((mp_obj_t)((((mp_uint_t)(qst)) << 3) | 2)) + let bits = (self.0 << 3) | 2; + unsafe { Obj::from_bits(bits as usize) } + } + + pub const fn from_obj_bits(bits: cty::uintptr_t) -> Self { + let bits = (bits >> 3) as u16; // See `Self::to_obj`. + Self::from_u16(bits) + } + + pub const fn to_u16(self) -> u16 { + // TODO: Change the internal representation of Qstr to u16. + self.0 as _ + } + + pub const fn from_u16(val: u16) -> Self { + // TODO: Change the internal representation of Qstr to u16. + Self(val as _) + } +} + +impl From for Qstr { + fn from(val: u16) -> Self { + Self::from_u16(val) + } +} + +impl TryFrom for Qstr { + type Error = Error; + + fn try_from(value: Obj) -> Result { + if value.is_qstr() { + Ok(Self::from_obj_bits(value.as_bits())) + } else { + Err(Error::InvalidType) + } + } +} + +impl From for Obj { + fn from(value: Qstr) -> Self { + value.to_obj() + } +} + +include!(concat!(env!("OUT_DIR"), "/qstr.rs")); diff --git a/core/embed/rust/src/micropython/runtime.rs b/core/embed/rust/src/micropython/runtime.rs new file mode 100644 index 0000000000..c392913480 --- /dev/null +++ b/core/embed/rust/src/micropython/runtime.rs @@ -0,0 +1,10 @@ +use cstr_core::CStr; + +use super::ffi; + +pub fn raise_value_error(msg: &'static CStr) -> ! { + unsafe { + ffi::mp_raise_ValueError(msg.as_ptr()); + } + panic!(); +} diff --git a/core/embed/rust/src/micropython/typ.rs b/core/embed/rust/src/micropython/typ.rs new file mode 100644 index 0000000000..4666ed5626 --- /dev/null +++ b/core/embed/rust/src/micropython/typ.rs @@ -0,0 +1,28 @@ +use super::{ + ffi, + obj::{Obj, ObjBase}, +}; + +pub type Type = ffi::mp_obj_type_t; + +impl Type { + pub fn is_type_of(&'static self, obj: Obj) -> bool { + if obj.is_ptr() { + // SAFETY: If `obj` is a pointer, it should always point to an object having + // `ObjBase` as the first field, making this cast safe. + unsafe { + let base = obj.as_ptr() as *const ObjBase; + (*base).type_ == self + } + } else { + false + } + } + + pub fn to_base(&'static self) -> ObjBase { + ObjBase { type_: self } + } +} + +// SAFETY: We are in a single-threaded environment. +unsafe impl Sync for Type {} diff --git a/core/embed/rust/src/trezorhal/common.rs b/core/embed/rust/src/trezorhal/common.rs new file mode 100644 index 0000000000..ea9ab5fc11 --- /dev/null +++ b/core/embed/rust/src/trezorhal/common.rs @@ -0,0 +1,24 @@ +use cstr_core::CStr; + +extern "C" { + // trezorhal/common.c + fn __fatal_error( + expr: *const cty::c_char, + msg: *const cty::c_char, + file: *const cty::c_char, + line: i32, + func: *const cty::c_char, + ) -> !; +} + +pub fn fatal_error(expr: &CStr, msg: &CStr, file: &CStr, line: i32, func: &CStr) -> ! { + unsafe { + __fatal_error( + expr.as_ptr(), + msg.as_ptr(), + file.as_ptr(), + line, + func.as_ptr(), + ); + } +} diff --git a/core/embed/rust/src/trezorhal/display.rs b/core/embed/rust/src/trezorhal/display.rs new file mode 100644 index 0000000000..8068d85706 --- /dev/null +++ b/core/embed/rust/src/trezorhal/display.rs @@ -0,0 +1,69 @@ +extern "C" { + // trezorhal/display.c + fn display_backlight(val: cty::c_int) -> cty::c_int; + fn display_text( + x: cty::c_int, + y: cty::c_int, + text: *const cty::c_char, + textlen: cty::c_int, + font: cty::c_int, + fgcolor: cty::uint16_t, + bgcolor: cty::uint16_t, + ); + fn display_text_width( + text: *const cty::c_char, + textlen: cty::c_int, + font: cty::c_int, + ) -> cty::c_int; + fn display_bar(x: cty::c_int, y: cty::c_int, w: cty::c_int, h: cty::c_int, c: cty::uint16_t); + fn display_bar_radius( + x: cty::c_int, + y: cty::c_int, + w: cty::c_int, + h: cty::c_int, + c: cty::uint16_t, + b: cty::uint16_t, + r: cty::uint8_t, + ); +} + +const WIDTH: i32 = 240; +const HEIGHT: i32 = 240; + +pub fn width() -> i32 { + WIDTH +} + +pub fn height() -> i32 { + HEIGHT +} + +pub fn backlight(val: i32) -> i32 { + unsafe { display_backlight(val) } +} + +pub fn text(baseline_x: i32, baseline_y: i32, text: &[u8], font: i32, fgcolor: u16, bgcolor: u16) { + unsafe { + display_text( + baseline_x, + baseline_y, + text.as_ptr() as _, + text.len() as _, + font, + fgcolor, + bgcolor, + ) + } +} + +pub fn text_width(text: &[u8], font: i32) -> i32 { + unsafe { display_text_width(text.as_ptr() as _, text.len() as _, font) } +} + +pub fn bar(x: i32, y: i32, w: i32, h: i32, fgcolor: u16) { + unsafe { display_bar(x, y, w, h, fgcolor) } +} + +pub fn bar_radius(x: i32, y: i32, w: i32, h: i32, fgcolor: u16, bgcolor: u16, radius: u8) { + unsafe { display_bar_radius(x, y, w, h, fgcolor, bgcolor, radius) } +} diff --git a/core/embed/rust/src/trezorhal/mod.rs b/core/embed/rust/src/trezorhal/mod.rs new file mode 100644 index 0000000000..3cf8ab10e0 --- /dev/null +++ b/core/embed/rust/src/trezorhal/mod.rs @@ -0,0 +1,3 @@ +pub mod common; +pub mod display; +pub mod random; diff --git a/core/embed/rust/src/trezorhal/random.rs b/core/embed/rust/src/trezorhal/random.rs new file mode 100644 index 0000000000..6b73f6c07d --- /dev/null +++ b/core/embed/rust/src/trezorhal/random.rs @@ -0,0 +1,16 @@ +extern "C" { + // trezor-crypto/rand.h + fn random_uniform(n: u32) -> u32; +} + +pub fn uniform(n: u32) -> u32 { + unsafe { random_uniform(n) } +} + +pub fn shuffle(slice: &mut [T]) { + // Fisher-Yates shuffle. + for i in (1..slice.len()).rev() { + let j = uniform(i as u32 + 1) as usize; + slice.swap(i, j); + } +} diff --git a/core/embed/rust/src/util.rs b/core/embed/rust/src/util.rs new file mode 100644 index 0000000000..0241012e25 --- /dev/null +++ b/core/embed/rust/src/util.rs @@ -0,0 +1,63 @@ +use core::slice; + +use crate::{ + error::Error, + micropython::{ + map::{Map, MapElem}, + obj::Obj, + runtime::raise_value_error, + }, +}; + +pub fn try_or_raise(func: impl FnOnce() -> Result) -> Obj { + func().unwrap_or_else(|err| raise_value_error(err.as_cstr())) +} + +pub fn try_with_kwargs(kwargs: *const Map, func: impl FnOnce(&Map) -> Result) -> Obj { + try_or_raise(|| { + let kwargs = unsafe { kwargs.as_ref() }.ok_or(Error::Missing)?; + + func(kwargs) + }) +} + +pub fn try_with_args_and_kwargs( + n_args: usize, + args: *const Obj, + kwargs: *const Map, + func: impl FnOnce(&[Obj], &Map) -> Result, +) -> Obj { + try_or_raise(|| { + let args = if args.is_null() { + &[] + } else { + unsafe { slice::from_raw_parts(args, n_args) } + }; + let kwargs = unsafe { kwargs.as_ref() }.ok_or(Error::Missing)?; + + func(args, kwargs) + }) +} + +pub fn try_with_args_and_kwargs_inline( + n_args: usize, + n_kw: usize, + args: *const Obj, + func: impl FnOnce(&[Obj], &Map) -> Result, +) -> Obj { + try_or_raise(|| { + let args_slice: &[Obj]; + let kwargs_slice: &[MapElem]; + + if args.is_null() { + args_slice = &[]; + kwargs_slice = &[]; + } else { + args_slice = unsafe { slice::from_raw_parts(args, n_args) }; + kwargs_slice = unsafe { slice::from_raw_parts(args.add(n_args).cast(), n_kw) }; + } + + let kw_map = Map::from_fixed(kwargs_slice); + func(args_slice, &kw_map) + }) +}