feat(rust/micropython): more ergonomic tuples
What changed, and why it matters
This commit is a routine internal code cleanup in the Trezor firmware's Rust/MicroPython bridge. It introduces a dedicated tuple helper module so developers can write tuple conversions more naturally, and removes an older, less ergonomic helper. There is no indication it fixes a security bug or changes security-relevant behavior.
No security action required; review as normal refactoring if desired.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The change refactors tuple creation in Rust code that interfaces with MicroPython. It adds a new tuple.rs module with a Tuple::alloc method, Gc<Tuple> conversions, and a macro that implements TryFrom<(T1, T2, ...)> for Obj for up to six elements. Existing call sites are updated to use the new ergonomic conversions instead of the removed util::new_tuple or manual TryFrom<(Obj, Obj)> implementations. The diff shows only syntactic/structural changes; allocation paths still go through mp_obj_new_tuple and exception handling remains unchanged.
Changed components
core/embed/rust/src/micropython/tuple.rs (new)core/embed/rust/src/micropython/obj.rscore/embed/rust/src/micropython/util.rscore/embed/rust/src/thp/micropython.rscore/embed/rust/src/translations/obj.rscore/embed/rust/src/ui/layout/obj.rscore/embed/rust/src/ui/layout_bolt/component_msg_obj.rscore/embed/rust/src/ui/layout_caesar/component_msg_obj.rscore/embed/rust/src/ui/layout_eckhart/component_msg_obj.rsInspect captured patch +141 / −62
### core/embed/rust/build.rs
@@ -140,6 +140,9 @@ fn generate_micropython_bindings(lib: &mut CLibrary) -> Result<()> {
.allowlist_function("mp_print_strn")
.allowlist_function("str_modulo_format")
.allowlist_var("mp_plat_print")
+ // tuple
+ .allowlist_var("mp_type_tuple")
+ .allowlist_var("mp_const_empty_tuple_obj")
// typ
.allowlist_var("mp_type_type")
// module
### core/embed/rust/src/micropython/mod.rs
@@ -18,6 +18,7 @@ pub mod print;
pub mod qstr;
pub mod runtime;
pub mod simple_type;
+pub mod tuple;
pub mod typ;
pub mod util;
### core/embed/rust/src/micropython/obj.rs
@@ -305,40 +305,6 @@ impl TryFrom<&'static CStr> for Obj {
}
}
-impl TryFrom<(Obj, Obj)> for Obj {
- type Error = Error;
-
- fn try_from(val: (Obj, Obj)) -> Result<Self, Self::Error> {
- // SAFETY:
- // - Should work with any micropython objects.
- // EXCEPTION: Will raise if allocation fails.
- let values = [val.0, val.1];
- let obj = catch_exception(|| unsafe { ffi::mp_obj_new_tuple(2, values.as_ptr()) })?;
- if obj.is_null() {
- Err(Error::AllocationFailed)
- } else {
- Ok(obj)
- }
- }
-}
-
-impl TryFrom<(Obj, Obj, Obj)> for Obj {
- type Error = Error;
-
- fn try_from(val: (Obj, Obj, Obj)) -> Result<Self, Self::Error> {
- // SAFETY:
- // - Should work with any micropython objects.
- // EXCEPTION: Will raise if allocation fails.
- let values = [val.0, val.1, val.2];
- let obj = catch_exception(|| unsafe { ffi::mp_obj_new_tuple(3, values.as_ptr()) })?;
- if obj.is_null() {
- Err(Error::AllocationFailed)
- } else {
- Ok(obj)
- }
- }
-}
-
//
// # Additional conversions based on the methods above.
//
### core/embed/rust/src/micropython/tuple.rs
@@ -0,0 +1,119 @@
+use super::gc::Gc;
+use super::runtime::catch_exception;
+use super::{ffi, Error, Obj};
+
+pub type Tuple = ffi::mp_obj_tuple_t;
+
+impl Tuple {
+ pub const fn empty() -> Gc<Self> {
+ unsafe { Gc::from_raw(&ffi::mp_const_empty_tuple_obj as *const _ as *mut _) }
+ }
+
+ pub fn alloc(values: &[Obj]) -> Result<Gc<Self>, Error> {
+ if values.is_empty() {
+ return Ok(Self::empty());
+ }
+
+ // TODO: after the gc alloc refactor lands, we can allocate a tuple directly
+ // without involving a exception-raising call
+ // // construct a memory layout, simulating mp_obj_malloc_var
+ // let base_layout = Layout::new::<Tuple>();
+ // let items_layout = unwrap!(Layout::array::<Obj>(values.len()));
+ // let (layout, offset) = unwrap!(base_layout.extend(items_layout));
+ // // check that we're extending what we expect
+ // assert!(offset == core::mem::offset_of!(Tuple, items));
+
+ // SAFETY: Although `values` are copied into the new tuple and not mutated,
+ // `mp_obj_new_tuple` is taking them through a mut pointer.
+ // EXCEPTION: Will raise if allocation fails.
+ catch_exception(|| unsafe {
+ let tuple = ffi::mp_obj_new_tuple(values.len(), values.as_ptr() as *mut Obj);
+ Gc::from_raw(tuple.as_ptr().cast())
+ })
+ }
+
+ pub fn as_slice(&self) -> &[Obj] {
+ // SAFETY:
+ // - micropython promises that items have the right len
+ // - items are part of the same allocation so the lifetime bound is correct
+ unsafe { self.items.as_slice(self.len) }
+ }
+
+ pub fn as_mut_slice(&mut self) -> &mut [Obj] {
+ // SAFETY:
+ // - micropython promises that items have the right len
+ // - items are part of the same allocation so the lifetime bound is correct
+ unsafe { self.items.as_mut_slice(self.len) }
+ }
+}
+
+impl From<Gc<Tuple>> for Obj {
+ fn from(value: Gc<Tuple>) -> 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<Obj> for Gc<Tuple> {
+ type Error = Error;
+
+ fn try_from(obj: Obj) -> Result<Self, Self::Error> {
+ if unsafe { &ffi::mp_type_tuple }.is_type_of(obj) {
+ // 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(obj.as_ptr().cast()) };
+ Ok(this)
+ } else {
+ return Err(Error::TypeError);
+ }
+ }
+}
+
+macro_rules! impl_try_from_tuple {
+ () => {};
+ ($t:ident $v:ident $($tt:ident $vv:ident)*) => {
+ impl<$t, $($tt),*> TryFrom<($t, $($tt),*)> for Obj
+ where
+ Obj: TryFrom<$t>,
+ Error: From<<Obj as TryFrom<$t>>::Error>,
+ $(Obj: TryFrom<$tt>,)*
+ $(Error: From<<Obj as TryFrom<$tt>>::Error>,)*
+ {
+ type Error = Error;
+
+ fn try_from(($v, $($vv),*): ($t, $($tt),*)) -> Result<Self, Self::Error> {
+ let values = [
+ $v.try_into()?,
+ $($vv.try_into()?),*
+ ];
+ Ok(Tuple::alloc(&values)?.into())
+ }
+ }
+
+ impl_try_from_tuple!($($tt $vv)*);
+ }
+}
+
+impl_try_from_tuple!(T t U u V v W w X x Y y Z z);
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::micropython::buffer::StrBuffer;
+ use crate::micropython::testutil::mpy_init;
+
+ #[test]
+ fn test_try_from_tuple() {
+ unsafe { mpy_init() };
+
+ let tuple = (true, 100, "hello");
+ let obj: Obj = tuple.try_into().unwrap();
+ let decoded = Gc::<Tuple>::try_from(obj).unwrap();
+ let slice = decoded.as_slice();
+ assert_eq!(bool::try_from(slice[0]).unwrap(), true);
+ assert_eq!(i32::try_from(slice[1]).unwrap(), 100);
+ let string = StrBuffer::try_from(slice[2]).unwrap();
+ assert_eq!(string.as_ref(), "hello");
+ }
+}
### core/embed/rust/src/micropython/util.rs
@@ -84,13 +84,6 @@ pub unsafe fn try_with_args_and_kwargs_inline(
unsafe { try_or_raise(block) }
}
-pub fn new_tuple(args: &[Obj]) -> Result<Obj, Error> {
- // SAFETY: Safe.
- // EXCEPTION: Raises if allocation fails, does not return NULL.
- let obj = catch_exception(|| unsafe { ffi::mp_obj_new_tuple(args.len(), args.as_ptr()) })?;
- Ok(obj)
-}
-
/// Create a new "attrtuple", which is essentially a namedtuple / ad-hoc object.
///
/// It is recommended to use the attr_tuple! macro instead of this function:
### core/embed/rust/src/thp/micropython.rs
@@ -113,8 +113,8 @@ extern "C" fn thp_message_out(channel_id: Obj, receive_buffer_obj: Obj) -> Obj {
// Something is very wrong if message is longer than 64k, OK to panic.
let message_len = unwrap!(u16::try_from(message_len));
(
- sid.into(),
- message_type.into(),
+ sid,
+ message_type,
util::get_slice(receive_buffer_obj, APP_HEADER_LEN as u16, message_len)?,
)
.try_into()
@@ -275,9 +275,7 @@ extern "C" fn thp_next_timeout(iface_num: Obj) -> Obj {
let thp = THP_CONTEXT.try_lock().ok_or(CANNOT_UNLOCK)?;
match thp.next_timeout(iface_num)? {
None => Ok(Obj::const_none()),
- Some((channel_id, timeout_ms)) => {
- (channel_id.into(), timeout_ms.try_into()?).try_into()
- }
+ Some((channel_id, timeout_ms)) => (channel_id, timeout_ms).try_into(),
}
};
### core/embed/rust/src/translations/obj.rs
@@ -45,13 +45,13 @@ static TR_TYPE: FullType = obj_type! {
static TR_OBJ: SimpleTypeObj = SimpleTypeObj::new(&TR_TYPE);
fn make_translations_header(header: &super::blob::TranslationsHeader<'_>) -> Result<Obj, Error> {
- let version_objs: [Obj; 4] = {
+ let version_tuple = {
let v = header.version;
- [v[0].into(), v[1].into(), v[2].into(), v[3].into()]
+ (v[0], v[1], v[2], v[3])
};
attr_tuple! {
Qstr::MP_QSTR_language => header.language.try_into()?,
- Qstr::MP_QSTR_version => util::new_tuple(&version_objs)?,
+ Qstr::MP_QSTR_version => version_tuple.try_into()?,
Qstr::MP_QSTR_data_len => header.data_len.try_into()?,
Qstr::MP_QSTR_data_hash => header.data_hash.as_ref().try_into()?,
Qstr::MP_QSTR_total_len => header.total_len.try_into()?,
### core/embed/rust/src/ui/layout/obj.rs
@@ -356,7 +356,7 @@ impl LayoutObjInner {
fn obj_button_request(&mut self) -> Result<Obj, Error> {
match self.button_request.take() {
None => Ok(Obj::const_none()),
- Some(ButtonRequest { code, name }) => (code.num().into(), name.try_into()?).try_into(),
+ Some(ButtonRequest { code, name }) => (code.num(), name).try_into(),
}
}
### core/embed/rust/src/ui/layout_bolt/component_msg_obj.rs
@@ -195,7 +195,7 @@ where
F: Fn(u32) -> TString<'static>,
{
fn msg_try_into_obj(&self, msg: Self::Msg) -> Result<Obj, Error> {
- let value = self.value().try_into()?;
+ let value = self.value();
match msg {
NumberInputDialogMsg::Selected => Ok((CONFIRMED.as_obj(), value).try_into()?),
NumberInputDialogMsg::InfoRequested => Ok((INFO.as_obj(), value).try_into()?),
### core/embed/rust/src/ui/layout_caesar/component_msg_obj.rs
@@ -110,8 +110,8 @@ impl ComponentMsgObj for CoinJoinProgress {
impl ComponentMsgObj for NumberInput {
fn msg_try_into_obj(&self, msg: Self::Msg) -> Result<Obj, Error> {
match msg {
- Self::Msg::Cancel => (CANCELLED.as_obj(), 0.try_into()?).try_into(),
- Self::Msg::Choice { item, .. } => (CONFIRMED.as_obj(), item.try_into()?).try_into(),
+ Self::Msg::Cancel => (CANCELLED.as_obj(), 0).try_into(),
+ Self::Msg::Choice { item, .. } => (CONFIRMED.as_obj(), item).try_into(),
}
}
}
### core/embed/rust/src/ui/layout_eckhart/component_msg_obj.rs
@@ -7,7 +7,7 @@ use super::firmware::{
SelectWordScreen, SetBrightnessScreen, StringInput, StringKeyboard, StringKeyboardMsg,
TextScreen, TextScreenMsg, ValueInput, ValueInputScreen, ValueInputScreenMsg,
};
-use crate::micropython::util::new_tuple;
+use crate::micropython::tuple::Tuple;
use crate::micropython::{Error, Obj};
#[cfg(not(feature = "clippy"))]
use crate::ui::component::{
@@ -152,8 +152,6 @@ impl ComponentMsgObj for SetBrightnessScreen {
impl ComponentMsgObj for DeviceMenuScreen {
fn msg_try_into_obj(&self, msg: Self::Msg) -> Result<Obj, Error> {
- let action_obj = msg.id_to_obj();
- let result_obj = msg.args_to_obj();
let next_menu_id = self.next_menu_id(msg);
let vertical_offset: u16 = match self.current_state() {
// If the same menu will be displayed, reuse current menu offset.
@@ -163,11 +161,12 @@ impl ComponentMsgObj for DeviceMenuScreen {
}
_ => 0, // Otherwise, don't reapply current menu offset.
};
- new_tuple(&[
- action_obj,
- result_obj,
- next_menu_id.to_u8().try_into()?,
- vertical_offset.into(),
- ])
+ (
+ msg.id_to_obj(),
+ msg.args_to_obj(),
+ next_menu_id.to_u8(),
+ vertical_offset,
+ )
+ .try_into()
}
}Why this scored 15/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.