BT

Facilitating the Spread of Knowledge and Innovation in Professional Software Development

Write for InfoQ

Topics

Choose your language

InfoQ Homepage Presentations Practical Robustness: Going Beyond Memory Safety in Rust

Practical Robustness: Going Beyond Memory Safety in Rust

46:56

Summary

Andy Brinkmeyer shares how engineering leaders and architects can use Rust to build failure-proof systems. Moving beyond memory safety, he explains how ownership, enums, and the typestate pattern embed complex runtime protocols into compile-time checks. Learn to eliminate entire classes of bugs, manage real-world resources safely, and maximize codebase robustness effortlessly.

Bio

Andy Brinkmeyer is a senior software engineer at arculus, where he works on autonomous mobile robots and builds robust production systems. Over the past two and a half years, he and his team developed a master control system entirely in Rust to coordinate fleets of autonomous mobile robots. Before joining arculus, Andy worked on mission systems for autonomous UAVs, among other projects.

About the conference

InfoQ Dev Summit Munich software development conference focuses on the critical software challenges senior dev teams face today. Gain valuable real-world technical insights from 20+ senior software developers, connect with speakers and peers, and enjoy social events.

Transcript

Andy Brinkmeyer: My name is Andy. The last couple of years I've been working in the fields of autonomous systems. That's also where I will be bringing in a couple of examples throughout this talk. This is a talk about Rust. There must be a talk about Rust at every conference, of course. This is your obligatory Rust talk. I want to start with some personal experience. I've talked to a lot of people about Rust. I noticed that either they don't like it at all or they like it very much. The people who don't like it usually didn't work with it a lot. They maybe tried it out once, then put it away because they didn't get it immediately. Everybody who stuck with it, who tried it out for a longer time, maybe even for a real project, they ended up loving it. I still remember two-and-a-half years ago, we started a big new project. People from different languages being moved into the team. The project was all Rust. One of my colleagues, he spent the first two weeks coming from C++, complaining every single day to me.

Then after about three to four weeks, it started turning around. He started really liking it and now says he never wants to go back. There must be something to it, to all those Stack Overflow surveys where people say it's the most loved language. What is it about it? Is it the memory safety? Memory safety is great. I highly benefit from it in the projects I'm doing. I think there's more to it. What if I tell you that Rust makes it so much easier to write software that's more correct in the first place, where it's more difficult to make easy mistakes, to make developer mistakes, software which is overall more failure-proof. This is what it's about today, and where I want to go in a bit more detail with you on what is there beyond memory safety in Rust.

Rust itself, I believe, is in its core a rather simple language. Because most people that haven't worked with Rust that just have heard about it, they know two things about it, that it's memory safe, and that it's supposed to be super hard with such a steep learning curve. There's some truth to it, I have to admit. It's not the easiest to get into. If you look at the core of it, it's really all about data and functions. Data in the sense of you have some types which have some data. Functions which do something with your types, with the values, with the data. A lot of concepts from other languages that make it more complicated are just missing, like no garbage collection, classes, inheritance, traditional object-oriented programming, null pointers, function overloading, type coercion, all those are missing. In its core, I've heard some people comparing it to being a bit more C-like actually, or nowadays also Zig. That's up to you to decide how it feels like for you.

Enums: More than Just Integers

Before we dive into those Rust-specific concepts, I want to start with enums in Rust, because enums in Rust are more than just integers. In Rust enums, variants can store data. This means that every variant of your enum can hold some specific data. It can have no data at all, or it can have some data which is different from the other variants. This is a bit comparable maybe to, some people know it from like TypeScript with tagged unions. In Rust, this is also a first-class concept there. In order to access this web data, you have to match the enum. Matching means that it's similar to a switch statement from other languages, where you can have multiple switch arms, or like cases, where you check against what value does my enum actually have. When you're in this switch case on Rust the match arm, then you can run some logic.

In addition to just being able to then just run some logic, you actually get access to this web data. Only if you're in the correct match arm, the compiler actually lets you access this data. Matches must be exhaustive, which means that you're forced to handle every single variant. This doesn't mean explicitly every single variant. It can also be through catch-alls, but everything needs to be handled, which prevents you from forgetting something. Of course, there's also some additional syntax available, which makes everything a bit more easy and ergonomic in special cases.

Let's look at what we can actually do with it. There's this concept of optional data, which is present everywhere. Often, it's modeled as null pointers, null value, nil value, or even specific standard library types, which provide some utility around this concept. In Rust, we just model it as an option. That's how it's done in the standard library. There's no compiler built in for it or something like this. It's just an option. You could write it yourself in less than a minute. An option in Rust is defined as an enum with two variants. The None variant, which has no data associated with it, and the Some variant, which can hold some data T. T is just a standard. It's a generic. We will talk about it later. T just means you can replace it at compile time with any type you want to. Due to those rules about accessing the data, the optional data is protected by accidental access, so you have to match it.

For example, in the field I'm working in, we have robots, and a robot can have an active job. It can either do something or currently not doing anything. We can just model this by an option. In order to do something with the active job, I have to match on it. You see when there's None, for example, in this case, I just return, I do nothing. If there is an active job, I now get access to the job here and I can do something with it, like publish an update to some external system.

Let's go further. State is present everywhere. One intuitive way of modeling state in Rust is also with enums. Because in a sense, all you want is some marker which tells you what state am I in. You want to have some specific data that's only applicable to those specific states. In Rust, we just model this as one big enum with those different variants, where each variant is now a single state and can wrap some data. For example, again, an example from robotics, I have this robot state and it can be uninitialized. I just know that it's there. It can be initialized, at which point I actually received a position from it and I know where in space is it. It can also be executing a job, so we just model it as just another variant, which now also has a job inside of it. The data now is not only protected by accidentally accessing it while being in the wrong state, but also the other way around. If I want to transition to a new state, I have to provide the data. Otherwise, I cannot construct this variant. I'm also ensured that I only construct valid states in the first place. Everything else is prevented by the compiler. That's it about enums.

Ownership: There Can Only Be One

Now we go to the first big concept. Ownership is a core concept in Rust, which you do not really find anywhere else. Ownership is actually quite simple. All it means is that each value in Rust has an owner. It needs to have an owner. It cannot have no owner at all. It is only allowed to have one owner at a time. This rule is enforced by the compiler. It's statically checked at compile time. What this also allows us, this concept of ownership, is that since we know who's the owner of our value, and since we then can easily track when is the owner created and when does the owner go out of scope, think of a variable. It's not so hard to figure out when does a variable go out of scope. You can now actually track the lifecycle of your value. Because when the owner goes out of scope, your value would be out of an owner, so it is just dropped.

Dropping just means it's destroyed in some way, like memory is freed or other resources are released. Ownership can also be moved. This is where things start getting a bit more interesting. Like in this example you see here, whenever I do an assignment, this automatically means a transfer of ownership. The data is not copied. It's actually the ownership is moved. You can also say the data is moved, but not in the sense that the data itself is being moved to some other memory location. This could happen in function calls, for example. It usually just means conceptually that you move the ownership from one owner to another owner, meaning the old owner gives up its ownership and the value can no longer be accessed through the old owner. We can already do some interesting things with it. Something you can prevent with it are what I call double-uses.

Double-use for me is this concept of, you have some entity which is only allowed to exist once. Not in the sense of a singleton, but rather a unique instantiation of some value. Like again, a job in a system where robots are executing jobs. You can have multiple jobs, but each job can only be executed once, because after it was executed, it's finished. It should no longer be able to work on it. Only a single robot is only ever allowed to work on a single job. The single job is only ever allowed to be executed by a single robot at a time.

How can we model this in Rust? Let's first look at the function signature. That's what we'll do a lot in this talk, look at function signatures. We will look at the function signatures of the assigned function on a robot. Ignore the first part about the self. This just refers to the robot you're calling it on. Now look at the job. We define a parameter, job, and it takes a value of type, Job. There's no reference symbol in front of it, which means that we take this job by value. If you remember from the ownership rules, this now means that this parameter, job, takes ownership over the value, Job. If I pass it in from the outside, the ownership is moved into the function or even more detailed, it's moved into the parameter, job. What does this mean in practice? When I now want to assign a job, and let's assume we have some queue where those jobs are stored, I fetch myself a new job by popping it from the front of the queue.

Here we already see ownership in action, because the queue used to be the owner of the job value. Now I pop it from the front of it and assign it to the new job variable. At this point, the queue gave up ownership over it. Of course, this also means practically in the inside that it got moved out of its internal buffer. Some pointers are being rearranged, maybe some memory allocation change. Ownership-wise, it means that the job queue no longer owns the job. Now we can assign it to a robot with the function we just discussed. If I try to assign the same job again to another robot, I get a compile time error, because we try to use moved value. In the assignment to the first robot, we actually moved the ownership of the job out of the new job variable, now into this robot_1 function. That's how we can effectively prevent what I call double-uses in Rust.

I also talked about the lifecycle of values. The lifecycle can be more than just managing the memory. Because we know the lifecycle of each value, we know when it is created and when it's being dropped, this also means we can associate resource management with this lifecycle. Rust allows us to hook into the drop of a value. Dropping again means this event when the value goes out of scope, so when the owner goes out of scope and the value would no longer have an owner, the value is being dropped. We can hook into this through the drop function. We can define some special behavior for it. In this robotics field, you often have in 2D space some regions, for example, some tight space around the corner where you say there's only one robot allowed in there at a time. You want to manage a real-world resource, like in this case, a 2D physical space, and want to make sure that this guarantee is actually upheld in your code.

How we can model this in Rust is we have some registry which manages those accesses. We request access by providing a zone ID. What we get back is a ZoneAccess. The ZoneAccess is a custom type I implement and it acts like a token. When I have this token, I'm allowed to drive into the zone. Since the ZoneAccess cannot be copied or cloned, it can only be moved around, this means that we can only ever have one ZoneAccess alive at a time. I can, for example, hand it around between robots and that's all fine, but I'm not able to accidentally give two robots the same ZoneAccess. Now it goes even further. There can be many cases where I have to free the zone, because, for example, the robot disconnects. I then would have to handle this case explicitly in code by saying, ok, if it's being disconnected, I have to free this.

If the robot changes state, I have to free it. If this, then free it. Instead of having to manage all those individual paths in code, I can just hook into the lifecycle of the ZoneAccess type and say, when it is dropped, then we just free the resource. If you look at this code on the bottom, we'd say in this drop function that you have some reference to the zone_registry, and if you're being dropped, just call free. The zone_registry will handle, for example, communicating to other systems or marking it as free in its internal state. This means that since our robot, when it takes ownership over the ZoneAccess, is being dropped, so the robot has ownership of the ZoneAccess. If it's being dropped itself, maybe because it disconnected and we just deleted from our application state, that since the robot was the owner of the ZoneAccess and the owner now itself goes out of scope, this also means that the ZoneAccess itself goes out of scope and the compiler just takes care for you, of freeing all the resources associated with it. This makes it so much easier to handle those real-world resources, which are more than just memory, and prevent all those leakage problems of you accidentally didn't consider this one path in code where this could leak because it's handled for you.

Borrowing: Safely Referencing Data

Oftentimes, we do not just care about owning data. Sometimes we just want to reference data. Because if you could just own data, the language would be quite limiting. That's where we get to borrowing. Borrowing is just a way of safely referencing data, but with some rules. This rule is that there can be either a single mutable reference to some value at a time or any number of immutable references. This is important for memory safety, of course, because if there's only ever one mutable reference at a time, this means that there are effectively no data races from multiple places in code trying to access the same variable. We will see that this can be also used for more than just memory safety. As the name suggests, with a mutable borrow, I'm allowed to mutate the underlying data. With an immutable borrow, I'm just allowed to look at it. Again, borrowing does not move the ownership. The original owner stays the same. Instead, you're just referencing the value.

Lifetimes: Is This Still Safe to Use?

This goes hand-in-hand with lifetimes, the next concept. Lifetimes tell you, is this thing still safe to use? Is this borrow still valid and safe to use at this specific point in time? What this means is that a borrow's lifetime, so a reference lifetime, cannot outlive the lifetime of its lender. That's for memory safety, of course. You cannot reference a value after it was being freed. There's also more to it. We'll look at it soon. Remember the lifetime, like the lifecycle of your values? A variable's lifetime always begins when it's created and ends when it is destroyed. This information is then, of course, used for checking those lifetimes. Also, most of the time, we do not have to refer to lifetime explicitly because the compiler takes care of it for us. Sometimes, though, we have to, and that's this notation of a tick and then some letter or a word, just to remember for the next slides.

Embedding Protocols into Types

Using ownership, borrowing, and lifetimes together, we can do something, in my opinion, very interesting. We can start embedding protocols, so runtime protocols at compile time into our types. Here, take a fascinating example or super interesting example from Rust's primary serialization library. Specifically, we will look at a serializer, so a type which facilitates serializing a single value. I simplified it a bit, so if you look into the actual library, which is called Serde, it's a bit more complex, but this captures this core behavior. We start with a serializer. Whenever I want to serialize something, something else passes me a serializer, and the serializer implements multiple methods for all kinds of high-level or general kinds of values, like for an integer 32, for float 64, for an enum, but also for struct. Let's look at how the serialization of a struct would work. We have the serialize_struct function, and it takes self by value.

Instead of just referencing the serializer instance that we are calling it on, we are actually consuming the serializer instance itself, which means that I call this function, and after this, I won't be able to access the same serializer again. Look at its return type. It returns a SerializeStruct. You can see it as a transformation. It transforms this original, more generic serializer into a more specialized one, which is specialized for serializing a struct. This struct serializer now implements two methods itself. One is called serialize_field, which is for serializing a single field. I simplified the signature a bit, but the core of it is that you see how it takes immutable reference to self, so you can call it over and again because it does not consume the instance it's being called on. You can call it over and again because it just references it. This is expected because you can have multiple fields you want to serialize one after another.

Then, look at the end function. The end function marks ending or finalizing this serialization step. This can actually have a lot of implications for a real protocol you want to serialize. It might mean that you now finally replace the closing brace in JSON. It can mean that you compute some file header that's being written, which has the total size of your struct in it in bytes. It holds some gravity calling this end function. Oftentimes, in serialization libraries, you then have, for example, a docstring which tells you, do not call any other functions which serialize data after calling end. You might even get a crash out of it, or if it's better, build this library, you get a runtime error. In Rust, this is not necessary because we can just say the end function consumes self. It consumes the SerializeStruct instance. In terms of ownership, this means that the value that holds the struct serializer gives up ownership of the struct serializer and moves it into the end function. In the end function, we can basically just do our finishing, and then the value goes out of scope and it's being dropped and it cannot be used again.

Now look at this in practice, how this would actually look like. I have a normal serializer, and get it from somewhere, and construct a struct_serializer by calling serialize_struct. Next step, I do number of serializations of fields. Now I say I'm done and I want to finish it, so I call end. This, remember, now moves the ownership from the struct_serializer variable into this end method, but it's not returned anywhere. The lifetime now of the struct_serializer ends and it's being dropped. If I would try to call it again, we actually get a compile time error which tells us, borrow of moved value: struct_serializer. This I think is super interesting because you eliminate a full class of developer bugs completely at compile time. You save a lot of error handling you would have to do otherwise. All this by embedding this runtime protocol into your type system. The Rust type system makes this possible.

Truly Protected Access with Mutex

Another example which is related to this is the one of modeling this kind of access to some shared data through Mutex. Oftentimes, this is modeled as two separate things. One, the data you want to protect and then a Mutex where you have this contract with the developer that in order to access this value you have to lock the Mutex first. Only if you locked it you are allowed to now, for example, dereference a pointer. In Rust this works a little bit more different and a bit more robust. First of all, let's look at the new method which creates us a new Mutex. You see again that there's no reference in the signature which means that the data we want to protect is moved into the Mutex. We give up ownership, we can no longer reference the data or use it outside of the Mutex because we give up ownership over it and move it into the Mutex itself.

Now, if we lock a Mutex and the locking is successful, we get back a MutexGuard. Here we for the first time see an explicit lifetime, this tick a. This tick a means that the lifetime of the MutexGuard is bound to the lifetime of the Mutex itself. To break this down a bit, this means that the MutexGuard is not allowed to outlive the Mutex itself. I'm not allowed to hold the MutexGuard for any longer than the Mutex itself is actually alive. It gets even more interesting. I can access the data now through the MutexGuard by just dereferencing the guard. Also, I get just a reference to my data. By lifetime rules, this reference of my underlying data, it's also bound to the lifetime now of the MutexGuard. Of course, since the lifetime of the MutexGuard is bounded by the lifetime of the Mutex itself, this also means that this lifetime of our borrow here of the reference we have can also not outlive the Mutex itself.

This is memory safety. It gets even more interesting, because the MutexGuard unlocks the Mutex when it goes out of scope, when it is being dropped. Now let's put all of this together. We know that our reference to our underlying data, which we can only get through a MutexGuard, cannot live longer than the MutexGuard itself. Accessing the reference to our underlying data after the MutexGuard was dropped would give us a compile time error. Since the MutexGuard itself, when it goes out of scope, unlocks the Mutex, this means that it's impossible for us to obtain or use a reference to our protected data when we do not own a lock. This is super interesting, because I've worked on heavily multi-threaded code and so many cases where people accidentally held a reference, passed it around, and forgot about it, and then unlocked the Mutex, and you suddenly have a concurrency problem.

Generics: Powerful Stand-Ins

The final concept for today, and this is the concept of generics. Generics, we've seen it, for example, in the option type. Generics are stand-ins in Rust. They are definitions that can be reused with different concrete types. For example, in the option, it can be replaced, or another example is a vector. It's often used in collections, because, of course, you usually just want to implement the type once, and then reuse it with all sorts of different types that it's storing. Generics are always replaced at compile time with concrete types, through a process called monomorphization. This means that for every specialization, some specific code is being generated, so eliminating any runtime overhead. It's just as fast as working with concrete types in the first place. We could also do more with generics. We can bound them by traits and lifetimes, like bounding something by lifetime. We've seen this before with the MutexGuard, but also by traits. Traits in Rust are just a concept of saying what kind of behavior must a type support. That's not so interesting for us today.

State Machine with Generics and Typestate

Let's revisit state machines. I told you that state can easily be modeled in Rust with enums. This is true and super practical in a lot of cases. In some situations, a different pattern can be more helpful. This is using the typestate pattern. Typestate just means this concept we've already visited of encoding state information at compile time into a type system. Now let's look at it in more detail. I define three new types, an Uninit type, init type, and an ExecutingJob type. Those are the same states we had in our original state machine in the enum, just encoded as variants. The Uninit type, remember we said we don't have any data associated with it. Here it's just an empty struct. It's a zero-size type. It doesn't take up any space. You can just view it as a marker. A marker that's used in the compiler as, that's some specific type.

Now we have the init type, which could have data like the position we talked about. Another type called the ExecutingJob type, which then has, in addition to the position, also our job that we're currently executing. We could use them as they are and just define state transitions between just those types. This would be completely fine. Often, we also have some additional data that's associated with this thing we're trying to model the state of that we also want to store somewhere. Like a robot can have a name and I want to refer to this name and use it somewhere. Instead of just using those types by themselves, I embed them into now like a higher-level type. In this case, let's say that's just a robot. The robot is generic over some state s. Those are those angled brackets and those s. Now I can use those s as a type stand-in in, for example, my fields, and say, ok, this robot has a state and the state is of type s.

At this time, I do not know the state yet. I just know that it will be some concrete state that will be specified out at compile time. I can implement behavior for this robot in this implementation block. Notice how I do not specify out the s. This means that this functionality works for every robot in any state. It makes sense because we said that we always have a name available of our robot. It shouldn't depend on what state we're in. That's what this is modeling.

Now let's assume we're in the Uninit state. I create another implementation block. Here I specify out the generic to be of type Uninit. We're no longer talking about a collection, like a set of types. I'm talking about a specific type, a Robot. That's what I would just call it now, a Robot type. For this, I implement an init function which takes ownership of self and returns a new robot in an init type and also takes a position. I'm just constructing a new robot type. Of course, oftentimes you only have then some additional functionality or behavior that runs under the hood. For simplicity, it's just like a constructor now. We could also now look at the robot in an init state. There we do the same again. State transition becomes now a method implementation on this concrete type, a Robot. There we again take ownership of self.

We take the data we then need. Or maybe this data is computed in the function itself from somewhere else. Then we return a new type, a robot in ExecutingJob state. Also again, here we say we have a position available. Let's make this available to people from the outside by implementing a public method, just call position, and now we return the position. Instead of putting this position method on the generic robot, where we would have to return it as optional data, and you as the caller would always have to check, is this there or is this not there? Or even then do all this error handling when you know it must be there but I do not get the correct type, so I have to force it into the correct type. This makes it so much easier because you know in this init state you must have a position.

Let's look at this in practice. We get a Robot. I just pop it from some theoretical queue that we have. It's in the Uninit state, so I explicitly actually marked it as Robot to give you a hint of what type this is. I can call the name method on it just fine. Calling the position method on it now gives me a compile time error saying, no method named position found for the struct Robot in the current scope. In this error message there will be even more. There's even then suggestions telling you, yes, but you know this method is available on a Robot or robot in ExecutingJob state. The compiler actually helps you out there a lot. Last example. This is now even a bit more advanced. This is leveraging this exactly same kind of pattern for builders. The builder pattern is just a pattern where you say you have some rather complex type or some type which has a complex creation flow.

For example, an HTTP request where you say that I want to set headers, I want to set a body, and then I want to finish it in some way, and I have to do some validation on there if this is correct or not. You do it in two steps usually. You create a builder type and this builder type facilitates this building process. Then you finish up this builder by calling some finish method which then returns you the final type.

Let's look at how we can model this in Rust. I will use the example of building a robot simulation, so a simulation of a single robot. Let's say we need to provide it two kinds of information, its initial position and the kind of map it should be using, so the map it's driving on. We create a RobotSimulationBuilder type. Here we use generics again, a P generic, so a stand-in for position, and an M generic for the map. Let's go one step back again. I define those four structs, and all those are zero size so they don't hold any data. They are just like markers for the compiler. NoPosition, PositionSet, NoMap, and MapSet. We always start out in this specific state of NoPosition and NoMap. This RobotSimulationBuilder with those two specified old generics which are replaced by NoPosition and NoMap is now a concrete type. On this type, what I can do is I can set a position or I can set a map.

Let's look at the function signature again. That's something that's probably for recurring but we consumed self to invalidate the previous value we had. We transform it now into a new type, in the set position case, for example, into RobotSimulationBuilder where we change the NoPosition type in the generic specialization to the PositionSet type, but we keep NoMap the same. Let's look at what we implement now for this concrete type of RobotSimulationBuilder PositionSet but NoMap. We only implement a set_map method because we already set the position. We do not need it again. We just say set_map. This can be very interesting for cases where resetting the same value is, for example, expensive because you do some expensive computation in the background or even some external requests or something. Or where it's just from the contract itself that you're trying to build invalid of providing the same value multiple times.

We can prevent this already at compile time doing this way. Now only the SetMap method is available on this specific type specialization. It returns us a RobotSimulationBuilder with PositionSet and MapSet. We're done now because all we have to do here is just call build, and build will now do the final building step. We know that we provided everything we need, so there's no reason for having any errors that could be returned. We just straight up return the robot simulation.

Robustness Does Not Need to Be Hard

What I wanted to tell you today is that robustness does not need to be hard. It can be hard, and achieving an extremely high level of confidence in a software will still stay a very challenging problem which requires strict processes and expensive tooling. For practical purposes, most of us are not building safety critical applications. We are just building applications that need to be reliable because people depend on them. Rust helps us a lot in achieving a level of robustness through leveraging the type system that's hard to achieve with other languages I know of.

Questions and Answers

Participant 1: This works well if you have two generic elements. I hope it is possible that you have like one P and one NoMap, for example, so you don't have to implement the SetMap in every permutation of your implementation.

Andy Brinkmeyer: Yes, I know we're getting it. Of course, if you have 10 different kinds of things you want to set, it becomes impractical to write out all permutations by hand. Rust has an extremely strong macros system. Macros in Rust are not just some text templating. You can in Rust actually write code that then generates new code at compile time. You can actually implement a macro that does all this method generation for you.

Participant 1: You couldn't write like RobotSimulationBuilder P and NoMap. That would not work, like having one of the parts being generic.

Andy Brinkmeyer: Yes, this doesn't work. You have to specify out. If you have something, you could of course write P and then just the generic M not specialized to a type. I don't know where this would be useful.

Participant 2: You mentioned the ownership and borrowing model. I assume this is completely done at compile time and no extra code is added.

Andy Brinkmeyer: Yes.

Participant 2: Also, then of course it's not possible to like during runtime reflect who is the owner of something or something like this.

Andy Brinkmeyer: No, there's no runtime reflection of who's an owner, because that's really a concept at compile time that check complete at compile time. At runtime you do not have a way of looking up what kind of other variable owns this value. That's nothing that's possible.

Participant 3: You mentioned monomorphization to transform generics into monomorphic types. How complex or hard is it on the compiler? Does it make it slow? All of these wonderful checks which are done at compile time, how heavy are those on the compiler?

Andy Brinkmeyer: You're getting to one of the core improvements the Rust compiler team is still working on which is about compile times. Compile times of course with such a complex compiler take longer than in other languages. That's just a fact. There are a lot of checks that are happening, super complex algorithms about figuring out ownership. Rust invests a lot into the development speed, with incremental compilation that you do not have to compile everything from scratch again. Most of the time you're working on a project, even if it's massive, your compilation times are usually in the seconds. Of course, then creating a production build, this can take some time. Oftentimes, you throw this off to your continuous integration worker anyway and this takes care of it. I can also tell you that I've seen TypeScript projects of web applications where the build takes longer than our big Rust application.

Participant 4: In an earlier slide you were talking about enums, they could have different states, could have different data, those kinds of things. Coming from a C++ background, do we need to worry about memory locality or things? Do I have a collection of data that could exist in many different places in memory? If I have this enum is collecting data and then I change it into another enum and that has additional data for the state, now am I actually pulling that from memory from many different parts of the application memory pool?

Andy Brinkmeyer: You're asking one of the data localities and the other about reallocation of data?

Participant 4: Yes.

Andy Brinkmeyer: No, enums are allocated once, so either on the stack or on the heap depending on where you allocate them. The size of the enum is the size of the largest variant. It's comparable to unions, for example, in C where the compiler figures out like what are the requirements for alignment and for size and everything, and then allocates it appropriately. If you then actually modify the enum into a different variant, you don't actually reallocate or pull data from different places into there. There are even optimizations to make them as small as possible. For example in Rust since there's no null value, Rust has this concept of actually then marking types as those types cannot just be null and this actually is then used in enums in cases where I know that the value that it holds that there's nothing which can be null that you can even then mark some variants as. That you can even then have a variant, for example, where just everything in memory is replaced by a null value and that's how you mark this variant to save even more data.

Participant 5: I understood that Rust has nightly support for partial specialization of generics.

Andy Brinkmeyer: What do you mean by partial specialization?

Participant 5: The first question, having an implementation that specializes only on some of the generic templates.

Andy Brinkmeyer: If you say so, of course, yes, then it's probably a nightly feature currently. There are only things like variadic generics which have been in the pipeline for a long time of being able to just specify generics and say you expect a various number of them which you do not know yet. There's still a lot of work on the generic system of course as well.

 

See more presentations with transcripts

 

Recorded at:

Jul 06, 2026

BT