Latest Posts (15 found)
Steve Klabnik 4 months ago

A tale of two Claudes

It was the best of times, it was the worst of times, it was the age of wisdom, it was the age of foolishness, it was the epoch of belief, it was the epoch of incredulity, it was the season of Light, it was the season of Darkness, it was the spring of hope, it was the winter of despair, we had everything before us, we had nothing before us, we were all going direct to Heaven, we were all going direct the other way—in short, the period was so far like the present period, that some of its noisiest authorities insisted on its being received, for good or for evil, in the superlative degree of comparison only. I recently had two very different experiences with Claude Code. I wanted to share them both, because I find the contrast interesting. I have really been enjoying TailwindCSS . I also have started several web projects using it in the past year. Back in January, Tailwind released version 4 . Honestly, I am not a good enough Tailwind user to be able to appreciate the new features, but I do prefer to keep my projects up to date. So I’ve needed to update my Tailwind projects to version 4. Claude… is nearly completely useless for this. You’d think that upgrading a very popular CSS framework would be a straightforward task. But Claude has failed to set it up multiple times. While it’s true that Claude Code claims that its training cutoff is in January of 2025 in its system prompt, it seems to have a cutoff in March. Regardless, a useful tool shouldn’t need to be explicitly trained on the latest version of a framework to be able to help with it. Yet, it consistently fails to either set up Tailwind 4 in new projects, or upgrade existing projects to this fourth release. I did manage to get it to work earlier this week, when I asked it to update this website , but at this point, it was mostly just me messing around. I wish I had saved the exact prompt, but basically I said something like “you have failed me consistently in this task, so please search for upgrade guides and the like to guide you in this update.” It did do that, but for some reason, the upgrade tool couldn’t update my configs, and so it decided to do it itself, and well, while this time it did manage to technically update, it didn’t do it completely correctly. It removed the typography plugin, which I had to ask it to put back. And it didn’t properly import the config into my global CSS, which also caused weird issues. Regardless, it did manage to sort of do it, after some coaxing, but this was a frustrating experience that was probably the third or fourth time I had tried to get it to do this. While Tailwind 4 is a new major version release, it’s still incredibly popular, and one of the hunches I have about LLM usage for coding is that more popular tools will be easier for LLMs to use. This didn’t seem to be the case here, though. For… reasons, I am working on an assembler. Don’t worry about it, you may find out about it someday, and you may not. It’s not for work, just for fun. Anyway, if you’re not familiar, basically this process turns assembly code into machine code. Specifically, in this case, I’m encoding x86_64 assembly into an ELF executable. ELF is a file format that is used by, among other things, Linux, for executables. I’m working with Claude on this, and I again didn’t save my prompts, which I am annoyed by, but I roughly said something like “please implement codegen such that it produces an ELF executable” and let it go. I did not expect this to work, or go well. As I said above, I sort of expect more common tasks to be easier for LLMs, given that they have more training data. While I’m sure there’s some code in Rust out there that does this, I still don’t expect it to be anywhere near as common as TailwindCSS. A few minutes later, Claude said “hey, I did it!” For completely unrelated reasons, I got a message from Ixi , a friend of mine who would be particularly interested in this project, and so I said something along the lines of “hey, I’m working on a project you’d be interested in, claude has just made some large claims: Steve, we’ve successfully implemented a complete end-to-end x86-64 code generation pipeline Do you want to look at the PR together?” and they said yes. So I shared my screen, and we went through the PR together. It was pretty fine. A lot of the stuff at first was normal, but a bit boring: the error handling was fine, and maybe slightly different than I would have done, but it wasn’t so bad that if a co-worker had sent in the PR, I would have objected to merging it. But Ixi noticed something: These sizes are… wrong. They should (probably) be one byte. Fun! Most of the code looked overall reasonable, though. So let’s look at that ELF it produced. I ran the executable, and it segfaulted. Surprise! While Claude did think it was done, because it had produced an ELF file, and the tests were passing, it didn’t actually work. Given that those sizes were wrong, this wasn’t really a surprise. We decided to dig into the code a bit more, but first: I told Claude “hey, this example you compiled segfaults, something is wrong, can you fix it?” Again, not the exact prompt. I wish I had saved it, but I didn’t think I was writing this blog post at the time, so I didn’t. But it was very basic like that. And I let Claude go in the background while we took a look. We used to see some of the information about the ELF file. We ended up using to see where it was crashing: There’s no debug info here, so we only have addresses. There was a jump going to an address that ends in . This is incorrect: was three bytes into a , so… we were jumping to the middle of an instruction, which is nonsense, hence the segfault. (Now that I’m looking at this, I also find these other addresses suspect. I wish I had taken better notes while we were looking at this… the point is this was a quick check as to what was going on, and it was very clear that this is just straight-up wrong.) How could this happen? Well, if the sizes of the instructions were wrong, when the code figures out where to jump, it would go to the wrong place, as it would mis-calculate how far away the jump target was. So this all made sense. Hypothesis: instruction sizes are incorrect, which means the jump calculations are incorrect, which means fail. I had to run, so we ended the call, but I left Claude running. A few minutes later, I checked in on it, and that’s when I was really surprised. Claude had fixed the issue! It wasn’t really even about that, but also, how it did so: Hmmm. It’s kind of right, but also maybe not? I kept scrolling: That also didn’t feel right? More scrollback through. It was creating sample programs in assembly, and then assembling them, and then examining the contents of the files. It was double checking its assumptions and understanding of the code, and then, there it was: Okay, wait… , and not / ? Sure enough, not only had it figured out that and were one byte, not two, but it had also figured out that had a few forms with different sizes, and that did too. After fixing that… the executables worked! But really, the most impressive part is that it had basically done exactly what we had done, but in the background, while we were looking at the code. It took a little longer to get there, because it went down one rabbit hole, but it had also found other bugs that weren’t immediately obvious to us. And, I’ll be extra honest: while I understand what’s going on here, I am not super great at and , and if Ixi wasn’t around, Claude would have probably found this before me, if I were alone. This is part of why having friends is great, but if I didn’t have that friend around at that moment, this would have tremendously helped. I am also impressed because, as mentioned above: I would expect this task to be much more rare than web development stuff. Yet, Claude did a far better job, finding the issue quickly and fixing it, as opposed to the multiple times I gave it a chance with Tailwind. What does this mean? I don’t know, but I figured I want to start documenting these experiences, good and bad, and so I wanted to share both of these stories, one good, and one bad. LLMs are not magic, or perfect, but I have found them to be useful. It’s important to share both the successes, and also the failures. I’m hoping to do a better job of tracking the prompts and details to give you a better understanding of the specifics of how I’m using these tools, but the perfect is the enemy of the good, and so these are the stories you’re going to get for now. Here’s my post about this post on Bluesky: A tale of two Claudes steveklabnik.com/writing/a-ta... A tale of two Claudes Blog post: A tale of two Claudes by Steve Klabnik

0 views
Steve Klabnik 4 months ago

Is Rust faster than C?

Someone on Reddit recently asked: What would make a Rust implementation of something faster than a C implementation, all things being the same? I think this is a great and interesting question! It’s really tough because it ultimately relies on what exactly you mean by “all things being the same.” And I think this is something that makes it hard to compare languages. Here’s some ways in which you can argue that things are “the same” but also, that they’re not, and what they imply for runtime performance. Rust has inline assembly built into the language. C has inline assembly as a very common compiler extension, to the degree where saying it’s not part of the language is an arguable nitpick. Here’s an example in Rust: This reads the time stamp counter with , and returns its value. Here’s the example in C: These (under in both rustc and clang ) produce the same assembly: Here’s a link on Godbolt: https://godbolt.org/z/f7K8cfnx7 Does this count? I don’t know. I don’t think it really speaks to the question asked, but it is one way to answer the question. Rust and C can have different semantics for similar code. Here’s a struct in Rust: Here’s “the same” struct in C: In Rust, this struct is 16 bytes (on x86_64, again) and in C, it is 24. This is because Rust is free to reorder the fields to optimize for size, while C is not. Is this the same, or different? In C, you can re-order the fields to get the same size. In Rust, you can write to get the same layout as C. Does this mean we should have written different Rust or different C to get “the same” thing? Some people have reported that, thanks to Rust’s checks, they are more willing to write code that’s a bit more dangerous than in the equivalent C (or C++), where they’d do a bit more copying to play it a bit safer. This would be “the same” in the sense of the same devs on the same project, but the code would be different, due to judgement calls. You can make an argument that that’s not the same, but is different too. An example of this from a long time ago is the Stylo project. Mozilla tried to parallelize Firefox’s style layout twice in C++, and both times the project failed. The multithreading was too tricky to get right. The third time, they used Rust, and managed to ship. This is the same project, (though not the same programmers, I believe) by the same organization, but one was possible and one was not. Is that “the same”? In some senses, but not in others. This also goes for a similar question: assuming we have a junior developer writing Rust, and also writing C, for the same task. Are we going to get faster code in one or the other? This controls for ability, but not for the same code. Is that “the same”? I don’t know. What about an expert in each language, someone who knows Rust super well but doesn’t know C, and vice versa, being given the same task? Is that different than a junior, or an “average” developer? Another redditor asks: I’m not Rust expert, but aren’t most (all?) of the safety checks compile time checks? They shouldn’t have any runtime impact. A great question! Part of this is another example of defaults being different. is valid in both languages. In Rust, there’s a bounds check at runtime. In C, there is not. Does this mean that they’re the same? In Rust, I could write , and get C’s semantics. In C, I could write a bounds check to get Rust’s semantics. Are any of those “the same”? In Rust, that check may be optimized away, if the compiler can prove it’s safe. In C, if we wrote the bounds check by hand, the check may be optimized away, if the compiler can prove it’s safe. Are any of those “the same”? They aren’t wrong that a lot of Rust’s safety checks are at compile time. But some are at runtime. But this raises another interesting question: That compile-time check may cause you to write different code for the same task as in C. A common example is using indices rather than pointers. That may mean that the generated code performs differently. Is that check truly “at compile time”? Technically, at the micro level, yes. At the engineering level? Possibly not! I think the most important part of this question is related to possibility, that is: I think the answer to that is “no,” even ignoring the inline assembly case. So on that most important, fundamental level, the answer is “there’s no difference between the two.” But we’re not usually talking about that. We’re usually talking about something in the context of engineering, a specific project, with specific developers, with specific time constraints, and so on. I think that there are so many variables that it is difficult to draw generalized conclusions. Here’s my post about this post on BlueSky: Is #rustlang faster than C? steveklabnik.com/writing/is-r... Is Rust faster than C? Blog post: Is Rust faster than C? by Steve Klabnik

0 views
Steve Klabnik 4 months ago

I am disappointed in the AI discourse

Recently, something happened that made me kind of like, break a little bit. I’m still not entirely sure why this whole thing bothers me so much, but here’s a blog post about it. I happened to ask ChatGPT something for fun. I often do this just to see what it says, and if I feel like it’s right or wrong. I tabbed over to another tab, and the top post on my Bluesky feed was something along these lines: ChatGPT is not a search engine. It does not scan the web for information. You cannot use it as a search engine. LLMs only generate statistically likely sentences. The thing is… ChatGPT was over there, in the other tab, searching the web. And the answer I got was pretty good. I’m not linking to the post because I don’t want to continue to pile on to this person, because it’s not really about them. It’s about the state of the discourse around AI. I am not claiming that ChatGPT is an amazing search engine. What is breaking my brain a little bit is that all of the discussion online around AI is so incredibly polarized. This isn’t a “the middle is always right” sort of thing either, to be clear. It’s more that both the pro-AI and anti-AI sides are loudly proclaiming things that are pretty trivially verifiable as not true. On the anti side, you have things like the above. And on the pro side, you have people talking about how no human is going to be programming in 2026. Both of these things fail a very basic test of “@gork is this true?” (sorry this is a bad joke, if you’re not terminally 38 years old on Bluesky, substitute “chat is this true?” (sorry this is a bad joke, if you’re over the age of 40 substitute “is this true?” (sorry humor is one way of coping with stress and writing this post is stressing me out))). Oh, and of course, ethical considerations of technology are important too. I’d like to have better discussions about that as well. For me, capabilities precede the ethical dimension, because the capabilities inform what is and isn’t ethical. (EDIT: I am a little unhappy with my phrasing here. A shocker given that I threw this paragraph together in haste! What I’m trying to get at is that I think these two things are inescapably intertwined: you cannot determine the ethics of something until you know what it is. I do not mean that capabilities are somehow more important.) But I also know reasonable people disagree with me on that. Let’s talk about it! I mean that for all dimensions of this topic, not solely the capabilities. Now, it’s not like I expect everyone to come to some consensus on a technology. For example, a lot of people like Rust and hate Go, and a lot of people like Go and hate Rust. That’s fine. That doesn’t bother me. But there’s something different about this that is driving me up a wall. To be clear, I am not particularly pro or anti AI. Here’s what I currently think: Anyway, just to be clear, (how many times can you say ‘just to be clear’ in one post, Steve, come on (at what level of nested parenthetical do I implement some sort of interlocutor into my blog like Xe )) I don’t think that if you think LLMs suck at software development, you’re wrong. What I want to be able to do is talk about it in a reasonable way, and figure out why we have different opinions. That is it. But for some reason, being able to do that feels impossible. I’m going to end up blogging more about AI/LLMs in the near future, to get some more of this stuff off of my chest and to try and be the change I want to see in the world. So expect some of that, probably. But until then, if you happen to have any links to reasonable discussion on these topics, I’d love to see them. Here’s two posts I recently read and really enjoyed thinking about what they were saying: Thanks. If you want to get mad about this post please just ignore me. Here’s my post about this post on Bluesky: I am disappointed in the AI discourse steveklabnik.com/writing/i-am... I am disappointed in the AI discourse

0 views
Steve Klabnik 5 months ago

Rust 1.0, ten years later

It’s kind of hard to believe it’s been ten years since Rust 1.0 was released. I stopped working on Rust three years ago, and frankly, the last three years have been pretty rough. But 2025 is shaping up to be a really good year for me, with lots of positive changes. With that in mind, this post is going to be a bit of a different one than you might expect. For a fantastic post that’s a bit more traditional, check out what Graydon wrote for the Rust Foundation: 10 years of stable Rust: an infrastructure story . It’s fantastic, of course. I’d instead like to reflect a bit on a comment I saw on the internet the other day: [Rust is] not a great hobby language but it is a fantastic professional language, precisely because of the ease of refactors and speed of development that comes with the type system and borrow checker. Patrick Walton , one of the earliest contributors to Rust, had this to say: I never thought I’d live to see the day when someone would say this. The first 5 years of Rust were all “this is interesting for hobby projects but nobody will ever adopt this in industry”. The release of Rust 1.0 was great, but it was also stressful. The few years before Rust 1.0 were marked by a deep, deep anxiety among the team. I’m sure someone else who was around back then will disagree with me, but that’s my recollection. It may not have always been overt, but it was there. A large question was looming over us: would anyone actually use this thing? Would it become a hobbyist language, or would it be something that people actually used in production? At this point, we now know the answer: yes, Rust is used a lot. It’s used for real, critical projects to do actual work by some of the largest companies in our industry. We did good. The release of 1.0 was the start of that journey in earnest. It was time to stop changing things all the time, and to start focusing on stability. It was time to gain more users, and to fix the issues that they found. We had to start shipping stable releases, and we had to start doing it on a regular basis. We had to start doing all of the things that a real programming language does. Over the last ten years, a lot changed. It wasn’t easy, and it took a lot of blood, sweat, and many, many tears. But we did it. We built a real programming language, and we built a real community around it. Rust is very much not perfect. There’s more work to be done, and arguably, someone out there should probably be starting a language that will someday eat Rust’s lunch. But ten years ago, we were pushing a button and praying that it would all work out. And now, ten years later, I’m doing this: and then going about my day. It is a rare privilege to be able to spend ten years building a tool you want to use, and then be able to spend your time at work using it. I am grateful to have had that opportunity, and I hope that everyone else who has worked on Rust over the years feels the same way. Here’s my post about this post on BlueSky: #rustlang 1.0, ten years later steveklabnik.com/writing/rust... Rust 1.0, ten years later

0 views
Steve Klabnik 5 months ago

Thoughts on Bluesky Verification

Today, Bluesky rolled out blue checks . And, I was given one. Now, I’m not the biggest fan of this sort of feature, however, I also don’t really think this kind of feature is for me. I really like the idea of domain verification; I have been like “oh okay that’s coming from a account” from time to time. But I don’t think most people really think about domains the way that programmers do. I have wanted to update my “How does Bluesky work?” post for a while now, but I’ve been super busy. So, let’s ignore the product question for now, and focus on the technical question. How does verification work? I’ve also been thinking about designing apps on top of atproto. There’s a kind of rule of thumb that I realized about doing this, a sort of “golden rule” if you will: You are the only person who can write records into your PDS. This has really interesting implications! For example, sometimes people ask for “soft blocks” on Bluesky. This is a twitter ‘feature’ where if you block someone, and then unblock them, they’re not following you any more. But on Bluesky, if you soft block someone, they’re still following you. Why does that happen? Following someone on Bluesky means that you write a record of the lexicon into your PDS. So you might assume that blocking someone would delete this record from their PDS. But that would violate the golden rule! You can’t delete records from someone else’s PDS. So, instead, blocking someone on Bluesky means that you write a record of the lexicon into your PDS. Unblocking them deletes that record from your PDS. But it doesn’t delete the record from their PDS. So, if you block someone, and then unblock them, they still have a record in their PDS that says they’re following you. This may not be the behavior that you want as a user, but it follows from the constraints of the overall protocol design. Now that we know the golden rule, we can understand how verification works. How does an account get verified? Well, someone has to be writing a record into a PDS somewhere. The natural design is the one they’ve chosen: someone becomes verified by someone else writing a record of the type into their PDS. Here is the record verifying me. It looks like this: This is in ’s PDS. It describes my DID, and my handle and display name. One thing not mentioned in the blog post, but is in the comments of the lexicon, is that changing your handle or display name makes this record invalid. I didn’t know that! Good to remember in case I decide to make some jokes in my display name. That’s the basics: a record exists, and a blue check shows up on my account. But wait, isn’t this centralized? I thought Bluesky was decentralized? Well, yes and no. You see, anyone can put a record of this type into their PDS. So, if I wanted to verify someone else, I can do that too: here’s me verifying . But if you go to Jerry’s profile, you won’t see a blue check. Why is that? Well, because the Bluesky AppView only shows a blue check if the record is from a “trusted verifier.” Who is that? Well, as far as we know, it’s from and . Why can’t we tell more? Well, they’ve implemented this as a database column in the AppView, it’s not “in-protocol,” in other words. EDIT: Samuel has provided us with the list: Replying to an unknown post @bsky.app @nytimes.com @wired.com @theathletic.bsky.social 26 10 Read 2 replies on Bluesky So in some sense this is a centralized feature: clients can display information however they choose, and they’ve made a product choice that only trusted verifiers will show up as blue checks. But in another sense, it’s not centralized, as anyone has the power to verify anyone else. Alternative clients can decide to show or hide any of this information; Deer Social , one of these alternate clients, has added a user preference to hide all blue checks, for example. A different one could show every verification as a blue check, or display them differently, say one badge per verification, styled like the avatar of the person who verified you. EDIT: turns out that the default AppView also lets you hide blue checks: Steve Klabnik @steveklabnik.com · Apr 21 Thoughts on Bluesky verification steveklabnik.com/writing/thou... Thoughts on Bluesky Verification steveklabnik.com jolheiser @jolheiser.com This was very informative, thanks for the write-up! I was curious about being able to hide the checkmarks, but unless I'm mistaken this appears to also be an option on the bluesky appview. At least I was able to check the preference in my settings->moderation->verification settings 2 0 Read 1 reply on Bluesky The point is still the same though: there’s choice here. Back to the original post! The underlying protocol is totally open, but you can make an argument that most users will use the main client, and therefore, in practice it’s more centralized than not. My mental model of it is the bundled list of root CAs that browsers ship; in theory, DNS is completely decentralized, but in practice, a few root CAs are more trusted than others. It’s sort of similar here, except there’s no real “delegation” involved: verifiers are peers, not a tree. This core design allows Bluesky to adjust the way it works in the future fairly easily, they could allow you to decide who to trust, for example. We’ll see how this feature evolves over time. I’m flattered that I was chosen to be verified; if I was trying to ship this feature, my name wouldn’t come up 181st on the list. I was really curious about the design when they announced this feature, I assumed it was going to be closer to a PGP web of trust, or delegated in some way. This design is much simpler and less centralized than I expected: this is arguably more horizontal than the web of trust would have been. At the same time, I am interested in seeing if this changes the culture of the site; some see the blue check as a status symbol of some kind, and think it means what you say matters more. I don’t think that’s true, personally, but it doesn’t really matter what I think. To redraw the analogy with DNS, the blue check is very similar to the green lock: it doesn’t mean that what you’re saying is true, or right, or good, it just means that you are who you say you are. But even though a blue check is technically isomorphic (or close enough) to a green lock, I think a lot of people perceive it as being more than that. We’ll just have to see. What I am glad about it is that it’s an in-protocol design, rather than an external one like DMs are. I understand why they did it that way, but I’d rather that remain the exception rather than the rule. Here’s my post about this post on BlueSky: Thoughts on Bluesky verification steveklabnik.com/writing/thou... Thoughts on Bluesky Verification

0 views
Steve Klabnik 6 months ago

Thinking like a compiler: places and values in Rust

A while back, someone on the internet asked about this syntax in Rust: They wanted to know how the compiler understands this code, especially if the pointer wasn’t a reference, but a smart pointer. I wrote them a lengthy reply, but wanted to expand and adapt it into a blog post in case a broader audience may be interested. Now, I don’t work on the Rust compiler, and haven’t really ever, but what I do know is language semantics. If you’re a language nerd, this post may not be super interesting to you, other than to learn about Rust’s value categories, but if you haven’t spent a lot of time with the finer details of programming languages, I’m hoping that this may be a neat peek into that world. Programming languages are themselves languages, in the same sense that human languages are. Well, mostly, anyway. The point is, to understand some Rust code like this: We can apply similar tools to how we might understand some “English code” like this: Oh, you also might ask yourself as you’re reading the next sections, “why so many steps?” The short answer is a classic one: by breaking down the big problem of “what does this mean?” into smaller steps, each step is easier. Doing everything at once is way more difficult than a larger number of smaller steps. I’m going to cover the classical ways compilers work, there’s a ton of variety when you start getting into more modern ways of doing things, and often these steps are blended together, or done out of order, all kinds of other things. Handling errors is a huge topic in and of itself! Consider this a starting point, not an ending one. Let’s get into it. The first thing we want to do is to try and figure out if these words are even valid words at all. With computer languages, this process is called “lexical analysis,” though you’ll also hear the term “tokenizing” to describe it. In other words, we’re not interested in any sort of meaning at all at this stage, we just want to figure out what we’re even talking about. So if we look at this English sentence: We follow a two step process in order to tokenize it: we first “scan” it to produce a sequence of “lexemes.” We do this by following some rules. I’m not going to give you a sample of the rules for English here, as this post is already long enough. But you might end up with something like this: Note how we do have in , but is separate from . These are the kinds of rules we’d be following: the is because this is a contraction, but the is not really part of , but its own thing. We then run a second step and evaluate each individual string of characters, turning them into “tokens.” A token is some sort of data type in your compiler, probably, so for example we could do this in Rust: And so the output of our tokenizer might be an array of something like At this point, we know we have something semi-coherent, but we’re still not sure it’s valid yet. On to the next step! Funny enough, this is an area where human-language linguistics and compilers mean things that are slightly different. With human language, parsing is often combined with our next step, which is semantic analysis. But we (most of the time) try to separate syntax and semantic analysis in compilers. Again, I’m going to massively simplify with the English. We’re going to use these rules for what a sentence is: Obviously this is a tiny subset of English, but you get the idea. The goal of syntactic analysis is to turn our sequence of tokens into a richer data structure that’s easier to work with. In other words, we’ve figured out that our sentence is made up of valid sequences of characters, but do they fit the grammatical rules of our language? Note that we also don’t need to store everything; for example, maybe our English data structure looks like this: Where’s the period? How do we know must be capitalized? That’s the job of syntactic analysis. Since every sentence ends with a period, we don’t need to track it in our data structure: the analysis makes sure that it’s true, and then we aren’t doing it again. Likewise, we don’t need to store our subject as a capitalized string if we don’t want to: we determined that the input was, but we can transform it as needed. So our value after syntax analysis might look like this: Often, for computer languages, a tree-like structure works well, and so you’d see an “abstract syntax tree,” or “AST” at the end of this stage. But it’s not strictly speaking required, whatever data structure makes sense for you can work. Now that we have a richer data structure, we’re almost done. Now we have to get into meaning. Imagine our sentence wasn’t “You can’t judge a book by its cover.” but instead this: This is a famous bit of text that’s incoherent. The words are all Latin words, and it feels like it might be a sentence, but it’s nonsense. We could parse this into a : But it’s not valid. How do we determine that ? Well, in the context of English, “Lorem” isn’t a valid English word. So if we were to check that the subject is a valid word, we’d successfully reject this sentence. In computer languages, we’d do similar things like type checking: might lex and parse just fine, but when we try and figure out what it means, we learn it’s nonsense. Except if your language lets you add numbers and strings! After semantic analysis, we’ve determined that our program is good, aka “well formed.” In a compiler, we’d then go on to generate machine code or byte code to represent our program. But that stuff, while incredibly interesting, isn’t what we’re talking about here: remember our original objective? It was to understand this: That’s semantics. So now that we have some background, let’s talk about how to understand this code. Well, to understand the code, we first need to understand how it lexes and parses. In other words, what the grammar of our language is. How our language would lex, tokenize, and then parse our code. In this case, it’s Rust. Rust’s grammar is large and complex, so we’ll only be talking about part of it today. We’re going to focus on statements vs expressions. You may have heard that Rust is an “expression based language” before. Well, this is what people mean. You see, when it comes to most of the things you say in a program, they’re often one of these two things. Expressions are things that produce some sort of value, and statements are used to sequence the evaluation of expressions. That’s a bit abstract, so let’s get concrete. Rust has a few kinds of statements: first, there’s “declaration statements” and “expression statements,” and each have their own sub-kinds as well. Declaration statements have two kinds: item declarations , and let statements . Item declarations are things like or or : they declare certain things exist. statements are probably the most famous form of statement in Rust, they look like this: That’s… a mouthful. We haven’t talked about or yet, and we don’t really want to cover some of the more exotic parts of Rust right now. So we’re going to talk about this via a simpler grammar first: This is how we create new variables in Rust: we say , and then a name, an , and then finally some expression. The result of evaluating that expression becomes the value of the variable. This is leaving out a lot: the name isn’t just a name, it’s a pattern, which is very cool. exists in Rust now, and that’s cool. We’re ignoring types here. But you can get the basics with just this simple version. Expression statements are much simpler: The there is an or, so we can either have a single expression followed by a , or a block (denoted by s, which can optionally (the ? means it can exist or not) be followed by a .) So to think like a compiler, you can start to figure out how to combine these rules. For example: Here, we have a let statement, but the expression on the right hand side of the is an . Here’s a pop quiz for you: is the part of the let expression, or part of the expression on the right hand side? The answer is, it’s part of the . The let expression has a mandatory , but the block does not, and so: If we had the semicolon with the block, we’d still need the one for the let, and so we’d have . Which the compiler accepts, but it warns about it. Going back to our original code: We don’t have a , and this isn’t an item declaration: this is an expression statement. We have a , followed by a . So now we have to talk about expressions. There are a lot of expression types in Rust. Section 8.2 of the Reference has 19 sub-sections. Whew! In this case, this code is an Operation Expression, and more specifically, an Assignment Expression : Easy enough! So the left hand side of the is an expression with , and the right hand side is . Easy enough! But these two expressions are in some ways, the entire reason that I wrote this post. We just finally got here! You see, the reference has this to say about assignment expressions: An assignment expression moves a value into a specified place. What are those? C, and older versions of C++, called these two things “lvalue” and “rvalue,” for “left” and “right”, like the sides of the . More recent C++ standards have more categories. Rust splits the difference: it only has two categories, like C, but they map more cleanly to two of C++‘s categories. Rust calls lvalues, the left hand side, a “place,” and rvalues, the right hand side, a “value.” Here’s two more precise definitions, from the Unsafe Code Guidelines: Both of these have expression forms, so a place expression produces a place when it’s evaluated, and a value expression produces a value. And that’s how works: we have a place expression on the left, a value expression on the right, and we put that value in that place. Easy enough! Once again, the code we were trying to figure out: So , the dereference operator, takes the pointer, and evaluates to the place where it points: its address. And gives us the value to put there. Case closed? Not quite! Rust has a trait, , that lets us override the operator. So let’s talk about this example, to make things easier: You can play with it here . We didn’t talk about the kind of expression that’s relevant here yet: a path expression. Path expressions that resolve to local or static variables are place expressions, other paths are value expressions. We talked about earlier: in above, the is a path expression, and since it’s resolving to our new variable, that means it’s a place expression. The is a value expression, because it doesn’t resolve to a variable. Let’s talk about , remember our assignment expression: expression = expression; And what it does: it moves a value into a place. To understand how works, we only need to add one more thing: the Dereference expression. It’s produced by the deference operator, . It looks like this: *expression Its semantics are pretty straightforward: That’s it. Now we have enough to fully understand : Whew! That’s that. Thinking like a compiler can be fun! Once you’ve mastered the idea of grammars and gotten used to substituting things, you can figure out all sorts of interesting stuff. In this specific case, sometimes people wonder why returns a reference if the whole goal is to show where something should point… and if you didn’t know that dereference expressions expanded to something with in it, it would be confusing! But now you know. And hopefully learned some interesting things about values, places, and how compilers think about code along the way. Here’s my post about this post on BlueSky: Thinking like a compiler: places and values in #rustlang steveklabnik.com/writing/thin... Thinking like a compiler: places and values in Rust

0 views
Steve Klabnik 7 months ago

Does unsafe undermine Rust's guarantees?

When people first hear about in Rust, they often have questions. A very normal thing to ask is, “wait a minute, doesn’t this defeat the purpose?” And while it’s a perfectly reasonable question, the answer is both straightforward and has a lot of nuance. So let’s talk about it. (The straightforward answer is “no”, by the way.) The first way to think about this sort of thing is to remember that programming languages and their implementations are two different things. This is sometimes difficult to remember, since many programming languages only have one implementation, but this general principle applies even in those cases. That is, there are the desired semantics of the language you are implementing, and then the codebase that makes programs in that language do those things. Let’s examine a “real” programming language to talk about this: Brainfuck . Brainfuck programs are made up of eight different characters, each one performing one operation. Brainfuck programs can only do those eight things, and nothing more. However, this simplicity leads to two interesting observations about the differences between languages in the abstract and their implementations. The first is that properties of the language itself are distinct from properties of an implementation of the language. The semantics of the Brainfuck language don’t provide an opportunity to call some sort of re-usable function abstraction, for example. Yet, when we write a Brainfuck interpreter or compiler, we can use a programming language that supports functions. That doesn’t change that Brainfuck programs themselves can’t define or call functions. And in fact, we often want to implement a language in another language that has more abilities than the language we’re implementing. For example, while it’s possible to implement Brainfuck in Brainfuck, it’s much easier to do so in a language that lets you define and call functions. Another classic example is interacting with hardware. Brainfuck has the instruction that allows you to produce output. But in order to actually produce output to say, a terminal, you interact with your platform’s API to do so. But inside of that API, eventually you’ll hit a layer where there are no abstractions: you need to talk to hardware directly. Doing so is not statically able to be guaranteed to be memory safe, because the hardware/software interface often boils down to “put some information at this arbitrary memory location and the hardware will take it from there.” Our Brainfuck program is operating in an environment just like any other; at the end of the day it has to coordinate with the underlying operating system or hardware at some point. But just because our implementation must do so, doesn’t mean that our Brainfuck programs do so directly. The semantics of programs written in Brainfuck aren’t changed by the fact that the implementation can (and must) do things that are outside of those semantics. Let’s take Ruby as a more realistic example than Brainfuck. Ruby does not let us modify arbitrary memory addresses from within Ruby itself. This means that pure Ruby programs should not be able to produce a segmentation fault. But in the real world, Ruby programs can segfault. This is possible if there’s a bug in the Ruby interpreter’s code. Sure, it’s our Ruby code that’s making the call to a function that results in a segfault, but the real fault lies with the code outside of Ruby’s purview. That this can happen doesn’t mean that Ruby’s guarantees around memory manipulation is somehow useless, or suffers segmentation faults at the same rate as programs in a language that allow you to manipulate arbitrary memory as a matter of course. But it does also mean that we don’t need to look at our entire program to figure out where the problem comes from: it instead comes from our non-Ruby code. Ruby’s guarantees have helped us eliminate a lot of suspects when figuring out whodunit. The second property is that certain implementations may extend the language in arbitrary ways. For example, I could write a Brainfuck interpreter and say that I support a ninth instruction, , that terminates the program when invoked. This would be a non-portable extension, and Brainfuck programs written in my variant wouldn’t work in other interpreters, but sometimes, this technique is useful. Many languages find that these sorts of extensions are useful, and so offer a feature called a “foreign function interface” that allows for your program to invoke code in a different language. This provides the ability to do things that are outside of the domain of the language itself, which can be very useful. Brainfuck does not offer an FFI, but if it did, you could imagine that could be implemented in terms of it, making programs that use portable again, as long as the extended functionality was included somehow, often as a library of some kind. Just like our implementation has the ability to do things outside of our languages’ semantics, FFI and similar extension mechanisms also give us the ability to do arbitrary things. I can write an extension for Ruby that writes to arbitrary memory. And I can cause a segfault. But we’re in the same place that we were with our implementation issues; we know that if we get a segfault, the blame lies not with our Ruby code, but instead, with either the implementation or our FFI. It may seem contradictory that we can call a language “memory safe” if real-world programs have the ability to cause memory problems. But the thing is, it isn’t really programs in that language that caused the issue: it was FFI, which is in a different language entirely, or it was implementation issues, and implementations must do memory unsafe things, thanks to a need to interact with the OS or hardware. And so the definition of “memory safe language” is commonly understood to refer to languages and their implementations in the absence of either implementation bugs or FFI bugs. In practice, these bugs occur at such a low rate compared to languages that are clearly not memory safe that this practical definition serves a good purpose, even if you may feel a bit uncomfortable with these “exceptions.” But there’s actually a deeper reason why these exceptions are acceptable, and that’s due to how we understand properties of programs and programming languages in the first place. That is, this isn’t just a practical “well these exceptions seem fine” sort of thing, they’re actually okay on a deeper level. So how do we know anything about how programs and programming languages work at all? Computer science is no more about computers than astronomy is about telescopes. A fun thing about computer science is that it’s closely related to other disciplines, including math! And so there’s a long history of using math techniques to understand computers and their programs. One technique of building knowledge is the idea of proofs. If you get a degree in computer science, you’ll engage with proofs quite a lot. I even took a class on logic in the philosophy department as part of my degree. I don’t intend to give you a full introduction to the idea of proofs in this blog post, but there’s some high-level concepts that are useful to make sure we’re in the same page about before we go forward. Here is a very classic example of a form of reasoning called a “syllogism”, given by Aristotle in 350 BCE: These first two lines are called “propositions,” and the third is a conclusion. We can base our conclusion on a logical relationship between the information given to us by the propositions. But how do we know propositions are true? Are all men mortal? What’s important here is that we, for the purposes of our proof, assume that propositions are true. And we do this because, on some level, we cannot know everything. And so to begin, we have to start somewhere, and make some assumptions about what we know. It’s true that later, we may discover a fact that disproves our proposition, and now our proof no longer works. But that’s just the way that the world works. It doesn’t prevent these sorts of proofs from being useful to help us gain knowledge about the world and how it works, as best as we can tell at the current time. So on some level, this is also why Ruby is a memory safe language even though a C extension can segfault: there’s always some kind of things that we have to assume are true. Memory safe languages are ones where the amount of code we have to assume is memory safe, rather than is guaranteed to be by the semantics, is small, and preferably indicated in the code itself. Put another way, the amount of code we need to trust is memory safe is large in a memory unsafe language, and small in a memory safe language, rather than zero. As time went on, so did our understanding of logic. And, of course, we even have competing logics! And then this gets fun, because terms can mean something slightly different. For example, in more recent logics, we’d call something like “All men are mortal” to be an axiom , rather than a proposition. Same idea: it’s something that we accept without proof. As computers appeared, people sought to apply the rules for mathematical logic onto them. We even call circuits that carry out classic logical operations “logic gates.” Number theory and logic were foundational to making computers work. And so, once high level languages appeared on the scene, there was interest in applying these mathematical tools to understanding programs as well. This discipline is called “formal verification,” and the general idea is to describe various properties we wish a system to have, and then use formal methods from mathematics to demonstrate that this is true. This area of study is very deep, and I don’t plan to cover the vast majority of it here. However, I do want to pursue one particular thread in this area. “Hoare Logic” is, well: In this paper an attempt is made to explore the logical foundations of computer programming by use of techniques which were first applied in the study of geometry and have later been extended to other branches of mathematics. This involves the elucidation of sets of axioms and rules of inference which can be used in proofs of the properties of computer programs. Examples are given of such axioms and rules, and a formal proof of a simple theorem is displayed. Finally, it is argued that important advantage, both theoretical and practical, may follow from a pursuance of these topics. Incidentally, C. A. R. Hoare, the author of this paper, and Graydon Hoare, the creator of Rust, are unrelated. How does Hoare logic work? Yet again, not going to cover all of it, but the general idea is this: In order to figure out if a program does what it is supposed to do, we need to be able to reason about the state of the program after execution happens. And so we need to be able to describe the state before, and its relationship to the state after. And so you get this notation: P is a precondition, Q is a program, and R is the result of the program executing with those preconditions. But most programs have more than one statement. So how could we model this? Hoare gives us the Rule of Composition: If and then . This allows us to build up a program by proving each statement in turn. Hoare logic is very neat, and I’ve only scratched the surface here. People did a lot of work to extend Hoare logic to include more and more aspects of programs. But then, something else happened. In 2002, Separation Logic: A Logic for Shared Mutable Data Structures was published. In joint work with Peter O’Hearn and others, based on early ideas of Burstall, we have developed an extension of Hoare logic that permits reasoning about low-level imperative programs that use shared mutable data structure. Hmm, shared mutable data structures? Where have I heard that before… Let’s see what they have to say: The problem faced by these approaches is that the correctness of a program that mutates data structures usually depends upon complex restrictions on the sharing in these structures. For sure. Well, what are we to do about this? The key to avoiding this difficulty is to introduce a novel logical operation , called separating conjunction (or sometimes independent or spatial conjunction), that asserts that P and Q hold for disjoint portions of the addressable storage. What disjoint portions of addressable storage might we care about? Our intent is to capture the low-level character of machine language. One can think of the store as describing the contents of registers, and the heap as describing the contents of an addressable memory. Pretty useful! Before we talk a bit about how separation logic works, consider this paragraph on why it’s named as such: Since these logics are based on the idea that the structure of an assertion can describe the separation of storage into disjoint components,we have come to use the term separation logics, both for the extension of predicate calculus with the separation operators and the resulting extension of Hoare logic. A more precise name might be storage separation logics, since it is becoming apparent that the underlying idea can be generalized to describe the separation of other kinds of resources. The plot thickens. Anyway, in Separation Logic, we use slightly different notation than Hoare Logic: This says that we start with the precondition , and if the program executes, it will not have undefined behavior, and if it terminates, will hold. Furthermore, there is the “frame rule”, which I am going to butcher the notation for because I haven’t bothered to install something to render math correctly just for this post: where no free variable in is modified by . Why is it interesting to add something to both sides of an equation, in a sense? Well what this gives us the ability to do is add any predicates about parts of the program that doesn’t modify or mutate. You might think of or even just ownership in general: we can reason about just these individual parts of a program, separately from the rest of it. In other words, we have some foundational ideas for ownership and even bits of borrowing, and while this original paper doesn’t involve concurrency, eventually Concurrent Separation Logic would become a thing as well. I think the paper explains why the frame rule matters better than I can: Every valid specification is “tight” in the sense that every cell in its footprint must either be allocated by or asserted to be active by ; “locality” is the opposite property that everything asserted to be active belongs to the footprint. The role of the frame rule is to infer from a local specification of a command the more global specification appropriate to the larger footprint of an enclosing command. What this gives us is something called “local reasoning,” and local reasoning is awesome . Before I talk about that, I want to leave you with one other very interesting paragraph: Since our logic permits programs to use unrestricted address arithmetic,there is little hope of constructing any general-purpose garbage collector. On the other hand, the situation for the older logic, in which addresses are disjoint from integers, is more hopeful. However, it is clear that this logic permits one to make assertions, such as “The heap contains two elements” that might be falsified by the execution of a garbage collector, even though, in any realistic sense, such an execution is unobservable. Anyway. Let’s talk about local vs global analysis. Proving things about programs isn’t easy. But one thing that can make it even harder is that, for many properties of many programs you’d want to analyze, you need to do a global analysis . As an example, let’s use Ruby. Ruby is an incredibly dynamic programming language, which makes it fairly resistant to static analysis. Here is a Ruby program. Do you know if this program executes successfully? Yeah, it prints . But what about this Ruby program? We can’t know. may contain no relevant code, but it also might include something like this: In which case, when we try and call , it no longer exists: Ouch. So in this case, we’re going to need the whole code of our program in order to be able to figure out what’s going on here. Incidentally, Sorbet is a very cool project to add type checking to Ruby. They do require access to the entire Ruby source code in order to do their analysis. But they also made some decisions to help make it more tractable; if you try Sorbet out on the web, the type checker is fast . What happens when you try the above code with Sorbet? This is a very fair tradeoff! It’s very common with various forms of analysis to choose certain restrictions in order to make what they want to do tractable. Ruby has so much evil in it, just not supporting some of the more obscure things is completely fair, in my opinion. The opposite of global analysis is, well, local analysis. Let’s consider a Rust equivalent of our Ruby: Can we know if this program works? Sure, everything is here. Now, if we tried the same trick as the Ruby code, we know that it would work, because Rust doesn’t have the ability to remove the definition of like Ruby does. So let’s try something else: Can we know if is properly typed here? We can, even though we know nothing about the body. That’s because takes any value that implements the trait, and we know that returns , which implements . Typechecking is local to main’s implementation, we don’t need to look into the bodies of any function it calls to determine if it’s well-typed. We only need to know their signatures. If we didn’t require to have a type signature, we’d have to peek into its body to figure out what it returns, and so on for any function that calls in its body. Instead of this arbitrarily deep process, we can just look at the signature and be done with it. So why is local analysis so helpful? The first reason is either speed or scalability, depending on how you want to look at it. If you are running global analysis checks, they will get more expensive the larger your codebase, since they will require checking the entire thing to work. Whereas local analysis only requires a local context, it doesn’t get any more expensive when you add more code to your project, only when you change that local context. So when you’re trying to scale checks up to larger projects, local analysis is crucial. But I also like to personally think about it as a sort of abstraction. That is, global analysis is a leaky abstraction: changes in one part of the codebase can cascade into other parts. Remember this line about the frame rule? The role of the frame rule is to infer from a local specification of a command the more global specification appropriate to the larger footprint of an enclosing command. If we have local reasoning, we can be sure that changes locally don’t break out of the boundaries of those changes. As long as the types of our function don’t change, we can mess with the body as much as we want, and we know that the rest of the analysis of the program is still intact. This is really useful, in the same way that abstractions are useful when building programs more generally. Okay, we have taken a real deep dive here. What’s this all have to do with unsafe Rust? Well, seven years ago, RustBelt: Securing the Foundations of the Rust Programming Language by Ralf Jung, Jaques-Henri Jourdan, Robbert Krebbers, and Derek Dreyer, was published: In this paper, we give the first formal (and machine-checked) safety proof for a language representing a realistic subset of Rust. But it’s more exciting than that: Our proof is extensible in the sense that, for each new Rust library that uses unsafe features, we can say what verification condition it must satisfy in order for it to be deemed a safe extension to the language. The paper begins by talking about why verifying Rust is such a challenge: Consequently, to overcome this restriction, the implementations of Rust’s standard libraries make widespread use of unsafe operations, such as “raw pointer” manipulations for which aliasing is not tracked. The developers of these libraries claim that their uses of unsafe code have been properly “encapsulated”, meaning that if programmers make use of the APIs exported by these libraries but otherwise avoid the use of unsafe operations themselves, then their programs should never exhibit any unsafe/undefined behaviors. In effect, these libraries extend the expressive power of Rust’s type system by loosening its ownership discipline on aliased mutable state in a modular, controlled fashion This is basically the question asked at the start of this post: can you honestly say that a function is safe if it contains code as part of its implementation? They go on to describe the first challenge for the project: choosing the correct logic to model Rust in (some information cut for clarity): Iris is a language-generic framework for higher-order concurrent separation logic which in the past year has been equipped with tactical support for conducting machine-checked proofs of programs in Coq. By virtue of being a separation logic, Iris comes with built-in support for reasoning modularly about ownership. Moreover, the main selling point of Iris is its support for deriving custom program logics for different domains using only a small set of primitive mechanisms. In the case of RustBelt, we used Iris to derive a novel lifetime logic, whose primary feature is a notion of borrow propositions that mirrors the “borrowing” mechanism for tracking aliasing in Rust. Separation logic! So how does this work? There are three parts to the proof. I’m going to summarize them because I’m already quoting extensively, please go read the paper if you want the exact details, it is well written. If all three of these things are true, then the program is safe to execute. This is also exciting because, thanks to 3, when someone writes new unsafe code, they can get RustBelt to let them know which properties they need to satisfy to make sure that their unsafe code doesn’t cause problems. Of course, there is more to do following up on RustBelt: We do not model (1) more relaxed forms of atomic accesses, which Rust uses for efficiency in libraries like Arc; (2) Rust’s trait objects (comparable to interfaces in Java), which can pose safety issues due to their interactions with lifetimes; or (3) stack unwinding when a panic occurs, which causes issues similar to exception safety in C++. However, the results were good enough to find some soundness bugs in the standard library, and if I remember correctly, widen one of the APIs in a sound manner as well. Another interesting thing about RustBelt is that it was published before Rust gained Non-lexical lifetimes, however, it modeled them, since folks knew they were coming down the pipeline. So, we have some amount of actual proof that unsafe code in Rust doesn’t undermine Rust’s guarantees: what it does is allow us to extend Rust’s semantics, just like FFI would allow us to extend a program written in a language that supports it. As long as our unsafe code is semantically correct, then we’re all good. … but what if it’s not semantically correct? At some point maybe I’ll write a post about the specifics here, but this post is already incredibly long. I’ll cut to the chase: you can get undefined behavior, which means anything can happen. You don’t have a real Rust program. It’s bad. But at least it’s scoped to some degree. However, many people get this scope a bit wrong. They’ll say something like: You only need to verify the unsafe blocks. This is true, but also a bit misleading. Before we get into the unsafe stuff, I want you to consider an example. Is this Rust code okay? No unsafe shenanigans here. This code is perfectly safe, if a bit useless. Let’s talk about unsafe. The canonical example of unsafe code being affected outside of unsafe itself is the implementation of . Vecs look something like this (the real code is different for reasons that don’t really matter in this context): The pointer is to a bunch of s in a row, the length is the current number of s that are valid, and the capacity is the total number of s. The length and the capacity are different so that memory allocation is amortized; the capacity is always greater than or equal to the length. That property is very important! If the length is greater than the capacity, when we try and index into the Vec, we’d be accessing random memory. So now, this function, which is the same as , is no longer okay: This is because the unsafe code inside of other methods of need to be able to rely on the fact that . And so you’ll find that in Rust is marked as unsafe, even though it doesn’t contain unsafe code. And this is why the module being the privacy boundary matters: the only way to set len directly in safe Rust code is code within the same privacy boundary as the itself. And so, that’s the same module, or its children. This is still ultimately better than any line of code in the whole codebase, but it’s not quite as small as you may think at first. Okay, so while RustBelt could give you some idea if your code is correct, I doubt you’re about to jump into Coq and write some proofs. What can you do? Well, Rust provides a tool, , that can interpret your Rust code and let you know if you’ve violated some unsafe rules. But it’s not complete, that is, miri can tell you if your code is wrong, but it cannot tell you if your code is right. It’s still quite useful. To get miri, you can install it with Rust nightly: And then run your test suite under miri with For more details, consult miri’s documentation . Do people write good unsafe code, or bad? Well, the obvious answer to the above question is “yes.” Anyone who says all unsafe code in Rust is sound is obviously lying. But the absolute isn’t what’s interesting here, it’s the shade of grey. Is unsafe pervasive? Does Rust have memory related bugs at the same rate as other MSLs, or closer to memory-unsafe languages? It’s still sort of early days here, but we do have some preliminary results that are helpful. In 2022, Google reported that: To date, there have been zero memory safety vulnerabilities discovered in Android’s Rust code. I am very interested to see a more up to date report about how that’s changed, but as they say: We don’t expect that number to stay zero forever, but given the volume of new Rust code across two Android releases, and the security-sensitive components where it’s being used, it’s a significant result. It demonstrates that Rust is fulfilling its intended purpose of preventing Android’s most common source of vulnerabilities. Historical vulnerability density is greater than 1/kLOC (1 vulnerability per thousand lines of code) in many of Android’s C/C++ components (e.g. media, Bluetooth, NFC, etc). Based on this historical vulnerability density, it’s likely that using Rust has already prevented hundreds of vulnerabilities from reaching production. We’ll see if these results replicate themselves, in the future, and also outside of Google. But it at least looks like in practice, most unsafe code has not led to problems. At least so far! Here’s my post about this post on BlueSky: Does unsafe undermine #rustlang 's guarantees? steveklabnik.com/writing/does... Does unsafe undermine Rust's guarantees?

0 views
Steve Klabnik 7 months ago

Choosing Languages

I tried to write this post yesterday, but I didn’t like my approach there. Last night, I had a conversation with one of my best friends about it, and he encouraged me to write this, but in a different way. I think this way is better, so thanks, dude. The other day, Microsoft announced that they are re-writing the TypeScript compiler in Go . A lot of people had a lot of feelings about this. I did too, but in a different way than many other people. The biggest questions people had were “Why not C#?” and “Why not Rust?”. To be clear, I do think that asking why someone chooses a programming language can be valuable; as professionals, we need to make these sorts of decisions on a regular basis, and seeing how others make those decisions can be useful, to get an idea of how other people think about these things. But it can also… I dunno. Sometimes, I think people have little imagination when it comes to choosing a particular technology stack. Here’s my opinion on the new typescript compiler: You should write programs in the language you want to. 392 43 Read 22 replies on Bluesky It is true that sometimes, there are hard requirements that means that choosing a particular technology is inappropriate. But I’ve found that often, these sorts of requirements are often more contextually requirements than ones that would be the case in the abstract. For example, you can write an OS in Lisp if you want. But in practice, you’re probably not working with this hardware, or willing to port the environment to bare metal. But it’s also the case that you often don’t know what others’ contextual situation actually is. I would have never guessed the main reason that Microsoft chose Go for, which is roughly “we are porting the existing codebase, rather than re-writing it, and our existing code looks kinda like Go, making it easy to port.” That’s a very good reason! I hadn’t really thought about the differences between porting and re-writing in this light before, and it makes perfect sense to me. At the same time, Jake Bailey @jakebailey.dev · Mar 12 Replying to Steve Klabnik Funnily I'm pretty sure I'm the only team member who could actually claim their favorite language is Go, but I tried so hard to prove rust out early on. It sure wasn't my decision either, Go just kept working the best as the hacking continued I think it's going well, though, besides the terrifying infohazard that is the "Why Go?" GitHub discussion thread, which contains some of the weirdest and most frustrating takes I've ever seen 38 1 Read 4 replies on Bluesky people are going way overboard here. Jake Bailey @jakebailey.dev · Mar 12 Replying to Jake Bailey I think it's going well, though, besides the terrifying infohazard that is the "Why Go?" GitHub discussion thread, which contains some of the weirdest and most frustrating takes I've ever seen I peeked briefly yesterday (and quickly ran away). Just took another look. 370 comments in 24 hours?? Wow. The proposal to add generics to Go (mildly contentious, to put it lightly) only got ~450 comments over a few months. 11 0 Read 1 reply on Bluesky There’s some sort of balance here to be struck. But at the same time, I hate “choose the right tool for the job” as a suggestion. Other than art projects, has anyone ever deliberately chosen the wrong tool? Often, something that seems like the wrong tool is chosen simply because the contextual requirements weren’t obvious to you, the interlocutor. I think there’s two other reasons why this situation is making me feel complicated things. The first is RIIR, and the second is a situation from a long time ago that changed my life. Many of you have probably heard of the “re-write it in Rust” meme. The idea is that there’s this plague of programmers who are always suggesting that every single thing needs to be re-written in Rust. While I am sure this happens from time to time, I have yet to see real evidence that it is actually widespread. I almost even wrote a blog post about this, trying to actually quantify the effect here. But on some level, perception is reality, and if people believe it’s true, it’s real on some level, even if that’s not actually the case. Regardless of the truth of the number of people who suggest this, on every thread about Rust, people end up complaining about this effect, and so it has an effect on the overall discourse regardless. And while I don’t believe that this effect is real overall, it absolutely has happened in this specific situation. A bunch of people have been very aggressively suggesting that this should have been in Rust, and not Go, and I get where they’re coming from, but also: really? Go is an absolutely fine choice here. It’s fine. Why do you all have to reinforce a stereotype? It’s frustrating. The second one is something that’s been on my mind for over a decade, but I’ve only started talking about it in places other than conversations with close friends. Long-time readers may remember that one time where I was an asshole . I mean, I’ve been an asshole more than once in my life, but this one was probably the time that affected me the most. The overall shape of the situation is this: Looking back at my apology, I don’t think it’s necessarily the best one. It was better than the first draft or two of what I wrote, but I’ve gotten better at apologizing since then. Regardless, this incident changed the trajectory of my life, and if you happen to read this, Heather, I am truly, unequivocally, sorry. There was a couple of things that happened here. The first is just simply, in 2013, I did not understand that the things I said had meaning. I hate talking about this because it makes me seem more important than I am, but it’s also important to acknowledge. I saw myself at the time as just Steve, some random guy. If I say something on the internet, it’s like I’m talking to a friend in real life, my words are just random words and I’m human and whatever. It is what it is. But at that time in my life, that wasn’t actually the case. I was on the Rails team, I was speaking at conferences, and people were reading my blog and tweets. I was an “influencer,” for better or worse. But I hadn’t really internalized that change in my life yet. And so I didn’t really understand that if I criticized something, it was something thousands of people would see. To me, I’m just Steve. This situation happened before we talked about “cancel culture” and all of that kind of stuff, but when the mob came for me, I realized: they were right, actually. I was being an asshole, and I should not have been. And I resolved myself to not being an asshole like that ever again. And to do that, I had to think long and hard about why I was like that. I love programming languages! I love people writing programs that they find interesting! I don’t think that it’s stupid to write a tool in a language that I wouldn’t expect it to be written in! So why did I feel the reason to make fun of this in that situation? The answer? A pretty classic situation: the people I was hanging out with were affecting me, and in ways that, when I examined it, I didn’t like very much. You see, in the Rails world at the time, there was a huge contempt culture at the time, especially around JavaScript and Node. And, when you’re around people who talk shit all the time, you find yourself talking shit too. And once I realized that, well, I wanted to stop it. But there was a big problem: my entire professional, and most of my social, life was built around Ruby and Rails. So if I wanted to escape that culture… what should I do? If you clicked on the link to my apology above, you probably didn’t make note of the date: Jan 23 2013. Something had just happened to me, in December of 2012: I had decided to check out this little programming language I had heard of called Rust. One of the things that struck me about the “Rust community” at the time, which was like forty people in an IRC room, was how nice they were. When I had trouble getting Hello World to compile, and I typed , I fully expected an RTFM and flames. But instead, I was welcomed with open arms. People were chill. And it was really nice. And so, a month and a day later, I had this realization about the direction my life was heading, and how I wasn’t really happy about it. And I thought about how much I enjoyed the Rust stuff so far… and combined with a few other factors, maybe of which I’ll write about someday, this is when I decided that I wanted to make Rust a thing, and dedicated myself to achieving that. And part of that was being conscious about the culture, and how it developed, and the ways that it would evolve over time. I didn’t want Rust to end up the way that Rails had ended up. And that meant a few things, but first of all, truly internalizing that I had to change. And understanding that I had to be intentional with what I did and said. And what that led to, among other things, was a “we talk about Rust on its own merits, not by trash talking other languages” culture. (That’s also partially because of Nietzsche, but I’m already pretty far afield of the topic for this post, maybe someday.) Anyway, the Eternal September comes for us all. The “Rust Community,” if it ever were a coherent concept, doesn’t really make sense any more. And it is by no means perfect. I have a lot of criticism about how things turned out. But I do think that it’s not a coincidence that “Rust makes you trans,” for example, is a meme. I am deeply proud of what we all built. So yeah, anyway: people choose programming languages for projects based on a variety of reasons. And sure, there may be better or worse choices. But you probably have a different context than someone else, and so when someone makes a choice you don’t agree with, there’s no reason to be a jerk about it. Here’s my post about this post on BlueSky: Choosing Languages steveklabnik.com/writing/choo... Choosing Languages

0 views
Steve Klabnik 7 months ago

A Happy Day for Rust

A while back, I wrote A sad day for Rust about the Actix-web unsafe controversy. The short of it was, Actix-web was using some unsound unsafe code. When this was discovered, people responded pretty harshly, and the maintainer quit. Others picked up the work, but it still wasn’t great: I’m not sure where we go from here, and I’m not sure what we could have done to prevent this from happening. But I feel like this is a failure, and it’s set Rust back a bit, and I’m just plain sad. While the Rust community surely isn’t perfect, today I’d like to share with you a different story: one where there was some controversy, but it was handled in the complete opposite way. Rustup is the official installer and toolchain manager for Rust. It has a much smaller team than Rust itself does, and releases on a much slower schedule. Back in August of last year, it was decided to change some behavior . The details aren’t super important, what matters is that it’s a breaking change to a critical component of the ecosystem. I’m not here to say if this change is good or bad; they had good motivations, there’s also good reasons to keep the previous behavior, it’s one of those classic “you can’t make everyone happy” sort of deals. The team knew that they’d need to let people know that this had happened: Yes, you’ve understood it correctly, and this breakage is intentional: #3635 (comment) Before the actual release, we’ll definitely clearly communicate this change with the community, and we’ll use the beta phase to collect user feedback. And they did make a post on internals about the change, seeking feedback. This also landed in a This Week in Rust, in my understanding. The problem is, not everyone reads internals, and not everyone reads This Week in Rust. And so yesterday, some folks woke up to a broken CI, leading to this bug report among other upset posts elsewhere on the internet. I have been on the other side of this before. It’s frustrating when you try and communicate something, and people don’t hear about it, and then they find out later and get mad at you about it. I also remember the Actix debacle, and many other controversies in the broader open source ecosystem. at one point i questioned my desire to help people get into open source image unrelated 1,173 88 Read 14 replies on Bluesky However, this time, things went differently. As you can see, the issue is very respectful, and the comments are also generally respectful. The team responded graciously , and decided to put in the work to release a new version to restore the previous behavior. I was kind of worried yesterday that this would end up being another Actix situation. The Rust community has changed a lot over time, and while it’s still pretty good overall, sometimes things can go poorly, just like with any random group of thousands of people on the internet. But today, I’m grateful that feedback was given constructively, it was taken with the spirit that it was intended, and everyone should end up happy. If only every controversy went this way, maybe I wouldn’t be wondering about coyotes. Here’s my post about this post on BlueSky: A happy day for #rustlang steveklabnik.com/writing/a-ha... A Happy Day for Rust I’m not sure where we go from here, and I’m not sure what we could have done to prevent this from happening. But I feel like this is a failure, and it’s set Rust back a bit, and I’m just plain sad.

0 views
Steve Klabnik 12 months ago

When should I use String vs &str?

Rust has two main string types: and . Sometimes, people argue that these two types make Rust code difficult to write, because you have to think about which one you should be using in a given situation. My experience of writing Rust is that I don’t really think about this very much, and this post is about some rules of thumb that you can use to be like me. The very first thing you can do is follow the simplest rule: Always use , never use . That looks like this: This style means you may need to add or for things to work sometimes: But that’s okay, the compiler will let you know when you need to: Hey, compiler, that’s a great idea. Let’s move on to level 2. A rule that’s a little better is this one: Always use in structs, and for functions, use for parameters and types for return values. This is what the compiler error for level 1 was suggesting we do instead of . That results in code that looks like this: We’re now doing much less copying. We do need to add a on to values that we wish to pass into , but that’s not too bad, the compiler will help us when we forget: Following this rule will get you through 95% of situations successfully. Yes, that number was found via the very scientific process of “I made it up, but it feels correct after writing Rust for the last twelve years.” For 4% of that last 5%, we can go to level 3: Here’s a slightly more advanced rule for certain circumstances: Always use in structs, and for functions, use for parameters. If the return type of your function is derived from an argument and isn’t mutated by the body, return . If you run into any trouble here, return instead. That would look like this: This lets us remove a copy, we no longer have a in the body of . Sometimes, we can’t do that though: How do you know that this is the case? Well, in this specific case, already returns a . So that’s a great hint. If we tried to return a , we’d get an error: would give us And that’s really it. Following this rule will get you through virtually every scenario where you need to wonder about and . With some practice, you’ll internalize these rules, and when you feel comfortable with a level, you can go up to the next one. What about that last 1% though? Well, there is a next level… Here’s the rule for level 4: Should you use a in a struct? If you’re asking that question, use . When you need to use in a struct, you’ll know. Storing references in structs is useful, for sure, and it’s good that Rust supports it. But you’re only going to need it in fairly specific scenarios, and if you feel like you’re worring about vs , you’re just not in the position to be worrying about the complexity of storing a in a struct yet. In fact, some people believe in this rule so strongly that they’re working on a language where storing references in structs isn’t even possible, and it’s a language I’ve found very interesting lately: Hylo . They go a bit farther than that, even: in Hylo, you think of everything as being values, rather than having references at all. They think that you can write meaningful programs with this model. But this isn’t a post about Hylo. I’ll write one of those eventually. My point is just that you can really, truly, get away with not storing s in structs for a lot of useful Rust programs. So it’s not really worth spending mental energy on, until you determine that you must do so. That is the case when you’ve profiled your program and determined that copying strings to and from your struct is a big enough issue to bother with lifetimes. I hope you’ve found this helpful, and if you have any other rules of thumb like this, I’d love to hear about them! Here’s my post about this post on BlueSky: When should you use String vs &str? Some #rustlang rules of thumb steveklabnik.com/writing/when... When should I use String vs &str?

0 views
Steve Klabnik 1 years ago

Against Names

There’s an old saying: There are only two hard things in Computer Science: cache invalidation and naming things. ― Phil Karlton I also appreciate the joke version that adds “and off by one errors.” Lately, I’ve been thinking about this saying, combined with another old joke: “The patient says, “Doctor, it hurts when I do this.” The doctor says, “Then don’t do that!” ― Henny Youngman Specifically, if naming things is so hard… why do we insist on doing it all the time? Now, I am not actually claiming we should stop giving things names. But I have had at least two situations recently where I previously found names to be kinda critical, and then I changed to systems which didn’t use names, and I think it improved the situation. One of the most famous examples of not giving something a name, lambdas/closures, took some time to catch on. But many folks already recognize that naming every single function isn’t always neccesary. I wonder if there are more circumstances where I’ve been naming things where I didn’t actually have to. Anyway, here’s my two recent examples: I haven’t written much about it on my blog yet, but I’m fully converted away from git to jj . I’ll say more about this in the future, but one major difference between the two is that jj has anonymous branches. If, like me, you are a huge fan of git, this sounds like a contradiction. After all, the whole thing about branches are that they’re a name for some point in the DAG. How do you have a nameless name? Here’s some output from : Here’s some sample output from jj log: Here, we are working on change . ( means the working copy.) There are colors in the real CLI to make the differences more obvious, and to show you unique prefixes, so for example, you probably only need or instead of to uniquely identify the change. I’ll use the full IDs since there’s no syntax highlighting here. We have two anonymous branches here. They have the change IDs of and . The log output shows the summary line of their messages, so we can see “create hello and goodbye functions” on one branch, and “add better documentation” on the other. You don’t need an additional branch name: the change ID is already there. If you want to add even more better documentation, (or again, likely in practice) and you’re off to the races. (jj new makes a new change off of the parent you specify.) (And if you’re in a larger repo with more outstanding branches, you can ask to show specific subsets of commits. It has a powerful DSL that lets you do so. For example, say you only want to see your commits, can do that for you.) That’s all there is to it. We already have the commit messages and IDs, giving an additional identifier doesn’t help that much. In practice, I haven’t missed named branches at all. And in fact, I kind of really appreciate not bothering to come up with a name, and then eventually remembering to delete that name once the PR lands, stuff like that. Life is easier. Another technology I have learned recently is tailwind . But Tailwind is just one way of doing a technique that has a few names, I’m going to go with “utility CSS”. The idea is in opposition to “semantic CSS.” To crib an example from a blog post by the author of Tailwind (which does a better job of thoroughly explaining why doing utility CSS is a good thing, you should go read it), semantic CSS is when you do this: Whereas, with Tailwind, you end up instead having something like this: We don’t have a new semantic name , but instead describe what we want to be done to our element via a utility class. So the thing is, as a previous semantic CSS enjoyer, this feels like using inline styling. But there’s a few significant differences. The first one is, you’re not writing plain CSS, you are re-using building blocks that are defined for you. The abstraction is in building those utility classes. This means you’re not writing new CSS when you need to add functionality, which to me is a great sign that the abstraction is working. It’s also that there is some sleight of hand going on here, as we do, on another level. An objection that gets raised to doing things this way is “what happens when you need to update a bunch of similar styles?” And the answer for that is components. That is, it’s not so much that utility CSS says that semantic names are bad, it’s that semantic names at the tag level are the wrong level of abstraction to use names. To sort of mix metaphors, consider the lambda/closure example. Here’s a random function in Rust: The is unfortunate, but this function takes a list of numbers, selects for the even ones, and then sums them. Here, we have a closure, the argument to , but it’s inside a named function, . This is what using Tailwind feels like to me, we use names for higher level concepts (components), and then keep things semantically anonymous for some of the tasks inside of them (markup). Heck, even the most pro-semantic-styles folks don’t advocate that you must give every single element a class. Everyone recognizes the value of anonymous things sometimes, it’s just a matter of what level of abstraction deserves to get named. Here’s my post about this post on BlueSky: Against Names: steveklabnik.com/writing/agai... Against Names

0 views
Steve Klabnik 1 years ago

How Does BlueSky Work?

One of the reasons I am enthusiastic about BlueSky is because of the way that it works. So in this post, I am going to lay out some of the design and the principles behind this design, as I understand them. I am not on the BlueSky team, so these are my takes only. Let’s begin. Here’s what the BlueSky Website says right now: Social media is too important to be controlled by a few corporations. We’re building an open foundation for the social internet so that we can all shape its future. This is the big picture. Okay so that’s a great idea, but like, what does that mean ? Currently, BlueSky is a microblogging application, similar to Twitter and Mastodon. How does that fit into the big picture? Well, while it’s true that BlueSky is a microblogging application, that’s not the whole story: BlueSky is an initial application to prove out the viability of the Authenicated Transfer Protocol , known as AT, ATP, or “atproto” for short. BlueSky is the “building” and atproto is the “open foundation for the social internet.” An important thing to note: BlueSky is also a company. Some people look at a company saying “hey we’re building something that’s too big to be controlled by companies!” with skepticism. I think that’s a healthy starting point, but the answer for me is atproto. The interplay between these two things is important, but we’re going to start by exploring atproto, and then talk about how BlueSky is built on top of it. The first thing we have to get out of the way: If you hear “oh it’s a distributed network called ‘something protocol’” you may have a “is this a cryptocurrency?” alarm bell going off in your head. Don’t worry, it’s not a cryptocurrency. It does use some technologies that originated in the cryptocurrency space, but this isn’t a blockchain, or a DAO, or NFTs, or any of that. Just some cryptography and merkle trees and the like. Here’s what the AT Protocol Overview says: The Authenticated Transfer Protocol, aka atproto, is a federated protocol for large-scale distributed social applications. Let’s break that down: a federated protocol atproto is federated. This means that the various parts of the system can have multiple people running them, and that they communicate with each other. Choosing federation is a big part of how atproto delivers on the “can’t be controlled by one organization” promise. There are other parts too, but this is an important aspect of solving this. for large-scale If you want to scale, you have to design with scale in mind. atproto makes several interesting choices in order to distribute the load of running the system more onto the actors that can handle load, and less on those that can’t. This way, applications running on top of atproto can scale up to large userbases without issue. That’s the hope, at least. Earlier this week, BlueSky hit five million users, and is far more stable than Twitter was in the early days. That’s not as big as many social applications, but it’s not nothing either. We’ll see how this works out in practice. distributed social applications atproto is for connecting to others, so it’s focused on social applications. It also is currently 100% public, there are no private messages or similar. The reasons for this is that achieving private things in a federated system is very tricky, and they would rather get it right than ship something with serious caveats. Best for now to only use this stuff for things you want to be public. These applications are “distributed” because running them involves running them on the network directly. There’s no “BlueSky server,” there’s just servers running atproto distributing messages to each other, both BlueSky messages and whatever other messages from whatever other applications people create. So that’s the high level, but what does that mean concretely? In atproto, users create records that are cryptographically signed to demonstrate authorship. Records have a schema called a Lexicon . Records are stored in repositories . Repositories run as a service , exposing HTTP and WebSockets. They then can then talk to each other and federate the records. These are often called PDSes, for “Personal Data Server.” Users either run their own PDS, or use one that someone else hosts for them. Applications can be built by looking at the various records stored in the network, and doing things with them. These services all called App Views , because they are exposing a particular view of the information stored in the network. This view is created via the Lexicon system: building an application means that you define a Lexicon, structuring the data that you want to deal with, and then look at records that use your lexicon, ignoring the rest. Now, if this were all there is, there would be pretty serious scaling issues. For example, if every time I post a new update on BlueSky, if I had to send my post to every single one of my followers’ repositories, that would be extremely inefficent, and make running a popular repository very expensive to run. To fix this, there’s an additional kind of service, called a relay , that aggregates information in the network, and exposes it as a firehose to others. So in practice, App Views don’t look at Repositories, but instead, look at Relays. When I make a post, my respository won’t notify my followers’ repositories individually. My repository will notify a Relay, and my followers will use an App View that filters the ouput of the Relay to show only the posts of people they’re following. This does imply that Relays are often huge and expensive to run, however you could imagine running a smaller relay that only propogates posts from a smaller subset of users too. They don’t have to show everything on the network, though bigger ones will, of course. Here this is in ASCII art: This is all you really need to know to understand the core of atproto: people create data, it’s shared in the network, and applications can interact with that data. However, there are additional service types being introduced, with the possibility of more in the future. But before we talk about those, we have to explain some ideological commitments to understand why things are shaped the way they are. Given that atproto is deliberately created to enable social applications, it needs to consider not just connecting people, but also disconnecting people. Moderation is a core component of any social application: “no moderation” is still a moderation strategy. BlueSky handles these sorts of questions by acknowledging that different people will have different preferences when it comes to moderation, and also that moderation at scale is difficult. As such, the protocol takes a “speech vs reach” approach to moderation. The stuff we’ve described so far falls under the “speech” layer. It is purely concerned with replicating your content across the network, without caring what the semantic contents of that content is. Moderation tools fall under the “reach” layer: you take all of that speech, but provide a way to limit the reach of stuff you don’t care to see yourself. Sometimes, people say that BlueSky is “all about free speech” or “doesn’t do moderation.” This is simply inaccurate. Moderation tooling is encoded into the protocol itself, so that it can work with all content on the network, even non-BlueSky applications. Moreover, it gives you the ability to choose your own moderators, so that you aren’t beholden to anyone else’s choice of moderation or lack thereof. But I’m getting ahead of myself: let’s talk about feed generators and labelers. Most social applications have the concept of a “feed” of content. This is broken out into its own kind of service in atproto, called a feed generator . A classic example of a feed is “computer, show me the posts of the people I follow in reverse chronological order.” Lately, algorithmic feeds have become popular with social networks, to the point of where some non-technical users refer to them as “algorithms.” Feed generators take the firehose produced by a relay, and then show you a list of content, filtered and ordered by whatever metric the feed generator desires. You can then share these feeds with other users. As a practical example, one of my favorite feeds is the Quiet Posters feed. This feed shows posts by people who don’t post very often. This makes it so much easier to keep up with people who get drowned out of my main feed. There are feeds like the ‘Gram , which shows only posts that have pictures attatched. Or My Bangers , which shows your most popular posts. This to me is one of the killer features of BlueSky over other microblogging tools: total user choice. If I want to make my own algorithm, I can do so. And I can share them easily with others. If you use BlueSky, you can visit any of those feeds and follow them too. Feeds are a recent addition to atproto, and therefore, while they do exist, they may not be feature complete just yet, and may undergo some change in the future. We’ll see. They’re working just fine from my perspective, but I haven’t been following the lower level technical details. A Labeler is a service that applies labels to content or accounts. As a user, you can subscribe to a particular labeler, and then have your experience change based on the labels on posts. A labeler can do this via whatever method it pleases: automatically by running some sort of algorithm on posts, manually by having some human give a thumbs up or thumbs down, whatever method the person running the labeling service wants. An example of a labeling service would be a blocklist: a label on the posts authored by people whose content you don’t want to see. Another example is an NSFW filter, which may run some sort of algorithm over pictures in posts, and labeling them if they believe they contain NSFW content. Labeling exists, but I do not believe you can run your own labeler yet. BlueSky runs their own, but there hasn’t been an external release that I am aware of. But once they do, you can imagine communities running their own services, adding whatever kind of labels they’d like. Putting this all together, we can see how moderation works: Feeds may choose to transform the feed based on labels, or App Views may take feeds and apply transformations based on asking a Labeler about it. These can be mixed and matched based on preference. This means you can choose your moderation experience, not just in applications, but also within it. Want a SFW feed, but allow NSFW content in another? You can do that. Want to produce a blocklist of people and share it with the world? You can do that. Because these moderation tools work at the network level, rather than at the application level, they actually go further than in other systems. If someone builds an Instagram clone on atproto, that could also use your blocklist labeller, since your blocklist labeller works at the protocol level. Block someone in one place, and they can be blocked on every place, if you so choose. Maybe you subscribe to different moderation decisions in different applications. It is 100% up to you. This model is significantly different from other federated systems, because you don’t really have an “account” on an “instance,” like in Mastodon. So a lot of people ask questions like “what happens when my instance gets defederated” which don’t exactly make sense as stated. You can achieve the same goal, by blocking a set of users based on some criteria, maybe you dislike a certain PDS and want to ignore posts that come from a certain one, but that is your choice and yours alone, it is not dictated by some “server owner” that your account resides on. So if you don’t have a home server, how does identity work? There are a LOT of details to how identity works, so I’m going to focus on the parts that I find important. I am also going to focus on the part that is controversial, because that is important to talk about. At its core, users have an identity number, called a “Decentralized Identifier,” or DID . My DID looks like this: . Feel free to follow me! Lol, of course that’s not the interface that you’ll see most of the time. Identity also involves a handle , which is a domain name. My handle is , unsurprisingly. You’ll see my posts on BlueSky as coming from . This system also works well for people who don’t own a domain; if you sign up for BlueSky, it’ll give you the ability to choose a name, and then your handle is . I started off making posts as , and then moved to . But because the DID is stable, there was no disruption to my followers. They just saw the handle update in the UI. You can use a domain as your handle by getting the DID your PDS generated for you, and then adding a record in the DNS you use for that domain. If you’re not the kind of person who uses or even knows what DNS is, I envy you, but you can also use BlueSky’s partnership with NameCheap to register a domain and configure it to use as a handle without any technical knowledge necessary. You can then log into applications with your domain as the handle, and everything works nicely. This is also how BlueSky delivers true “account portability,” partially because, well, there isn’t really a concept of an account. The person who uses a given DID uses cryptography to sign the content they create, and then that content is replicated across the network. “Your account” can’t really be terminated, because that would mean someone forcibly stopping you from using keys that they don’t even have access to. If your PDS goes down, and you want to migrate to a new one, there’s a way to backfill the contents of the PDS from the network itself, and inform the network that your PDS has moved. It is real, meaningful account portability, and that is radically different from any similar service running today. 1 The devil is in the details, and I think this is one of the more meaningful criticisms of BlueSky and atproto. You see, there are different “methods” of creating a DID. BlueSky supports two methods: , which is based on domain names. There are some drawbacks with this method that I don’t personally fully understand well enough to describe, I’m sure I’ll write something in-depth about DIDs in the future. So because of that weakness, BlueSky has implemented their own DID method, called . The stands for “placeholder,” because even though they plan on supporting it indefinitely, it too has its weaknesses. And that weakness is that it involves asking a service that BlueSky runs in order to resolve the proper information. For example, here is my lookup . This means that BlueSky can ban you in a more serious way than is otherwise possible thanks to the network design, which some people take to be a very serious issue. So, is the flaw fatal? I don’t think so. The first reason is, if you really don’t want to engage with it, you can use . Yes that isn’t great for other reasons; that’s why was created. But you do get around this issue. Another is that the BlueSky team has demonstrated, in my personal opinion, enough understanding and uncomfortableness with being in control here, and it’s designed in such a way that if other, better systems develop, you can move to them. They’ve also indicated that moving governance of to some sort of consensus model in the future is possible. There are options. Also, others could run a service and use that instead if they prefer, too. I personally see this as an example of pragmatically shipping something, others see it as a nefarious plot. You’ll have to decide for yourself. So, now that we understand atproto, we can understand BlueSky. BlueSky is an application built on top of the atproto network. They run an App View, and a web application that uses that App View to work. They also run a PDS for users that sign up through the web app, as well as a relay that those PDSes communicate with. They publish two Lexicons, one as and one as . The former are low level operations that any application on the network will need, and the ones specific to BlueSky are in the latter. But one nice thing about BlueSky in particular is that they’ve taken the product goals that nobody should know any of this nerd shit to be able to use BlueSky. The lack of instances means there’s no “I need to pick an instance to create an account” flow, and the portability means that if my host goes down, I can move, and my followers are none the wiser. You can create an atproto app by creating a Lexicon. You’ll then want to run an App View that does things with data on the network involving your lexicon, and your application will want to give people the ability to write data to their PDS using your lexicon. I myself am considering doing so. We’ll see. So yeah, on the technical side of things, that’s an overview of how atproto and BlueSky work. I think this design is very clever. Furthermore, I think the separation of concerns between atproto and BlueSky are very meaningful, as having a “killer app” for the network gives a reason to use it. It also is a form of dogfooding, making sure that atproto is good enough to be able to build real applications on. I’m sure I’ll have more to say about all of this in the future. Here’s my post about this post on BlueSky: With BlueSky finally opening up federation, I think it's time to write up something that explains the whole thing, BlueSky, atproto, the works, in one easy to find place. Just the high level bits, hopefully something that any developer can understand: steveklabnik.com/writing/how-... 678 241 Read 47 replies on Bluesky Footnotes A commentor points out https://book.peergos.org/ , which I had not heard of, but apparently was known to the creators of BlueSky before they made it. Neat. ↩

0 views
Steve Klabnik 1 years ago

Using the Oxide Console

A very, very long time ago, I was introduced to Gerrit . To be honest, I hated it. However, lately I have become interested in divesting from and GitHub, and so have decided to re-visit various “forges” to see what’s out there. The “why” for this will come later, and I’m not leaving them just yet, just, you know, doing some exploring. Anyway, in order to play around with Gerrit, I need a server somewhere. This website runs entirely on Vercel at the moment, so I don’t happen to have a more traditional server lying around to give Gerrit a shot. But as it just so happens, I work at Oxide , and so have access to a rack that I can play around with. So I thought it would be fun to share what that looks like, given that people are interested in what we do, but don’t actually have access to one of our machines. If you’d like to poke at the console yourself, there’s a live demo ! Quoting the README : At https://oxide-console-preview.vercel.app the console is deployed as a static site with a mock API running in a Service Worker. You can create mock resources and they will persist across client-side navigations, but they exist only in the browser: nobody else can see them and the mock “DB” is reset on pageload. Request and response bodies in the mock API match the Oxide API’s OpenAPI spec, but behavior is only mocked in as much detail as is required for development and testing of the console and is not fully representative of the real API. I happen to think this is extremely cool. If you want to, you could stop reading this post and go play with that instead. It’s also, like the rest of Oxide’s software stack, fully open source! Before we begin, I would like to say that I am going to be doing this “manually” via the console and a web browser rather than via the CLI. I am doing this for two reasons: To be clear, I imagine a lot of real production usage of the Oxide rack will be driven by the API , either via our CLI tools or terraform or whatever. Heck, the console is built on those APIs, so on some level, that’s the only way to do it. My point is, don’t think of this as being the only workflow to interact with Oxide. Maybe if people like this post I’ll make some more covering the CLI driven workflow. Let me know. After you SSO into the console, you’re put on the Projects page: We’re in a “silo” named Oxide. A silo is a grouping of users, projects, and resources that are isolated from one another. But we don’t care about that: we want to make a new project. As you can see, I have censored my co-workers’ projects, as I didn’t tell anyone I was making this post, and even though the description here is hilarious, it wouldn’t be right to share without asking, and I’m already like ten layers deep in this yak shave. Let’s make a new project: It’s just that easy. After we push the button, we’re put on the instances page: There’s a nice little toast in the bottom left that I didn’t manage to get a screenshot of because I was re-sizing my window too. Anyway, now that we have our project, we can add one or more instances to it. Instances are virtual machines, and so setting one up looks like you might expect. You can give it a name and description, but for this screenshot I’m only showing off one part of this page because this is already a ton of screenshots: We can size instances however we’d like. Even though Gerrit is a Java application these days, I’m going to keep the instance small, because it’s just for me and performance isn’t a huge deal. Yes, I have made massive instances for fun in the past. We’re using a Debian image that’s been provided by this silo, because I am lazy and that’s fine. Is four gigabytes of disk enough? I don’t know, let’s find out. A few seconds later, our instance is up and running: I am obscuring the external IP because, well, no reason to give that out. Anyway, that’s it! We can in: … how, you may ask? Well, I didn’t show off the entire “Create instance” page: I have set up my ssh keys in my account previously, and so by default, because this image uses to configure itself, my keys are already injected into the image. Furthermore, I left it on the default VPC , which is currently configured with a firewall rule that allows for incoming connections. All of this is of course absolutely customizable, but given what this rack is being used for, these defaults make sense for us, so I didn’t have to actually do much setup at all. That being said, I do need to open port 8080 so that I can poke at Gerrit in a web browser. If I go into the VPC configuration, I can add a new firewall rule: Further down, I can pick port 8080, and that I want to allow TCP. After setting up Gerrit, we have success! And that’s it! It’s just that easy. There’s of course a ton of stuff I didn’t talk about, but easy things should be easy, and for basic usage, it’s just that easy.

0 views
Steve Klabnik 1 years ago

Memory Safety is a Red Herring

I think that a focus on memory safe languages (MSLs) versus non memory-safe languages is a bit of a red herring. The actual distinction is slightly bigger than that: languages which have defined behavior by default, with a superset where undefined behavior is possible, vs languages which allow for undefined behavior anywhere in your program. Memory safety is an important aspect of this, but it is necessary, not sufficient. Rust’s marketing has historically focused on memory safety, and I don’t think that was a bad choice, but I do wonder sometimes. Finally, I wonder about the future of the C++ successor languages in the face of coming legislation around MSLs for government procurement. Additionally, I would like to thank ixi for discussing some of this stuff with me. I do not claim that they agree with me, these opinions are solely my own! But talking about it helped me form those opinions. Today I have been programming in Rust for 11 years. I usually write some sort of blog post reflecting on the year. This year, I am reflecting on some old questions: But before we get there, we have to talk about some other things. The other day, someone asked an interesting question on Hacker News : Can someone with a security background enlighten me, on why Python is on the list of “memory safe” languages? Most of the python code I have worked with is a thin wrapper on C. Wouldnt that make python vulnerable as well? This is a very reasonable question! If you click the link, you’d see my answer: You are correct that if you call C from Python, you can run into problems. But the fault there lies with the C, not the Python. Pure Python itself is memory safe. Because memory safety is built on top of memory unsafety, it is generally understood that when discussing things at this sort of level, that we are speaking about the Python-only subset. (Same as any other memory safe language.) But this has been gnawing at me for a while. Like it’s one of those things that “everybody knows what I mean.” But isn’t that kind of… bullshit? Like something is wrong with your definitions. But on the other hand, if we accept FFI as being not safe, and that since you can FFI from anywhere, that means it’s also unsafe, then every single program is unsafe. Your operating system does not offer memory-safe APIs. To do anything useful, you must call into the operating system. This means every program would be infected via this conception of safety, and therefore, as a definition, it would be useless. If we instead make an exception for a language’s runtime, which is allowed to make unsafe calls, but users’ code is not, that would draw an appropriate boundary: only write code in the guest language, and you don’t have to worry about safety anymore. And I think that this sort of definition has been the default for many years. You have “managed” languages which are safe, and you have “unmanaged” languages which are unsafe. Unmanaged languages are used to implement managed languages. What’s that? A Java runtime written in Java? A Ruby runtime written in Ruby? Yeah those are weird exceptions. Ignore those. And those “native extensions” that segfault? Well they aren’t written in the host language, so of course that will happen, and yeah it’s kinda annoying when it happens, but it’s fine. Sometimes you have to risk it for performance. I just don’t find this satisfying. It’s too vague. You just kinda know it when you see it. Ignore FFI, and we’re good. And sometimes, you just can’t have safety. What if you want to write a runtime? You’re gonna need to call into the system directly, and now we’re back to unsafe, so just use an unsafe language. It is what it is. That comment is on an article titled The Case for Memory Safe Roadmaps: Why Both C-Suite Executives and Technical Experts Need to Take Memory Safe Coding Seriously . This document, published by the Five Eyes , suggests… that executives and technical experts need to take memory safety seriously. Call it boring if you want, but it is what it says on the tin. More specifically, it says: Memory safe programming languages (MSLs) can eliminate memory safety vulnerabilities. As well as an appendix, “Memory Safe Languages,” which describes C#, Go, Java, Python, Rust, and Swift as memory safe languages. Finally, it says: Programming languages such as C and C++ are examples of memory unsafe programming languages that can lead to memory unsafe code and are still among the most widely used languages today. But it also acknowledges the reality that sometimes you can get around memory safety: MSLs, whether they use a garbage collection model or not, will almost certainly need to rely on libraries written in languages such as C and C++. Although there are efforts to re-write widely used libraries in MSLs, no such effort will be able to re-write them all anytime soon. For the foreseeable future, most developers will need to work in a hybrid model of safe and unsafe programming languages. Developers who start writing in an MSL will need to call C and C++ libraries that are not memory safe. Likewise, there are going to be situations where a memory unsafe application needs to call into a memory safe library. When calling a memory unsafe component or application, the calling application needs to be explicitly aware of—and limit any input passed to—the defined memory bounds. The memory safety guarantees offered by MSLs are going to be qualified when data flows across these boundaries. Other potential challenges include differences in data marshalling, error handling, concurrency, debugging, and versioning. Okay, that’s enough quoting from the report. And I swear I’m not about to say nice things about it because I am cited as #42, though to be honest it’s real fucking weird that searching “klabnik” on a page on defense.gov returns 1/1. But I think this is an excellent formulation of the critical distinction between the languages on one side of the line versus the other, though they make the same conceptual mistake that I believe Rust may have: it’s not just about memory unsafety. “safety” itself is basically the idea that most of the time, you’re in a universe where everything is peachy keen, and occasionally, you’ll need to reach across that line into the scary other place, but you gotta do what you gotta do. But there is a line there. But that line is more abstract than just memory safety. Rust often describes itself as “data race free.” But what the heck is a data race anyway? Here’s John Regehr : A data race happens when there are two memory accesses in a program where both: There are other definitions but this one is fine; it’s from Sebastian Burckhardt at Microsoft Research. Oh. Well, I guess “Here’s Sebastian Burckhardt:” instead. Point is, data races are undefined behavior in Rust, and they’re also undefined behavior in C++. When looking for a succinct citation for that fact, I came across this 2006 post by Hans Boehm . Its first line: Our proposed C++ memory model gives completely undefined semantics to programs with data races. I believe this was the model that was accepted, so that’s the best citation you’re gonna get from me. Okay, as soon as I read that I said “come on Steve, that’s stupid, make sure it’s not just trivial to actually find in the standard” and lo and behold n4849 says: The execution of a program contains a data race if it contains two potentially concurrent conflicting actions, at least one of which is not atomic, and neither happens before the other, except for the special case for signal handlers described below. Any such data race results in undefined behavior. Okay, the real citation was easier. The article by Bohm is still fascinating. But data races aren’t undefined behavior in every programming language. For example, let’s consider both Java and Go. Now I hate that I even have to preemptively say this, but I am not saying Java and Go are bad. I think these are good decisions for both of the two languages. And I think their decisions here are very informative. Language wars are stupid. Stop it. Java was the first time I remember hearing, “yeah you can have data races but like, it’s fine” about. I kinda lodged that fact in the back of my brain, and didn’t think about it much. But lately, I was wondering. So I went and read the Java specification, and more specifically, Chapter 17 . It contains this text: The semantics of operations other than inter-thread actions, such as reads of array lengths (§10.7), executions of checked casts (§5.5, §15.16), and invocations of virtual methods (§15.12), are not directly affected by data races. Therefore, a data race cannot cause incorrect behavior such as returning the wrong length for an array. This is morally equivalent to “not undefined behavior” even though the specification doesn’t define “undefined behavior” in the way that the C, C++, and Rust specifications do. Notably, the specification also defines “data race” in a slightly different way: When a program contains two conflicting accesses (§17.4.1) that are not ordered by a happens-before relationship, it is said to contain a data race. Not just any ordering or synchronization primitive, a happens-before relation specifically. Fun! We have three technically-slightly different but basically morally equivalent ways of defining a data race. Let’s do a fourth. Go also allows data races. Sort of. From the Go memory model : A data race is defined as a write to a memory location happening concurrently with another read or write to that same location, unless all the accesses involved are atomic data accesses as provided by the sync/atomic package. Given that Rust provides atomic ordering operations in a submodule of its standard library, Rust and Go agree here, though Go’s more specific about naming it. But they also have this to say: While programmers should write Go programs without data races, there are limitations to what a Go implementation can do in response to a data race. An implementation may always react to a data race by reporting the race and terminating the program. Otherwise, each read of a single-word-sized or sub-word-sized memory location must observe a value actually written to that location (perhaps by a concurrent executing goroutine) and not yet overwritten. So it’s not right, but it’s also not exactly UB. It goes on: These implementation constraints make Go more like Java or JavaScript, in that most races have a limited number of outcomes, and less like C and C++, where the meaning of any program with a race is entirely undefined, and the compiler may do anything at all. Go’s approach aims to make errant programs more reliable and easier to debug, while still insisting that races are errors and that tools can diagnose and report them. While a Go program may exhibit what a Rust or C++ program would consider undefined behavior, and it does also consider it an error, the consequences are very different. You don’t get time travel. You get 998 instead of 1,000. (After publication, Edoardo Spadolini reached out to me and we had a productive conversation about the above paragraph: while my overall point here is still valid, there are slightly more serious consequences to UB in a Go program than slightly incorrect sums. For example, torn writes to a slice can lead to serious consequences, and there are other ways to write code that exploits UB to be actively malicious, such as a function with the signature that works. However, it’s also worth mentioning that Go ships with a race detector, available by simply passing . For some interesting usage of this at scale, check out this post by Uber . At the end of the day, this is still very different than the “your whole program is invalid” style of issues you can end up with in C or C++, I am slightly underselling the danger above.) Go also provides an unsafe package : The built-in package unsafe, known to the compiler and accessible through the import path “unsafe”, provides facilities for low-level programming including operations that violate the type system. A package using unsafe must be vetted manually for type safety and may not be portable. Oh yeah, I forgot, Java also has , essentially the same thing. They have actually wanted to remove it for a long time, and some progress has been made, but it’s not quite gone yet. The Unsafe Class: Unsafe at Any Speed is a good explanation of the topic. One weird thing about suggesting that the borrow checker is only about memory safety, and that memory safety means the absence of data races, is that memory safety is more than just the absence of data races. Consider a problem that requires no threads to become an issue: iterator invalidation. And different languages have different answers to this issue: But you don’t get full-blown time-travel undefined behavior. Sort of like this code in Rust: Okay in this case because it’s all at compile time Rust can detect it: But in the general case, you won’t get a compile error. You may get a panic, you may get . It’s weird. But it’s not full-blown time-travel undefined behavior. Anyway, back to iterator invalidation: This is neat! This is cool! This is good! And it’s pretty unique to Rust. It is nice when our programs do what we expect, instead of what we don’t expect. It is even nicer when, if our programs don’t do what we expect, we have some sort of waypost, some sort of sign where the problem might be. What “nice” means may be a bit different in different languages, but the boundary is the important part: we’re gonna have to potentially violate the rules somewhere, so at least give us some help when that inevitably goes wrong. Over here, logic bugs may happen, some weird behavior may result, but over there? There be dragons. Nobody can save you out there. Tread carefully. If we think about all of these designs, they all are very similar conceptually: safe code is the default, but you can call into some sort of unsafe facility. And everyone is very clear on the relationship between the two: while the unsafe facility exists to be used, it must uphold the rules that the safe world relies on. And that means that safety and unsafety have a super/sub-set relationship. In the core is unsafety. But at some point, we draw a line, and on top of that line, safety exists. In Rust, we often talk about how unsafe is great, because it clearly draws a line around code with which serious bugs may happen. More specifically, while most people describe it as “where memory safety can originate from” or similar, it’s actually slightly broader than that: it’s where undefined behavior can originate from. The nomicon : Rust code is incorrect if it exhibits any of the behaviors in the following list. This includes code within unsafe blocks and unsafe functions. unsafe only means that avoiding undefined behavior is on the programmer; it does not change anything about the fact that Rust programs must never cause undefined behavior. It is the programmer’s responsibility when writing unsafe code to ensure that any safe code interacting with the unsafe code cannot trigger these behaviors. unsafe code that satisfies this property for any safe client is called sound; if unsafe code can be misused by safe code to exhibit undefined behavior, it is unsound. This is not a new insight, but basically, unsafe is like FFI in a managed language. It’s not something you do often. But it is something you sometimes have to do. And when you do, you can at least contain the danger: the problem has to lie somewhere behind that veil. You have clear points to begin your search into where the issues lie, should the abyss claim another soul. In some ways I’m also repeating another older slogan Rust had: “memory safety without garbage collection.” But it’s more like “no UB and no garbage collection.” Gosh, why didn’t we put that on the website? Practically rolls right off the tongue. What makes Rust appealing, and I think especially to the “non-systems” crowd, is that it shares a property of many managed languages, that of “no undefined behavior by default,” with a property of many systems languages, that of “no runtime, as little overhead as possible.” And sure, there’s an escape hatch, but it’s rarely used, and clearly separates the two worlds. This is a fundamentally different way of viewing the world than “unsafe by default.” Another way in which there’s an interesting split here in language design is between the scripting languages like Ruby, Python, and JavaScript, and languages like Rust, Go, and Java, is that the boundary in the former is purely FFI, while in the latter, there are also unsafe faculties in the host language itself, as well as with FFI. In all three cases, they’re behind either a specific package or , but they give you some interesting tools that don’t require you to use another language to use. I think this is an under-appreciated part of the overall design space. So: did Rust focus too much on memory safety? And what should newer languages do in this area? Well, I think that in some sense, the answer is obviously “no, Rust did not focus too much on memory safety.” Rust has been wildly successful, reaching heights that I only could dream about eleven years ago. Yes, usually I have tried to be fairly tempered when talking about Rust’s successes, and some folks still try to claim Rust is still super niche. But when I think about the ways that Rust touched even this blog post, it’s staggering: I wrote this post in VS: Code, which uses Rust, on Windows, which has Rust in the kernel. I published it to GitHub, which uses Rust for code search. I previewed it in Chrome, which uses some Rust today and may start having more tomorrow. It’s hosted on Vercel, which uses Rust in projects like turbopack, though maybe not in the critical path for this post. When you read it, the bytes probably passed through CloudFlare, which uses a ton of Rust, and maybe you’re reading this in Firefox, which is about 12% Rust. And maybe you’re on Asahi Linux, where the GPU drivers are written in Rust. Rust has Made It, and is here to stay. And the marketing we used got us here. So in some sense, it would be silly to declare said messaging a failure. However, I also wonder what else could have been. Could Rust have grown faster with a different message? Maybe. That said, what would that message be? “Undefined behavior is scary?” I’m not sure that’s as compelling, even if I think it’s actually a bigger deal. While memory unsafety is only part of undefined behavior, it is the part that is easy to understand: pointers are the bane of every C newbie. It has consequences that are easy to understand: security vulnerabilities are bad. Has Rust really conflated “memory safety” and “safety” that much? I don’t actually think so. People still talk about SQL injection, about Remote Code Execution, about Cross Site Scripting. People don’t really believe that just because it’s written in Rust, bugs are impossible. Sure you can find a few comments on the internet as such, but the big organizations that are using Rust for meaningful work do not take this position. Given Amdahl’s Law , if 70% of security vulnerabilities are related to memory safety, focusing so strongly on it makes sense, as fixing that will give you a much larger impact than other things. So what should new languages do here? Well, pretty much the only language that is gaining developer mindshare that’s new and is memory unsafe by default is Zig. I think Andrew is very sharp, and a tremendous engineer. I am very interested to see how things shake out there. But as a language, it does not appeal to me personally, because I think we’ve sort of moved past “unsafe by default.” That being said, it’s also not that Zig is exactly like other languages in this space either: if we are willing to go beyond “memory unsafe vs memory safe at compile time” as a strict binary, and instead look at “memory unsafe at compile time” itself, we see a pretty big gradient. Zig is doing a bunch of things to mitigate issues where possible. Maybe “safe enough” truly is safe enough. Time will tell. But beyond that, you also don’t have to go full Rust in the design space. Rust wants to be a language that you can (and at Oxide, we do) use at the lowest levels of the system. But also memory safe. And extremely fast. These constraints mean that Rust takes on a lot of complexity in the type system, where languages that are willing to relax these things wouldn’t have to. Here are three languages that I think are sort of “post-Rust” in a sense, that are learning from Rust but putting their own twist on things: I think there are some other languages that are forming the next big cohort of programming languages, and they’re interesting too, but these three I think have the most explicit connection to Rust in the design and goals of the language itself. Shout out to Nu, Inko, Gleam, Roc, Oil Shell, Unison, and others. Lots of cool stuff going on. There is one way in which the question “is the focus on memory unsafety misguided?” is much more serious than battles in the comment sections of various fora: the government. We talked about this earlier, but various government organizations have been slowly putting out recommendations that organizations should be moving towards MSLs. And the first reactions to these were always “well they’re not laws so they don’t have teeth” as well as “laws are inevitably coming.” To be honest, I have mostly been in the former camp, and viewed the later as fearmongering. Sure, maybe someday, but like, dipping your toes in does not mean that you’re about to cannonball moments later. But then I got this interesting reply from David Tolnay on Hacker News . In it, he refers me to ADSP: The Podcast , and more specifically, Episode 160: Rust & Safety at Adobe with Sean Parent . In it, Sean makes reference to two pieces of legislation, one in the US and one in the EU . Now, I am not a politican, and these bills are huge, so I wasn’t able to figure out how these bills do this specifically, but to quote Sean: The one in the U.S. that’s pending basically says that the Department of Defense is going to within 270 days of the bill passing (and it’s a funding bill which means it will probably pass late this year - early next year) that the Department of Defense will establish guidelines around safety and security including memory safety for software products purchased by Department of Defense. The E.U. has a similar wording in a bill that’s slowly winding its way through their channels. I don’t have insight into when that will pass. The U.S. one will almost certainly pass here within a month or two. This sounds much more realistic, of course. Now, this does not mean that C++ is outlawed or something silly like that, but it does mean that using Rust could become a more serious competitive advantage when it comes to selling to government: if you don’t need an exception for your product, that’s an advantage over a product which does. This all also may go the way of another anecdote Sean talks about in the past: when a similar bill tried to mandate POSIX compliance. What I’m saying is, go listen to the podcast. And while this still isn’t something as wild as “not using MSLs is illegal,” it is really shocking to me how quickly things seem to be moving in that vague direction, though of course it will never actually arrive there. There seems to be at least some sort of thinking that’s not immediately lauged out of the room that memory unsafety could be a consumer safety issue in a similar sense that other unsafe materials are a consumer safety issue. Is that right? I’m not sure, but it seems to be where the conversation is heading. So where does that leave existing widely used memory-unsafe languages? Well, the C++ community has… had a wide variety of discussions on this topic. I should also say I mean “community” at large, and the committee feels like it has a big divide as well. Now I should re-iterate that while I pay attention to the C++ standardization process, I am very much an outsider: I have never attended a meeting, I have no direct involvement in these things, I just read a lot. And while historically most of the community wrote Rust off as a novelty, I think that at this point, those with their head in the sand about Rust’s usage are mostly a fringe minority. But the big question that remains is, what to do? Two approaches have appeared: What is interesting to me about these approaches is that they are both good, for different reasons. The former is a sort of harm reduction approach: don’t let the perfect be the enemy of the good. What can we do today to improve the lives of a ton of people? But if you want to move into the realm of an MSL, you have to break backwards compatibility. This is tantamount to just doing #2 in the first place, and so some are cutting straight to that approach. But if we look at it in a slightly different way, an interesting thing happens: This leads to four approaches: The first option is seemingly untenable, but is also where I see C heading. There’s seemingly not nearly as much desire for movement in the C space as the C++ space, even though I am following what JeanHeyd Meneide is doing and appreciate it. The second option is how I view at least one of the possible C++ successor languages, Circle . Circle is incredibly interesting and is worth its own post. But in short, it is a C++ compiler that also implements a lot of extensions, both ones that are proposals in the committee, but also ones that its author is interested in. He has been doing lifetime experiments. The third option is where the most famous successor languages live: Carbon and cpp2 . I haven’t gotten to spend a ton of time with either of these yet, but they explicitly aim to do to C++ what C++ did to C, or what TypeScript did to JavaScript: create a new language for new code, while allowing you to use the older language for older code. This allows them to break backwards compatibility more easily: new code can more easily be written under the new constraints. This gives them a larger degree of freedom to make changes, which may be necessary to move the needle significantly on the safety issue. But to my knowledge, they do not attempt to be memory safe by default. They’re conceptually similar to Zig in this way. But what about a fourth option? What if someone did a TypeScript for C++, but one that was closer to what Rust does? You might argue that this is basically just Rust? (Interestingly, it is also sort of what Zig is with C: you can use the same compiler to compile a mixed codebase, which is close enough in my opinion.) What makes me worry about option #3 is that it doesn’t pass the seeming test that is coming: memory safe by default. I still think moving to a #3 style solution is better than staying with #1, but is that enough? I don’t know, time will tell. I wonder to what degree you can get “C++ but memory safe” without just being Rust. As we talked about earlier, most languages that are trying to improve on Rust in this space are willing to trade off some core constraints on Rust’s design to get them, but C++ and Rust share the same constraints. That said, it would take a tremendous amount of hubris to declare that Rust is the final, ultimate, best language in the “memory safe without GC” space. Rust has made a lot of mistakes too. Maybe reflecting on those will make for a good twelfth year post. Someone get started on Rust++, please! The reason that today is my Rust-a-versary is that I usually play with a new language over Christmas break, and in 2012, that language was Rust. Maybe this Christmas, I’ll give cpp2 a try. In the final minutes before publishing this, I also found another interesting report . This is a report to the CISA director by the Technical Advisory Council on how to engage with the memory safety question. Rust has 17 mentions. I haven’t read it yet, but I figured I’d include it here.

0 views
Steve Klabnik 2 years ago

Updating Buck

Hey there! A shorter post today, but I wanted to continue my series on Buck by going over some things that have changed since this series started. Lots of great stuff has been happening since the initial release of buck2, and we’d like to take advantage of that. If we try and update things, though, we’ll get (well, you might not, but I did) an error: (Why yes, my console prompt has changed…) See on Windows, we can’t replace a program that’s running, while it’s running. On other systems, this may work for you. However, it’s probably a good idea to not take advantage of this particular feature, because of the underlying cause: buck runs a daemon in the background. This daemon is still going. Trying to replace it while it’s running means you’ll still have the old one going around in the back, and while that’s fine (is it? I don’t actually know), best to cleanly shut down first. So do this: will kill every instance of the daemon. As you can see, it found two of mine, and shut them down. As you can see, you want to run this command from within one of your projects. And now an upgrade works. If you’re building Rust code and using Reindeer, like we talked about in the last post, go ahead and grab that too. Lots of good stuff in there: Now that we’re all updated, let’s fix up our project to make use of the latest changes that are relevant. I no longer need my weird toolchain hacks to get MSVC Rust working. Thank you dtolnay! 🙏 Let’s pull in changes to the prelude: And remove the hack (if you had to do it too) in : No more stuff! Some good changes have landed in the subcommand that we’ll want to add ourselves. First up, we should add an empty file named to the root of our repository . Because buck works with your filesystem hierarchy, it can and will traverse upwards looking for things at times. Adding this file ensures that if it does so, it will stop before it starts trying to traverse even higher. There’s no need for anything in the file, as the contents are ignored. I think this kind of change is an interesting way to look at the usability of various systems. Adding an empty file here is sort of “more complex” than say, Cargo. But it’s also more explicit. Which means it can be more… I want to say “legible”, if you also read insufferable books like I do sometimes. And therefore, easier. Anyway, more thoughts on all of this in the future. Next, we have a change that is demonstrated by this example. Can you guess what it is? There’s no reason for buck to be watching our directory for changes. And that’s why it is now put in the default configuration when you . But we’ve gotta make that change ourselves. Open up your and add this at the bottom: We want it to ignore the directory. Seems good. … I lied though, there’s one more improvement we want to make: we also don’t want buck to bother listening to the directory either, as those files are for Cargo’s output. So what we actually want is this: After doing that we’ll want to to shut the daemon down, so that it can pick up our new configuration on the next boot. Since we’ve got new bugfixes in Reindeer too, let’s regenerate our config for our dependencies: We still have to deal with the build script! We didn’t talk about the contents of last time, and we won’t this time either. If you want to see what’s changed, you can take a peek though. One change that we didn’t explicitly talk about before, but you may end up noticing, is that it did not generate a target to try and build our build script. Let’s try it out now: Nice. Also note that we aren’t seeing any more or changes, not that we’ve run anything that would inherently change those files, but go ahead, invoke Cargo or git, and then build again. You shouldn’t see notifications about those directories anymore. Speaking of invoking Cargo, remember how I said this in the last post? We do have two different Cargo.tomls now. That is a bit of a bummer. But at least it is easy to determine if there’s a problem: dependency failures are loud, and if you’re building with both in CI, you’ll notice if stuff goes wrong. There also may be a solution to this I’m just not aware of. Going back and re-reading my last post, I did have a in there, so maybe I just forgot to commit that in my last post. Just in case, we’ll fix that: With that, and are back in business. We also have… well “breakage” isn’t exactly right, but we have a buck configuration issue. Let’s try to build every target: We didn’t declare that our binary was visible anywhere, and so when we try and build it, it isn’t happy. We do want this to be public, so change by adding this line near the end. It should look like this: And now that will work: Okay, now that we’ve fixed both Cargo and buck… let’s make sure that isn’t gonna happen again. We aren’t testing any of this. So it broke. Just like I said it was easy to not let break. Sigh. We’re going to use GitHub Actions because this is already on GitHub. I’m sure you can adapt it to your setup of choice. Put this in : This is… you guessed it, based off of dtolnay’s CI for cxx . What can I say, he writes good code. This version is a bit stripped down, since this is primarily a project for showing off a build system, rather than a regular project. This has: This does not have: or other things you may want out of a build. Again, you’ll probably want to customize this heavily, but this is what I’m going to do here. And with that, we are done! Next post we’re going to deal with that build script. And use some crates that have more intense dependencies, and get them all working. As always, you can check out the code at this point if you’d like.

0 views