Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
333 changes: 333 additions & 0 deletions crates/core/src/endian.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,333 @@
//! Newtypes for dealing with endianness.

use core::{fmt, num::NonZero};

macro_rules! define_endian_wrapper_types {
Copy link
Copy Markdown
Member

@cfallin cfallin Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the newtype-correctness efforts here!

I'm finding the naming/conceptual definition a little confusing, to be honest, because (in my head) the integer types themselves have an abstract domain (0..2^width) and their byte representations have endianness. This aligns with e.g. the builtin uNN::to_le_bytes() -- the LE representation is a sequence of bytes, not a uNN.

So to that end, what does it mean when we have a Le(1234)? Does that mean the "actual value" is 1234 but the value is stored in memory as either 1234 (little-endian host) or bswap(1234) (big-endian host)? Or vice-versa, the value is stored in memory however the host does and we interpret it as LE ("1234-native-endian, interpreted as LE")? (EDIT: I guess these two views are actually equivalent in results, because on a LE host, Le and Ne both mean "identity", and on a BE host, Le means "byteswapped"; but that still leaves a confusing definitional question unanswered)

I think I might have an easier time if we rename things a bit -- or failing that (since these types are very pervasive), document better. Suggestions:

  • ExplicitLittleEndian(n) and HostOrdered(n)
  • store [u8; N] in the newtypes and make these traits wrappers around {from,to}_{le,ne}_bytes
  • something else I can't think of right now

I kind of favor the second, assuming that LLVM can see through it and not pessimize -- Godbolt example to demonstrate that I think it should: link

( $(
$( #[$attr:meta] )*
pub struct $name:ident(is_little = $is_little:expr): From<$other:ident>;
)* ) => {
$(
$( #[$attr] )*
pub struct $name<T>(T);

impl<T> From<$other<T>> for $name<T>
where
T: ToFromLe
{
#[inline]
fn from(x: $other<T>) -> Self {
if Self::is_little() {
Self(x.get_le())
} else {
Self(x.get_ne())
}
}
}

impl<T> Default for $name<T>
where
T: ToFromLe + Default
{
#[inline]
fn default() -> Self {
Self::from_ne(T::default())
}
}

impl<T> fmt::LowerHex for $name<T>
where
T: fmt::LowerHex + Copy + ToFromLe,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::LowerHex::fmt(&self.get_ne(), f)
}
}

impl<T> fmt::UpperHex for $name<T>
where
T: fmt::UpperHex + Copy + ToFromLe,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::UpperHex::fmt(&self.get_ne(), f)
}
}

impl<T> fmt::Pointer for $name<T>
where
T: fmt::Pointer + Copy + ToFromLe,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Pointer::fmt(&self.get_ne(), f)
}
}

impl<T> $name<T> {
#[inline]
const fn is_little() -> bool {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FWIW the is_little name was a bit confusing for me at first because is_little = false sounds (naively) like "big endian"; it's not, it's "native endian", which could be actually big or little depending on host.

Maybe is_explicit_little?

Copy link
Copy Markdown
Member

@cfallin cfallin Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(After writing this comment, see more general thoughts below on the confusion)

$is_little
}

/// Wrap the given little-endian `T` value.
#[inline]
pub fn from_le(inner: T) -> Self
where
T: ToFromLe,
{
if Self::is_little() {
Self(inner)
} else {
Self(ToFromLe::from_le(inner))
}
}

/// Wrap the given native-endian `T` value.
#[inline]
pub fn from_ne(inner: T) -> Self
where
T: ToFromLe,
{
if Self::is_little() {
Self(ToFromLe::to_le(inner))
} else {
Self(inner)
}
}

/// Get the inner wrapped value as little-endian.
#[inline]
pub fn get_le(self) -> T
where
T: ToFromLe,
{
if Self::is_little() {
self.0
} else {
ToFromLe::to_le(self.0)
}
}

/// Get the inner wrapped value as native-endian.
#[inline]
pub fn get_ne(self) -> T
where
T: ToFromLe,
{
if Self::is_little() {
ToFromLe::from_le(self.0)
} else {
self.0
}
}
}
)*
};
}

define_endian_wrapper_types! {
/// A wrapper around a native-endian `T`.
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(transparent)]
pub struct Ne(is_little = false): From<Le>;

/// A wrapper around a little-endian `T`.
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(transparent)]
pub struct Le(is_little = true): From<Ne>;
}

/// Convert to/from little-endian.
pub trait ToFromLe {
/// Convert from little-endian.
fn from_le(x: Self) -> Self;
/// Convert to little-endian.
fn to_le(self) -> Self;
}

macro_rules! impls {
( $($t:ty),* $(,)? ) => {
$(
impl ToFromLe for $t {
#[inline]
fn from_le(x: Self) -> Self {
<$t>::from_le(x)
}
#[inline]
fn to_le(self) -> Self {
self.to_le()
}
}

impl ToFromLe for NonZero<$t> {
#[inline]
fn from_le(x: Self) -> Self {
Self::new(<$t>::from_le(x.get())).unwrap()
}
#[inline]
fn to_le(self) -> Self {
Self::new(self.get().to_le()).unwrap()
}
}

impl TryFrom<Le<$t>> for Le<NonZero<$t>> {
type Error = <NonZero<$t> as TryFrom<$t>>::Error;

#[inline]
fn try_from(x: Le<$t>) -> Result<Self, Self::Error> {
Ok(Self::from_le(NonZero::try_from(x.get_le())?))
}
}

impl TryFrom<Ne<$t>> for Ne<NonZero<$t>> {
type Error = <NonZero<$t> as TryFrom<$t>>::Error;

#[inline]
fn try_from(x: Ne<$t>) -> Result<Self, Self::Error> {
Ok(Self::from_ne(NonZero::try_from(x.get_ne())?))
}
}

impl TryFrom<Ne<$t>> for Le<NonZero<$t>> {
type Error = <NonZero<$t> as TryFrom<$t>>::Error;

#[inline]
fn try_from(x: Ne<$t>) -> Result<Self, Self::Error> {
Ok(Self::from_le(NonZero::try_from(x.get_le())?))
}
}

impl TryFrom<Le<$t>> for Ne<NonZero<$t>> {
type Error = <NonZero<$t> as TryFrom<$t>>::Error;

#[inline]
fn try_from(x: Le<$t>) -> Result<Self, Self::Error> {
Ok(Self::from_ne(NonZero::try_from(x.get_ne())?))
}
}

impl From<Le<NonZero<$t>>> for Le<$t> {
#[inline]
fn from(x: Le<NonZero<$t>>) -> Self {
Self::from_le(x.get_le().get())
}
}

impl From<Ne<NonZero<$t>>> for Ne<$t> {
#[inline]
fn from(x: Ne<NonZero<$t>>) -> Self {
Self::from_ne(x.get_ne().get())
}
}

impl From<Le<NonZero<$t>>> for Ne<$t> {
#[inline]
fn from(x: Le<NonZero<$t>>) -> Self {
Self::from_ne(x.get_ne().get())
}
}

impl From<Ne<NonZero<$t>>> for Le<$t> {
#[inline]
fn from(x: Ne<NonZero<$t>>) -> Self {
Self::from_le(x.get_le().get())
}
}

impl Le<$t> {
/// Wrap the given little-endian bytes.
#[inline]
pub fn from_le_bytes(bytes: [u8; core::mem::size_of::<$t>()]) -> Self {
let le = <$t>::from_le_bytes(bytes);
Self::from_ne(le)
}

/// Get the wrapped value as little-endian bytes.
#[inline]
pub fn to_le_bytes(self) -> [u8; core::mem::size_of::<$t>()] {
self.get_le().to_ne_bytes()
}

/// Wrap the given native-endian bytes.
#[inline]
pub fn from_ne_bytes(bytes: [u8; core::mem::size_of::<$t>()]) -> Self {
let ne = <$t>::from_ne_bytes(bytes);
Self::from_ne(ne)
}

/// Get the wrapped value as native-endian bytes.
#[inline]
pub fn to_ne_bytes(self) -> [u8; core::mem::size_of::<$t>()] {
self.get_ne().to_ne_bytes()
}
}

impl Ne<$t> {
/// Wrap the given little-endian bytes.
#[inline]
pub fn from_le_bytes(bytes: [u8; core::mem::size_of::<$t>()]) -> Self {
let le = <$t>::from_le_bytes(bytes);
Self::from_le(le)
}

/// Get the wrapped value as little-endian bytes.
#[inline]
pub fn to_le_bytes(self) -> [u8; core::mem::size_of::<$t>()] {
self.get_le().to_ne_bytes()
}

/// Wrap the given native-endian bytes.
#[inline]
pub fn from_ne_bytes(bytes: [u8; core::mem::size_of::<$t>()]) -> Self {
let ne = <$t>::from_ne_bytes(bytes);
Self::from_ne(ne)
}

/// Get the wrapped value as native-endian bytes.
#[inline]
pub fn to_ne_bytes(self) -> [u8; core::mem::size_of::<$t>()] {
self.get_ne().to_ne_bytes()
}
}
)*
};
}

impls! {
u8, u16, u32, u64, u128, usize,
i8, i16, i32, i64, i128, isize,
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn round_trip() {
let x = Le::from_ne(0x12345678u32);
assert_eq!(x.get_ne(), 0x12345678);

let le_bytes = x.get_le().to_ne_bytes();
assert_eq!(le_bytes, [0x78, 0x56, 0x34, 0x12]);

let y = Le::from_le(x.get_le());
assert_eq!(x, y);

let z = Le::from_ne(x.get_ne());
assert_eq!(x, z);
}

#[test]
fn round_trip_non_zero() {
let x = Le::from_ne(NonZero::new(0x12345678u32).unwrap());
assert_eq!(x.get_ne().get(), 0x12345678);

let le_bytes = x.get_le().get().to_ne_bytes();
assert_eq!(le_bytes, [0x78, 0x56, 0x34, 0x12]);

let y = Le::from_le(x.get_le());
assert_eq!(x, y);

let z = Le::from_ne(x.get_ne());
assert_eq!(x, z);
}
}
1 change: 1 addition & 0 deletions crates/core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ extern crate alloc as std_alloc;
extern crate std;

pub mod alloc;
pub mod endian;
pub mod error;
pub mod math;
pub mod non_max;
Expand Down
8 changes: 8 additions & 0 deletions crates/cranelift/src/func_environ/gc/enabled.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,10 @@ fn unbarriered_load_gc_ref(
flags: ir::MemFlags,
) -> WasmResult<ir::Value> {
debug_assert!(ty.is_vmgcref_type());

// GC refs are always stored little-endian.
let flags = flags.with_endianness(ir::Endianness::Little);

let gc_ref = builder.ins().load(ir::types::I32, flags, ptr_to_gc_ref, 0);
if ty != WasmHeapType::I31 {
builder.declare_value_needs_stack_map(gc_ref);
Expand All @@ -101,6 +105,10 @@ fn unbarriered_store_gc_ref(
flags: ir::MemFlags,
) -> WasmResult<()> {
debug_assert!(ty.is_vmgcref_type());

// GC refs are always stored little-endian.
let flags = flags.with_endianness(ir::Endianness::Little);

builder.ins().store(flags, gc_ref, dst, 0);
Ok(())
}
Expand Down
5 changes: 4 additions & 1 deletion crates/cranelift/src/func_environ/gc/enabled/null.rs
Original file line number Diff line number Diff line change
Expand Up @@ -379,7 +379,10 @@ impl GcCompiler for NullCompiler {
flags: ir::MemFlags,
) -> WasmResult<ir::Value> {
// NB: Don't use `unbarriered_load_gc_ref` here because we don't need to
// mark the value as requiring inclusion in stack maps.
// mark the value as requiring inclusion in stack maps. That does,
// however, mean we need to ensure that the little-endian flag is set
// ourselves, since GC refs area always stored little-endian.
let flags = flags.with_endianness(ir::Endianness::Little);
Ok(builder.ins().load(ir::types::I32, flags, src, 0))
}

Expand Down
2 changes: 1 addition & 1 deletion crates/environ/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ pub use self::error::ToWasmtimeResult;
#[doc(inline)]
pub use wasmtime_core::error;

pub use wasmtime_core::{alloc::PanicOnOom, non_max, undo::Undo};
pub use wasmtime_core::{alloc::PanicOnOom, endian, non_max, undo::Undo};

// Only for use with `bindgen!`-generated code.
#[doc(hidden)]
Expand Down
Loading