I'm currently starting the Crust of Rust series from the start. Just here to thank you for your work. The topics are interesting and your examples /explanations well crafted.
These videos are great! For me, the first third is usually "I totally understand what's going on", next third is "I understand some of these words", and make it another 10-15 minutes before starting the video over. The Q&A is really helpful too, since someone in the chat always asks about a specific corner case or exception. It doesn't seem like there is a lot of good intermediate-level material out there, so I'm always excited to see a new video.
You don’t even know how helpful this particular episode has been for me. And the detailed chapter markers too have been so nice when I need to refer back. Seriously. Thank you
I really appreciate that you touch on very advanced topics while teaching beginner/intermediate ones. I've learned the most from your videos so far and have a deeper understanding of how Rust actually works
Man, I wish I had your skill of explaining things. You're awesome! Of course, the amount of knowledge you have is helping here, but it's a skill on its own.
32:55 this has to be the best explanation. I literally had this doubt and I was about to google how a function would know the size of normal array otherwise. But you explained it so well right after that. Thank you so much for these videos.
Wow. This video was so incredible helping me understand rust deeper. Getting to understand on a more complex level and pushing my knowledge. Thank you so much for this video. I am so grateful for this.
thank you so so much for these streams! I'm very interested in rust internals and I often come to your streams for clarification of complex stuff. really much appreciated
Really Great content. Opening up the internals explaining how things works under the hood. The missing pieces. And why I am hitting the head in the wall (compiler).
For those, who prefer visualizations over the long documentation text, visit cheats.rs. For example, the idea of fat pointers can be grasped from the cheats.rs/#references-pointers-ui section. Of course, this site is pretty much of any value if you already know Rust to some extent, but if you do, those little condensed explanations and visuals might be beneficial to the understanding of the whole picture! Thanks for these series, Jon! You should consider doing more visual explanations, whether by hand-drawings (as you rarely do on live coding streams) or by using cheats.rs!
I disagree with the implementations at 7:34. I think they should still be `s.as_ref().len()` because you should be able to call it with any `T: AsRef`, even if T did not have a `.len()`. The optimizer might then see that for `&str`, `as_ref` does nothing and remove it. Having just `s.len()` makes it seem like there is something magical between `AsRef` and generics, which I don't think there is.
Thanks Jon for this enlightening video! Pure Gold. In case someone wants to see in action handcrafted vtables in C-land he/she can have a look at the headers from the Gtk toolkit or even have a look at the C-code output that the vala language compiler produces.
At 1:41:05, should the drop function to accept `v: Box`? In the code as written, `v` is just a reference, so the compiler can't run `Drop::drop()` when it goes out of scope, since its owner might still be using it.
15:33 Technically not entirely true. "Dynamic Libraries" is a broad term, and if you include for example, Windows' Dynamic Link Libraries, those can also contain .NET managed code. Those contain generic functions and types, and it is up to the runtime to "monomorphize" them just in time. But yeah, this is likely not what that question meant, since the context is dynamically linked *native* code
There really isn't much magic to Any. It's just a method that returns a unique type identifier that then allows safe downcasting as I mention at the end :)
@@jonhoo I feel like "Can I downcast trait object back to original type?" is a fairly important question when discussing dynamic dispatch. And "Any" trait seems to be the idiomatic way to do it.
@@KohuGaly Ah, so, the answer is that you can only do it *if* your trait object includes the Any trait. You can't use Any to downcast an arbitrary trait object.
Isn't a vtable generated for each _type_ , not instance? What is the problem then in inserting a static method inside the vtable? Every other method takes atlest one parameter, but I don't see how not taking a parameter is problematic.
1:29:25 I still don't understand why it's not possible to monomorphize the vtables. We compile the standard library at the same time as the rest of our code, so it should be possible to know the size of the vtable for a type+trait after monomorphization, even if it is large, and use that everywhere in our program. My vtables will be differently sized than yours, but that's just a different flavor of the same issue preventing us from having dynamic libraries.
Listening to your explanation again, I think my mistake is that we don't actually compile the standard library at the same time, we compile one crate at a time and just have access to the source for other crates. So for a normal generic function, if we introduce a use that std doesn't have, it would get compiled along with our crate, not std
44:30 "you can keep using box after the stack frame of the caller has gone away", how is this situation possible? Won't the caller return only after this function has returned?
Missed opportunity for ``` match vial_broken { true => Box::new(DeadCat::new()), false => Box::new(AliveCat::new()) } ``` but Schrodinger hai is good too ;)
please please.. sir , if u can cover unsafe rust , it would be a great help for the community , but after all iam so glad u prepare this content for us ! tq so much 😃
at around 1:05:52 when you talked about the multiple trait vtable, you said to create a new trait that requires the needed traits and use that. It would give a bigger vtable but it would still be 1 pointer right? And it would lead to code duplication as either the compiler would copy the trait implementations to another location or it would copy the function labels and call the original implementation methods. Wouldn't that be inefficient? Is there a way to say that I do want to generate a 3 pointer wide or even wider (based on number of traits) argument? with each pointer pointing to one traits vtable?
You can already do what you describe yourself by using custom vtables but trait objects don't work like that. And compiler only copies vtables, which usually has the size of (pointer_size * method_count). Basically it is a very very little overhead.
Also you said that we can only include "+ " in our variable signature if some_trait is an auto-trait, because auto-traits have no functions. But isn't Any also an auto-trait? It surely has functions, and which have different implementations for each concrete type. Now I'm not really sure how that comes into play.
No, Any isn't an auto-trait. It has a blanket implementation (impl Any for T), but that's not the same. An auto-trait is one whose impls are generated by the compiler directly based on the structure of the type, and they never have any methods, which is not the case for Any.
I’ve been doing a bunch of minor projects in rust and I haven’t even ran into a use case where I’ve needed to use “dyn”. In other languages I’ve used the pattern a lot and maybe it’s just that I’ve worked on a different class of problems than I did in those languages. But I’ve found that I can often get away with using some different pattern in rust. There definitely are use cases where it’s needed, or at least very useful. But where I in some languages would make an interface and put a bunch of generic objects implementing that interface, often I find it more clear to define an enum with variants holding types. That limits how generic the code is. By quite a lot actually. It only allows the possible values I explicitly put in there. But quite often I don’t want to be generic over any object that happens to implement a trait. I just want to be very restrictively generic to exactly 2 or 3 different things that I implement myself. I’ll probably run into it more when I ramp up complexity a bit. That’s why I’m eating the video. But still. We don’t have to be maximally generic for everything we write, just generic enough to make writing the code convenient.
To paraphrase Dijkstra, "purpose of abstraction is not to be vague, but to be precise". A good support for abstraction lets you state precisely what you mean, without forcing you to be more vague or more specific than you want to be. Languages that have interfaces (traits) but no sum-types (enums) force you to over-generalize and therefore force vagueness. They push you to express closed sets of types (enums) as open sets of types (interfaces), even when you actually want a closed set (which is most of the time, actually, as you correctly point out). The main use case for traits and generics is when you want to provide new functionality to types that you have no control over (ie. when you write a library). Or you you have control over those types, but you want to keep it separated to reduce complexity.
It's tempting to say that type erasure causes all trait-related problems. Is it required that much to generate an efficient code? Why can't we preserve types?
Hey, great video - I have a noobie qn. Early on you discuss how in the trait object case you need the function argument to be sized so you can make a collection of them where each thing, the pointer type, is of the same size. However later on you say we can have a trait not add a static method to the vtable by requiring Self be sized, saying that the trait, dyn Hei, has no size. But didn't we pass a reference so that it was sized? Thanks!
Because fields of struct are accessed as pointer offsets. For sized types, compiler may choose to reorder them, but compiler still knows where each field is located relative to the head of the struct. That does not work for unsized types. Because compiler can't know its size, then it can't access fields after it as offsets, because it can't figure out how big the offset should be. The only place where you (and the compiler) can put it is at the end of the struct.
in `struct Foo {f: bool, t: [u8], x: bool}` why can't the offset of `x` be determined by `len-sizeof x`? (such a struct combined with a #[repr] would be good for some zero-copy network encoding stuff)
In general the compiler expects field offset to be constant. You could imagine extending it to be dynamic, but that's a big language feature in and of itself.
s::weird is no possible because s doesn not name a type As you said type erasure occurs and the only thing you have is pointer to data and pointer to methods table Compiler does not need to know s type to call needed method I think the main reason is because in machine code level calling method through indirection is compiled into some stuff, which requires pointer to data to be available at this point. That's the main goal of dynamic dispatch and polymorphism. Method without self parameter can only be invoked through concrete type, which make no sense to include it in vtable which is needed only for magic like calling method without knowing type of object this method is called on
This is clearly an outlier: Usually I watch tutorials on youtube at playback speed = 1.5 (+- 0.25) because of relatively low information density. But crust of rust is often < 1.0. Thick (high quality) material to digest.
I'm currently starting the Crust of Rust series from the start.
Just here to thank you for your work.
The topics are interesting and your examples /explanations well crafted.
This is some of the highest quality rust content I’ve seen. Thanks for your work and knowledge-sharing!
Thanks for adding timestamps to these videos! It makes it easy to watch a couple 'chunks', and come back to the video later.
These videos are great! For me, the first third is usually "I totally understand what's going on", next third is "I understand some of these words", and make it another 10-15 minutes before starting the video over. The Q&A is really helpful too, since someone in the chat always asks about a specific corner case or exception. It doesn't seem like there is a lot of good intermediate-level material out there, so I'm always excited to see a new video.
You don’t even know how helpful this particular episode has been for me. And the detailed chapter markers too have been so nice when I need to refer back. Seriously. Thank you
I really appreciate that you touch on very advanced topics while teaching beginner/intermediate ones. I've learned the most from your videos so far and have a deeper understanding of how Rust actually works
Excellent video as usual! Thanks, Jon. I feel like dynamic dispatch and its constraints make a lot more sense to me now!
I came here after reading the book Rust for Rustaceans and this helped me understand a lot better. Thanks a lot Jon!
Man, I wish I had your skill of explaining things. You're awesome! Of course, the amount of knowledge you have is helping here, but it's a skill on its own.
32:55 this has to be the best explanation.
I literally had this doubt and I was about to google how a function would know the size of normal array otherwise. But you explained it so well right after that. Thank you so much for these videos.
Wow. This video was so incredible helping me understand rust deeper. Getting to understand on a more complex level and pushing my knowledge. Thank you so much for this video. I am so grateful for this.
I love these streams! Please keep them up!
thank you so so much for these streams! I'm very interested in rust internals and I often come to your streams for clarification of complex stuff. really much appreciated
I feel like at 1:05:26 you should have also implemented HeiAsRef:
pub trait HeiAsRef: Hei + AsRef {}
impl HeiAsRef for T {}
Yeah, in practice you'd want that blanket implementation too, but it wasn't really relevant to the discussion around vtables :)
Really Great content. Opening up the internals explaining how things works under the hood. The missing pieces. And why I am hitting the head in the wall (compiler).
For those, who prefer visualizations over the long documentation text, visit cheats.rs. For example, the idea of fat pointers can be grasped from the cheats.rs/#references-pointers-ui section. Of course, this site is pretty much of any value if you already know Rust to some extent, but if you do, those little condensed explanations and visuals might be beneficial to the understanding of the whole picture!
Thanks for these series, Jon! You should consider doing more visual explanations, whether by hand-drawings (as you rarely do on live coding streams) or by using cheats.rs!
Wow this is cool, thanks a lot
I love your intro animation.
3:04 "Let's get rid of this test, we don't need no test" Famous last words!
I disagree with the implementations at 7:34. I think they should still be `s.as_ref().len()` because you should be able to call it with any `T: AsRef`, even if T did not have a `.len()`. The optimizer might then see that for `&str`, `as_ref` does nothing and remove it. Having just `s.len()` makes it seem like there is something magical between `AsRef` and generics, which I don't think there is.
Great video. You gracefully explained what's behind the dyn keyword
Thanks Jon for this enlightening video! Pure Gold.
In case someone wants to see in action handcrafted vtables in C-land he/she can have a look at the headers from the Gtk toolkit or even have a look at the C-code output that the vala language compiler produces.
This video and th-cam.com/video/1-_EBEr0fxI/w-d-xo.html which goes over GObject have been very handy for a project of my own.
At 1:41:05, should the drop function to accept `v: Box`? In the code as written, `v` is just a reference, so the compiler can't run `Drop::drop()` when it goes out of scope, since its owner might still be using it.
Same question
15:33 Technically not entirely true. "Dynamic Libraries" is a broad term, and if you include for example, Windows' Dynamic Link Libraries, those can also contain .NET managed code. Those contain generic functions and types, and it is up to the runtime to "monomorphize" them just in time. But yeah, this is likely not what that question meant, since the context is dynamically linked *native* code
I wish you have covered the Any trait. It seems like it's super relevant to this topic.
There really isn't much magic to Any. It's just a method that returns a unique type identifier that then allows safe downcasting as I mention at the end :)
@@jonhoo I feel like "Can I downcast trait object back to original type?" is a fairly important question when discussing dynamic dispatch. And "Any" trait seems to be the idiomatic way to do it.
@@KohuGaly Ah, so, the answer is that you can only do it *if* your trait object includes the Any trait. You can't use Any to downcast an arbitrary trait object.
@@jonhoo So basically, the Any trait is optional... as per "don't pay for what you don't use" principle...
@@KohuGaly Yeah, you can think of it that way!
This was super informative, cheers!
Thicc pointer
Thank you so much, this is such good content man much appreciated.
Isn't a vtable generated for each _type_ , not instance? What is the problem then in inserting a static method inside the vtable? Every other method takes atlest one parameter, but I don't see how not taking a parameter is problematic.
Thank you so much Jon! Very helpful!
Great content. Like always!
I just have to comment that this is a brilliant video if you are interested in Rust on a slightly deeper level. Kanonbra! :-D
One day implementing that compiler error at 1:06:05 sounds awesome!
1:29:25 I still don't understand why it's not possible to monomorphize the vtables. We compile the standard library at the same time as the rest of our code, so it should be possible to know the size of the vtable for a type+trait after monomorphization, even if it is large, and use that everywhere in our program. My vtables will be differently sized than yours, but that's just a different flavor of the same issue preventing us from having dynamic libraries.
Listening to your explanation again, I think my mistake is that we don't actually compile the standard library at the same time, we compile one crate at a time and just have access to the source for other crates. So for a normal generic function, if we introduce a use that std doesn't have, it would get compiled along with our crate, not std
44:30 "you can keep using box after the stack frame of the caller has gone away", how is this situation possible? Won't the caller return only after this function has returned?
Thank you a lot! I love those!
Missed opportunity for
```
match vial_broken {
true => Box::new(DeadCat::new()),
false => Box::new(AliveCat::new())
}
```
but Schrodinger hai is good too ;)
Thank you for this video
please please.. sir , if u can cover unsafe rust , it would be a great help for the community , but after all iam so glad u prepare this content for us ! tq so much 😃
Thank you! Very good Video! Keep it up! Greetings from Germany 👋
Making me a better swift programmer, thanks. But now you got me curious, how does swift handle dynamic linking of generics?
gankra.github.io/blah/swift-abi/ has all the gory details!
@@jonhoo Thanks!
at around 1:05:52 when you talked about the multiple trait vtable, you said to create a new trait that requires the needed traits and use that. It would give a bigger vtable but it would still be 1 pointer right? And it would lead to code duplication as either the compiler would copy the trait implementations to another location or it would copy the function labels and call the original implementation methods. Wouldn't that be inefficient? Is there a way to say that I do want to generate a 3 pointer wide or even wider (based on number of traits) argument? with each pointer pointing to one traits vtable?
You can already do what you describe yourself by using custom vtables but trait objects don't work like that.
And compiler only copies vtables, which usually has the size of (pointer_size * method_count). Basically it is a very very little overhead.
Also you said that we can only include "+ " in our variable signature if some_trait is an auto-trait, because auto-traits have no functions. But isn't Any also an auto-trait? It surely has functions, and which have different implementations for each concrete type. Now I'm not really sure how that comes into play.
No, Any isn't an auto-trait. It has a blanket implementation (impl Any for T), but that's not the same. An auto-trait is one whose impls are generated by the compiler directly based on the structure of the type, and they never have any methods, which is not the case for Any.
can't the generic method thing be solved by more dynamic dispatch? I.e. The vtable for Extend has extend?
I'm reading your book and decided to look up a video on this concept to understand more... and guess who I find.
Thank you for your videos. Could you do a video about epoll and io_uring for file I/O?
Thanks!
I’ve been doing a bunch of minor projects in rust and I haven’t even ran into a use case where I’ve needed to use “dyn”.
In other languages I’ve used the pattern a lot and maybe it’s just that I’ve worked on a different class of problems than I did in those languages.
But I’ve found that I can often get away with using some different pattern in rust.
There definitely are use cases where it’s needed, or at least very useful. But where I in some languages would make an interface and put a bunch of generic objects implementing that interface, often I find it more clear to define an enum with variants holding types.
That limits how generic the code is. By quite a lot actually.
It only allows the possible values I explicitly put in there. But quite often I don’t want to be generic over any object that happens to implement a trait. I just want to be very restrictively generic to exactly 2 or 3 different things that I implement myself.
I’ll probably run into it more when I ramp up complexity a bit.
That’s why I’m eating the video.
But still. We don’t have to be maximally generic for everything we write, just generic enough to make writing the code convenient.
To paraphrase Dijkstra, "purpose of abstraction is not to be vague, but to be precise". A good support for abstraction lets you state precisely what you mean, without forcing you to be more vague or more specific than you want to be.
Languages that have interfaces (traits) but no sum-types (enums) force you to over-generalize and therefore force vagueness. They push you to express closed sets of types (enums) as open sets of types (interfaces), even when you actually want a closed set (which is most of the time, actually, as you correctly point out).
The main use case for traits and generics is when you want to provide new functionality to types that you have no control over (ie. when you write a library). Or you you have control over those types, but you want to keep it separated to reduce complexity.
What font do you use?
@1:05:40"HeiAsRef". I wonder who that Ref guy is and why he is so hei ;-)
Awesome video!
Who is the guy who disliked the video?? Let's locate him and sacrifice his soul for the sake of nightly rust.
@@w-i-s-e_a-p-p-l-e
.Its going out of hand , now there are six of them
sorry, but I accidentally read guy asgay 😂 but anyways, nice vide
o
Hey Joh, what's the font you use in your terminal?
good tutorial, respect
does anyone know what theme is he using? looks pretty neat
edit: looks like gruvbox or something
It's tempting to say that type erasure causes all trait-related problems. Is it required that much to generate an efficient code? Why can't we preserve types?
How do you get your navigation bar/tabs on the bottom? Is that a firefox extension?
Hey, great video - I have a noobie qn. Early on you discuss how in the trait object case you need the function argument to be sized so you can make a collection of them where each thing, the pointer type, is of the same size. However later on you say we can have a trait not add a static method to the vtable by requiring Self be sized, saying that the trait, dyn Hei, has no size. But didn't we pass a reference so that it was sized? Thanks!
Reference is sized not the object
Why does an unsized type have to be the last field in a struct if the compiler is allowed to reorder fields anyway?
Because fields of struct are accessed as pointer offsets. For sized types, compiler may choose to reorder them, but compiler still knows where each field is located relative to the head of the struct.
That does not work for unsized types. Because compiler can't know its size, then it can't access fields after it as offsets, because it can't figure out how big the offset should be. The only place where you (and the compiler) can put it is at the end of the struct.
in `struct Foo {f: bool, t: [u8], x: bool}` why can't the offset of `x` be determined by `len-sizeof x`?
(such a struct combined with a #[repr] would be good for some zero-copy network encoding stuff)
In general the compiler expects field offset to be constant. You could imagine extending it to be dynamic, but that's a big language feature in and of itself.
I have my own MIT professor as a Rust mentor for free. Well, don't we live in exciting times!
Document describing your vim setup.
Please :)
Holy shit you’re Norwegian!? Your English is amazing.
Hilsen fra Molde :)
s::weird is no possible because s doesn not name a type
As you said type erasure occurs and the only thing you have is pointer to data and pointer to methods table
Compiler does not need to know s type to call needed method
I think the main reason is because in machine code level calling method through indirection is compiled into some stuff, which requires pointer to data to be available at this point. That's the main goal of dynamic dispatch and polymorphism.
Method without self parameter can only be invoked through concrete type, which make no sense to include it in vtable which is needed only for magic like calling method without knowing type of object this method is called on
IMHO It would be nice if you showed (traits) fat pointer representation with the debugger
25:50 so we want something similar to Duck Typing in Typescript? Interesting
This is clearly an outlier: Usually I watch tutorials on youtube at playback speed = 1.5 (+- 0.25) because of relatively low information density. But crust of rust is often < 1.0. Thick (high quality) material to digest.
Where is Norwegia?
@Leon Tepe - yeah, i know. I always just think it sounds funny
Thanks!