CoRecursive: Coding Stories - Tech Talk: Rust And Bitter C++ Developers With Jim Blandy
Episode Date: May 16, 2018Tech Talks are in-depth technical discussions. Rust, the programming language, seems to be really trendy these days. Trendy to me means shows up a lot on hacker news. Rust is really interesting ...language though, and I think the growing popularity is deserved. Today I talk with Jim Blandy, one of the authors of Programming Rust. We talk about what problems rust is trying to solve, the unique language features and type system of rust. It includes both algebraic data types, type classes, and generics. There is even a proposal somewhere for adding HKT. We also touch on why it is so hard to secure code. Jim works on Firefox and his insights into the difficulty of writing secure code are super interesting. Show notes Rust Programming Rust Book MESI protocol Constraint-based Verification of Parameterized Cache Coherence Protocols Formal Methods in System Design Rust Validation - 3d game demo - (not sure where this is, post in comments if you find it) integer overflow
Transcript
Discussion (0)
Welcome to Code Recursive, where we bring you discussions with thought leaders in the world of software development.
I am Adam, your host.
So it is sort of asking them to give things up.
But the thing is, what you get in return is exactly this guaranteed memory safety, right?
And this guaranteed freedom from data races.
And it's just this huge win.
Writing secure, performant, multi-threaded code is very difficult.
Today, I talked to Jim Blandy about Rust, a programming language that is trying to make this a lot easier.
We also talked about why it's so hard to write secure code.
Jim works on Firefox, and his insights into the difficulty of writing secure code are super interesting.
I also asked Jim about Red Bean Software, a software company that refuses to sell software
at all. Jim Blandy is the co-author of Programming Rust, among many other things. Jim, welcome to the
show. Hi, thanks for having me. Yeah, it's great to have you. I have
your book and I really hoped I would have made it a long way through it before I talked to you,
but I can see the bookmark. It's about a quarter of the way through.
Yeah, it ended up a lot longer than we wanted it to be.
Oh, I don't think it's your fault that i haven't made it
what we had in mind originally we wanted to do something that was more like you know k and r
like this slim volume just covers exactly what you need um but basically rust is just it's a big
language that you know is trying to address the things that people need. And it ended up just being, we took a little while to cover that.
Yeah, it's not a small language, I wouldn't say.
Yeah.
So what is Rust for?
What is it targeting?
Well, I mean, the funny answer is that it's targeting all those bitter C++ programmers
who are sick and tired of writing security holes.
Basically, Jason and I, my co-author Jason and I,
we both have worked on Mozilla's SpiderMonkey JavaScript engine.
And that is a big pile of C++ code.
It's got a compiler front end.
It's got a bytecode interpreter.
It's got two JITs.
It's got a garbage collector,
which is compacting and incremental and generational. And working on SpiderMonkey is kind of hair-raising, right? Because Firefox, even as it's not the lead browser,
we still have hundreds of millions of users.
And that means that when we make a mistake in the code, we can expose just a huge number of people to potential exploits.
And of course, when you're working in this field, and this is true, I think, of all the browsers,
you know, you get the CVEs, things get published,
you find out about exploits.
And so it really is very humbling, right?
And so whether you are writing the code,
which is going to go out in front of all these millions of people,
or whether you are reviewing the code, right?
You know, somebody has finished their patch,
and they flag you for review. You know, if you're reviewing the code, right? You know, somebody has finished their patch and they flag you for review.
You know, if you're the reviewer,
you're sort of like the last line of defense, right?
This is the last chance for a bug to get caught
before it becomes, you know, becomes an exploit.
And, you know, and you sort of get used to it.
You sort of accept that this is the way,
that this is the way things are done.
Then you start working in Rust.
I just was curious about Rust because the original creator of the language, Graydon Hoare, is a personal friend of mine.
We worked together at Red Hat.
Modern Rust has gone far beyond what Graydon started with, so I wouldn't say that it's his language anymore.
But I was curious.
I've been following its development since the beginning, so I was curious about it.
And once I really started getting into it, I realized that this is systems programming, where I, as the programmer, am in control of exactly how much memory I use.
I have full control over how
things are laid out in memory. The basic operations of the language correspond closely to the basic
operations of the processor. So as a systems programmer, I have all the control that I need, But I don't have to worry about memory errors and memory safety.
And just like I say, you know, when you're working on a security-critical C++ code base,
like Jason and I have been, you sort of get used to it, right?
And you sort of internalize that, like, this is just, you know,
the standard that you're being held to is actually perfection, right? Because that's what it is, right? The smallest mistake. And these people, when you read about the exploits that people show off at Black Hat, it's just amazing. Just the ingenuity and work and just blood, sweat, and tears that people put into breaking things is really impressive.
You know, you've internalized that. And then suddenly you work in rust, and that weight is lifted off your shoulders. And it is like getting out of a bad relationship, right?
You know, you just sort of got used to being like, just treated badly. And then
suddenly somebody is reasonable to you. And you're like, holy cow, I am never going to do that ever
again. Right? Because it's really and then and then the then the, the next thing is that when
you get to work on concurrent code, in Rust, right, actually trying to take a problem
and distribute it across multiple cores.
Rust is constructed so that when your program compiles,
once your program compiles,
it is free of databases by construction,
assuming that you're not using unsafe code.
And in C++,
everybody thinks that their multithreaded code is fine.
Everybody understands what a mutex is and how it works.
The primitives are not difficult to understand at all.
But then you end up getting surprised by what's actually going on in your code when you have to work on it.
We had one of the engineers here at Mozilla.
Firefox is a heavily multi-threaded program.
I think when you start up, there's like 40 or 50 threads that get going.
And the garbage collector does stuff off-thread.
The JavaScript compiler will push compilation work off to a separate thread.
We do I.O., like, for example, when a tab is trying to write something to local storage,
that I.O. is often pushed off to a worker thread, and it's sort of handled asynchronously on the main thread.
So it's a very heavily multithreaded program. the thread sanitizer tool, to look for data races,
to actually look at our code and observe, you know,
how well we were doing in keeping data properly synchronized.
And what he found was that in every case where Firefox uses threads,
we had data races.
Oh, wow.
Not most, every single case.
So, yeah, that's kind of astounding.
So let's back up.
So what's a data race?
Oh, okay.
So a data race is when you have one thread
right to a memory location,
and then another thread reads it,
but there is no synchronization operation that occurs between those two.
That's not like nobody releases a mutex, the other person acquires the mutex.
Or the write isn't atomic, or there isn't a sort of message sent.
There are any number of primitives the language provides
that ensure memory synchronization.
And the reason this is an issue,
it's an issue for two reasons.
One is that whenever you have
any kind of non-trivial data structure,
the way they're always implemented
is you have a method, right?
Or a function, just any operation on that data structure.
And the method will temporarily relax the invariance that that data structure is built on,
do the work, and then put the invariance back into place.
For example, if you're just trying to push an element
on the end of a vector, right?
Usually it will write the new element to the end of the vector, and then it will increment the vector's length, right? Well, at that midpoint between those two operations, the vector's got this extra element that it's supposed to own, but the length doesn't reflect that, right?
So there's this momentary relaxation of the invariance of the type that the length actually is accurate.
Or even more so, if you are appending an element to a vector and the vector has to reallocate its
buffer. So first, it's going to allocate a larger buffer in memory. Next, it's going to copy over
the existing elements to that new buffer, right? At which point, there are actually two copies of
every element, right? Which is kind of strange, right? Which which point there are actually two copies of every element, right?
Which is kind of strange, right?
Which one is the owning copy?
Which one is live?
And then it frees the old buffer
and then it sets the vector's pointer
to point to the new buffer and, you know, like that, right?
So that's a more complicated operation
where in the midst of this operation,
the vector is actually in this wildly incoherent state, right?
But by the time the method returns,
the vector is guaranteed to be back in shape
and ready to use again, right?
And so when you have data races,
getting back to data races,
the problem with unsynchronized access is that it
means that you can have one thread observing the states of your vector, or really of any
non-trivial type, while it is in the midst of being modified by some other thread.
And so whatever invariance the vector's methods are counting on holding in order to function correctly may not hold.
And so that's sort of the language level view of things.
But then modern processors, of course, add even further complication to the mix where each processor will have its own cache.
And although they do have cache coherency protocols trying to keep everything synchronized,
it turns out that even Intel processors, which try to make fairly strong promises about memory coherence,
it's still visible that each processor will queue up writes to main memory.
That is, if you are one core on a multi-core machine and you're doing a whole bunch of
writes to memory, those writes actually get queued up and reads on that same core will
actually, you know, if you try to read a location that you just wrote to, it'll say, oh, wait,
I see in my store queue that I just wrote to this.
And so I'm going to give you the value that I just wrote, even though the other cores will
not have seen those writes yet. And so the other thing that synchronization ensures is that at the
processor level, the memory that you are about to read is guaranteed to see the values that were
written by the processor that wrote to them,
assuming that both have executed the proper synchronization.
So a data race is a write to a location by one processor, by one thread,
and then a read from that location from another thread without proper synchronization.
And it can cause invariance to be violated,
and you can encounter memory coherence errors.
The hardware thing you mentioned is interesting.
Maybe it's a bit of divergence.
So how does that work?
So if there's writes queued up to a certain sector or something,
and you are reading from it, does it block until those writes go through?
Is that what you're saying?
Okay, so this is something that...
So the processors change over time,
and the different processors have different details
about exactly how they do this.
And so I'm not sure that I'm going to be able
to accurately describe current Intel processors,
but this is as I remember it.
What you've got at the basic level, you've got the caches that are communicating with each other about what they have cached locally.
Like, for example, if nobody has read a particular block of memory, then that's fine, right? But when one core brings a particular block of memory into its cache,
it'll actually mark that and say, okay, I've got this,
but I haven't written to it yet.
And it's okay for other cores to read that memory.
And so maybe all the cores, maybe it's a big block of read-only memory,
maybe it's, I don't know, maybe it's strings, static strings
or something like that.
And so all the cores can bring copies of that memory into their caches and then use it. However,
before a core is able to write to a block of memory, it says, I need exclusive access to that.
And it actually broadcasts out on this local bus for the purpose of this kind of
communication and says, okay, all you other cores, I'm about to write to this block of memory,
please evict it from your caches and mark it as exclusively mine. And so all the other cores,
they kick out that block from their caches. They say, we don't know what's in this lock of memory anymore. Only that guy knows
what's in it. So then that processor that's obtained exclusive access to that block of
memory can do what it pleases, right? And then in order for the other cores to actually even read
from that memory now, they have to go and get a copy of it back from or you know force the the core that was
writing to it to flush it's what it had back to me in memory and so then it goes back into the
shared state um and so they call it the messy protocol it's m e s i which is like uh geez i
can't remember what it is um but like um e stands for, there are four letters are the names of the four
states that a particular block can be in an E is exclusive access, which is when you're writing
something s is for shared access, when you're when it's actually just everybody has the same
copies, and everybody's just reading from it. And I think I is invalid, where like,
somebody else is writing to it. And so your copy of it is bogus.
So that's just keeping the caches coherent.
But then the other thing is that writes are a lot slower than reads.
And so each core has a queue of the writes to memory that it has made
that it is waiting to write out to main memory.
And so if you do a whole bunch of stores,
your store queue will get filled up
with a list of these things going out.
And if the core which has done the writes
tries to read,
then certainly it knows
what the current values of things are.
But the other cores can't see it yet, can't see those writes yet.
And so the way that you can get incoherence,
the way that you can end up with different cores having a different idea
of what order things happened in,
is when one core gets the result out of its star queue,
and then the other core gets the result out of its store queue, and then the other core gets the result out of main memory.
And so you can end up with different cores seeing writes to memory seem to happen in a different order.
And the history of this is actually really interesting. For a long time, Intel would have sections of their processor manuals where they tried to explain how this worked.
And they would make these very friendly promises like, oh, yes, everything's coherent.
Don't worry.
You just do the writes you want to, and everybody sees them.
And then there was this group.
I could look up the reference later if you're curious.
But there was this group in either Cambridge, I think,
or Oxford. Anyway,
a very theoretically
inclined group who
basically said, we're going to actually make a
formal model
of the memory
that we're actually going to formalize the memory
model that Intel has
formalized it in.
They made up
a little logic that says which
executions are acceptable and which executions
are permitted by this and which executions
are not permitted by this. Now, again,
the specification doesn't say
exactly what happens.
It just says what the rules
are. So it says,
this could never happen. This might
happen. So it identifies a set of acceptable
executions, not a specific, it doesn't tell you exactly which one the processor is going to do,
right? It just specifies a set of acceptable executions or a predicate that you could run
on execution to say this was real or this is not acceptable, right? So anyway, so what this
research group did is they said, well, let's take them at their word, and we're going to write tests.
We're going to use this formal, we're going to use this specification that we've written, that we made up, right, because all we've got is English to work with.
And we're going to generate a ton of tests that we will run on the actual processor to see if the processors actually behave the way they
are claims to behave in the manual. And I mean, you can tell, obviously, the answer is no, right?
That Intel themselves in their own documentation did not correctly describe the behavior of their
own processors. And so this group, and the great thing about it, what was really powerful was that their techniques allowed them to just, you know, generate lots of tests and then find ones that failed.
And then they were able to reduce them.
So when they published, they had very short examples.
If you run this sequence of instructions on one core and this sequence of instructions on another core, you will observe these results, which are forbidden by the spec.
Right. So it's really nice. It was really just like, here's your book, you know.
And basically what they found was that in general, yes, the the the the messy protocols do work as advertised.
But the thing that you really have to add,
the thing that you have to put it,
you add to the picture to make it accurate is the,
the,
the store cues,
the right cues.
Because if you have a right that hasn't happened yet,
then you're going to have this.
If you have a right,
if you have a right that you've done,
if you have,
if you've just done a right,
you will see that right before other cores will see it.
So anyway, this is the kind of thing, right, just to bring this back to Rust.
This is the kind of thing where, you know, it sort of raises, I think, a programmer's sort of macho hackles.
You say, well, you know, that seems pretty tough for most people, but I can handle it.
Everybody says that.
I catch myself thinking that.
It's not true.
You're not up to the task of being perfect. pushing your algorithm out across multiple cores, pushing your code out to run in multiple threads,
and just know that you may have bugs,
but they're not going to be these bugs
that depend on exactly the order
in which memory writes happened
and the exact instructions that the compiler selected
and things like that. It is just a huge win.
So data races, data races are out.
Yeah, data races are out.
So how?
Well, so the key idea of Rust, which is something, and this is, I think, really the thing that
most programmers get hung up on when they learn it, is that Rust takes control of aliasing.
And by aliasing, I mean the ability to reach the same piece of memory under two different sort of names under two different expressions right
the example that i give in the book um is uh i actually give a c++ example right um and i say
okay you've got this is c++ mind you this is not rust um so you got int x right and and it's it's
beautiful right it's an order it's not x, it's just int x, right?
And then you take a const pointer to it.
So I say const int star x or star p, right?
I've got a const int pointer p and I say, you know, equals ampersand x.
So I've got a constant pointer to a non-const x. Now, the way C++ works, you cannot assign now to star p, right? If you try to,
you know, assign a variable to star p or, you know, use the increment operator on it or something
like that, then that's a type error. You're forbidden from using p to modify the referent
to the pointer. But you can assign to x, no problem, right?
And so you can go ahead and change the value of x
any time you want.
And so it's not the case that just because p
is a pointer to a constant int,
that the integer it points to is constant, right?
How perverse is that, right?
I mean, like, what does const mean if it can change?
Okay, the thing is, I want to make clear that there are uses for this kind of thing, right?
It is pretty useful to say, well, through this pointer, I'm not going to change this value, right?
So I'm not saying it's useless, but it is kind of, you know, not what you expect.
And so, but if you think about what it would take to fix that, right, to say, well, if I'm going to say that you know that this pointer to this thing
that this point this is really a pointer to a constant thing that would mean that for as long
as that pointer p exists the pointer to a constant that all other access or that all other modification
of the thing that it points to has to be forbidden, right? You have to basically, as long as P is pointing to X,
you have to make sure that X can't be modified, right?
And so that's what I mean by aliasing,
that star P, that is dereferencing the pointer P,
and X are both things that you can write in your program
that refer to the same location, right?
And this kind of aliasing can arise under pretty much same location, right? And this kind of aliasing can arise
under pretty much any circumstance, right?
Anytime you have, you know, two paths through the heap
that all arrive at the same object, right?
Anytime you have a shared object, right,
in the graph of objects,
that's two ways to get to the same location.
And there will generally be two different expressions that you could write to refer to the same location and there will generally be two different expressions
that you could write to refer to the same object right so javascript lets you do this
java lets you do this basically every language lets you create aliases
and what rust does is it actually restricts your ability to use pointers such that it can tell when something is aliased,
and it can say, okay, for this period, for this portion of the program,
these objects are reachable by, basically, there's two kinds of pointers.
There's shared pointers, and then there's shared references, and there's mutable references. So it'll say
these objects are reachable by shared references, and thus they must not be changeable.
Right? And so you know, not
just that you can't change those values through those shared pointers,
but you know that nobody else can change them either. So it's really
powerful. When you have in Rust, if you have a shared reference to an int,
you know that int will never change.
If you have a shared reference to a string,
you know that string will never change.
If you have a shared reference to a hash table,
you know that no entry in that hash table will ever change
while you have that shared reference,
as long as you have that shared reference.
And so once that reference goes out of scope,
then changes could happen.
Exactly. Exactly. Exactly. And then the other kind of reference is a mutable reference,
right? Where what it says is you have the right to modify this, but nobody else does,
right? Nobody else has the right to even see it. And so a mutable reference is like, it's basically, it's a very exclusive kind of pointer. So when you have a mutable reference to a hash table,
nobody else can touch that hash table while you have it. And that's statically guaranteed. It's
part of the type rules. It's guaranteed by the type rules around mutable references.
And so you can imagine that any type system which can guarantee this thing about like, oh,
there's nothing else, there's no other live way in the system
to even refer to the referent of this mutable pointer.
That's a pretty powerful type system.
And, you know, working through the implications of that,
I think is where most people stumble learning Rust.
That there is this strict segregation between shared references where you
have shared immutable access and where you have mutable references where it is exclusive access.
So there's this strict segregation between sharing and mutation. And the way that Rust
accomplishes that is, I think, really novel and something people aren't used to.
And honestly, when you tell – I was having lunch with a very accomplished programmer who is an old friend.
We hadn't talked in years.
And we were talking about Rust, and he says, yeah, but I can't create cycles.
I mean, I'm a programmer.
I know exactly what I want to do with those cycles.
I want to have data structures that are arbitrary graphs, and I need those data structures.
And Rust won't let me make them, and so I'm not interested.
And so I think he's wrong, but I think he's making a poor choice.
But he is correct in his assessment that basically Rust really is asking you to give up something that is just such a fundamental tool that most programmers have just internalized, and they've learned to think in those terms.
So it is sort of asking them to give things up. But the thing is, what you get in
return is exactly this guaranteed memory safety, I think I mentioned the programmer machismo.
I want a gender neutral term for that.
But like basically the programmer's pride, right?
The programmer's like that little bit of little confidence that you've got, right?
You want to flip that from people saying, oh, I can handle, you know, data races.
I can handle, you know, unsynchronized memory access.
No problem, right?
You want to flip them from thinking that to thinking, oh, I can write my code in this
restricted type system, right?
You want to say, you want to make them say, I can feel, I can handle, I can get things done even though Rust is restrictive, right? You want to say, you want to make them say, I can feel, I can handle,
I can get things done, even though rust is restrictive, right? I can overcome these things.
I can take this, uh, this limited, uh, you know, buttoned down system and make it.
Maybe people just shouldn't be so invested in their own pride. I don't know.
I'm not optimistic about that ever happening.
But one thing is, it sounds like what you're talking about, right, is it's like changing
the relationship you have with the compiler. I mean, I think some people view a compiler
as like a teacher with like a ruler that hits you on your hands, like, don't do that.
But there's an alternative way where maybe it's more like an assistant.
Yeah. Yeah. Yeah, yeah, yeah.
And what's going on a lot with Rust is that your debugging time
is getting moved from runtime, right, to compile time.
That is, the time that you would spend chasing down pointer problems in C++,
you instead spend in negotiation with the compiler
about your borrows and about your lifetimes.
And the thing about it is,
the difference is that tests
only cover a selected set of executions.
My tests cause the program to do this.
It runs it through its spaces in this specific way,
whereas types cover every possible execution.
Definitely.
Right?
And so that's the property that really makes it wonderful for concurrency,
which is that with concurrency, you have to just give up on your tests
really exercising all possible, you know,
executions. Because, you know, the rate at which different cores run the code and the way we have
the threads get scheduled and what else happened to be competing for your cache at the time,
none of that stuff is really something that you can get a grip on. And so having a type system that is all possible executions are okay is exactly what
what the doctor ordered for that so is there are we at risk of there just being a problem with the
type system um yeah sure i mean if the type system isn't sound, then you lose, right? Or we lose. In fact, so one nice thing is that the people who are sort of the lead designers of the type system right now, as I understand it, are Aaron Turin and Nico Matsakis.
And in particular, Nico is the one who had this insight about,
hey, we have the possibility of really separating sharing and mutation
and keeping those two things completely segregated.
And that's what I think is really the defining characteristic of Rust,
or rather the defining novelty of Rust.
And so they work, when they talk about type systems, they're playing with,
um, with PLT redux, which is a, which is a system from the, from the PLT group, um,
that, that made a racket and all that stuff, um, uh, for looking at, for playing with formal
systems and looking at derivations in formal systems
but they're not proving things about it there is then a project uh called rust belt um i mean
there's also a conference called rust belt but uh rust belt is a project um at a german
uh university where they're actually trying to formalize rust it's a research
program where they say okay we are a group of people and we're going to work on um finding
formal models of the rust type system and and rust semantics um and um uh in in particular, there's a guy, Ralph Young, who is really taking this on.
And he is working on machine-verified proofs of the soundness of Rust's type system.
Now, it turns out that there are aspects of Rust that make this very interesting and challenging
and turn it into something that just has never been done before.
In particular, all of Rust is built on a foundation of unsafe code.
And unsafe code is code where it uses primitive operations
whose safety the compiler cannot check.
These operations, they still can be used safely. whose safety the compiler cannot check, right?
These operations, they still have, you know,
they still can be used safely, right?
They just have additional rules in order to be used safely that you as the programmer can know.
So what do you mean to say
that it's built on a foundation of unsafe code?
Well, the Rust vector type, for example,
the vector type itself is safe to use, right?
If you are writing Rust code and you use the vector type,
it's a fundamental type in the standard library.
It's like, you know, it's the list of...
It would be the analog of Haskell's list or something like that.
You can't get away with it. You can't not use it.
And so, basically, if you are using vector, then you are at no risk, right? Any mistakes that you make using
vectors will be caught by the type checker and the borrow checker, right? At compile time.
So Rust, so vector is safe to use. Vec is safe to use.
But the implementation of vec uses operations whose correctness the compiler itself cannot be sure of.
In particular, when you want to push a value onto the end of a vector,
what that's doing is that's taking this section of memory,
which, like, again, you've got to imagine the vector has a big buffer, right?
And it's got some spare space at the end of the buffer.
And you're going to push a new value.
Say you're going to push a string onto the end of that vector, right?
So vector strings.
And, well, okay, that's a...
You're transferring ownership of the vector from...
You're transferring ownership of the string
from, you know, whoever's calling the push method to the vector itself.
And so there's a bit of uninitialized memory at the end of the vector's buffer,
or towards the end of the vector's buffer, which is now having a string moved into it.
And in order for that to be correct, in order to make sure that you don't end
up with two strings thinking they're both same on the same amount of memory and in order to make
sure that you don't that you don't leak the memory um uh it has to be guaranteed or it has to be the
case it has to be true that the memory that you're moving the string into is uninitialized, right? And whether or not the location that something gets
pushed onto is uninitialized or not depends on the vector being, you know, being correct, right?
That is, the vector knows the address of its buffer, it knows its length, and it knows its
capacity, right? The actual in-memory size of the buffer. And so the vector has to have, A, checked that there is spare capacity, that the length is less than the capacity,
right? And that length has to have been accurately maintained through all the other operations on the
vector. If there is a bug in the vector code, and the length ends up being wrong, then this push operation,
which transfers ownership, can end up, say, overwriting some existing element of the vector,
right? And then that could be a memory flaw, a memory problem. But the nice thing is that VEC is a pretty simple type. It's built on some modules which have a very simple job to do, right?
And so that is a small piece of code that we can audit
to ensure that the vector is using its memory correctly.
And once we have verified by hand, by inspection,
that the vector is using its memory correctly, then we can trust the types of the vector's methods to ensure that the users will always use it correctly.
Right?
So the users have no concern. It's only we who implement the vector who are responsible for this extra level of vigilance and making sure that we're getting the memory right. So the type system can be
and is being formally verified, but the libraries need to be hand audited? What's vector written in?
Is it written in Rust? Vector is written in Rust, right? And that's the key, right?
Is that unsafe code in Rust is sort of this escape hatch
that lets you do things that you know as the programmer,
that you as the programmer know are correct,
but that the type system can't recognize as correct, right?
So, for example, implementing vector is one of them, right?
So vector itself is written in Rust.
It uses selected unsafe code.
This is exactly what the Rust Belt project is
tackling. In order to really make meaningful statements
about Rust, you're going to have to actually be able to handle unsafe code
because the primitive operations of Rust, like the
synchronization operations,
the stuff that implements Vue texts,
the stuff that implements inter-thread communication channels,
or the basic memory management things
that get memory, that obtain memory,
free memory for a vector's buffer,
or that free a vector's buffer when the vector is disposed of,
or the I.O. operations that say, look, we're going to read
the contents of a file or data from a socket into this memory
without wiping out random other stuff around it.
All of those things are sort of
code that no type system can really...
Well, yeah, I think you can say that. They're primitive operations, and so no type system can really, well, yeah, I think you can say that.
They're primitive operations, and so no type system can really say what they do.
But you can use unsafe code and make sure that you use them correctly.
And then, assuming that your unsafe code is right,
you can build well-typed things on top of those that are safe to use.
And so this two-level structure of having unsafe code at the bottom
and then having typed code on the top is what allows people to have some confidence in the system.
And so the Rust Belt people actually want to understand the semantics of unsafe code
and actually spell out what the requirements are
in order to use these features safely. And then they want to verify that Rust standard library
does indeed use them correctly. So they're really going for the whole enchilada. They want to really
put all of Rust on a firm theoretical foundation. And it's really exciting.
And the trade-off,
like as a user of the language,
it seems to make sense to me.
So you're saying like,
rather than, you know,
needing to audit my code
to make sure these issues don't exist,
I can trust that the system
has been formally verified,
except for these unsafe primitives,
which have been audited themselves.
Yeah.
Yeah. Well, yeah.
Basically, if you don't use unsafe code,
then the compiler prevents all undefined behavior,
prevents all data races, prevents all memory errors.
If you don't use unsafe code, you are safe.
If you do use unsafe code, you are, or unsafe features,
you are responsible for making sure that you meet the additional requirements that they impose above and beyond the type system.
And so, yeah, I mean, and so either you can figure out how to fit your problem into the
safe type system. And the nice thing about Rust is that the safe type system is actually really good and quite usable.
And most programs do not need to resort to unsafe code.
So you can either work in that world,
which is what I almost always try to do,
or if you really need, if there's a primitive
that you really know is correct,
but that Rust, that the type system can't handle,
then you can drop down to unsafe code, and you can implement that. And one of the things we, one of the strategies that we emphasize
in the unsafe chapter of the book, it's the very last chapter after we presented everything else,
is one of the strategies that we encourage people to use is to make sure that, or to try to design interfaces such that once the types check, that you know that all of your unsafe code is A-OK, right?
And then that means that you've exported a safe interface to your users.
And so if you have an unsafe trick that you want to use, you isolate that unsafe trick in one module, right, that has a safe interface to the outside world.
And then you can go ahead and use that technique
and not worry about its safety anymore.
You use the module,
and then the module's own types ensure that it's okay.
The unsafe code doesn't escape, right?
Exactly, exactly.
It sounds
similar to the, to the idea, like, you know, people be writing some Haskell function that
claims to do no side effects, but for performance reasons, maybe it's actually doing some sort of,
um, you know, generating a random number or maybe that's a bad example, but, uh, it's totally
hidden from the user, right? It acts, It acts pure from the outside, whatever may happen.
Yeah, that's a good example.
That's a good example because the question comes, the question arises, is it really pure from the outside?
If they did it right, if they really actually kept all of the statefulness local and isolated so that you can't tell from the outside, then everything's fine.
The people and the rest of the, whoever's using that from the outside
can use it and not worry about it.
And they get the performance and they don't have to worry about, you know, the details.
But then inside, the people who wrote that code are, they have extra responsibilities, right?
And the normal Haskell guarantees of statelessness don't, don't apply to them
because they've, they've broken the rules or they've stepped outside the rules and they are,
they're now responsible. You mentioned, um, the type system of Rust and, uh, actually it has a
lot of, a lot of features that, that I guess you wouldn't expect from something that's,
well, it may be, I didn't expect it. it has a lot of functional uh feeling features oh yeah
you know i'm really glad i'm really glad that you brought that up because i mean so i've talked
about safety and i think i've talked about performance right but but rust the nice thing
really nice thing about rust is that it is not by any means a hair shirt right it is actually
really comfortable to use it has a a very powerful generic type system.
The trait system is a lot like type classes in Haskell.
If you've used type classes in Haskell,
I mean, everybody uses type classes in Haskell,
whether they know it or not, right?
And yeah, so Rust has traits.
The whole standard library is designed around,
you know, those generics and those traits,
and it puts them to full use.
And it's actually a super comfortable system to use.
I did a tutorial at OSCON in Austin last May where we went through,
to the extent that you can in three hours,
writing a networked video game.
And so that involved 3D graphics, it involved a little bit of networking,
and it involved some game logic, right?
And when I was working on it, obviously I had to have the game ready for the talk.
And, you know, I, I didn't, it took me, it took me, I put it off.
And so I had to do the last stages of development in a rush.
And it was fantastic.
It was like I had, I had wings or something because once i'd gotten something
written down right once i'd really gotten the types right um it was done it was done if it
had been if i had been working in c++ i would have had to randomly take three hours out of
the schedule to to fix to track something down uh and debug it. And because it was Rust, I just got to keep going forward, forward, forward.
And so it was just like really great progress.
And Rust has all these sort of batteries included kind of things.
Like there's a crate out there called Serdy,
which is for serializing, deserializing.
Serializer, deserializer.
And it is a very nice collection of formats like
there's json there's a binary format there's xml there's there's you know a bunch of other stuff
right and then a set of rust types that can be serialized string hash table vector um you know
what what have you.
And Serity is very carefully constructed so that if you have any type
which it knows how to serialize or deserialize,
then you can use that with any format
that it knows how to read or write.
So you just pick something off of this list
and then pick something off of that list
and you're immediately ready to go
for sending something across the network. uh and in fact it will actually you can actually and that
naturally if you define your own types you can specify how they should be serialized or
deserialized right um you know you define your own custom struct and say well here's how okay
but the thing is that's real boilerplate right? So there is actually this magic thing that you can say,
you can slap on the top of your own type definition.
You can say, derive, serialize, and deserialize.
And what that does, I mean, I guess Haskell has something like this too,
that automatically generates the methods.
It looks at the type and automatically generates the methods
to serialize and deserialize that type. And so it is super easy to get your stuff ready to communicate across
the network. And so for, you know, for communicating people's moves and communicating
the state of the board, that was, it was just, just a blast because there was all of this sort of
boilerplate stuff that I didn't have to worry about.
And those are just the kind of power tools that are wonderful.
Just for a callback, I think that's like generic derivation. And I did have Miles on the show earlier.
He wrote something similar for Scala.
And yeah, Haskell has it.
I think it was originally called Scrap Your Boilerplate.
But yeah, very cool feature, right?
A lot of boilerplate can be removed by things like that.
Yeah, Scrap Your Boilerplate is done within the Haskell type system,
if I remember that paper right.
And Surdy is doing a little bit of procedural macro,
kind of exactly looks at your type definition
and decides what to do with it.
And I wonder
maybe that stuff
could be done in a scrap your boilerplate
style.
I don't
understand scrap your boilerplate well enough to say.
But yeah, it's that
style of thing.
And those are just wonderful power tools.
One thing, so it sounds, I think you're making a good thing. And yeah, and those are just wonderful power tools. One thing, so it sounds,
I think you're making a good argument.
So Rust, you know, apparently hard to learn.
This is what I've heard.
However, once you learn it, there's a superpower.
Yeah.
So is this superpower applicable to non, you know, C++ devs?
Is this a useful skill for somebody
who's throwing up web services?
I think so.
So I work in...
Okay, so you had Edwin on talking about Idris,
and Edwin made a comment that I want to push back on.
He said,
I don't think that types really help people
eliminate bugs that much
because unit tests are still useful.
And so I work, right now I work in the developer tools part of Mozilla.
And we have a JavaScript front end.
The user interface for the developer tools in Firefox,
they're written themselves in JavaScript.
And it's a React Redux app, basically,
that talks over a JSON-based protocol to a server that runs in the debug key
and looks at the web page for you.
And we are, I'm proud to say that my colleagues are enthusiastic about the potential for types,
and they really see the value of static typing. And we are more and more bringing,
we're using flow types in JavaScript.
We're bringing flow types into our code base.
But it's not done, right?
We haven't pushed them all the way through.
There's plenty of untyped code still.
Because JavaScript flow types let you type one file
but leave the rest of the file,
leave the rest of the system untyped.
And so you can gradually introduce types
to more and more of your code as you go.
So we're in that process.
And of the bugs that I end up tracking down,
I think, and I don't want to put a number to it because i mean i haven't been like
keeping statistics but it feels like uh at least half of them uh would have been caught immediately
by static typing right i've heard i've heard people say this when uh when typescript like
moving to typescript which is similar right that? They're like often. Yeah, yeah, yeah.
Same idea. Often they found a, yeah, like a, you know, not a super obscure bug,
but like a little bit like a corners where things would go wrong,
that the type system was like, what are you doing here?
Well, the thing is, the thing is, I think people like the people who work in Haskell
or, you know, certainly somebody who works on Idris, I don't think
they really know what the JavaScript world is like, right? It's just insane what people do.
Okay. In JavaScript, if you typo the name of a property on an object, right? It's just a typo,
right? You capitalize it wrong or something.
That's not an error.
JavaScript just gives you undefined as the value. And then undefined just shows up at some random point in your program.
And so until you have complete unit test coverage of every single line,
you don't even know whether you've got typos.
That's crazy.
That is just not what humans are good at
and it's exactly what computers are good at and so to to not to make that to put that on the human
programmers shoulders is doesn't make any sense um so now to be fair to edwin, he does have t-shirts that say, it compiles, ship it.
Oh no, I thought that was a really good podcast.
We're all fans of, or we're all
really curious about Idris.
But I think
that we don't want to undersell
the benefits of static typing.
Back to your question, for
people who aren't doing systems
programming, why would they be interested in Rust? Rust is just a really good productive language to
work in. It will make you worry about a bunch of things that maybe you thought you shouldn't have
to think about. But in retrospect, I kind of feel like I'm happy to have those things brought to my attention. Like, for example, at the very beginning,
I talked about how data structures,
the method of a data structure will sort of bring it out of a coherent state
and then put it back into a coherent state.
You want to make sure that you don't observe it in the midst of that process.
Well, you can get those kinds of bugs even in single-threaded code, right?
You can have one function which is working on something
and modifying something,
then it calls something else, calls something else,
goes through a callback,
and you have several call frames, right?
And then suddenly you have something that tries to use
the very data structure that you were operating on
at the beginning, but you weren't aware of it.
And so basically nobody knows that they're trying to read from the data structure that you were operating on at the beginning, but you weren't aware of it. And so basically nobody knows that they're trying to read from the data structure that
they're in the middle of modifying. And that's something that's called the iterator invalidation.
In C++, it's undefined behavior. In Java, you get a concurrent modification exception.
And I just mentioned this to a Java programmer, and he's like, oh, yeah, CMEs.
That's, you know, they had a name for them, right?
They knew.
And that's also, that's totally a single-threaded programming error.
And that's also prevented by Rust's type system.
So I feel like Rust's types actually have a lot of value, even for single-threaded code,
which is not performance-sensitive. But it's just really nice to have a...
It's really got your back in terms of letting you think
or making sure that your program works the way you think it does.
And so, yeah, I mean, I think it has a lot of applicability
as a general-purpose programming language.
And the one thing we didn't talk about,
but I think that you touched on briefly at the beginning,
was to do with security.
So we talked about data races, but you also mentioned security.
Right. Yeah.
So most security, well, sorry,
there are lots of different kinds of security holes.
And according to the collected data,
there are a few people who collect statistics on the published security vulnerabilities and sort of what category they fall into.
You know, is it SQL injection? Is it cross-site scripting? Is it, you know, they sort of categorize them.
And the category that I'm interested in for this particular case is the memory corruption, memory errors. And those have been a consistent, you know, 10, 15% of all the security vulnerabilities
being published altogether, right?
And so there's still a very big issue.
And most of the time, almost all the time, what's happening there is you've got a buffer overrun. After you have seen enough of these attacks,
you start to feel like pretty much any book
could be exploited with enough ingenuity.
It turns out that I can't find this post anymore,
but the Chrome security team,
Google's Chrome browser security team,
had a blog post just about security errors caused by integer overflows.
Integer overflows sounds so innocent, right?
But it turns out that if that integer is the size of something,
you are 90% of the way to perdition, right?
Because basically you can make it think that something is much bigger
than it actually is in memory,
and then you've got access to all kinds of stuff you shouldn't have access to,
and you're really out of the – you've broken out of jail.
And so, yeah, so having a type system which prevents memory errors and which basically makes sure statically that you're not going to, that your program doesn't behave in an undefined way, really does close off a very significant opportunity for security holes. And one of the quotes we open up,
one of the chapter quotes we open up the book with
was a tweet by Andy Wingo,
who is a great JavaScript hacker
and a great free software hacker.
And his tweet was simply,
basically there was a bug in a TrueType parser, right? A font parser. And that was one of the basically there was a bug in a true type parser right a font parser and that was
one of the bugs that was used to break into the machines that were controlling the iranian um
nuclear uh purification facilities oh i didn't know that what what was that what was that called
a stuxnet right yeah stuxnet yeah that that basically was built around a flaw in TrueType, right?
So TrueType, it's a font parser.
TrueType is security-sensitive code.
So basically, all code is security-sensitive.
There's no longer, you can no longer say,
oh, you know, it's just graphics code.
You know, it's not true. If
you're writing C++
code and it's
got controlled memory and it's doing pointer arithmetic,
you've got to be on your toes.
And the standard is
perfection. And so Rust,
same as a data race, it takes
a certain class of these
vulnerabilities off the table?
Yeah, actually it takes
in Rust without unsafe code,
if your program types, then we are saying
it will not exhibit undefined behavior.
And undefined behavior is like often the...
Yeah, is often the root of the security hole.
Awesome.
So we're reaching the end of our time here.
One thing when I was Googling you that I found is your Red Bean software site.
Oh, sure.
I actually ended up forwarding this to a couple of my friends.
It says on it, to all intents and purposes, it appears you have a consulting company that does not sell software.
Is that correct?
Well, first of all, that's really, really old.
My friend Carl Fogel and I, we ran a little company called Cyclic Software, and we were selling CVS support.
We were the first group to distribute network transparent CVS.
We didn't write it, but somebody else wrote it, and they said they didn't want responsibility for it.
And so we were the ones who were distributing it.
And so I'm kind of proud of that because it was Network Transparent CVS
that was really the first version control system
that open source used to collaborate on a large scale.
And then it got replaced by Subversion and Git and Mercurial.
But network transparency at CVS was really how things got started.
So we had Cyclic software, and then we decided we didn't want to run it anymore.
We couldn't run it anymore, and so we sold it to a friend,
and we realized we had to change our email addresses.
We had to tell everybody, don't email us at JimB at Cyclic anymore.
And that's kind of a bummer. We realized that we were going to be changing our email addresses. We had to tell everybody, don't email us at, you know, Jim B. at Cyclic anymore. And, you know, that's kind of a bummer. We realized that we were going to be changing our email addresses every time we changed jobs. So we resolved to create a company whose
sole purpose was never to have any monetary value, right? So we would never have to sell it. And so
we could keep a stable email address for the rest of our lives. I mean, it's a vanity domain and
lots and lots of people have vanity domains.
So,
but,
but our joke is that it's a,
it's a company whose purpose is never to have any value.
Yeah.
I found on the front page,
it says,
let me read this by buying our products.
You will receive nothing of value,
but on the other hand,
we will not claim that you have received anything of value in this.
We differ from other software companies who insist in face of abundant
evidence of the contrary,
that they've sold you a usable and beneficial item yeah that's that's carl
well it's been a lot of fun uh jim thank you uh so much for your time yeah thanks for having me
it was fun and i enjoyed your book uh i'll get through it eventually i think i'm on chapter four
i'm gonna keep working yeah yeah yeah stick with it through the traits and generics chapter.
Because once you've gotten to traits and generics,
then that's really where you've really got everything you need to know
to really read the documentation and understand what you're looking at.
Awesome. Thank you.
And we're really sorry it's chapter 11.
We tried to make it as fast as possible.
No, it's all good.
All right. Take care.
Take care. Take care.