Skip to content

primitives: Remove alloc requirement from HexPrimitive#5480

Merged
apoelstra merged 5 commits intorust-bitcoin:masterfrom
mpbagot:feature/hex-no-alloc
Mar 3, 2026
Merged

primitives: Remove alloc requirement from HexPrimitive#5480
apoelstra merged 5 commits intorust-bitcoin:masterfrom
mpbagot:feature/hex-no-alloc

Conversation

@mpbagot
Copy link
Copy Markdown
Contributor

@mpbagot mpbagot commented Jan 5, 2026

The HexPrimitive type in primitives provides encoding and decoding functionality to/from hex for arbitrary Encodable/Decodable types in primitives. Currently, this type relies on alloc to implement this functionality, which is less than ideal for types that don't otherwise require alloc (e.g. Header).

  • Patch 1 converts the decoding logic in from_str to be alloc-less, and adjusts the feature gates on the relevant impls on Header.
  • Patch 2 converts the encoding logic in hex_write_with_case to be alloc-less and adjusts the relevant feature gates.
  • Patch 3 moves the hex_codec module from lib.rs into a new file. No logic changes.
  • Patch 4 rearranges the hex_codec module to match the style of other modules.
  • Patch 5 adjusts function docs and replaces all pub visibility with pub(crate) in hex_codec.

Closes #5469

@github-actions github-actions bot added C-bitcoin PRs modifying the bitcoin crate C-primitives labels Jan 5, 2026
@apoelstra
Copy link
Copy Markdown
Member

Merged #5381. This needs rebase now. (Sorry, I think maybe the formatting PR conflicted with a ton of stuff.)

@mpbagot mpbagot force-pushed the feature/hex-no-alloc branch from 3b8b8ad to 97b26bf Compare January 6, 2026 06:24
@github-actions github-actions bot removed the C-bitcoin PRs modifying the bitcoin crate label Jan 6, 2026
@mpbagot mpbagot marked this pull request as ready for review January 6, 2026 07:43
tcharding
tcharding previously approved these changes Jan 15, 2026
Copy link
Copy Markdown
Member

@tcharding tcharding left a comment

Choose a reason for hiding this comment

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

ACK 97b26bf

Comment on lines +77 to +78
let extra_len = if f.alternate() { 2 } else { 0 };
let total_len = len + extra_len;
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.

I already wrote it today so I won't hark on the point but these two lines are new and not a code move but are in commit titled primitives: Move hex_codec to independent module.

Copy link
Copy Markdown
Contributor Author

@mpbagot mpbagot Jan 15, 2026

Choose a reason for hiding this comment

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

I must have messed up a rebase/amend at some point. I thought that I had definitely made the last patch move-only.

The new patches move this and one other difference into the second patch. The third patch should be only a straight code move now.

@tcharding
Copy link
Copy Markdown
Member

FTR the buffer stuff looked like it should be supported by the language but from a brief look at Iterator there is only array_chunks that is a nightly feature.

@tcharding
Copy link
Copy Markdown
Member

Good effort @mpbagot

tcharding
tcharding previously approved these changes Jan 15, 2026
Copy link
Copy Markdown
Member

@tcharding tcharding left a comment

Choose a reason for hiding this comment

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

ACK 852adb5

pub mod witness;

#[cfg(feature = "hex")]
pub(crate) mod hex_codec;
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.

Suggested change
pub(crate) mod hex_codec;
mod hex_codec;

pub(crate) mod is equivalent to a private module (unless I'm missing something).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

You're right. It is. I tend to try and be more explicit with things than I really need to be.

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.

hmm, that is fair and we often do such things (e.g., our usage of ref in matching: match *self { Self::Foo(ref e)).

However uniformity is king IMO, if you want to introduce this style I rekon either raise an issue to discuss it or raise a PR that does it everywhere in the repo. If you feel super strongly about it I probably wouldn't NACK it but FWIW it doesn't seem useful to me because it is such a basic language feature. Don't let me hold you back though.

@@ -0,0 +1,193 @@
use core::fmt;
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.

This file needs an SPDX licence string.

Also some module level docs would be great please. FYI the linter does not enforce this because its a private module.

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.

And since I'm doing a nit-picky as all hell review:

Can you re-order the file so that HexPrimitive is at the top, the error code is at the bottom, and the hex_write_with_case is below where its called (the fmt::LowerHex and fmt::UpperHex impls).

This is based on the principle that the most important thing goes at the top.

Copy link
Copy Markdown
Member

@tcharding tcharding Jan 15, 2026

Choose a reason for hiding this comment

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

This is one instance where an additional patch on top of the current ones is the best because it preserves the move-only nature of the existing one. Could also be a follow up PR but I don't rekon Andrew is going to get to merging this before you get back to it.

Copy link
Copy Markdown
Contributor Author

@mpbagot mpbagot Jan 16, 2026

Choose a reason for hiding this comment

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

I've done this as two new patches, since adding module/license docs is a different operation than rearranging code. If you'd rather it be squashed or reordered, I can do so. Is it acceptable for code to exist for a patch without the license string?

tcharding
tcharding previously approved these changes Jan 18, 2026
Copy link
Copy Markdown
Member

@tcharding tcharding left a comment

Choose a reason for hiding this comment

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

ACK ddec4b9

@tcharding
Copy link
Copy Markdown
Member

Thanks for humouring me and re-arranging the code. I'm aware that not all devs are as retarded as me and have to have everything exactly in the right place.

@mpbagot
Copy link
Copy Markdown
Contributor Author

mpbagot commented Jan 23, 2026

Rebased to fix a conflict in primitives/src/block.rs.

@mpbagot mpbagot force-pushed the feature/hex-no-alloc branch from 42deb5a to 7033611 Compare January 24, 2026 03:07
@mpbagot
Copy link
Copy Markdown
Contributor Author

mpbagot commented Jan 24, 2026

Rebased to fix a conflict in primitives/src/block.rs.

tcharding
tcharding previously approved these changes Jan 27, 2026
Copy link
Copy Markdown
Member

@tcharding tcharding left a comment

Choose a reason for hiding this comment

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

ACK 7033611

@mpbagot
Copy link
Copy Markdown
Contributor Author

mpbagot commented Feb 2, 2026

Rebased to fix a conflict in primitives/src/lib.rs, squashed license + doc patch into move patch, and fixed a bug with the no-alloc from_str that Jamil's tests caught.

tcharding
tcharding previously approved these changes Feb 2, 2026
Copy link
Copy Markdown
Member

@tcharding tcharding left a comment

Choose a reason for hiding this comment

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

ACK 5da334d

@apoelstra
Copy link
Copy Markdown
Member

Needs rebase again (in the API files, ugh)

tcharding
tcharding previously approved these changes Feb 4, 2026
Copy link
Copy Markdown
Member

@tcharding tcharding left a comment

Choose a reason for hiding this comment

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

ACK 6b38d69

@apoelstra
Copy link
Copy Markdown
Member

Needs rebase.

Copy link
Copy Markdown
Member

@tcharding tcharding left a comment

Choose a reason for hiding this comment

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

ACK b24baea

@apoelstra
Copy link
Copy Markdown
Member

In 5c22144:

primitives does not build with cargo test --no-default-features --features alloc. (This affects all the commits except the tip I think)

@mpbagot mpbagot force-pushed the feature/hex-no-alloc branch from b24baea to 0dfb450 Compare February 21, 2026 03:30
@mpbagot
Copy link
Copy Markdown
Contributor Author

mpbagot commented Feb 21, 2026

Rebased on latest master and should have fixed rbmt test runs for all commits in the chain.

@mpbagot mpbagot force-pushed the feature/hex-no-alloc branch from 0dfb450 to 1b3958b Compare February 23, 2026 02:14
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
fmt::UpperHex::fmt(&crate::hex_codec::HexPrimitive(self), f)
}
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { crate::hex_codec::HexPrimitive(self).fmt(f) }
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.

How does the compiler resolve this call to fmt? Said another way, looking at this change I can't tell if the UpperHex::fmt method is the one being called and not for example Display::fmt?

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.

But it does because we have a test

        // And these should yield uppercase hex
        let upper_hex = lower_hex.to_ascii_uppercase();
        assert_eq!(upper_hex, format!("{:X}", header));

Copy link
Copy Markdown
Member

@tcharding tcharding Feb 23, 2026

Choose a reason for hiding this comment

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

Is the compiler using trait method function name because the implementation is a simple call through? This seems like a lot of magic. Or is it something to do with the fact that we call the method chained onto the constructor call?

@tcharding
Copy link
Copy Markdown
Member

I've reviewed this much more in depth now. For the coming suggestions feel free to ignore, use, or do follow up. While reviewing I hacked a bunch locally.

@tcharding
Copy link
Copy Markdown
Member

tcharding commented Feb 23, 2026

I moved the logic from hex_write_with_case to an inherent method on HexPrimitive and then called through like this

impl<T: Encodable + Decodable> fmt::Display for HexPrimitive<'_, T> {
    #[inline]
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { self.fmt(f, Case::Lower) }
}

In case its useful:

    /// Writes an Encodable object to the given formatter in the requested case.
    fn fmt(&self, f: &mut fmt::Formatter, case: Case) -> fmt::Result {

I messed with the HexPrimitive type like this:

/// Hex encoding wrapper type for `Encodable` + `Decodable` types.
///
/// Implements `Display`, `Debug`, `LowerHex`, and `UpperHex` as well as an inherent `from_str`
/// method that returns `T`.
pub(crate) struct HexPrimitive<'a, T: Encodable + Decodable>(pub &'a T);

I messed around with from_str function like this:

    /// Parses a given string into an instance of the type `T`.
    ///
    /// Since `FromStr` would return an instance of `Self` and thus a &T, this function
    /// is implemented directly on the struct to return the owned instance of T.
    /// Other `FromStr` implementations can directly return the result of
    /// [`HexPrimitive::from_str`].
    ///
    /// # Errors
    ///
    /// * `OddLength` or `InvalidChar` if decode the hex string to bytes fails.
    /// * `Decode` if consensus decoding the hex-decoded bytes fails.
    pub(crate) fn from_str(s: &str) -> Result<T, ParsePrimitiveError<T>> {
        // The iterators are ustable (exist but private in 1.0).
        let iter = hex_unstable::HexToBytesIter::new(s).map_err(ParsePrimitiveError::from)?;

I removed any pub from hex_codec and used pub(crate) just to be explicit.

And finally one last nit, I imported HexPrimitive anywhere its used instead of using crate::hex_codec::HexPrimitive.

@tcharding
Copy link
Copy Markdown
Member

I investigated the error stuff from hex and you've got it spot on, its the hex-conservative v1.0 crate that is making this code more ugly than it needs to be. \me hangs his head in shame.

/// [`ParsePrimitiveError::OddLengthString`] if the input string is an odd length.
/// [`ParsePrimitiveError::Decode`] if an error occurs during decoding of the object.
pub(crate) fn from_str(s: &str) -> Result<T, ParsePrimitiveError<T>> {
let iter = hex_unstable::HexToBytesIter::new(s).map_err(ParsePrimitiveError::from)?;
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.

I did rust-bitcoin/hex-conservative#203 to help with this change. No need to wait though because all the unstable stuff is hidden.

The from_str implementation for parsing types from hex strings currently
relies on alloc due to the use of hex::decode_to_vec. This locks Header
FromStr decoding behind alloc.

Implement from_str without alloc, and adjust feature gates on
HeaderDecoder to match new relaxed requirements.
The Display/Debug/UpperHex/LowerHex implementations on HexPrimitive
all relied on alloc due to the implementation of hex_write_with_case.
This locks LowerHex and UpperHex on Header behind alloc, and led to
an unnecessary custom implementation of Display.

Change hex_write_with_case to remove alloc requirement, and remove
corresponding feature gates. Move hex_write_with_case to inherent
function on HexPrimitive, and rename to fmt_hex. Replace Display impl
on Header with call through to HexPrimitive. Remove feature gates on
Header Upper/LowerHex.
The hex_codec module in lib.rs of primitives now makes up 2/3rds of
the file. At this scale, it is better suited to be moved into its
own file.

Move the hex_codec module in lib.rs to a new module file, and import
it as a pub(crate) mod.
The hex_codec module was created over various patches and thus doesn't
follow best practice for ordering. Errors should be near the bottom,
with important types near the top.

Rearrange hex_codec to put HexPrimitive at the top, and
ParsePrimitiveError at the bottom.
The hex_codec module should be entirely hidden to outside the crate.
While all types are pub(crate), the inner field of HexPrimitive was
pub.
@mpbagot mpbagot force-pushed the feature/hex-no-alloc branch from 1b3958b to 118791e Compare February 24, 2026 04:22
@mpbagot
Copy link
Copy Markdown
Contributor Author

mpbagot commented Feb 24, 2026

I've incorporated most of your suggestions, @tcharding. I changed the fmt inherent function to fmt_hex. I know the function signature already differs from the fmt traits' fmt functions, but I feel like there's getting to be too much pollution with the plain name fmt.

Copy link
Copy Markdown
Member

@tcharding tcharding left a comment

Choose a reason for hiding this comment

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

ACK 118791e

Copy link
Copy Markdown
Member

@apoelstra apoelstra left a comment

Choose a reason for hiding this comment

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

ACK 118791e; successfully ran local tests

@apoelstra apoelstra merged commit b7ab8bb into rust-bitcoin:master Mar 3, 2026
28 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Remove the alloc feature for the HexPrimitive format helper

3 participants