The Unsafe Chronicles: Exhibit A: Aliasing Boxes

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments

These streams are really good and are making me learn a lot more about rust, and other programming languages, that I use, in general. They also help understand how other, more accomplished, people approach writing software. That knowledge is invaluable. Thank you!

👍︎︎ 6 👤︎︎ u/div_curl_maxwell 📅︎︎ Dec 13 2020 🗫︎ replies

Love your videos! Thank you!

👍︎︎ 10 👤︎︎ u/drown_in_dry_tears 📅︎︎ Dec 13 2020 🗫︎ replies

Nice video!

&mut *((&mut right_hashmap) as *mut _ as *mut _)

When we said "don't use mem::transmute, it's dangerous", this isn't what we meant ;)

👍︎︎ 3 👤︎︎ u/cramert 📅︎︎ Dec 15 2020 🗫︎ replies

Thanks, this was insightful and TIL about left_right!

👍︎︎ 2 👤︎︎ u/dozniak 📅︎︎ Dec 13 2020 🗫︎ replies

The emergent behavior of MaybeUninit here is incredibly interesting. Since the pointer it gives you is valid for reading the data but doesn't necessarily guarantee at any point that it will be exactly an instance, it allows "partially" initialized data, like a Box which has its data initialized but isn't Unique

👍︎︎ 2 👤︎︎ u/ineffective_topos 📅︎︎ Dec 13 2020 🗫︎ replies
Captions
hello everyone uh welcome back to i guess or or welcome for the first time i suppose to the first episode of uh the unsafe chronicles uh i feel like maybe i should be wearing some like scary outfit or something but um but the reality is that um unsafe code has this this reputation in the rust world of being like scary and hard and somewhat unknowable and like to an extent like that that's a that's an earned reputation right like the number of ways you can shoot yourself in the foot with unsafe there are a lot of them and sometimes it is really hard to get right but but i think being scared of it is not the right approach the right approach is to sort of look at what can go wrong and try to learn for it from it so that you don't make the same mistakes in the future um to me unsafe code has always sort of been like uh if you're in unsafe world you're just writing c so if most of your code is not unsafe you're still better off than what you would have been otherwise regardless though this this series is focusing on a subset of that problem which is just as the name implies talking about stories with the unsafe like cases where um i guess starting off like cases where i run into oddities with unsafe where i didn't know that something was okay and then i learned and then i fixed the problem um and i'm hoping that just by sort of surfacing a lot of these stories about where where you might go wrong with unsafe or where there might be tricky um tricky scenarios to get right just talking about those cases and what the sort of underlying principles are might be helpful for other people who are trying to write unsafe code and do so safely um and in particular for this for this first first chronicle what we're going to be looking at is a crate called left right and left right is a crate that i built in the my previous stream which is sort of a longer live coding stream i'll link it in the video somewhere around here i guess and at its core it's a concurrency primitive that allows you to have really fast concurrent read access to some backing data structure and it does this with very little synchronization between the readers which means that it scales really well as you as you add more threads that end up reading from the data structure where left right gets a little complicated or one of the places it gets complicated is that it it does this by keeping two copies of the the backing data structure if it's a map for example you would have a left map and a write map and the data structure sort of keeps these in sync and then make sure that the readers only read from one and the writers only write to the other and this is how it ensures that the reads can be fast because they don't have to do any synchronization they can just read from the the read half if you will and left right sort of realizes that duplicating all of your data is probably not okay so it wants to pull this trick of aliasing where you have um think of it as instead of having all your values be stored in both the left and right data structure you have both of them store a sort of shallow copy if you will or an alias of the true value that's stored somewhere else so the the trivial example here right is if you have something that's on the heap instead of having one heap allocation for the left map and one or for the left copy and one heap allocation for the right copy you just have one heap allocation you have the left and the right both point to that same heap allocation um and sort of conceptually this works because left right only ever gives out uh read access to the values that are stored in the copies like the moment after you've inserted it you can no longer get a mutable reference to it anymore except when the value is actually dropped and so this should be fine like aliasing should be fine because you only have readers and it should be fine to have shared references to to a value like multiple shared references we know from the rust borrow checker is okay um and what we're gonna be looking at is uh is specifically that mechanism for aliasing because there there sort of necessarily has to be some unsafe code if you think of this as something like um if you have a box what you're now gonna be doing is you're gonna construct two boxes that point to the same underlying allocation which means you need to use the actually let me pull this up here box so the box trade has a from raw method uh or function i guess this is a freestanding function that takes a raw pointer and gives you a box t and similarly there's a two raw or into raw that takes a box t and gives you a mutable pointer to that t um and going from a box to a pointer is a safe operation because it gives you a raw pointer and dereferencing a raw pointer and rust is unsafe anyway but going from a raw pointer into a box is unsafe there are many reasons for this uh the primary one being that you need to guarantee that this pointer actually is the box that it's like a pointer to a heap allocated thing because box when you drop it is gonna free the underlying object right um so uh so far so good right there is a mechanism for for if you have one box to sort of construct a second box uh the points to the same underlying heap allocation and this was the trick that um that left right and the in its predecessor evmap um used for the longest time it would just create two boxes to the same heap allocation or if you had a vec you would create two vect elements that both pointed to the same underlying data structure on the heap and that seemed fine enough um and i i thought that this was entirely safe because i was remember the data structure guarantees that it only ever gives out shared references to the backing memory so what's the problem well it turns out that there are two problems uh and both of these actually came up in the previous stream when we were working through how to factor out the concurrency primitive in evmap and some of this is described in uh this pr that i started working on uh shortly after that stream um but i'm gonna try to summarize it here um with some actual code so let's do a cargo new lib uh aliasing okay so the basic problem we have is that we have a let's say struct aliased of tea it stores internally a box t and then we're going to be giving out is aliased t right we're going to have like uh uh we're gonna have i guess how we're gonna describe this like we're gonna have a here let me actually write out the code that's gonna help um let's imagine that we have a left which is an aliased where the reel is a box new let's just use a number for now i'll just this and then we're gonna construct an alias just by doing um real box from raw box into raw of left dot uh real this won't actually compile and i'll talk about that in a second too but sort of conceptually this is what's going on right we have a left and right they both contain a box but the box is the same underlying pointer and as long as we only ever use left dot real and oops and write dot real like if we only ever use those behind shared references then why is this not okay and the the first issue we're going to talk about here is about aliasing rules so box um i don't know let me pull up the documentation for box oh why is my scroll bar look weird that's weird um [Music] where is this so this might actually not be documented so the documentation box talks about a bunch of the sort of invariants about boxes right so it says that a boxy value will always be fully aligned a box will always be a non-null pointer and if we look at from raw which is the unsafe function we're using um it says that for safety this function is unsafe because improper use may lead to memory problems okay that's fine but it doesn't really say very much about what is and isn't okay it just sort of assumes that you know what the the correct thing is um it does say for this to be safe the memory must have been allocated in accordance with the memory layout used by box which is sort of trivially guaranteed for us because we know that we constructed this from a box in the first place but it turns out that in general in rust when you have a box t what this compiles into in like lvm like in the underlying code generation pipeline as it turns into or it uses an attribute called no alias or no aliased i forget whether i think it's no alias um and what this means is that rust is telling the underlying code generation tool that the pointer that the box contains is not aliased anywhere else in the code and normally that's true right if you have a box t um then you own that box d and you know that no one else has the actual box so if you're operating on this box you know that that pointer itself is not aliased it doesn't exist anywhere else in the system and you might wonder why does this matter it matters because lvm gets to optimize your code under that assumption the the silliest example of this might be um let's say that you let's let's say that we didn't have right we didn't do anything funky with this we just have like a a normal like box somewhere because the compiler knows that box is is not aliased it doesn't exist anywhere else and it sees that 5 fits inside a u size what's to stop the compiler from just not doing the heap allocation right just have five be stored in the the field where the pointer normally would have been stored i don't think lvm does this currently but if we know that there are no aliases of this pointer we know that no one else has the actual pointer then why do we have to have the actual pointer if the value fits in the pointer itself anytime we move the pointer somewhere else we can just copy the value that's stored in the pointer field instead if we give out pointer it like references to the box those are just references to the actual view size so everything sort of just works right and the compiler might not do this optimization today we have no guarantee that it won't do in the near future because we are telling the compiler when we construct a box that this pointer is not aliased but if we write code like this where we are aliasing the box rust doesn't know that we are aliasing this box so it will continue under the assumption that box the box is not alias because the rules for box is that it's not aliased and therefore let's imagine that it did the same optimization for the box here right so it it just didn't do the heap allocation here and instead just stuck the value 5 into the pointer field of box and then when we did this alien thing it would basically just copy the 5 into this other pointer but now let's say that we modified this value right we modified the value that's pointed to by left in theory and we do this sort of guaranteeing that no one is using rightly yet like right is right someone has an alias pointer but they're not using it for anything so we're allowed to mutate it let's assume that that's the case well now let's say we set that value to 10 if this optimization has been applied it's just going to change the value inside the box pointer to be 10 because it's using this optimization but anyone who reads from the other alias is not going to see that change because the optimization has removed the pointer there's no longer a shared thing they're pointing to that the the change to left actually mutated they just mute at their own local copies you can think of this as box sort of becoming copy uh under this optimization right and so now this is just gonna lead to completely weird behavior where uh changing one side doesn't change the other it's true that we only ever access them through um these these shared references maybe you think everything was okay because we can't mutate anything anyway but then you have types like unsafe cell or mutex or i guess ref cell technically right or or cell for that matter that give you interior mutability they let you mutate a type with a shared reference well what happens if left or right contains one of these like let's say that this contained a unsafe uh or a cell of you size the optimization can still happen but now changing the left is not going to reveal that to things on the right so basically by aliasing box we've we've invalidated an optimization that the compiler still thinks is legal and again this particular optimization isn't really the point i don't know that this is an optimization that even makes sense or one that lvm does it probably does not but it's more that because because of the aliasing rules of box which is you're not allowed to alias a box the compiler tells lvm that doing any kind of optimization like this is okay it's telling lvm that this pointer is not aliased and therefore you can make optimizations accordingly um and so it's just not okay to alias a box now this is a problem for us right because the whole point of left right was to use aliasing to share the underlying values um and uh and so i was trying for a while to try to figure out what like how to fix this problem because ultimately if we can't ille as a box we just like can't do this trick we just can't share values between left and right because you can imagine that you have some data structure that internally contains a box somewhere and then this optimization would become invalid again um isn't noely is still fine as long as they're all the no alias pointers are only used for reading uh which is how reference t can be no alias no so that's the point that even if we only use the box through shared references on both sides this is still not okay because this optimization again if if what the box contained was a cell of you size the optimization can still happen but now if you mutate through one right you're doing a completely safe operation which is to mutate a cell through a shared reference but it doesn't but that change doesn't actually propagate to the other side um which that in itself is not unsound behavior and i i don't know what unsung behavior this might trigger but that's sort of not the point and this is something that comes up a lot in discussions of unsafe which is uh you don't know what optimizations might happen in the future right so you need to code against what the what the guarantees are and what the requirements are of the underlying types so if box says it can't be aliased even if there's no current problem with aliasing a box you should still avoid it because there might be an optimization in the future that then randomly breaks your code now as we saw from the documentation on box there it's not actually documented anywhere that you're not allowed to alias one it just sort of kind of implied um and so after the discussion on uh the previous stream someone filed an issue wow uh someone followed an issue against the repository that that um that i was working on for the stream saying that like basically uh ralph jung who's one of the one of the big people working on like safety and rust and how to do things like formal verification of the safety of rust programs and just figuring out what the spec for unsafety even is um pointed out that like you're not allowed to alias a box at least that was his opinion um and i i think he's probably right about that that russ currently assumes that that is the case and this ultimately ended up with an open issue in the unsafe code guidelines working group so this working group is working on basically trying to codify what the what the actual requirements of unsafe rust code is what do you need to guarantee in order for your code to be sound and really what came out of this discussion was box probably should have the requirement that the value that the pointer is not aliased but it doesn't currently specify this anywhere like this is not a current part of the requirements of box the question is whether it should be and so i recommend you read through this discussion it's gotten pretty long if if you're curious about sort of the trade-offs that are involved here uh and and the arguments for why should aliasing for aliasing a box be okay why should alice in a box not be okay and where that discussion sort of ended up was that we probably want box t to not be aliasable but we probably also want some way to override that behavior like if you do want to alias a box there has to be some way to do it that doesn't violate the the sort of soundness requirements of russ code and what we ended up with there is uh rust has this type called maybe an init so maybe uninit of t is is a wrapper type this wrapper type is this wrapper type is repre-transparent so that is it has the same memory layout as whatever the underlying type t is and maybe on in it is interesting because you know it um it doesn't require that the sort of bits inside the bits that make up the t actually make a valid t um the the most simple example of this is in rust if you have a boolean this the sort of validity of boolean says that you can it the bit pattern for a boolean can only be zero or one it's not allowed to be anything else if you have some byte whose value is like 255 that is not a valid boolean um and there are a bunch of other types like this like if you have a if you have a box the bit pattern of all zeros is not a valid bit part pattern for a box because the pointer is not allowed to be null right so in general rust always requires its values to be valid and maybe uninit is a way to tell the compiler that the bit pattern for this type might not be valid yet and this basically forces russ to disable a bunch of optimizations because it can't assume the same things that normally assumes about the inner type and then on maybe on init on maybe uninit there is a so for example if you if you try to create a t by just zeroing a bunch of bits using mem zeroed like that's not a valid reference for example because references have to be non-null but if you stick them inside a mem unin it then you're fine because rust doesn't assume assume that that this is actually a valid t yet and then maybe an init has a method uh this documentation is great by the way you should read it if you're curious um it has a method called where are you assume in it and assume in it consumes the maybe uninit and gives you the inner tee and this is unsafe because when you call this function you are telling the compiler now the bit pattern of the inner type is valid and so now you can start to assume have all the normal assumptions you have about that type like for example for a bull you can now assume that it's zero one if it's a pointer if it's a reference you can assume that it's a valid reference it's a live reference it hasn't been dropped it's aligned all of those good things the reason this comes up as as being relevant for our box case is that if we stick a box inside a maybe uninit we're disabling the optimization of passing things like no aliased right um and and this means that now we can aliase the box we can have a maybe uninit box t and then an alias of that maybe unintended with an alias of that same box and the compiler won't emit no alias because it's not allowed to assume anything about the inner bit pattern about the inner type uh and then what we can use is we can use the uh the as mute pointer method on the maybe uninit or the ask pointer method to get up a reference to the inner tee which we can then sort of de-reference and go the whole way because we know that while it while rust assumes that you're not allowed to alias a box if we cast it into a reference into the inner t we know that that is a valid reference right so um to try to rephrase that if we have a actually maybe i should write it down so if we have a maybe uninit box of tea right rust assumes nothing about the validity of this box but if we now call dot ask pointer right so that gives us uh this gives us a a star const box t right um and then we do a this on that ah um right that's going to give us a reference to t and this reference to t matches all the validity requirements of a reference to t right specifically it has to be a valid pointer which it is because we know that the the value still lives it's aligned we know that that is true because a box creates an aligned value um it hasn't been dropped because we haven't dropped the target value um and crucially a reference to t does not have any requirements about it not being aliased because shared reference or references are always the aliased um and so by doing this transformation we're now able to alias the box t and still get out shared references okay that was a lot of technical detail um so so i'm gonna take some questions on how this works before we talk about a related problem around aliasing boxes but there's a lot of complexity to unpack here so please like ask questions about what why this is or is not okay or any part that was unclear and i'll try to go into more detail this is some pretty hairy stuff and it's hard to talk through too because there's so many like subtle interactions and a lot of new types probably for many of you um and it's also hard to it's hard to explain the problem concisely because we're just working with this sort of very basic example here um but the the the basic summary of the the problem and the solution is that you're not allowed to have two box tees that point to the same t uh and this is because box t requires that it is not aliased that is a validity requirement of box t but we can use a maybe uninit because maybe uninit relaxes the in the the requirements on the inner type it for example removes the requirement that the type is valid um and that makes things okay uh what are the aliasing rules for manually dropped t that's a great question that we're gonna get to next uh does this fix the issue with nested boxes being alias because we will sometimes reveal both shared references um that is a good question so so the question if i understand you correctly is what if we have a type like box uh box box t right or we have some like wrapper type that has a box t or something right then what we're doing is we're constructing a maybe an init um of that type which we're then going to turn into a reference to this type and the question is is this okay because now we're this box is still aliased right because we were aliasing this type and the answer is that yes this is okay and the reason this is okay is because i'm allowed to write this code right this is totally valid russ code so rus doesn't assume it it's not that rust doesn't allow you to have multiple pointers to a box multiple references to a box it's that it doesn't allow you to have multiple owned boxes that are aliased so this is fine which is also why this is fine because here the the shared reference part is what means is what sets the um the sort of aliasing rules for this value if you try to access this box through the shared reference rust knows that it can't give no alias because you're already going through a shared reference um where this is not the case if you have like we had above right if you had left and right that are actual aliases of the box then if you do a left dot alias dot i don't know like something that accesses the inner value right let's say you did these concurrently as far as russ is concerned this is not going through a shared reference and neither is this it's just accessing a box that you own and therefore it does give the no alias attribute does that make sense um can you unpack the types of ampersand star star more dear of the pointer getting a reference yeah so um the the transformation here is from maybe on init un init box t goes to a star const of box t goes to a uh star const of t so then we do so just to give like the methods we call uh this is the first so the ask pointer gives you this the star gives you this the second star gives you this and the reference gives you reference t is the progression and we can't just do a reference here because if we did a reference here you would end up with this which is not what you wanted so you need the star and then the reference which ends up being a no op right this ends up just being a cast and you could do it as a cast instead if you wanted to uh why is it called maybe uninit it's called maybe on init because maybe an init is primarily used for cases where the inner type is like just zeroed bits for example so the value is not initialized it just so happens that it's sort of poorly named right because really it's like maybe valid would maybe be a better word for it because it it's not only that it hasn't been initialized it can also be there's some other reason why it isn't valid yet um or at the moment why is box no alias in the first place box is no alias because of optimizations i'm sure there are other cases where no alias allows you to do optimizations um but i'm not sure i think this is part of the reason why this is still an open issue in the unsafe code guidelines group is because it's not entirely clear whether box itself should be no alias so it makes a lot of sense for mu t to be no alias right mu t is no alias because uh what's the the the sort of canonical example here is the one that's given in in the nomicon um let's see if i can find this uh aliasing yeah so this is a let me zoom in a little maybe so the nomicon is great for anything that has to do with unsafe and this function is kind of neat so it takes an input and an output and the output is a mutable reference in rust because and this is specifically because of the no alias optimization lvm the code generation tool knows that input and output cannot point to the same value and it knows this because output is no alias so it knows that even though input is a reference it cannot be a reference to whatever output is pointing to because the output pointer or the output reference is not aliased and that lets it do a neat optimization here where it only needs to read the input once instead of twice which in turn means that it can do some other optimization so no alias does enable some cool optimizations for mutable references to t whether it also enables cool optimization for things like box t is a little unclear so far but that's what that discussion is about um why would you need to alias a box uh so the read to need to alice box is so that the two the left and right copy both share the same underlying data what's the advantage of using maybe united box t over turning the box d into mu t and just aliasing that one um the biggest advantage is actually one of um generality so it's true that any time where you have a box t that you want to alias what you can do is you can turn it into a star mute t and then you can just alias the star muti but the more general case is you have some type x and you want to alias x this is the case we run into with a left right map where x is a user defined type that may be a box but it might also be a vector right and if if someone gives you a vect t then the correct way to alias is like u size u size star mute t right that's a real way to alias a vector so what we want is some way to do aliasing in general rather than how to aliase a particular type because left right is generic over whatever the backing type is and maybe on it let's just do that because we can alias box t by doing maybe uninit box t and we can alias vec t by doing maybe an init vecti right so it's a general purpose mechanism uh rust doesn't give no alias to mu t um i think it's currently that optimization is disabled at the moment um but it is an optimization that they would like to have enabled um when can you assume or maybe uninit is safe um it is valid you mean so uh there may be uninit assume init method which is the one that goes from a maybe on in a t to an actual t which is unsafe the safety requirement is there is basically you need to know that the bit pattern that's stored inside the maybe on in it is now a valid representation for t um sorry uh so so for example if you have a maybe unknown box t in order to get the box t you would have to ensure that it now meets all the validity requirement of box t which would be things like the box is not aliased and also things like the pointer is not null the pointer is aligned the pointer points to a heap allocation that was originally allocated by box basically every every requirement for the backing type is there a way to disable those optimizations and in the example of the double fetch actually fetch the value two times so the way you disable the optimizations is to uh well in general you should not assume that you can turn these optimizations off uh even with optimization level zero some of this might leak through because these are these are optimizations about the compiled code that may or may not result in optimizations remember the code generation tool is always converting your code into assembly and it might choose to do so in a different way if it knows that something is not aliased in some sense maybe uninit here is one way in which we disable that optimization um and and if you're thinking of disable these optimizations you think really carefully through why that is um keep in mind that generally rust's rules around borrow checking are for a good reason and if you're choosing to overwrite those what's the reason if you're trying to do things like mutate through a shared reference then use the unsafe cell type because that's what it's for and one of the reasons you would specifically want to use the unsafe cell type rather than doing pointer like one thing that's just never okay in rust is to if you have a ref t to cast that into a mutable reference there are all sorts of ways for you to do this like with a transmute you can make this happen but you should never do it because even if you manage to do it it is not safe and one of the reasons is things like no alias the only safe way to go from a shared reference to immutable reference is using the unsafe cell wrapper type and the reason for that is the unsafe cell wrapper type basically the compiler knows about this particular type and knows not to emit things like no alias for it think of it as this is the way that you disable those optimizations um is there a difference when you say shared reference and alias yes so this is a shared reference to a t whereas if i have a box t whose value is a pointer like this and i have a an a second box t i have a second box t that has the same pointer value then this is this is aliased so a shared reference to t is always in always aliases t right because you can have multiple shared references and those are aliased pointers to the t but you can have aliases that are not shared references so they're not quite the same does maybe init do more than basically just transmuting no maybe an init is really just a transmute but it's a type that rust knows about and knows to for example not assume aliasing about it it mostly just changes the flags that it passes to lvm um why not use raw pointers instead of an alias box uh the reason is the the one that i talked about before which is we want a mechanism that works for any user defined type and not just specifically for box um what does maybe on init do for pointers if there's no null maybe an init doesn't uh doesn't deal with pointers maybe uninit is just it you can think of it as whatever type is the inner type so if it holds a box t then the maybe unedit is a box t but with all the but but that you can't directly use without unsafe code um because it doesn't have the same validity requirements so maybe uninit is just a transparent type that makes it unsafe to access the underlying type okay i think we've we've now talked through most of the stuff around uh around aliasing but there is one point that uh comes up and which did also come up in in chat a little bit which is um one trick that left right was trying to pull was it didn't actually start a box because what would happen is if you have if you have this case right you have a left and a right to both point to the same heap allocation then if we drop left and then we drop right this is a double free right the drop of left is gonna drop the box and that's gonna do free the heap allocation and then we drop the right it's gonna drop its box we're just gonna try to free that heap allocation a second time clearly not okay so what left right did was it used a type called manually drop in the standard library and manually drop let me pull it up here [Music] uh so manually drop is also a transparent wrapper type uh sort of similar to maybe uninit but not really the same um manually drop is a wrapper that just disables dropping that's all it really does it doesn't require any unsafe to access the inner tee maybe arguably it should but it does not manually drop just is a t but will never drop that t and there are a bunch of cases you might think of that where where this is useful and this is one of them right where now if you drop left it's not going to drop the t because manually drop prevents the drop but now if you drop the right that's also not going to drop the box t uh and so now you never end up dropping but at least that's sound it's going to leak a bunch of memory but it is sound behavior but if you look at manually drop you'll see i wish this was the default expansion that manually drop implements dref and draft mute so if you have a manually dropped t you can easily just get a shared reference to that t or an exclusive reference to that t just by using like ampersand mute or or stars to get at the inert or dot for that matter to get at the inner type there's also an into inner which just removes the manually drop wrapper and gives you the inner t and you'll notice that that function is not unsafe right that function just gives you the t and that's all there is to it so in other words manually drop just disables drop it doesn't do anything else it doesn't prevent it doesn't enable you to do aliasing or anything like that so so far so good that looks nice but we do obviously sometimes need to drop the box t no that this would the the code i've written so far like if you have manually drop would never drop the box t like when you drop left nothing gets dropped or the box doesn't get dropped when you're up right the box doesn't get dropped and when the scope ends the box doesn't get dropped the box just leaked and this is obviously not okay um the way that this the way that i did this in um or the way you're supposed to do this let's start with that part is that you're supposed to like drop something like left right and then when you know that you actually want to drop the inner type in our case because we know that this is the last copy then what you do is you do write into inner and remember into inner gives you the the t that's inside a manually drop and then that you drop or just let go out of scope like you could just do this as well and it will work just fine um but basically you sort of tell manually drop okay now i do want to drop the t and at that point you would only end up with a single free okay so far so good where we run into issues is that imagine that um i don't actually have a manually dropbox t what i have or right i do have a manual dropbox t but what i have is actually like uh uh let hash map equals hash map new insert i don't know zero uh right i don't know i'm not necessarily writing valid code but that's fine let me comment this out so actually what we have is we have a left map and a right map and they each one contains an aliased copy right and now imagine that the user wants to execute an operation like they want to call retain right so you want to call like left hash map dot retain retain if you don't know of it is a method on most collection types that just um walks the it walks all the elements of the collection and if the retain closure returns true for that element it remains in the collection if retained returns false for that element it gets removed from the collection it's a really easy way to do just like remove a bunch of things that are related or keep only the things that have a certain property so let's say like e i don't know in this case e not equal to five or yeah so this is going to remove any element in the map uh who's i guess technically this is key value so let's do this so it's going to remove any value in the map whose value is not 5 and that includes left right because left's value is 5. so here that's going to get removed which means that the map is going to drop the value now that's fine because in this case let me add these annotations here too manually drop new and here as well manually drop you this all right let me just get rid of some warnings here manually drop and collections just to make this a little less noisy uh yeah yeah yeah that's fine uh box five box into raw i'm just cleaning this up so that um the code will actually compile because that way there are fewer distracting error messages and it also shows you more what the code actually used to look like uh hash map i can't can't spell um because you're expected to take two distinct arguments fine and v dot real we're not real ah now what uh this great okay so now that compiles and now you can believe me that it's possible to write this code okay so the retain here is gonna remove left right because the left value is indeed five so it doesn't match retain so that map when you call retain the inner code of retain is going to end up dropping the left now left has a manually drop for the box so it's not going to drop the box great but now we need to do the same operation to the other map right so we call the same thing on the right hash map great but this also ends up dropping right but it drops right with the manually drop wrapper so this means that the underlying box the box that's aliased does not actually get dropped anywhere and we don't really here have a way to use into inner remember how before we would call like write into inner when we actually wanted to drop it well how do we do that here because we are not the ones writing the drop code of course we could re-implement retain ourselves and have it like walk the thing and anything that we decide to remove we call like into in or on we could do that but it's really annoying it'd be nice if the code could just look the same so this is where the the second trick uh that ended up biting me in the ass earlier uh came up which is i pulled this trick where i did the following i'm gonna write it out and then we're going to talk about why it's wrong hmm sorry let me just i think i now have it right do user op just to signify that step specifically ah this is actually what that used to be and then the manual the drop was on the alias sorry i i promise i will explain what this does i just need to write it out first so i just moved the manually drop from being unreal to being around the aliased and then this was aliased new size and this is not real and this is the start okay great uh so what i decided was to do this cast and in order to understand this cast which is looking really ugly we need to look really carefully at the types here so the type of write hash map is a hash map from in this case i guess the key is an i32 and the value is a manually drop aliased you size right that's the type of the right hash map and this should make it pretty clear why removing something from the right hash map doesn't drop it it's because the values are manually dropped and so dropping the manually dropped does nothing so what we're doing here is we're casting the reference that we have to this thing the mutable reference we have to this thing into immutable reference to this thing and if you look really carefully you'll see so we have this and we're trying to turn it into this thing so essentially what we're doing is we're doing a typecast that casts away the manually drop right and you might think this should be okay right manually drop has the annotation repre transparent so it has the the repper transparent which means that casting from this to this should be fine because this should be laid out in memory exactly the same as this is right and why does it solve the problem well if we have one of these and then we drop this thing there's no manually drop here so it is actually going to drop the inner value which means that it's going to drop the box so now we can keep using retain and it will do the right thing we'll end up actually calling drop when we do this operation the second time on the on the right map on the second alias which is the last alias this is before i talk about why this doesn't actually work uh does this make sense like why why this seems like it should work uh repper transparent does represent work across generics uh yes so so repper transparent all wrapper transparent does is it says it when you put repper transparent on a type that type has to have only a single field that's it has to have a only a single non-zero sized field which is to say it basically just has to have one field and the layout of the outer type has to be exactly the same as the layout of the type of that one field so that includes things like generics it's just the outer type is exactly the same as the inner type for any value of the outer type for any instance of the outer type sorry so in that sense yes it does work across generics uh oh up here is operation uh operation right so we do some operation on the map here i've chosen retain because it's sort of an obvious case of where we don't choose how the dropping happens so the the idea behind this cast right is that it lets us just not worry about what this operation does internally and instead just cast away the manually drop so that whatever it does when it drops things it drops them correctly so this trick seems nice enough unfortunately this is not sound this is a problem and to understand why we need to dig into the trait system so let's do start some code up here so we assume the standard library hash map but let's just make our own hash map so our hash map is going to take a k and a v and it's going to hold like values which is going to be a vec of tuples of knb this is not actually how hashmap works but we're going to go ahead and do that seems fine and then we're going to have a bunch of methods on hashmap but now this particular hashmap that i have is wonky it's going to require that the v is wonky and wonky is going to be a trait that we define and wonky has an associated type foo it doesn't have any requirements on that type it just has an associated type and we're actually going to store a foo right here we're going to have a prefix which is going to be a v foo ah calm up uh that's fine let's do this well actually let's do hash map let's do my hash map uh all right so it's a little weird like who would write hashmap this way but there's nothing stopping anyone from writing code that looks this way um right like this could be any wrapper type that is in some other crate uh or even in the center library who knows right and now what i'm going to do is i'm going to implement wonky for aliased t oops uh and here foo is going to be a ua and then i'm going to implement wonky for manually drop alias t and foo is going to be u32 do you see the problem the layout of my hash map depends on what this type is and this type is u8 if you have an alias t and u 32 if you have a manually drop alias t so this means that the layout of a my hashmap i i32 aliased t right is a u8 followed by a vector of i32 and alias t but the layout of a my hash map manually dropped lst is also a vector of these but it's prefixed by u32 instead because that's what the associated type foo is for the wonky trade so these are not the same they're not laid out the same at all and this is a trivial example where this is like another field but you can imagine that like it sticks some of those in the vector itself like the whole structure could be laid out differently depending on whether you use this or this now this seems a little malicious but there's no unsafe code here right and now imagine that we don't like here either imagine that the user could choose what hash map implementation to use here but but crucially imagine that they just the the value type the t that we're wrapping in our box has well not the type we wrap in our box but but the collection type we use if the user if if there's anything at all in that wrapper type right inside of like hashmap contains a vector and maybe the vector contains some other structure if any of those are dependent on any associated types depending on the value then now this transmute is not sound because going from this to this is like going from casting from a mute u8 to a mute u32 it's just not okay they're not the same you can't do the same things with both of these so this transmute even though it seems like it should just be identical it isn't they can be different if you have a particularly wonky outer structure all right so how do we fix this this sounds like a huge pain and it is uh finding a way to work out this work around this was really hard and i have a solution that is probably sound but it depends on another thing that has still not been decided in the type system which is okay we have this observation that the reason why this isn't sound is because someone could depend on some implementation that uses an associated type and really what that comes down to is some someone somewhere else someone outside of the current crate can tell the difference between these two types and therefore can choose that the layout of their type depends on it so the solution has to be to construct the types that we cast between in such a way that no other crate can distinguish between them and the way we're going to do this is as follows instead of having manually draw what we're going to have is this we're going to cast we're going to add an associated we're going to add a sorry not an associated type we're going to add a another generic type parameter to aliast that dictates whether or not it should drop the inner type and so so you can imagine that we implement drop for aliased right and then depending on what the type the second type is we choose whether to actually drop the inner t and sort of call manually drop into inner or we're going to not do that and casting between these two should be sound as long as these two types are both private and the reason for that is if no other crate knows about this type if they can't name this type and they can't name this type they have no way of implementing a trait for this differently than how they implemented for this this also means that these types no drop and do drop have to not cause any differences in what alias looks like so for example if alias implemented debug only if it was no drop but not if it wasn't do drop sorry if alias implements debug if it has no drop but it doesn't implement debug when it's dewdrop then now we have the same problem so we can implement the wonky trait for any aliased sort of tu where you use debug and then implement the wonky trait differently for any alias tu where u does not implement debug so they have these two have to be indistinguishable outside of the current crate this is complicated but it does work the way we're going to do this is we're going to have a trait uh drop behavior that just has like a do-drop method and dewdrop doesn't doesn't need to know about the current value it's just a method on the trait if you will returns a bool and we implement drop behavior we have a struct no drop and we have a struct dew drop and they must not be public and we implement drop behavior for no drop which says false and we implement drop ever for dewdrop which returns true and then we implement we changed aliased so that it takes a t and a u it holds a manually drop box t again so it no longer holds we don't put the manual drop outside the alias we put it inside the alias again and it holds a u it has to be repre-transparent uh and i'll talk about the u here and represent transparent in a second and u is going to have to implement drop behavior and then we do this impul drop for aliased tu where you implement drop behavior and when you're asked to drop one of these if you do drop then self.real dot into inner drop that the code can't actually look quite like this but it's close enough to sort of explain what's going on and so now um there's no way for sort of an attacker if you will or just for an oculus library that doesn't know that you have this requirement that they're castable to implement like they have no way to distinguish between no drop and do drop it can't because no drop and do drop are not public they're not available outside of the crate the defined alias so if we don't export them all it can do is this but these are overlapping implementations so the compiler will reject them it doesn't have a way to change the layout depending on what this uh this other type is which means that these the associated type here must always be the same which means that the layout should always be the same which in turn means that this cast from alias no drop to alias dewdrop should be valid because there's no way that this type can vary in its layout depending on what this inner type is so it's a fairly sophisticated argument and it's not actually guaranteed that this is sound so this gets back to another discussion that came up which is talking about deterministic but undefined layout this is also an issue from the unsafe code guidelines issue number 35 and here i recommend you read through the discussion it's fascinating stuff um and what we got to hear was basically i proposed is this a valid cast um let me see if i can dig into this some more so further down here um yeah so here is where i give this proposal a transmute from some rapper my type t and then some private second type to the same type with a private type b where their unit starts to implement no traits and where the sort of my type is repre transparent and the response was uh that sounds like a very reasonable request right for there to be some way to have this kind of type parametricity uh cast be safe now it's not currently guaranteed that this is valid but it is much more likely to be sound than the previous version which we could clearly demonstrate was not sound this one i haven't been able to demonstrate is unsound uh and this is why this issue is still open because it's not clear that this is something the compiler currently guarantees or something it wants to guarantee um and this is why the unsafe code guidelines working group is so important because they're trying to figure out like what are the actual rules and what are the rules that we're willing to commit to right because if you commit to some kind of behavior being sound you're not allowed to add an optimization that requires different behavior in the future because people have written code that assumes that a given behavior is sound um so yeah this is the the solution that i've come up with and that i think the sound and hopefully it will remain sound um i didn't actually modify this cast but the cast remains basically the same right it just turns into this um and yeah there's one more thing here uh to get back to which is wrapper transparent requires that there's only a single non-zero size field so we're not really allowed to store you here because even though we only ever use it with do no drop and do drop which are zero sized um there's nothing stopping someone from creating like an aliased you size you size right and now this couldn't be transparent because it's not just one field and so we use this is where the marker trade phantom data comes from which is we want to retain information about u at compile time but we're not actually going to store a u in this type and this means that this field is always zero sized all it this this field basically disappears at runtime it's only you to use that compile time for for sort of uh carrying around the generics information um and the reason why it's okay for us to store phantom data here is because the drop behavior trait does not take a reference to self it doesn't require that you actually have an instance of the trait in order to answer whether or not to drop because the drop behavior is determined by the type overall like dew drop always drops no drop never drops and there's no value for us to care about okay so this now gets at how we ended up getting hopefully at least sound aliasing in left right we use um so we had to combine both these approaches which i haven't showed you in the code so far which is aliast has to contain say maybe uninit it can't be a manually drop it has to be maybe uninit because we have to remove things like the aliasing requirement from box so the real alias in left right if you go look at it actually holds a maybe on in it not a manually drop and then we need to have this associate or this not associated type this second generic type parameter that is entirely private that we cast between in order to make sure that the um the cast is actually valid all right that's a lot of complexity that i've thrown at you and so let's do some more questions around this um i don't what's a little weird about this particular type of stream is that there's like not that much coding it's really just talking through the complexities and and how to resolve them so i don't actually have anything more i want to talk about today but we have talked about a lot of really complex stuff and interactions um and so i'm sure there are a lot of just related questions and and things that i can go into more detail on and help you understand why things work or don't work so please like fire away with questions and that will probably take some time too if i understand correctly the user of this library is only given an abstract alias all the fields are private yes that is correct so if we go back here and look at left right so left right has this uh aliasing module that actually talks a bunch about this and it has an alias type and the alias type has no fields but the alias type the alias type does implement dref into the inner type so if you have an alias t then you can always get you can safely get at the um at the inner t the reason why you can do this is because it's sort of like the same argument as manually drop that it's unsafe to construct the alias in the first time and it's well uh it's unsafe to cause the alias to be dropped to drop the inner tee and therefore as long as that part is unsafe and you have to manually guarantee that it's safe then it should always be safe to de-refine alias because of the safety invariant of actually dropping these arguments are all understandable but any unsafe seems so difficult to get right accidentally being unsound is so easy i feel unsafe somewhat deserves its reputation you're not wrong it is true that unsafety is complicated um and it sort of is complicated for a good reason right like the the reason why this code is so hairy or this where the reasoning here is so hairy is because there's a very sophisticated property that we're trying to guarantee like think of if you if you were to do this in in c code right all the same conditions would apply um aliasing not so much because in c you generally don't pass no alias to lvms you don't get the same optimizations but if you try to cast between two different types and they have a different layout you would have the same problem it is true that in c it's sort of hard for them to not have the same layout because you don't have things like generics in the same way so the the same problems can't arise but the the properties you're trying to maintain are the same i think the reason why it becomes complicated and rust is because there's so many interactions with the rest of the rust type system to think about um and it's hard like evmap did this the wrong way for years but it worked right like it was being used in sort of a real research system for lots of benchmarks and testing and everything worked just fine so like it's tricky right because uh unsound and unsafe behavior is um or unsound behavior specifically is like it is a problem because it could randomly break at any time right the next compiler release might suddenly cause everything to go wrong and it might go wrong in really bad ways so that that's why you should avoid unsound behavior undefined behavior but if it currently works then that means as long as you don't upgrade anything you're probably fine that's not a good safety requirement to rely on but but often times it's like good enough and i i have a lot of understanding for people who write unsafe code and go this might technically be unsound in some way but it generally works the way that i use it so therefore i won't fix it i understand the argument i think it really depends on what the um what the sort of blast radius of being wrong is right if you if you're just like writing your own code base for some hobby project it probably doesn't matter but if you're writing like code for the space shuttle this probably really matters so you get all the unsafety right and that does require you to do a lot of pretty sophisticated reasoning i think in general you can get pretty far by just being extremely skeptical of anything that's as unsafe in general the the best rule of thumb i've heard for unsafe is that unsafety is a crate property um if you do anything unsafe in your crate it's not just about like whether the surrounding lines are right or anything it's that you need to guarantee that all of the other code in your crate is still sound now that this unsafe thing is happening and that requires really hard reasoning and it's why for basically any piece of unsafe code you write you should write a safety comment above explaining why this doesn't invalidate any of the other safe code in your in your system but it's hard um the the other way to get at this is to run everything through miri um so miri if you don't know about it is a really handy uh tool that basically it sort of runs your runs is a fudgy word when it comes to miri but it basically runs your rust code in a in a mode where it can detect unsafe behavior now miri is a little limited in that it only detects unsound behavior if it occurs so it can't detect if your code like sometimes does something on sound if your tests don't trigger that particular case but but it is really good at if you do something that is unsound it will tell you um and this is why for for unsafe code like testing is just absolutely key you want to run it through miri you want to run it through things like um like address sanitizer uh or through valgrind um you probably want to run it through something like loom which is a sort of concurrency testing tool that's really handy in rust um you just need to be really thorough about vetting unsafe code um how did you figure out that the cast without do drop no drop trait was incorrect in the first place so this came up in the previous stream actually where someone was like are you guaranteed that this cast is safe um and i was like i think this cast is safe but let me ask so um if we go back to here see if i can find my particular comment yeah i recently came across a case where i specifically wanted to cast a some type t into a some type manually dropped t and then trying to figure out whether that was safe i found this discussion i think that case works correctly in the compiler today but i suspect that it's still an open question whether it should or will be guaranteed by the language to be okay subject to the discussion in this issue so that's really where the stem from that i was like i think this transmute is okay but it might not be okay because this is super subtle stuff i googled around for a bit i found this open issue i commented on it and then a lot of really interesting discussion happened um can you explain it like at five like i'm five that unsafe cast line okay yeah so this is a sort of fun aside uh this cast line so let's dig into what this is doing uh it's like worthwhile to go through because casts are nasty so right hashmap here is really a hash map right of uh aliased use i32 aliased use size snowdrop right we can't cast the actual own type we want to cast a mutable reference to it so that's why this takes a mutable reference out of that so that gives us a mutash map of that same inner type um then we you're not allowed to cast directly between reference types so if you have a like um you you can't just cast directly from this to this uh you can only do that kind of type casting either by using mem transmute which you should generally avoid because it lets you do really unsound things but you can cast between raw pointer types to things like this so for example if you have a one thing that comes up a lot is if you have a mute u32 and you want to cast it to a mute u84 this is sort of a this is safe because they're the same layout right um you can't cast directly between them without using a mem transmute which you want to avoid so what you do is you turn this into a star mute u32 and a star mute u32 you can cast into a star mute u84 and then use a star starmute star to turn it into a refmute so so that's the same song and dance that goes through here this this turns it into a this turns the mutable reference to the hashmap into a mutable pointer to the hashmap this is what actually does the cast to drop uh the reason this is a cast to do drop is because i've specified that the type should be do drop here so the compiler can sort of reason backwards to what type uh what type i wanted this cast to turn into uh and this starmute star is the way that we go from a mutable pointer to immutable reference um have you found that most unsafe usages can be replaced with safe code at no performance penalty yes unsafe code generally you should only generally need it if you're writing like core primitives like i write a lot of concurrency primitives things like left right or um like concurrent hash maps and stuff where you're you're really in the low level uh and there you sort of need to unsafe because you're relying on invariants that the compiler doesn't know about and that aren't common um and when i say common what i mean is like if you look back at i did a stream a while back about interior mutability and things like cell and the interface to sell is completely safe and it's common enough that someone actually wrote the cell type and put it in the standard library usually if you're writing like your own concurrency primitive the invariants you're relying on are so specific to your particular problem domain that no one else has written the thing that you need and so you need to implement it but outside of writing like really low level code you probably don't need unsafe there's probably already a safe wrapper that you can use and in general those do the unsafe internally to mitigate the performance cost so it's probably just as fast as the safe code you should not think of safety as requiring a performance cost because if you can do it with unsafe code and have it be safe that means that there's some way to write a safe wrapper around the code that does exactly the same thing you did and therefore has no performance cost um uh isn't the compiler going to drop the manually drop in case of memory optimized programs since the code won't require the variables again um no so the whole point of manually drop is that when you drop it it never drops the inner type how could an implementation of the left right look like that does not use unsafe aliasing at all the safe implementation of left right well there's other unsafety in left right that's not related to this particular problem but if we look just at this particular problem the version of left right that doesn't use this unsafe aliasing is that you use arc so use an arc of you size or an arc of whatever the inner type is and then you just keep one clone for each map and that's totally safe left right could do this there are a couple of reasons why i didn't want to do it one is that this means that you have to um the the copies are well you end up having to do reference counting every time you want to uh create an alias and then anytime you drop the aliases and that reference counting isn't really needed because it just sort of does the right thing already um like it the left right primitive already knows when to when you should when you're dropping an alias so you shouldn't drop and when you're dropping the last copy in which case you should drop so the reference counting is sort of just overhead the other reason why i didn't want to do an arc t and this is sort of the primary one is that imagine that the user specifies a t of like uh box foo then now what this data structure is gonna store is arc of box of foo which is sort of wasteful right like you're having an extra heap allocation just to store a box which is heap allocation even worse yet imagine that the user gives a type of arc foo then now we're gonna store an arc of arc of foo so every access is going to be sort of penalized because it has to go through an extra level of indirection now it's this is not impossible um it's not like you could totally do this the cost would be fairly small for many applications but left right is specifically for where you have really really really high throughput reads where you care a lot about the latency of each individual read if you didn't you just use like a reader writer lock or something like that and so i'm assuming that the people who use left right will care about things like an extra point or dereference and therefore i didn't want to force the additional use of like a reference count update and uh the extra pointer dereference which the aliasing avoids that said like this would fix another outstanding unsafety problem in evmap which is um so actually this is worth touching on briefly which is imagine that the retain here right retain here is doing a comparison right of the inner value now let's say that instead of being five this was some user-defined type like foo and i imagine the implementation of eek for foo is non-deterministic it just like randomly returns true or false then when you do the operation on the left map you might choose not to remove the value because the randomized implementation of eek returns false and then in the right hash map when you do the retain it randomly turns true and so it does get removed then now you didn't remove the value from the left map but you did remove it from the right map but because you thought it was removed from both you end up actually dropping the value even though an alias still exists and guarding against this is really really difficult there's a there's an open issue already on evmap for this problem and with arc this wouldn't be a problem because the reference counting would notice that it's not safe to drop the second alias because one still exists so i i don't have a good solution for this um arc would solve the problem but it comes with its own costs it might be the solution is that um creating an ev map has to be unsafe and that you need to guarantee that that every operation like hash eek partially is deterministic an analogy i was thinking of here is that russ has a lot of powerful tools and the more powerful the tool the worse things can go wrong when you have to take off the safety guard it's a good way to think about it the alternative way to think about it is that you have a really complicated machine and you're fiddling with one little bit on it but everything else is connected so fiddling with this one bit might cause a problem over here and so this is like the interconnectedness of there are lots of features that interact in interesting ways and you need to reason about how all of them interact with the change that you're making it's sort of tight coupling if you will and and i think this point came up in chat too that one challenge especially with generics and unsafety is that you have to ensure that your unsafety holds for any value for the generic type which might be tricky um why not transmute to maybe uninit instead that should always be fine um here you have the same problem if if this was like casting between not not maybe uninit and maybe on in it or the other way you still have the the wonky problem now of course internally alias does have to do have a maybe in it so if you look at the code for left right you'll see that that's there don't u32 and u84 have different alignments uh yes they do so you're not allowed to cast this into this but you are allowed to transmute this into this so you can go one way but not the other doesn't alias already introduce a layer of indirection with the box to me that looks like an arc without the reference counting no not quite so maybe uninit is not a heap allocation maybe on init it's not a pointer same with manually drop it's not a pointer the definition of manually in it you can maybe in it you can sort of think of as this it doesn't store like if this means that if you have a maybe maybe on in it if you have a maybe un init box t that really is just a box t there's no there's no additional pointer or anything um it's just that the compiler knows that you can't actually assume that it that it's a valid box t um but there's no extra indirection uh all right let's see this test seems kind of simple at this point might as well just make it wrong um okay so hopefully that was useful hopefully you feel like you learned a little bit about um learned a little bit more about how this stuff works um under the hood um and and there are many issues with unsafe like there this journey is not over um and and the point of the stream was not necessarily to like teach you how to write safe code and that's not that's never going to be my goal with this particular series it's just to expose you to the kind of things you have to think about when writing unsafe code i don't know when the next stream will be it'll be whenever i have another interesting thing to talk about but hopefully just sort of talking through the kind of problems that that can arise is is useful and interesting um there is and hopefully hopefully it was possible to follow like this stuff is super involved and i highly recommend that you look at the um the issues both on the ncf coding guidelines and the ones on evmap and maybe even look at the code of left right i'll link all of those in the video description eventually um because there's some really fascinating discussion around it as well that i think is is useful if you're gonna write code like this yourself just getting into the right mindset helps a lot um someone asked about uh structural eek for the eek bug um structurally doesn't actually solve the problem because first of all structural leak is really limited um it it's so the advantage of structural weak is that it's derived by the compiler and so you're guaranteed that it's deterministic but it doesn't actually but but it means that if someone has their own type that is not structurally then they can't use your data structure which seems really sad the other is that structural weak is not the only problem imagine that we do this here's an example of something that is not does not rely on eek uh so imagine that we have this instead and of course we do the same thing for um do the same thing down here for the right map right now this depends on the iteration order of the map right whichever thing you iterate to first that's the thing you're not going to drop you're going to drop all the all the subsequent elements well what if so the iteration order of a hash map depends on the hashes of the values that are in there so if the user controls the key type right here let's imagine this is like foo some user type and the user implementation of hash for foo is non-deterministic you have the same problem because the iteration order will be non-deterministic so which thing is first it also varies so we actually need to require both that oh sorry let me leave the code up we actually need to require both that the keys and values are deterministic in their information of eek and of hash and maybe even of or depending on what the value type is um i think maybe the takeaway from from this is like do not write unsafe code unless you have to there are some cases where you have to where there's some invariant that you know is maintained but the compiler can't know but for most of the code you write that shouldn't be true right for most of the code you write there someone probably ran into the same sort of data structure problem you had and wrote a data structure that is safe that internally does the unsafe stuff like don't do unsafe stuff yourself unless there really is no safe implementation of it that you can use because getting unsafe stuff right is hard it requires a lot of thinking and not just a lot of thinking but a lot of understanding of what the actual rules that the compiler and forces are and that the type system and forces are um all right i think i think that's everything for today uh hopefully that was useful uh i'll upload this to youtube and then i will see you next time i don't know when the next stream will be i don't even know what topic it'll be on uh whether it'll be sort of a crust of rust or a long live coating or another unsafe chronicles i guess we'll see but hopefully this was interesting if you think that this particular series is like valuable then please like please at me which is the opposite of what people usually say uh or just like mention it in the comments it's really useful for me to get a sense for what people find interesting and useful and educational and what they don't if this kind of stuff just sort of goes over everyone's head and you feel like the only takeaway was everything is hard then this is not a valuable stream um but so any input you can give me is always valuable all right everyone thank you for coming and i'll see you next time so long farewell bye
Info
Channel: Jon Gjengset
Views: 11,616
Rating: 4.9774013 out of 5
Keywords: rust, unsafe, soundness
Id: EY7Wi9fV5bk
Channel Id: undefined
Length: 97min 9sec (5829 seconds)
Published: Sat Dec 12 2020
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.