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{}?)

  • 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.