So I’m slowly digging my way through the stm32l0xx-hal crate. As written, the crate works with an older version of embedded-hal. My ultimate goal is to bring it up to the latest version or at least start the process.
My question surrounds the implementation of the I2c struct in i2c.rs.
Starting on line 57 is a struct that contains three elements of generic type.
/// I2C abstraction
pub struct I2c<I2C, SDA, SCL> {
i2c: I2C,
sda: SDA,
scl: SCL,
}
I want to specifically talk about the first element. The bounds on what the generic <I2C> type can be start in the implementation where the first element must implement the type Instance.
impl<I, SDA, SCL> I2c<I, SDA, SCL>
where
I: Instance,
{
pub fn new(i2c: I, sda: SDA, scl: SCL, freq: Hertz, rcc: &mut Rcc) -> Self
where
I: Instance,
SDA: SDAPin<I>,
SCL: SCLPin<I>,
{...}
...
}
Going one step higher, I2c::new() is called inside a macro on line 522 inside an implementation of the I2cExt trait on I2C1, I2C2, etc.
impl I2cExt<$I2CX> for $I2CX {
fn i2c<SDA, SCL>(
self,
sda: SDA,
scl: SCL,
freq: Hertz,
rcc: &mut Rcc,
) -> I2c<$I2CX, SDA, SCL>
where
SDA: SDAPin<$I2CX>,
SCL: SCLPin<$I2CX>,
{
I2c::new(self, sda, scl, freq, rcc)
}
}
So the generic type in the I2cExt trait is the same as the type for which the trait is implemented in this macro. The <$I2CX> type is passed to the I2c return type which ultimately creates an instance of the I2c struct with the first element of type <$I2CX> and along the way, it’s verified that it implements the Instance type.
So my question is: couldn’t we ignore all of this generic nonsense if we just defined the I2c struct to expect a certain trait for its first value?
For example, couldn’t the first line of the struct just be i2c: Instance or some other defined trait that encompasses all of the I2c drivers on the chip? If we’re bounding the type to an Instance anyway, why bother with all the generics?


Something else to consider is how a function returns an owned value and why knowing the size of a struct at compile time is required for that. At first this might seem like a simple thing if you think about ownership in an abstract sense, but when you actually start to think about how it’s implemented you can start to see some of the complexity.
Consider the following code:
use std::any::type_name; use std::default::Default; #[derive(Default, Debug)] struct Foo { x: u32, } #[derive(Default, Debug)] struct Bar { y: f32, } fn frobnicate<T: Default>() -> T { println!("Creating a new {}", type_name::<T>()); Default::default() } pub fn main() { let foo: Foo = frobnicate::<Foo>(); let bar: Bar = frobnicate::<Bar>(); println!("Made a pair of owned {:?} and {:?}", foo, bar); }Where is the memory for
fooandbarallocated at? It’s on the stack of themain()function. But theFooandBarinstances are constructed inside of the call todefault()inside offrobnicate(), so how is that memory ending up in the stack ofmain()? First, lets break this down using what I explained previously about generics being specialized at compile time. This code is roughly equivalent to the following (unchanged parts elided for brevity):fn default_Foo() -> Foo { Foo { x: 0, } } fn default_Bar() -> Bar { Bar { y: 0.0, } } fn frobnicate_Foo() -> Foo { println!("Creating a new Foo"); // technically there's a genericized version of type_name() invoked here default_Foo() } fn frobnicate_Bar() -> Bar { println!("Creating a new Bar"); default_Bar() } pub fn main() { let foo: Foo = frobnicate_Foo(); let bar: Bar = frobnicate_Bar(); println!("Made a pair of owned {:?} and {:?}", foo, bar); }But this still doesn’t explain how the
Fooinstance created indefault_Foo()ends up on the stack ofmain(). The answer to that is that Rust is pulling a fast one and there’s a hidden argument on all these functions. The code can’t actually be expressed in Rust in a way that clearly illustrates this, so here’s the C equivalent of the previous Rust code:struct Foo { unsigned int x; }; void default_Foo(struct Foo *foo) { foo->x = 0; } struct Bar { float y; }; void default_Bar(struct Bar *bar) { bar->y = 0.0; } void frobnicate_Foo(struct Foo *foo) { printf("Creating a new Foo\n"); default_Foo(foo); } void frobnicate_Bar(struct Bar *bar) { printf("Creating a new Bar\n"); default_Bar(bar); } void main() { struct Foo foo; struct Bar bar; frobnicate_Foo(&foo); frobnicate_Bar(&bar); printf("Made a pair of owned Foo { x: %u } and Bar { y: %f }\n", foo.x, bar.y); }Now that you can see the way that
fooandbarare allocated on the stack ofmain()hopefully you can also see why knowing at compile time what the size ofFooandBarare is important, otherwise how could the compiler make sure enough space has been allocated on the stack. Now consider what might happen if you tried to return aimpl Defaultfromfrobnicate()instead of using generics. Setting aside that there would be no way to indicate which version ofdefault()to invoke, if the compiler didn’t know what the concrete type being returned fromfrobnicate()was, how would it be sure it had allocated enough room on the stack ofmain()to store the return value?