Just finished chapter 10 of the Rust book and I thought I’d try digging into some real code. I’m a rust noob, but I’ve got 15 years with embedded C.

I’m working my way through the source code of the stm32l0xx-hal crate. Inside adc.rs on line 96, an implementation for the Adc struct is created for the type Adc<Ready> where Ready is defined on line 324.

/// Analog to Digital converter interface
pub struct Adc<State> {
    rb: ADC,
    sample_time: SampleTime,
    align: Align,
    precision: Precision,
    _state: State,
}

impl Adc<Ready> {
    pub fn new(adc: ADC, rcc: &mut Rcc) -> Self {
        // Enable ADC clocks
        ADC::enable(rcc);
        adc.cr.modify(|_, w| w.advregen().set_bit());

        Self {
            rb: adc,
            sample_time: SampleTime::T_1_5,
            align: Align::Right,
            precision: Precision::B_12,
            _state: Ready,
        }
    }
...
}
...
/// Indicates that the ADC peripheral is ready
pub struct Ready;

I’m a little confused about how the associated function new() works. I understand that it generates a new Adc<Ready>, but I don’t fully understand why it’s inside the impl Adc<Ready> block.

It’s not a method that would be called on an Adc<Ready> type, and it doesn’t take any arguments of type Ready. Couldn’t it just as easily have been inside a impl<T> Adc<T> block?

Or is it that impl types go both ways. Like you can’t return an Adc<Ready> unless you’re inside an impl Adc<Ready>?

Sidenote: Since the Ready struct has no fields, does “Ready” actually create a new instance of a Ready type? (like you don’t need Ready{}?)

  • orclev@lemmy.world
    link
    fedilink
    English
    arrow-up
    5
    ·
    2 months ago

    You’ve essentially understood things except for one minor detail. The piece you’re missing is that the function returns the special type of Self, which because it’s in the impl block for Adc<Ready> is effectively an alias for Adc<Ready> as opposed to some arbitrary Adc<T>. new could have been put into the generic Adc impl but then Self couldn’t have been used and it wouldn’t be conventional. The Rust convention is to have a function called new on a type that returns Self to construct instances of some type.

    Structs with no fields are known as Zero-Sized Types and are commonly used as markers or tags for state, and can be “created” using just their name as you assumed. You’ll sometimes also see PhantomData<T> which is used to keep track of some type param T that is otherwise not used without taking up extra memory in a struct. The reason that isn’t used for Adc is that while Active is a ZST, some of the other states aren’t, so space for them will be reserved in the Adc struct.

    Edit: also be careful not to confuse Adc with ADC, those are two different types and you mixed them up a couple times in your post.

    • ch00f@lemmy.worldOP
      link
      fedilink
      English
      arrow-up
      2
      ·
      2 months ago

      So it really comes down to convention? It’ll compile fine in either case? What’s the motivation behind that convention?

      Thanks for the tip on PhantomData and the edit! I corrected my post.

      • Ephera@lemmy.ml
        link
        fedilink
        English
        arrow-up
        2
        ·
        2 months ago

        If you put it into a generic impl<T> Adc<T> block, then the type needs to be specified during initialization, like so:

        let adc = Adc::<Ready>::new();
        //or like so:
        let adc: Adc<Ready> = Adc::new();
        

        But what’s much more tricky, is that you can’t create a generic T object inside the new() function.
        If you don’t need to transport data, then you can make it work with PhantomData (which does not require initialization): https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=ea9c3eed4897a1a480eb5c5b4708fc2d

        If you do need to transport data, then you may need to require all Ts to implement some shared trait with a constructor, or you could switch to an enum: https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=2b995b4f854061fad8aa4c0a05d8c304

        Enum is often the simplest, but wouldn’t work for stm32l0xx-hal here, because they use the generic type to limit how their API can be used (certain methods can only be called on Adc<Ready>, not on Adc<Active>, for example).

        But yeah, using an impl Adc<Ready> block, like in the original code, is a good solution here. This is definitely on the advanced side, though. You only need this amount of nuance, if you’re designing library APIs for others to use. I also have to say that while I’ve probably seen usage of impl Adc<Ready> before, I did not have it on my radar that this is a possible solution here.

        • ch00f@lemmy.worldOP
          link
          fedilink
          English
          arrow-up
          2
          ·
          2 months ago

          You only need this amount of nuance, if you’re designing library APIs for others to use

          Maybe I bit off a lot in my first foray, but my ultimate goal is to write a driver for the segment LCD controller in this chip. I have a very simple project already written in C that uses it, and I’d like to try getting it working in embedded Rust. There isn’t much nuance to the controller (just setting a few register values), so it’ll likely be simpler than the ADC, but I’m digging into the existing drivers for some guidance.

          • Ephera@lemmy.ml
            link
            fedilink
            English
            arrow-up
            1
            ·
            2 months ago

            Yeah, I mean, you’ve got previous programming experience. You evidently know that you’re tackling a harder problem and probably know when to step back and try an easier problem first, if it becomes too frustrating. I just wanted to give a bit of context, so that you know it’s more exotic knowledge, because that’s not always going to be obvious when learning a new language…

      • orclev@lemmy.world
        link
        fedilink
        English
        arrow-up
        2
        ·
        2 months ago

        The problem with putting new into impl<T> Adc<T> is that because self and T aren’t used by the function the compiler has no way to work out what T is supposed to be which would force you to define it (even though it doesn’t actually matter). So E.G. if you attempted to invoke let adc = Adc::new(...); the compiler would complain that it was unable to determine what T is even though it doesn’t matter. You would for instance need to do something like let adc = Adc::<()>::(...). By putting it inside the impl Adc<Ready> block you’ve constrained the type of T to Ready and therefore there’s no need to specify it when calling new. So while both approaches are functionally identical, one is more convenient to use than the other.

        • ch00f@lemmy.worldOP
          link
          fedilink
          English
          arrow-up
          2
          ·
          2 months ago

          That makes sense! Thank you!

          It’s funny how much of Rust appears to be writing footnotes for the compiler.

          • orclev@lemmy.world
            link
            fedilink
            English
            arrow-up
            2
            ·
            2 months ago

            When you think about it, that’s all programming really is, it’s just that Rust pays a bit more attention than other languages and does a better job at spotting problems at compile time instead of letting them turn into runtime problems.