 Hello folks, welcome back to yet another crust of rust this one. I I'm hoping I didn't Bite off too much to cover in like an hour and a half to two hours I guess by the time you watch this recording afterwards. You'll you'll see whether I succeeded or not So for this one what I wanted to cover was a sort of chunk of semi-related topics around traits and Dynamic and static dispatch of the size trait like wider fat pointers And some of the things around like v-tables coherence they all have this That they're all sort of tied together Some more so than others and I get a lot of questions about each one And I figured I should just do one stream or we talk through how all of this stuff works and how it fits together I'm hoping that I'm hoping that we don't End up too far down any of the any one rat hole I think what I'll try to do is cover most of it in sort of workable detail And then we might do sort of a deep dive on one of them in some later stream But but in order to try to contain it to the time You might find that I skip over some of the detail and hopefully that'll be okay So I actually wanted to start this time on the rust book so the rust book has a chapter on generic types and traits and lifetimes and This is one subsection in there on the performance of code using generics And this is a section that like if you've read the book and I highly recommend you do if you haven't already What might have like struck you as you might have read it and be like that sounds like magic And in some sense what the stream is all about is trying to deconstruct that magic and figure out what's inside and In particular it talks about this idea of monomorphization And monomorphization is the as the text says the process of turning generic code into specific code by filling in the concrete types that are used when compiled But that's sort of sort of abstract So let's try to pick up a slightly more concrete example and then work from there. Let's go with new lib We're gonna call this one. We're gonna call this one Hmm Naming is hard It doesn't matter, but that makes it even harder because there are no constraints. We're gonna go with A Example written in region All right, let's get rid of this test we don't need no test So let's say that you have a function Greater It is generic over T. Actually, let's go even simpler. Let's say that We're gonna have a function Stir Len and it's gonna take anything that implements asref stir And it's gonna give you back to use size So we're gonna do asref Real simple function here Let me make it a little bit larger So this is a generic function. You could just as well have written it as As Is s To use size where s implements as ref stir these two are equivalent that's as rough when And I guess we'll make these pub to make the oops To make the compiler stop complaining So these two are equivalent They're not quite equivalent, but they do the same thing And both of them are generic functions that can take in any type that implements That they can be turned into a reference to a stir, which is what this says and same as what this trade Bob does And what happens is if we write any function, let's say foo And it calls stir Len of let's say hello world And then it also calls stir Len of String from hello In Norwegian a little good good handy shortcut for for picking examples So this let the just to sort of see why these are different, right? This is giving in a type of Static stir oops This is giving in the type of string whoa Right, so this is where we get into the point of this function is generic It doesn't just accept a string reference it accepts any type that can be turned into a string reference So string for example implements as ref stir Because if you have a reference to a string you can trivially get a reference to a stir Because the internal representation of a string is a star, right? So here we immediately see sort of why generics are useful it because it enables you to call into a given function in a more flexible or ergonomic manner and Behind the scenes what happens and this is what the rust book is talking about when it talks about monomorphization is that the compiler actually Generates two copies of this function One for each of these types, so I compile time what we're actually going to end up with is like a stir Len Ref stir which actually takes a stir and Calls I stopped Len And we're gonna get end up and we're gonna end up with a stir Len String which takes a string and does the same thing Right, so this one generic definition gets turned by the compiler into these multiple Non-generic implementations. That's the process of monomorphization And this is sort of key to how generics work in rust and and this process doesn't just happen for Functions they happen for types to so imagine that you have a a hash map to generic over like the key and the value you actually get a full copy of the entire Struct and all of its methods One for each type that it is used by notice that it doesn't generate Copies of this for types that we didn't invoke it with it This is not like a generate for every possible type. It is generate for any type like sort of on demand But on demand determine at compile time if you see code that needs this function for a given type Then generate it at that point in time and a type would be the same like Hash map would only a particular instance of the hash map type would only be generated if that Type combination actually appeared somewhere in the in the code. This is actually also one of the reasons why it's a little bit hard to make rust to Compile rust code and then ship people a binary that they can use as a library Because imagine that if you had this in a in your rust library that you wanted to sort of Distribute in binary form think like dynamically linked libraries and stuff. It's actually fairly complicated because The contract is that rust is supposed to generate Distinct copies of this function for each type But it might be instantiated with a type in a consumer that isn't defined where the source function is defined in the originating library and therefore you need the source in order to generate all those instances And the reason why monomorphization is great is because you end up Producing potentially much more efficient code, right? Stirland is a bit of a bad example here, but let's imagine that we had something a little more complex Actually a good example is hash map I'm not gonna write out the full definition of a hash map here But what do you end up with a hash map is because you'd get a copy of the type for each concrete type you use imagine that one instance of the hash map has the key be a String and another instance has the key be a number You would actually end up with The compiler when it generates the code for each of the methods of the hash map It would generate like it would sort of inline the definition of say the hashing function for each Key type so the one the hash map version of the code That uses string would sort of put in the code for hashing a string directly into the the hash map code And it could optimize that code based on the fact that that type is a string similarly for something like a number it might be able to skip hashing all together because it realizes once it sort of Monomorphizes all the generics that actually the hash is just the value of the number That might not actually be true But but you get the sense of like the compiler gets to see the concrete written out code For the particular types that are used which lets it optimize a lot better Does this all make sense before we move on to the sort of more complicated aspects of this Does the general idea of sort of generics and why why monomorphization is is nice make sense? so The downside as someone observed in chat is yeah for for monomorphization One of the downsides is that your binary ends up being larger because you need to generate a Copy of the type or the function for every type that it's used with It's not quite as bad as like just multiply by the number of types One of the reasons is because for hash map for example You might only use a subset of the methods for any given type combination and it will it won't generate like Like if you ever have a hash map that has string as the key type It generates the entire hash map like all of the methods for hash map for the string key type It will only generate the methods you actually end up calling So that amortizes the cost a little bit but but it is true that it leads to more code in the in the final binary and A sort of side effect of that is that your code can be a little less efficient in the sense that Now rather than having one function called Stirling that like one Sequence of like assembly like machine code really That the the computer can jump to and then it can keep those lines of machine code like in cash Which lets it execute it more more efficiently the next time around what you end up with instead is you have two different functions and some code jumps to one and some code jumps to the other and they have to be cashed separately because they're The separate regions of memory as you actually end up with a slightly worse Cash efficiency for your instruction cash Which is a downside? It's also a little bit expensive in the sense that the compiler does need to generate these copies But but usually that's not too much of a cost like actually generating these unless you end up instantiating with with a Very large number of types Is that one of the primary reasons rust binaries are larger than C ones So that that is slightly different rust binaries Statically compile more stuff Which might be part of the reason part of it is monomer for station part of it is I forget the exact details But I think there's more stuff from the standard library that gets up compiled in And another really big one is that rust Often builds in debug symbols Which if you don't strip them from the binary you end up with a binary that has very little actual machine code But a lot of debug symbols as you might want to try to just strip the binary Well the compiler try to inline generated functions Yeah, so this is one of the reasons why a monomer for station is really good It's because it doesn't just like in life whether it's string or stir, but if you have say a pubfn like Boole then Which takes a Boolean and it takes an f which is a function And it returns you an option t and Then let's say the implementation something like if be then some f Else and this this function actually just ran landed in the standard library Although as a method on bool. So here if you ever ran this function with a particularly provided Closure or function it would actually generate a copy of bool then with the Code for the closure you passed in directly inlined into this function. And so it could be optimized accordingly See I it does end up inlining or Inlining is the wrong word here And it has the option of inlining and optimizing as based on that it doesn't have to It's just that because it gets monomorphized to the particular type It knows exactly what that type is and it can choose to inline if it wishes Which is an important distinction Big reason for larger sizes standard lib that's not quite true Because only the parts of the standard library that you actually use end up getting put into your binary It is true though that if you're using a lot of generics from the standard library that ends up growing the size How big of a cost is duplication of methods Basically none. I think actually the actual generation of multiple functions isn't that important But what is important is if you generate many many copies of a function You have to compile each of them into machine code which slows down the compilation process Wouldn't Stirland string take ref string rather than the string by value it can take either actually they will both work Which is one of the reasons why Taking as ref stir is nice How do dynamic libraries handle generics they don't This is like one of the reasons why it's challenging to do Dynamic linking of rust libraries or even just distributing rust libraries in in in binary form and It's something that I think in Swift they've been trying to stabilize something like this. We don't quite know how to use it for rust yet And yeah, you can't use it for something like if you want to see dilib like a Dynamically linked library in rust that has a bunch of like external functions. Those can't be generic All right So now that we have an idea of What monomorphization is? I'm I want to talk a little bit about What? the actual process of dispatch here, so dispatch is the idea of Actually, let me give you a slightly better example That we can work with for a little bit longer throughout the stream So let's define our own trait We're gonna call this trait Let's just call it hello, it's simple enough or I guess let's go with The Norwegian high and it just has one method And Let's say it actually returns nothing. It just has a symbol as a single function That just lets you say hello in Norwegian And we can imagine that we implement that trait for let's say stir And All that's gonna do is it's gonna print line self Oops and then we're gonna do I Guess Let's just go with J. Hi Yeah, I can't type today All right, so so what happens behind the scenes here is that when the compiler tries to generate the code for this, right? It needs to It determines the the type of this then it looks up like what? It looks at which methods are available on just the stir reference type It doesn't find one called high And then it looks at the traits that are in scope It finds that there is a high method on this which is implemented for the receiver type And so it ends up calling that Seems fine. Now, let's imagine that we have a sort of a Generic version of this where we take something that implements high And I guess H and it's just gonna call H dot high So this one's trickier, right when the compiler has to generate the machine code for this It needs to somehow call this method But it doesn't actually know the type of H and this was where monomorphization comes into play, right? That in reality what's gonna end up being generated is a bar stir, which has H B stir right and now Dispatching this like figuring out where to what to call here is trivial because we know the concrete type So this is what's known as static dispatch that is at compile time The compiler knows what the actual type is and therefore when it tries to call this method It just knows that that is this method and it can just jump to Like this basically becomes a call in like assembly code to call this function Which is at some known location in memory, right because it's just where this method is Does that make sense for static dispatch? right that actually Figuring out how to compile this code becomes trivial once you can generate this method because this method is really the same as this And these are both trivial to figure out where the high method lives So now that we've looked at this sort of static dispatch, you might wonder well, what is the alternative, right? What if we don't want to generate a bunch of different copies for this? So this is where we go back to Dynamically size types oh Sorry, yeah, Impel high here is is just syntactic sugar for generics This is equivalent like I showed with a sterlin before this is equivalent to H is high H these these two are equivalent Yeah, so so what then is the alternative we don't want this monomorphization We don't want multiple copies or the other reason why you might imagine this is Let's say that you want to Let's say you have a vector of things that implement high right so I want to in foo here I want to have a vector of This and I want to do like 4h in this vector Call h.hi That's fine if they're all the same type But what if I wanted a sort of collection or sequins or set in some way of Things that are high But I don't I don't need them to actually be the same type all I care about is that they implement this trait right so so More concretely imagine that I want to write bar and I want to take anything that is a say slice of Like Impel high, let's start start with this version right so for h in s H.hi This works fine I can call bar with say a With something like this That works just fine that compiles just fine Similarly, I can do I can turn each of them into strings That also works just fine. Oh Right, I didn't implement this for String so I was just fine But let's imagine that I want to be able to call it with like this Should really be fine But I can't do that right like the I can't create a vector of things that are different types That's not a thing that I can make in the first place what I really but but in this case I don't actually care what the concrete type is I don't care that these are Strings all I care about is the fact that I want them to implement the high trade That's all that matters to me So I should be able to create a vector of things that have this behavior without caring about the concrete type And this is where we get into the the sort of area of dynamic dispatch and trait objects And the book has a chapter on this too. Let me Find where that one is It's Have it up. I've opened it in a wrong order, which was silly of me Sorry about the jumping around It is down here in the chapter here so The the book has a pretty decent chapter where it talks about the ability to treat Things that are different concrete types as the same type So another example that they use is in a in the case of like a GUI You might have lots of things that are drawable like they implement a draw trade And you might be able to take you might want to have like a draw function That just takes a sort of list or iterator of all of the things to draw and some of those might be buttons Some of those might be images. Some of them might be text boxes But it doesn't really matter for the draw function all it cares about is that it gets an iterator of things that are drawable Which has a similar sort of flavor to this right where we want to just take a slice of things that I can call high on and What you need is what's known as a trait object a trait object is Where you see the din keyword, this isn't actually going to compile So what I'm going to do is sort of this is sort of what I want, right? I so impulse remember is a shortcut for and this is where it'll be useful to actually write out the types as a shortcut for this and This sort of gives a clue to what's going on bar is generic over one type Which has to be either a stir reference or a string It can't be both because only generic over one type and you can imagine that we had like an h2 hi here But given that we're taking a slice or let's say an iterator There is nowhere to put h2 there is only the one iterator which only have has items of one type So this won't really work in some sense What we want to say is that each of the things is a hi It it doesn't have any other concrete type that we care about and in fact if we write this we get a compiler error saying Let me pull this up in a new screen It says trait objects without an explicit din are deprecated. Okay, let's get rid of the warning first It's saying we need to put din here. So we're just going to do that And now it says the size for values of type din hi cannot be noted compilation time Right, so it points to this and says doesn't have a size noted compilation type. The trait sized is not implemented for din hi Slice and array elements must have a size type This is a an error that you might have come across before And part of what we're going to be talking about for some of the remainder of the stream is why we get this error What it means what the fixes what sized means and sort of how how this ties into dynamic dispatch and trade objects Let me pause here for a second because I've just thrown a bunch of stuff at you. Let's see whether it makes sense So It's a bunch of discussion of dark mode and light mode So really what we want to say is which you can think of this as sort of type erasure, right? We want to say that I want to take a collection of things that and I only care about the fact that they implement this trait I sort of want to take an abstract notion of this trait All right, so the the challenge of the compilers pointing out is In this case Where we're gonna even start with this all right, I think we need to talk about the size first So let's go to the sized trait So the size trait has no methods It is a marker trait. That's what it's in the the marker module And the description just says types with a constant size known at compile time Now most types are sized like if you create a struct food It's probably sized most types in the standard library are sized In fact, there are very few types that aren't sized so few in fact that Every trait bound you have even if it's just like a food T Has an implicit bound of requiring that the type is sized And to see why think about what happens when let's just sort of comment this out for a second Let and comment this out for a second. I just want to get actually I guess I can do this smarter So Let's sort of go back to our very simple Stirland example in the very beginning, right? It's gonna be generic over some s that implements as Refster It takes that s And it returns you size As RefLang Right so Imagine that some something wants to call Stirland Well, Stirland takes an argument that is that s. So it has to be passed a thing At like somehow it needs to be passed that argument Well, the argument that it gets passed has to take up space on the stack or has to be passed in a register either or Which sort of means that The compiler has to make sure that there's sort of code in that in the resulting assembly that like Allocates the required space on the stack for whatever this argument is And that requires the compiler knows how large the argument is right if if If s here, I guess think of the concrete implementation, right? So Stirland stir Here this is just a Mmm Let's go with the string implementation so string is really just a It's sort of two numbers and a pointer, right? It's the Length of the string It's the size of the allocation and it's a pointer to the first character in the actual string on the heap So it's size the compiler knows that the size of this argument is three words Three you sizes if you will right and so it can generate the assembly code for this function And for calling this function trivially because it knows how much space the arguments take up You can imagine something similar right where if we were generic over the return type It would need to know how much space to actually allocate for the return type on the stack And so even though we didn't specifically say that s is sized here It just is basically always a requirement that the type is sized because if it wasn't the compiler wouldn't know how to generate the required code for it right All right, so Why Why then is it compiling when we're trying to use this? Well din high is really just saying Anything that implements high like just something that generates high without giving a concrete value because remember We want this to be a slice of potentially like string and stir and whatever other types might implement this trait And those are all different sizes right a string is two numbers and a pointer a stir is one number and a pointer And you could imagine that we have some type foo that is like Like a gigabyte large as a struct or something insane right and we implement high for it And that is also something that we might want to pass in here. So this slice Doesn't have a size It's it's just not like if you if you a slice is really just a contiguous piece of memory Right where each chunk is the same size It's an array where all of them are the same size But if we don't know the size of them We can't guarantee that they are the same size if I told you if I gave you one of these So didn't high here is not sized right because it could be any type that was high which could be any size And I asked you give me the fourth element Normally in an array because all the elements are the same size the fourth element is just the pointer to the start plus like Three times the size of one element right this is a pointer arithmetic to get to the fourth element But but if they're different sizes you can't generate that code and this is why it's saying Slice and array elements must have a size type because otherwise the type just doesn't make sense So how can we fix this like we really want to be able to do this right? It would be insane if rest didn't didn't give you the ability to talk about things that implement a trait as a collection and Arrays aren't the only problem here right even if we took Even if we wrote What's a good example of this let's say we had a Stirland 2 here that took a like didn't as refster It has the same problem right if We if I try to compile this you see it says the same thing Let me get rid of the other message It says The size for values of type didn't as refster cannot be known at compilation time Sized is not implemented for din as ref function arguments must have a statically known size Borrow types always have a known size and then it tries to give you up a suggestion for how to fix it But this is sort of the key insight again that we need to the compiler needs to know how large the type of an argument is Otherwise it can't generate the function call code And so this is sort of like a fundamental requirement of the compiler that these types are sized That makes sense so far like why this is a problem Can you show us implementing sized so size is auto implemented for any type that can implement it so if you do like a Struct foo with no fields it is sized If you add a string in there string is sized so foo is sized If you add another bool like this type is now still sized Types are always sized if they can be The exception would be if I did something like and in fact even if I did foo tea and said that this holds a tea It's still sized because remember that there's a an implicit requirement for every trait bound that it is sized So there's sort of an implicit tea sized here And so that means the tea is sized so all the fields are sized so therefore foo tea is sized for any tea As you you never implement sized yourself it is only ever uses an auto trait The the issue is not stack size The issue is not that it might not fit on the stack It's that in order to call a function right just at the machine code layer or think of it as the assembly layer Right in order to call a function you need to know When you call that function how much space do you allocate for the like stack variables of that function? We might include some of its arguments or on the caller side when you have to allocate the space for the return value How large should that space be? Fundamentally that just requires that you know how large the space is because you can't generate the machine code for Making that stack There are like sneaky ways you could do this But basically you require the compiler in order to generate efficient code has to know the size of these things Let's see what things aren't sized. So we've talked about like Sort of bear traits. So this is if I write pubfn foo And I just say that I take a high right High here is a trait and lots of things implement a trait and they might all be different sizes So this doesn't have a defined size If I say impulse here, right impulse is a shortcut for H Hi So this at the H is sized because we get a copy of foo for every concrete type And for each of those copies the size of the type is known So if we use static dispatch here from one more visation, this is fine But if we just try to take like the trait itself saying basically this function takes anything that implements the trait This won't work because this is not sized and this is equivalent to writing din high Like this is not sized what we'll talk in a second about how you how you make something like this sized The other example is something like If we go back to my my struct foo If you try to have it hold a stir stir is not sized Because a string can be any size sort of if you think about not the pointer to the string But the string itself can be any size the same with like This just like a slice without a reference is also not sized because you don't know how many elements there are right So it doesn't have a well-defined size Those are the two Best examples, I know of of things that aren't sized and then anything that that in turn contains those types I There's a lot of discussion in chat trying to Explain the things that I'm about to explain on how we fix this and why you need references and stuff I will get to it. I promise Great so I think roughly where we've gotten to so far make sense it seems like So The question now becomes how can we resolve the situation right we want to be able to take a For our Stirland 2 here. Let's call it Stirland din. Maybe We want to be able to have a method like this that doesn't get monomorphized, but this isn't sized. So what do we do? The trick here is to Make this be a type that is always sized and and the way that we do that is by Indirecting it through some type that is itself sized for any Inner type an example of this is a reference, right? So a reference is always the same size It's just the size of a pointer or in some cases two pointers. We'll get to that in a second, right? This does have a size Right the compiler always knows how large one of these are no matter what the type that follows is Right because this is just a pointer The same would be if you have a box Right a box also just a pointer to the heap And so once we do this this the size of this argument is now known and we're fine and This is in fact how you take something take a Trait itself without monomorphizing is you place it behind some kind of pointer type like Like a reference like a box like an arc The way this works in practice is that if you look at the definition of box So it has magic in it, right? It's not magic, but it has a bunch of stuff inside of it. Basically. It just has a pointer It's not quite true but close enough a box actually has a has this definition on it And the question mark here means does not have to be So it's basically opting out of the auto bound that gets added of everything has to be sized saying for box the T does not have to be sized and that's why you can create a box of of Something that itself is not sized and the reason this works right is that when you create the box initially You make an allocation. That's a size to whatever you're gonna put inside of the box, right? So Let's say the let me sort of make a main here that's gonna exemplify this at some point I have to do like box new of say String from hello Right, and then I can say that now this is gonna be a din as ref stir Should say X and This should say stir And then I can call stir Len din with Y So at some point when I created the box I had to give it something with a concrete type and at that point it allocated on the heap space for that That type but the box is still of a known size because it's just the pointer When I turn it into a box for this it's still just a pointer conceptually Even though the thing it points to might have an arbitrary size But the actual argument that we passed to this function does have a size and therefore we can generate the relevant code So it makes sense why why in direction through a type like this where the in direction type size is known Does that make sense? We'll talk about combination of traits to Let's see. I Promise we'll get to multiple traits Can't you just put box din as ref stir directly on X. Yeah, so you can do this too As ref stir That's also fine But even so you still had to provide a concrete type to box new That was sized When should you use box over references like when you choose this over this? There's no hard and fast rule there in general the the advantage of boxes that it's static so You can keep using it even after the stack frame of the collar goes away So it's the same as when do you choose box over references normally? Uh, can I also then give the function a reference to the stack? Yeah, exactly. So you can also do Let me do that down here So I can totally do And then sterlend in why So this let's say I have a din to that takes this And this can called in too. Why is this complaining? It's because box has an asterisk method too. That's why the double asterisk is needed Yeah, so you see I can do the same for something that's on the stack. I can create a Sort of go through a pointer in direction here and then pass it to something that takes a referenced in and in fact I think I can even do This Yeah, that's fine It's not not terribly important why that doesn't work So what we just constructed is known as a trait object It is an object that has the only has the property that it represents a trait So this is what trait objects are. They are things that only behave as some underlying trait This is a trait object and this is a trait object. It doesn't matter what the wrapping type is Box is kind of a pointer type. Yep box is a pointer type Can I convert a trait object to its original concrete type? I'm gonna go with no, but technically yes sometimes But but in general no You can think of it as this is essentially type erasure You can technically go back through like unsafe transmutes and stuff But in general the moment you turn it into a trait object you you erase the knowledge about what type it used to be But yeah, there's like some magic around Unsafe and some magic around the any trait that you can use but but in general you should ignore that I'm also note that when you do this You only retain the ability to use this trait You erase all other knowledge of what the concrete type was So the only thing you can do on a boxed-in asref stir is call asref on it. Nothing else same thing here And we'll get into why in a second All right So this raises an interesting question. Let's let's take the example Let's make this the the two and this the one and just swap these around so this still compiles Let's now think about what actually happens when the compiler has to generate the machine code for this function Right at some point it has to actually do the compilation and produce Assembly code that it can in turn be turned into like runnable machine code So s here is some type that implements asref In fact, let's go back to our high trait. That's gonna be a little bit nicer So let's go back to here and let's say that I want a say hi Which takes a din hi returns nothing and just calls hi Right, so this is gonna be this takes a trait object for the high trait But when the compiler generates the code here, it doesn't know what type s is Right, it's just it's the trait object like it. It's really just a pointer. So how does it call the high method? Remember in the generic case static H in this case We actually get a copy of this for each concrete type right so really this turns into this and For this concrete implementation Generating the machine code is trivial because this is really just like an assembly call to like whatever it is To this line to line six It's not actually line. It's memory locations in the binary But you get the idea right that the compiler knows like statically at compile time where to go here For the dynamic case though, what do we call? We don't know that this is a stir or a string, right? So we don't know whether to generate the address of this the address of this or the address of something else entirely So what machine code does this turn into? well, this is where we get into dynamic dispatch and V tables or virtual dispatch tables so the trick to constructing a trait object is that the It's actually not quite true that the reference of the box is just one point or wide It actually carries a little bit of extra information About the type that is pointed to So here let's go back to the Back to the the rust book. So the rust book has a chapter on dynamically sized types and the size trait this is under the advanced type chapter and what it's talking about is In fact, it doesn't even talk about as much as we wanted to talk about Here all right, let's use the rust reference instead So the rust reference on dynamically sized types says Most types have a fixed size that is known as compile time and implement the type the trait size like we've talked about a Type with a size that is known only at runtime is called a dynamically sized type or informally an unsized type And this is the slices and trait objects or two examples of dynamically sized types The the reason why this is only known at runtime is at compile time We don't know what type this is going to be right It's only at runtime that we'll know and we can sort of to give to make this a little bit more concrete Imagine I have a main function where like if Rand. I don't have a random number Random is for If random is for then say hi of This Otherwise say hi of String from world Right, let's imagine that random was more random than four You don't know until runtime like let's let's say this was like read from the user At compile time you don't know which Actual type is going to be used here. It's only determined at runtime by some input that can't be predicted at compile time Right, so that's why this is determined at runtime And it says pointer types to dynamically sized types are sized, but have twice the size of pointers to size types So pointers to trait objects also store a pointer to a v-table and this is where we get into what is a v-table Well When you have a trait object something like at din hi What actually gets stored in the in the reference is one a pointer to the actual concrete implementing Implementing type And two a pointer to a v-table for the referenced trait So then What is a v-table? So a v-table or a virtual dispatch table is a little data structure that has Pointers to each of the methods for the trait for the type So in the concrete case of the high trait, so let's say we have a din hi The v-table is going to be Sort of you can think of it as a high v-table struct That only has one member which is hi, which is a pointer to an fn An fn actually that itself takes a pointer to Whatever the t is so basically a Different v-table ends up being constructed for each Concrete type turned into a trait object. So when we start out with Let's say that we have a Stir and we want to go to a din hi What that actually gets constructed is a let's say we have a Well number one Becomes a pointer to the stir and number two Becomes a high v-table where high is Stir as high high And a pointer to that When you make this conversion The compiler knows that it has to construct this v-table And it sticks the address of that v-table Inside of that reference. So this reference Contains two things the pointer to the actual type and the pointer to the v-table that the compiler generated during this particular conversion So there'll be one such v-table for each type that gets turned into a trait object And so now when this code When the compiler tries to generate this code what it actually generates is like s dot v-table dot hi s dot pointer And so if someone passed in and say a string instead it would have a different v-table So the s dot v-table would be a different pointer So in this case, this would be Where stir Hi Like line six Right. So this would be a pointer to line six But if we instead went I'll stop for questions in a second If this instead went from a string to a din hi, it would be a pointer to the string And string as hi, which is this method up here on line 12 So in other words this code that gets generated Will work regardless of which type is passed in Because we indirect through the v-table and each type has its own distinct v-table All right, that was a lot to throw at you at once, but hopefully it wasn't too bad Let's see Are the v-tables themselves statically built at compile time or are they also allocated dynamically the v-tables are The v-tables are built at compile time. They're built statically And so in general the the second part of the so this is what's known as a fat pointer or wide pointer Because it has two pointers in it, not just one So anytime you see anytime you see this it's actually a wide pointer And In general the second pointer in there will always be known statically because it's determined by The the original construction of the trait object The reason I say generally is because you technically can construct it on the fly Like there's there's nothing in the calling here that requires that it was known statically So you could you can imagine manually constructing a v-table and this is used. We'll actually talk about that a little bit later Refster also contains the length of the string slice Yes, so in general this will actually be this So it's not a pointer to the actual string. It's actually a pointer to the Stir reference which itself holds the length Can we debug print that v-table struct somehow? I don't know of a way to get the compiler to print its v-table struct, but it's basically just this It just there's a member for each method of the corresponding trait that this is a trait object for Where each member is Each member is a corresponding method And the value is the pointer to the implementation of that method for the the concrete type Then why are trait functions that don't take self not object safe? Couldn't the compiler just generate an fnmute self that doesn't use self? We'll talk about that in a second Uh, does it construct a new v-table every time we create an instance? No, the v-table is generally statically constructed for the type Um, it doesn't get constructed, um dynamically Our identical v-tables detected. No, there's no deduplication of these And in fact, they are guaranteed to not be duplicates if you implement a trait for two different types Then the implementations even if they contain the same Code are still distinct locations in the source code and in the resulting binary and so would have different addresses Uh, trait objects of more than one trait we'll get to in a second um So box din is a thin pointer the points to a wide pointer the points to the object. No, um a if you have a box din high Uh, that box itself is a wide pointer. So box is a little bit of a special type Or actually it's not that special. It's just the box internally contains a mute din high um, and the rust compiler knows For references and pointers that if they are trait objects, they're actually with two Um, so box isn't special, but pointer types are Uh, so so box din high is itself wide You don't end up with an extra indirection Um Yeah, we'll talk about nightly apis for trait objects, too All right So I think we're we're now at a point where that roughly makes sense Um, so there are a couple of reasons why uh, trait objects are a little bit more constrained than being fully generic um So for example Uh, let's imagine that you wanted to have a, um Pub fn baz um That took an s that is din din din um Let's say high plus asref Stir Right, and we're gonna do like s dot high um And then we're also going to say let s is s dot asref and then we're going to do s dot high again because we know that Uh, strings themselves implement high I guess maybe that's confusing. Maybe I should just do length Uh, the compiler here is uh, oh it's ambiguous. All right, let's get rid of the ambiguity. It's not really relevant to the discussion Uh, it still won't let me do it. Why not? Uh, do I have to do this maybe for it to not be ambiguous? No, all right. I'm just gonna let's pretend that the syntax worked for a second here So basically I want to say that I want a trait object of two different traits Um, this won't actually compile and ignoring the parsing error for a second There's actually a bigger reason why this won't work, which is Um We need in order for this to work. We need to know both where the high method is and where the asref method is But those are contained in two different v tables One is the v table for high For the concrete type and one is the v table for asref for the concrete type Um, and so what we'd actually need is not just a two wide pointer, but a three wide pointer A pointer to the data a pointer to the the v table for high and a pointer to the v table for the for asref And that's possible. There's nothing that's sort of inherently impossible by that But it would mean that as you add more traits the size of this reference just keeps on growing And that's probably not what you intended And you might go well can't the compiler just generate a v table for the combination and pass that in Um, that is something that in theory it could do the rust compiler doesn't currently do this I don't know of a proposal to do it Um part of the reason is because you can get away with you can get around this by saying, uh pub trait high asref And it requires that the type is both high and asref Stir with no methods of its own and now I can do this Right because now there's only one v table and that v table is high asref And the compiler knows when it has to generate the v table for this trait That includes all the methods from both of these traits And so this v table is going to be larger because it is going to have both the high method and the asref method And so I think part of the reason this hasn't landed in rust itself is because you can you can opt into it yourself And by not so not automatically turning the plus into a larger v table um, you basically tell the You tell the developer that This might not have been what you meant, but if it is you can do this thing instead Um, I agree the compiler error here could probably have been better, right? someone asked about kubernetes That uh that maybe we could sort of inform people that they can generate their own trait here I think it's someone wrote I think it's high plus asref Stir, let's see if that's true yeah Okay, so so that is the the way to get the right compiler error Which is only auto traits can be used as additional traits in a trait object Whereas here you have an example of um two different traits that are neither of them are auto traits We'll talk about that in a second and here it does actually give the the suggestion We talked about right consider creating a new trait with all of these as super traits and using that trait here instead And it also points out that auto traits like send and sync are fine um And basically the idea here is that for marker traits like if I want to say high plus send Send doesn't have any methods. So it's fine for the v table to be empty And therefore we don't need to store that additional part of the v table because there aren't any methods in it So that's why it's okay to have additional traits with no methods um, I don't think you can use your own like if I did a Fuchs trait and I said this plus fuchs I don't think that'll work But there's been some proposal of having like a marker annotation to declare a trait as never having methods But that's not something we currently have Um, so so at least for the time being this is the way that you should do it um Great all right, um So that is is how this works with uh combinations of traits But that's not the only example where trait objects are a little bit limited So the other one is let's imagine that for high we added a um Actually, let's start with saying that we added an associated type Um, let's call it name And we don't actually use it for anything. We just add one Um, the code already doesn't compile Why doesn't it compile? Well The value of the associated. Oh, that's the implementation. Let me do that. So this is going to be type name Uh equals unit It's going to use that for both of them because we don't actually care about the type Uh Yeah, so here it says the value of the associated type name from the trite high must be specified So it says here where we take a din high, we actually need to specify the associated type We can't just say we take any high regardless of what his associated type is And the reason here is because that information can't be captured in the v table Because this is a type. It's not it doesn't have an address in the binary It doesn't have anything we could stick in the v table um, and therefore This won't work with a trait object directly Instead what you would have to say is I take a high where name is this And I think we need to do the same down here Uh, and then that works because now we have a v table specifically for anything that implements high Where the associated type is named? So that's fine. Um, let's undo this because it's not a very interesting change. Um The other perhaps more interesting case is let's say that we want a um What do we call I'm going to call it we're going to call it like Uh, weird Uh, we're going to have a weird function that does nothing No one needs to implement it Um, and you see that this also does not compile If everyone cargo check Uh, this let's make this a little bit smaller So that hopefully it's a little easier to understand Hopefully it's still readable. Um Oh, that's very verbose. Let me comment out the combination trait bit just to make the Um Errors a little nicer All right, so this says uh, the trait high cannot be made into an object So when we say din high, um, the basically it's telling us that the high trait is not object safe That is it cannot be turned into a trait object And it says for a trait to be object safe It needs to allow building a v table to allow the call to be resolvable dynamically So this is what we've been talking around through so far And in particular here it says the trait cannot be made into an object because the associated function weird has no self parameter It doesn't really talk about why that's necessary though But let's try to work through it. So again, um, let's say that down here in our say hi Right, um We now try to call s dot, uh weird So imagine what what would the compiler do if we wrote this code? Well We can't really write this code, right? Because weird doesn't take self. It doesn't take a pointer to anything And we can't just like randomly construct a pointer to it because that would mean that we need to have a valid instance to it To have a pointer to it. So This is really just saying like Then hi like colon colon weird Which I don't have to tell you seems weird because which type are we calling weird on here There there is no like nothing here specifies Which actual weird implementation we want in this particular case because it's an over because it provides a default Um, you might think it's not a problem Well, what if there was like an implementation of weird for string that was different from the one on the trait which is allowed Right, you can override a default implementation Well, how would this know whether to call the one for stir or the one default for the trait or some entirely different one It can't be in the v table because there is no s here, right the weird method Doesn't take itself. It's not associated with any given instance of any type So this just doesn't work That it's not a meaningful thing to say Um, but what if you really wanted this like you wanted the weird function to exist But you're like I don't care about calling this through a trait object. I only care about the high thing through this trait object So I want high to be traits trait objects safe. Um, but If you have a trait object to high, I'm fine if you can't call it weird So you can do this Uh This um And let me go back down here have this be din high again s dot a so What we're saying here is that the weird function Requires that self that is the type that implements the trait is sized And we remember that traits aren't sized if you just have like din high That's not sized. It's only sized if it's behind Uh, a pointer type So this is basically a way to opt out of the v table is the way that you can think about it It's saying this function shouldn't be placed in the v table and crucially shouldn't be callable through a trait object What this means is um In here, let me get rid of the static versions because they're just in the way Uh, what this means is now the code compile is just fine. I can call high I can make a trait object for it, but if in here, uh, in fact I can go even further and say let's say that this did have a self, but I wanted to opt out Then this still compiles. I can still make a trait object for it. But let's say that I try to call weird in here The compiler is going to tell me The weird method cannot be invoked on a trait object because the method has a sized requirement And so this is the way to opt out for any given function From having it be included in order to mean score that the the trait remains, um object safe You can also opt you can also say that the Entire trait should not be possible to turn into a trait object by saying where self is sized So if you do this you're basically Disallowing using this trait as a trait object. It's rare that you see this in practice um, usually, uh The trait will Like the trait just isn't object safe because there's some methods in it Opting into disallowing trait objects is is rare. Um, sometimes people do it for Um backwards compatibility reasons like if you know that you might add non object safe methods later Um, you might add this proactively Um, but but it's pretty rare All right, do the restrictions so far make sense Does the associated type problem also occur with static dispatch? No, it doesn't And the reason is with static dispatch Because it gets more demorphized. You actually know the concrete type for any given function implementation Um S colon colon weird could be possible Um, so this is back when we had the f n weird Right, and let's say we also had an override here Um, and now let's say you wanted to be able to like let's say we added like special syntax like this saying For the type of s called the weird method um You could imagine that weird got included in the v table for um For the trait object, but it like knows to not call it with a self argument The the challenge here, right is that this is sort of odd Right, like if you require an instance of the type anyway in order to call it then Why doesn't it just take self right the the traditional example of this right is that weird is actually called new Or something like new Right where It doesn't if you already have an instance of the type you wouldn't need to call the method in the first place It would just take self Um, so I agree like something like this maybe could be possible I think in practice is just not usually very useful And that's why it it hasn't been a priority There there might be a deeper reason why it's not added to i'm not sure Um This where self colon sized also disallow implementing the trait for concrete dsts um You know, that's a good question. So so the question is basically if you have where self is sized Can I implement high for box din asrefster? For example, uh, I believe I can The reason I say this should be possible is because uh Yeah, this this now won't work because it can't be a trait object, but you can still have this implementation I believe Why does this All right, because it's a box So we need to have the double asref This is fine because this is not a dynamically sized type Right again, like when you wrap it behind a pointer, it now has a size is not dynamically sized So it doesn't prevent that even if you have uh self sized on the trait The associated type restriction feels weird because the point of associated types is that only one exists for any given concrete type So shouldn't the v table know what the associated type should be? Like it can't have a pointer to the type but the implementation of the trait should know what the type is when it's compiled Yeah, so the problem is that the type is erased all that remains is the v table And so you can't tell from the v table what the concrete type used to be There there is an exception to this which is the any trait Uh, so the any trait has a method that returns a descriptor of the type of the concrete type that it used to be Um, if you didn't follow that that's fine ignore it But basically there are some ways to sort of finagle around this but basically Trait objects are type erased like you don't get to keep information about the The type it used to be Oh, you meant types such as stir or u8 Uh, no, you can't do that. Uh self has to be sized when you have where self is sized um There's one other restriction uh that you get with trait objects, which is that the methods cannot be generic And for this rather than sort of keep working on our weird high trait I'm actually going to go to the standard library and look at Uh, the from iterator trait. So the from iterator trait Um is something that's implemented by vek for example. This is how collect works Is that vek implements from iterator? and when you have Well So if we look at iterator collect, I guess is the best way to do it collect on an iterator requires that the thing that you collect into implements from iterator So that's why you can have an iterator and collect into a vek is because vek implements from iterator The from iterator trait is a little weird though. Um, it a the the a type of the trait is the type of the items of the iterator But you also see that the from iterator type itself Takes a t and the t here is the type of the iterator, right because it has to be vek for example It doesn't care what the iterator is It can be it can you can construct a vek from any type of iterator as long as the I well you can create a vek t from any iterator where the item is t um This arguably should be called I for iter But um, it's not terribly important here a is the the sort of thing that you'll end up with a vek of So if this a was say bool, you would end up with a vek of bools And the t is the type of the iterator, which might be something like, um, I don't know like a hash map into it or whatever Whatever that type ends up being Um, this poses a problem for a trait object though, right? Let's imagine that we try to write code against this So I'm gonna Erase this for a second In fact, I'm going to erase all of the remainder of this file. Uh, and I'm going to have like, um What are we going to do here? Let's do collect standard iter from iterator We're going to take a from iterator of bool Uh, and we're going to return a vek of bool Um, and I'm going to do s.collect Oh, what did I do? I did something weird, didn't I? Great, um So this seems like it should be fine, right? I take anything that can I take Well, actually this is weird for a number of reasons. Maybe from iterator is a bad example here because it doesn't take self either Um, so it actually requires self to be sized anyway Uh Extend is a better one Uh, let me dig up extend here Uh, great So we're going to use extend instead Uh, and this is going to have to take a mute then Um, and so we're going to do that mute v is vek new Then we're going to do v dot extend Uh Actually, we're not even going to do that. We're going to take This is going to be the v we're going to do v dot extend And we're going to give it just Itter once true. So we're going to just extend it with a Extended with a single bool add true Right, so we take in anything that can be extended with an iterator of bools Um, and then we try to extend it with an iterator that yields a single bool So we're just going to add true. We're going to append true to whatever thing we get And here too, it tells us the trade extend cannot be made into an object Because extend is is not object safe And if we look at extend that doesn't immediately make sense from what we've seen so far, right? So again, it's generic over the Type of the items of the iterator. That's fine Extend takes self So we do have something we can stick into the v table and given an instance we we sort of know how to call this method But again, notice that it's generic over the type of the iterator And this is where it gets really weird um You can imagine the like inside of veck, right? That's going to be uh, import t Extend t for veck t and it's going to have this implementation I'm going to go go ahead and call this i because I think it's more helpful Um, and who knows what's going to be in here, right? Yeah Just to get rid of that warning Uh, I'm going to do this Um All right, I guess be a little bit more helpful of my veck Um, like this implementation is going to exist somewhere in the standard library, right except just for veck instead but This method is generic So we know from the monomorphization discussion that actually at compile time we don't end up with a single extend We end up with multiple copies of this method Right, we end up with like an extend bool that takes, um non-extend I guess hash map into iter which takes a standard collection Hash map into iterator Right And in fact even more than that we're actually going to end up with because the whole impulse is generic We're actually going to end have this That is the actual method that gets implemented Or get sort of generated by the compiler assuming that someone tries to extend using a hash map iterator of bools There is no one extend There is one for every combination of iterator and item types So what gets put in the v table? We've nailed down that we want to extend bool so that's good, right? So so there's only we don't have to consider that the t up here because we've already chosen that t But for extend itself, we really in the v table Really needs to have a pointer to extending for this iterator type over bool But that's not that's not expressed in the type of the function And we don't even have a way to express it, right? You could just sort of imagine like where like These implementation of extend is standard iter Once bool or something super weird like that But the way that it's written nothing here says that we want the v table to point to The extend implementation for a standard iter once And therein lies the problem when the compiler tries to generate this code There is no pointer to the appropriate implementation of extend Therefore the the v table can't be generated you can't generate a v table for Didn't extend because it would have an infinite number of entries one for each possible implementation of extend And so therefore the the answer is just didn't extend cannot exist You cannot create a trait object for extend All right. Does that make sense? That's a somewhat intricate explanation, but but hopefully it's all sort of ties together here Um Could rossi add a monomorphized version of extend extend for each t it's called with to each type that implements extend That's tempting, but it's not always possible right to imagine that you're compiling this code but um I mean even just for the standard library right like The standard library has a bunch of uses of extend But in crates that depend on that crate They might call extend with even more types So the size of the v table Like when you compile the the bottom most crate if you will like the The standard library essentially the that would mean that the v tables for didn't extend in the standard library It's different than the v table for didn't extend In other crates further up because there are more Possible iterator implementations that you might want to use As you now to end up with lots of different v table implementations or lots of different v tables for didn't extend And that's like it would be a combinatorial explosion kind of problem And it would also mean that you can't pass a didn't extend from the standard library to A thing that takes didn't extend in the crate further up because their v table types are different All right, uh, so this is the reason why Why you can't have Like the basically in order for a trait to be object safe. Um, it needs to Not have generic methods It needs to all of its methods need to have a receiver That includes self And there's another requirement, which is that It the trait can't have a type that returns self Um, so if we backtrack a little here and say Clone and we want to take a thing that is didn't clone And we're just going to call clone because it's the only thing we can do with the didn't clone Right this if you have a didn't clone You can literally only do the things that the trait lets you do and the only thing clone lets you do is clone Um, so This says the trait clone cannot be made into an object This is now an error that we've seen a decent amount The reason why you can't do this is because the clone trait right has a fn clone That takes self and returns self All right, so let's say I say let x is v dot clone. What's the size of x? didn't clone Is just any type that is cloneable And we're supposed it's reported to return self like not the reference So it's not supposed to return a reference didn't clone. It's returned supposed to return self given a reference So that sort of means that it should return didn't clone Because that's what self is here because we don't we've erased the concrete type But didn't clone isn't sized So it would mean that the return value of this method isn't sized And the return type of a function must be sized for us to generate code for it Therefore this can't work Um, so you can't have methods that return self Uh for a trait to be object safe And that's why you can't do clone Now there are some cases where you have a you have a trait where As we talked about with high right you you might have some methods that are object safe and are still useful on their own but you have a bunch of other methods that Are would make the trait non object safe But you want to include them for types that aren't going through a trait object because just because it's convenient And in fact a good example of this is if we look at the iterator trait So the iterator trait right has the associated type item. That's fine. It has next And next is object safe, but it also has a bunch of methods like chain, which is generic it has Enumerate which returns self it has By ref which returns mute self It has collect which depends on from iterator, which we already know isn't isn't object safe So how can iterator be object safe because it is right if we do If I say it's here and I say I want to take a DIN iterator where item is bool Um the collect Oops iterator Well, I can't call collect because this method cannot be invoked on a trait object But I can call next I'm allowed to call next on this Uh mute otherwise it doesn't work That works fine And the way that the iterator trait has achieved this is that if you look real carefully You'll see that If I actually go to the source of the iterator trait you should see this Um, let me just find here. So count for example, which consumes self, right? So it's not allowed to go behind a reference which would make it sized Has where self is sized if you scroll down to last which is the same self is sized if we look to Chain You see the chain which is generic. So normally would make the trait non-object safe has where self is sized And the reason for this is you can basically this is the same thing we did for the weird function, right? You can use the where self is sized bound to say Ignore this function for the purposes of using this trait as a trait object Basically, don't try to put it into the vtable What that means is if someone has a trait object, they can't call the method But if they don't have a trait object if they actually have like a a veck into iter or like they have the concrete type Then they can call this method Um And so this is a way to have a trait that has some methods that are Nice to have but wouldn't be object safe without making the whole trait object not object safe Um, can the receiver be anything that includes self or does it have to be ref self or mute self? Um, so the rules for this are actually Where do I have this? I have it open somewhere Give me a second here So this is from the rust language reference On object safety and in fact by now we've covered most of these things All the super traits must also be object safe, right? Because we construct a vtable from the union of all the all the super traits Sized would not be a super trait. So that is if we require a self self colonized It's not object safe must not have any associated constants. This again is because You don't have anywhere to put the constants theory. Maybe you could put them in the vtable But now the vtable becomes really really large And also the values in the vtables are no longer all like function pointers They might be arbitrarily sized things All associated functions must either be dispatchable from a trait object or be explicitly non dispatchable So dispatchable functions require that they don't have type parameters. So they're not generic Um, they have to be a method that does not use the self type, right? The concrete type except from the type of the receiver So this is we can't have something that returns self for example It can't have a receiver with anything but the following types, right? So it can be a reference to self it can be mutable reference to self it can be a box self rc self arc self or pin self Pin to self rather so anything that's behind a pointer Basically anything that can take That anything that can turn something that's not sized into something that is sized And you can see that explicitly excludes Where self is sized bounds So that you can have a an object safe trait even if some of the methods aren't object safe Uh, this is the non dispatchable functions Um Should library writers always consider adding where self is sized to non object safe methods just in case someone downstream wants to use it as a trait object I think yes, um It depends a little bit on whether your trait is even usable So if your trait isn't usable as a trait object like think of clone, right? Then sure you could add where self is sized to the clone method So that people could create a trait object clone But they wouldn't even even be able to call the main function of that trait So it's probably not worthwhile But I do think that in general if you have a trait where it is useful Even if you could only call the object safe methods, um, then it might make sense to to opt out for the other one So that the trait overall is object safe Um That iterator last have the size restriction Uh iterator last I think does Um The reason why last Has where self is sized you can't see it here. It gets hidden by rust doc Um is because its receiver doesn't go behind a reference It consumes self Which means that it takes self Um and self is a din iterator Which is not sized and function arguments must be sized therefore last can't be called through a trait object Um There's one more thing I haven't told you well There are many things I haven't told you but there's at least one when it comes to trait objects Which is that there's a little bit of a secret with trait objects Um, so let us say that I have pub trait actually Just do this This is a drop is actually object safe Um, you might think that it's weird like if all you can do with an object is drop it then Is it really that interesting? It turns out this is useful in a couple of situations like, um Uh, I know crossbeam does this for example where you want to do garbage collection But you don't want to drop objects immediately You want to like stick them in like a linked list or something and then periodically go and collect all the garbage Because you want to store lots of potentially different things in one type You need trait objects and so it just stores Mutant drops or I think technically boxed in drops, but but same effect. You can't actually do anything with this um, but here when v When when v goes out of scope Drop drop is called through the v table This should make you go wait a second because Uh, let's go with our say hi Right, we take the din asref stir um Or actually let's even go with box What happens when s goes out of scope Think about this for a second We get a box so we have a memory allocation on the heap We might use that in here might call s dot asref this can be high whatever But then we drop s when this function returns But when we drop a box, we have to free the memory But this is a trait object. So the only method it has is asref The answer to this is every v table includes drop You can sort of think of this as like an implicit drop here But in practice the v table for any trait object includes a pointer to the drop function For the concrete type because it's just it's just necessary It also technically includes a little bit of extra information includes the size and alignment of the concrete type The reason those are in there is because for something like a box where you have to deallocate the memory That information is necessary to pass to the allocator to do the deallocation Um, and so every trait object includes the v or the v table for every trait object includes the methods for the trait Plus drop plus size plus alignment Normally, you don't have to think about this, but it's just worth knowing while we're on the topic um To shift gears a little bit. Um, so far we've only really talked about trait objects But there are other types that aren't sized Uh, so for example, like we know that din trait is not sized But u8 is also not sized and stir is not sized This one gets turned when when you place it behind a reference or a pointer Right gets turned into a tuple of like mute a pointer to the data And a pointer to the v table This one if you make it sized or put it beyond a pointer also becomes a wide pointer Where one is a pointer to the data, but the other is just a u size Which is the length of the slice And same thing for stir as a pointer to the data and a u size of the length So this is why if you try to write a function foo That takes a u8 The compiler is going to yell at you and say that um the size for the value of type u8 is not known Because u8 this is just saying an arbitrarily long list of u8s Which is not sized therefore we can't call the function with it as the argument same as if you try to return just a straight up u8 sequence right if I had a bar It's returned a u8 It also complains Is it even going to let me do that? There we go. I I constructed the type So here it says the same thing the size for the value of type u8 cannot be known And the return type of a function must have a statically known size This is the same problem And here too the way that we go from something that isn't sized to something that is sized is we place it behind A type that can sort of mask the unslicedness Which you do with a with a reference or you can do it with box So you can also do box of this I think i'm allowed to do this Great So this is fine because it's now sized it the size of it is A pointer that is wide where one part is the data and the other size is the length And the compiler then knows That when you access this type it knows to sort of deconstruct the pointer to get the length Same with something like box Or or raw pointers for that matter and same for string Uh, so so these are a little bit magical. Um, there's been some work on trying to standardize this So currently the sort of dynamically sized types are A little bit special in the compiler because you need to know whether the pointer is sort of wide or not So it's fairly hard to Sort of deal with unsized types yourself Like if you wanted to implement box yourself and take a type that wasn't sized It's possible. It's it gets pretty annoying once you get into Casting and allowing trait objects and stuff But there was recently an rfc that landed RFC number 2580 Which talks about adding a generic api for manipulating the metadata of fat pointers So this lets you do things like say what the type of the The second part of the fat pointer is right so in the case of um, in fact, we can scroll down here a little and look at A din trait now becomes a pointy where the metadata is din metadata um, and where din metadata is um A pointer to a vtable that represents all the information such as type size type alignment a pointer to drop in place And a pointer to all the methods for the type simplification of the trait. So exactly what we just talked about um similarly You could imagine that the um, this pointy trait would be implemented for something like a u8 Um a u8 slice without the reference where the metadata would be a u size that is the length of the thing Um, this is still very much like new and experimental. I do recommend you read through this rfc It's really fascinating if you want to learn more about this. It has things like the trait l is thin, which is any Type that implements pointy where the metadata is empty Which would be anything that doesn't have Associated metadata in the pointer. So anything that is a thin pointer and not a fat pointer um And it has essentially methods for Introspecting the metadata of a pointer so that you can do things like actually look at the vtable There were some questions about this in chat earlier. Um, this rfc would actually let you uh, introspect that information and crucially construct vtables on the fly so You could have one that isn't known at compile time, but you build at runtime And in fact this already is something that um Exists one place in the standard library as far as I know, which is for wakers instead of the land of async Um, so the waker trait we're not going to go too much into detail But the waker trait which I'll show you here The waker That's I'm lying. I mean the wake trait uh awake Not the wake trait the raw wakers Oh, there isn't a trait for it. That's why Let's go show you context actually Um, so context Gives you a waker Waker is a struct that has the methods wake wake by ref will waken from raw and you can drop it and clone it Um, but in practice waker is really generic It's just that it isn't generic when you look at it But inside of a waker is a raw waker Uh, and a raw waker If you look inside of it Is a data pointer and a vtable pointer So it really is dynamic dispatch, but in sort of a hidden way Um, and the vtable is a raw waker vtable and a raw waker vtable You construct by giving the function pointers for the clone method for the wake method for the wake by ref and for drop And so it's basically a manually constructed vtable That gives you dynamic dispatch through a type rather than a trait Um, I'm not going to go into too much detail of this But I just wanted to show it as like an example of a manually constructed vtable, um in the standard library Um Is there anything more I wanted to talk about from here? I don't think so All right, um, let's do questions because I've rented for a little while um I thought a reference to u8 had a start pointer and an end pointer not a length I think it has the length It doesn't the two are basically equivalent You might be right I forget. In fact, I wonder whether this reference says Yes, you see this rfc also deprecates the trait object Um Stuff that exists at nightly because this is a replacement. Um Pointy trait thin metadata From raw parts Where is the definition? Uh, oh it does actually give Yeah, I was hoping it would say here what the actual associated um metadata type is for um Slices, but I don't see it. I think it's just a length um Can we make our own types dynamically size types? Um sort of so You can write this Um Foo here is now not sized In fact, you can add more fields Um, you can't add a field after though After this is not sized right So if I try to do this, it'll tell me um the size for values of u8 cannot be known at compile time Uh, only the last field of a struct may have a dynamically sized type Change the fields type to have a statically known size. Um Barrow types always have a statically known size. So what this is getting at is If it's the last field Then think back to the the argument I made very early on right if you have a non-size type in the middle here and I asked you Like if I wanted to look up like foo dot x And you only have a pointer to foo. How do you get a pointer to x? When the type in the middle can have an arbitrary size and the answer is you can't there's no You don't know how to get the address of x because t is arbitrarily sized But if it's at the end, then it's fine You can statically know the offset of f the offset of x and the offset of t And the only information that's needed in order to make this size would be the length of the last field As you are allowed to construct a type like this This type is now not sized But if you have an at foo then that type will be sized by storing the pointer to the foo Alongside with the length of t So you can create your own dynamically sized types this way Is box u8 the same thing as a vec u8? So this is if you have a box Of a u8 slice not behind a reference Is vec u8? They're not the same So no I guess Let's do this They're not the same They feel the same, but they're not the same oops A vec u8 can grow So a vec u8 first of all has It's three three words. It's the pointer to the vector on the heap It's the length of the vector and it's the capacity of the vector And this is so that if the If you try to push beyond the capacity or below the capacity The length just gets incremented if you try to push beyond the capacity Then the entire vector reallocates itself copies over and then changes you with a box u8 You can't do that this This will never grow or shrink. You can't push to it. It's just think of it as a it really is just a slice You can turn a vec u8 into a box u8 Um, and you can also go the other way around But they're not they're not the same no Um What's the difference between a din fn And an fn So basically I can write a fn food that takes a din fn And I can write an fn bar that takes an fn What's the difference between these? These are not the same this Uh, is a this has to be a function It can't be a closure and the reason for this is fn is really a function pointer Like it's just a single pointer that has to be an address This is a v table. It's a trait object Which means that it both has a sort of the v table you can think of here has a pointer to Calling the function, but it also has a data pointer, right because the the v the Structure of this is that it's a wide pointer where one part is the v table Which has the the pointer to call actually calling the function But the other part is a data pointer And crucially the difference here is that um, if I have fn main I can call foo with a closure Um, so I can do This But I can't do the same with bar Why is it complaining? Oh Right, so so I can call foo with a closure because the Um, this captures x right so so the it's not just a function pointer It's a function pointer and the the Data that the closure captures from its environment That is it needs to when you call the closure You also need to supply the address of x because it's needed by the body of the closure And that's what's passed in the data part of the the wide pointer For bar it requires an actual just function pointer Which means that you can't pass a closure because you wouldn't have anywhere to stick the data When we use din fn over impole fn Yeah, so so there's also Baz Impole fn And I can call baz with a closure What is it? What's it complaining about? I can call baz with a closure Um, because remember impole fn is sort of sugar for a generic function over anything that is fn So we actually get a concrete copy of baz for every type of closure that's passed in And therefore you can trivially pass in the the data as well because it's monomorphized to each individual closure Um, the the question then becomes when do you use a din fn when you use an impole fn? Uh, the the basic answer is Impole fn is is More generally usable Like you don't have to indirect behind a pointer for example But you end up generating a copy of baz for each closure type you pass in Which which might get become quite a lot And the other reason is because Sometimes you want to take a trait object instead of making a generic Because um, otherwise you have to propagate the generic type up. So imagine that I had a struct wrapper Then internally I wanted to contain Um a function pointer if I have it hold Like I could write this Right I could do this but this means that anyone who wants to any user of my library that wants to hold a wrapper Would also themselves need to be generic over f or name the type of f Whereas if this instead stored a box din fn The now wrapper is no longer generic. So my callers don't have to think about and propagate that generic parameter Uh, so sometimes it it cleans up your interface and makes it um, just nicer to use There's also the example of trait, uh, making traits object safe Uh, so if I have a trait Uh x, um, that has an fn foo If I made this take an impole fn Then x is not object safe I couldn't write a quacks um I guess let's have this take self Um Who? um Trait the trait x is not object safe, uh, because this is generic right impole fn is equivalent to saying F is fn And this is f Right and this cannot be made um object safe like we talked about but on the other hand if I made this din fn It can be object safe because There is only one foo Right, so we don't have this problem if we need an infinite number of entries in the v table There's only one foo and it itself takes a wide pointer Which then works for any type that can be uh object safe. So there's not a clear cut answer here Nice Okay, I think I think, uh, that captures Everything I wanted to talk about So the one thing that we didn't get around to talking about is, um Coherence, um I think coherence is different enough that we're not going to talk about it this stream and I might just do a separate stream on it Especially because we're on like the two-hour mark. Um, so I think this is a good time to stop Stop And then just do this separately Um, are there any sort of questions towards the end about all the stuff we've talked about so far? Is the the general difference between static dispatch and dynamic dispatch um The size trait v tables object safety wide and fat pointers, um Does all of that roughly make sense now or is there anything more I can go through to sort of help Crystallize it in your mind and make it clear why we have these what they're for what their limitations are Um, I'm happy to take some questions towards the end here Um, could runtime trait detection be implemented in the future using a types of v table? Um If I understand your question correctly, I don't think it can so so the idea is that can you Can you take a trait object or can you take like? a Wide pointer and figure out which traits it implements Um, just by looking at its v table Um, I don't think you can part of the reason is because They're like a different v table gets generated for each trait object. So it's not like There's one v table for star or for string, right? There are many v tables for string There's one v table for string for each din trait right um, and so there's not just one One v table that you could then look at to figure out what what traits it implements um, like if you had like a Find traits that took an s that was a din like didn't trait um Then like the only things in the v table would be Whatever methods are on the trait that you name here. You wouldn't get a v table of all possible methods on the provided type So no, I don't think you can do dynamic Dynamic detection of which traits something implements this way um Does calling a din fn involve a double d reference once for the v table pointer once for the actual function pointer within Or does that get optimized away? um, so if I do call takes an f of fn Uh, and in here I do f um You know, that's a good question um I think it'll end up being a double d reference because You first have to go through It's not quite a double d reference because This is a wide pointer, right? It's not it's not a pointer to A tuple of a pointer and a v table, right? It it's really what gets passed in is a Pointer to some t. You just don't know what the t is and a pointer to T's fn v table Right, that's that's really what's passed in here. Um, and so this ends up calling like The sort of ends up calling like v table dot call of t um and so There is a dereference here Like this is a pointer to the v table. So you do have to dereference the v table um I guess I guess this is a better way to sort of type it out So you do have to dereference the v table to get to the call um It's really I guess this right You have to dereference the v table to Find the address of the call that you then call so it does end up being two dereferences, but it's not quite pointer chasing either Um It is true Okay, so so there is one thing that that's a little bit funky, right? Which is if you if you have an x that takes an s of let's say din asref stir Um, then one thing you could do and this was mentioned in chat like with the new pointer v table dynamic metadata rfc you could imagine like s like if s dot v table is equal to like string as din asref stir Dot v table then like s as referenced s dot data as references string Dot I guess if this was a mute uh Dot push right like maybe you could do some really weird ugly magic like this I would recommend just don't do it. Um But yeah, you you could imagine you could compare v tables this way I don't know if this is even a guarantee that the compiler will uphold that a type will always have uh, only exactly one v table Um And yeah, the the compiler does do a decent amount of optimization here too Like if you do a box din fn I think it like might not do a heap allocation always if you like if just like escape analysis and stuff But in general if you use generics like if you use an actual generic type parameter or an impulse block Sorry an impulse trade In those cases you will get better optimization just because the code get the compiler gets full insight at compile time into what all the relevant types are and can co-optimize based on The actual concrete implementations whereas once you have Indirection through dynamic dispatch the compiler loses some of the the information it would otherwise have and be able to optimize based on Um, can you do a quick example of the slice veck of dins? Oh, yeah uh Say highs So if you actually wanted to do like I guess we had din high, but let's make a din asref Stir And you wanted like 4 h in s Let's make it v For smv s.asref So to actually get this to work, what you end up with is you have to do this To make this sized Right So that all of the elements of the array are the same size Because the array is required of that so you can find the given index, right? And once you do that this all just works And you could put a box or arc or whatever for the inner type too Um Different compilation units can lead to different v tables. Uh, that sounds about right. So Uh, this is a little bit anecdotal But when you compile rust code rust might compile one crate using multiple independent threads to compile different subsets of the crate To sort of speed up compilation, but because you want those separate units to work in Like entirely Concurrently without too much synchronization They might both encounter say String being used as a din asref They might both generate their own v tables because they don't want to coordinate about generating the v tables And therefore you have multiple v tables even for the same type for the same trait So that's one example why that invariant might not hold That's a good point Um And yeah, there is also any um, which I haven't talked about I'm not gonna actually demo it but um So there's the any trait And any is super magical It's not actually like any is just a trait that has a function that returns an identifier for a type That is unique guaranteed by the compiler And so if you have something that's a trait object over any You can use type id on it because it gets added to the v table to get a unique type identifier for that value And then you can use that to um Downcast from a din any to uh The concrete type because we know what the type identifier is I'm not going to go into any too much But but any Uses a lot of the stuff that we've talked about so far to actually be able to go from a din Um a din trait as long as that trait includes any Into a reference to the actual underlying type It's really cool and actually surprisingly simple now that you know what you know So I recommend you uh, give a read to the standard any module documentation. It talks about why this is safe Um All right, I think we're gonna end it there two hours and 15. I'm pretty happy with that. We covered a lot Um, I hope that was useful. I hope you feel like you learned something and some of this might actually stick um I have made a little bit of progress on my Coding livestream that I really want to happen. Uh, I'm hoping it'll it'll happen sooner rather than later But it's it does not escape me. I'm still on the case um, thanks all for coming out and uh I will see you next time so long for well