Posts in Rust (20 found)
codedge 4 days ago

Prevent deploying broken links to your blog

Having your own blog is fun. Checking internal links or also having an eye on all old URLs you ever linked is not. Fortunately you can automate link checking every time you deploy your website. I recently read about how links you once posted on your personal page or block become outdated. They are either put in private (403), they vanish completely (404) or they get a proper redirect (302). Whatever the case is, it would be cool the get all your links checked automatically when deploying your site, so you can either start fixing or removing them. For my Hugo site I wanted to do exactly, without going the write a scraper to extract links from my site and letting them run through curl . I wanted something to be run against my static HTML files, than I generate before deploying a new version of my page. I came across a very handy tool called lychee , that does exactly that. On their website they advertise it with Catch broken links in seconds Async, rust-powered simplicity for docs, sites, and codebases The cool stuff is, lychee works with I implemented it into my deployment workflow, scanning a folder , where my newly generated files are - and voila, I get a list of URLs with all their HTTP status codes. Of course you can configure ( see documentation ) which status codes are treated as good or errors. For example, I consider a not an error per se. You can also exclude specific URLs (or via regexp) to not being checked. I run this now on PR and on new deployments of my main branch. Works very well! Markdown files Websites (scraping all links)

0 views

A Syncthing and SQLite Gotcha

So, I have this little app, Epoch , that I use to keep a journal. It’s a tiny Rust web app that runs as a systemd service and uses SQLite as the database. I use a desktop and a laptop regularly, and use Syncthing to synchronize them, including Epoch’s database. That way I can use the app on both devices without needing a server to synchronize them, the tradeoff being that I have to make sure the sync is finished before performing any mutations. But I had this bug. Say I edit today’s entry on the laptop, come home, wait for Syncthing to finish, then I’d open today’s entry on the desktop, and the text would be missing. It’s not that the server is holding a lock on the file and preventing the sync: opening the database with the command line tool shows the new text is there. If I restart the server, Epoch can read the new text. My mental model was: The rusqlite object points to the database file. Syncthing swaps the file’s contents from under it. Subsequent queries go to the new file. Turns out there’s a very important part of POSIX filesystem semantics I was ignorant of. The standard way to replace a file safely (i.e. atomically) is the system call: Which Syncthing uses. This I know. What I didn’t know is: what happens if other processes had open file descriptors pointing to ? Do they see the new contents? No: those processes can keep reading and writing to the old file object , but the file is orphaned in that no path points to it. And once all file descriptors are released, the file becomes inaccessible. I’m used to thinking of filesystem operations in terms of “this syscall takes a path and gives you a pointer to the file, which you mutate directly”. Whereas works at the level of directory entries: it atomically mutates the mapping from pathnames to files but doesn’t touch files at all.

0 views
Ginger Bill 1 weeks ago

Everyone Says Assembly Is Untyped—Everyone Is Wrong

TL;DR: I believe Odin;s inline assembly is currently the best out of any language.The most important aspects are of this article listed below. I am not aware of any other assembly (GCC/Clang/Rust/Go...) that would combine all of these aspects:Inline assembly is organized into ;templates;, similar to and callable as procedures. templates integrate with rest of the code, through bindings specifying clobbers, pinned, tied, and scratch registers.Assembly syntax is unified across ISAs and consistent with Odin syntax.Assembly is fully type checked...

0 views

Concurrent Servers: Part 7 - Rust

This is part 7 in a series of posts on writing concurrent network servers. In this part, we discuss how the challenges described in earlier parts are tackled in the Rust programming language. All posts in the series: Several years have passed since the previous parts were published. I've recently went over them to make sure the information presented is still relevant and all the code samples build and run using modern toolchains. I strongly recommend reviewing the previous parts before reading this one. This post assumes a basic familiarity with the Rust programming language. It will only explain Rust constructs when we encounter code that wouldn't appear in an introductory book or tutorial. The first few parts in the series focused on a socket server that implements a simple state machine protocol. See part 1 for a complete description of the protocol. Let's start by showing how this protocol is implemented in a basic sequential Rust server: With the function serve_connection defined as: As a reminder, this server version is sequential because it accepts clients one by one; the main loop blocks on serve_connection until it's done (the client closes the connection), and only then goes back to accept the next client. Clearly, handling clients one by one won't do. In part 2 , we've discussed approaches that use OS threads to handle clients concurrently. Let's start with the unbounded one-thread-per-client solution in Rust: The spawn method returns a Result<JoinHandle<T>> ; on success, we allow the handle to be dropped at the end of the loop iteration. In Rust, this detaches the thread; we don't actually wait for it to complete. This is reasonable for our code sample, because the loop is infinite ; it never terminates anyway. The potential for runaway threads is just one of the issues with the unbounded threads approach discussed in part 2. The solution is to use a fixed thread pool. Before diving into the code, a quick note on the design: the thread pool is a fixed set of threads that await "jobs" and handle them to completion. In our case a "job" is serve_connection for a specific client. There are many ways to implement a thread pool; for our use case, I went with a set of threads that all get a shared channel to which the main thread sends jobs. A worker thread picks up the next job from the channel, serves it to completion, and goes back to waiting for the next job. Here's how this looks in code: What is Receiver ? It's a type from the crossbeam_channel crate: Rust's builtin channels in std are mpsc - multi producer, single consumer, but what we need for our job queue is a channel that supports multiple consumers (the worker threads). While std does have mpmc , this is an experimental API only available in nightly versions at the time of writing. Therefore, I've opted to include the crossbeam_channel crate that provides well-tested mpmc channels for this sample [1] . And here's the main function: Note that our job channel is bounded - it has a fixed size. This helps naturally implement a backpressure mechanism - if too many clients connect, the following clients will have to wait - the main loop blocks on tx.send and won't accept additional clients on the socket until jobs are cleared from the channel. In parts 4, 5 and 6 of the series we've discussed event-driven , or asynchronous servers. Let's see how it's done in Rust. Specifically, part 6 presented a gradation from callbacks to promises to async/await mechanisms; Rust supports all of these and - as you'd expect - modern code is usually written with async/await while hiding all the details of promises (called futures in Rust) underneath. Without further ado, here's our simple state machine protocol in asynchronous Rust: Rust takes an interesting approach to async programming: it supports some of its fundamental building blocks (like futures and the async and await keywords) in the core language, but leaves the actual async engine implementation (the thing that implements the event loop) to external crates. By far the most popular crate for async programming in Rust is is tokio , so that's what we're using here. After reading the JS code in part 6, the Rust snippet above should appear fairly familiar, except perhaps the explicit tokio task "spawn". Instead of enqueuing a callback on the connection returned by listener.accept , the code spawns a tokio task, which can be seen as a green thread , and hence uses similar terminology [2] . These tasks must not issue blocking calls; therefore, they are supposed to use tokio's I/O utilities instead of the usual, blocking std utilities. In fact, we have to implement an async version of serve_connection to make this work: Note how similar this code is to serve_connection from earlier; the only real differences are the await calls on socket reads and writes [3] , and the types involved. For example, instead of a std::net::TcpStream used in the synchronous samples, here we're using tokio::net::TcpStream . Tokio has an underlying dependency called mio to handle non-blocking APIs for all kinds of I/O. It wraps OS-specific event loops like epoll to do so efficiently. While most of the series has been using a simple state machine server as the driving example, part 6 switched focus to a server for primality testing which simulates long compute tasks. Let's see how this is done in Rust with tokio: This code is very similar to the previous snippet conceptually; isprime is: Note that this sample demonstrates a job that can block (simulated with a sleep in this case). This can be problematic in an async context, as the tokio documentation explains . One potential solution would be to dispatch a blocking task to a separate thread pool and use tokio channels to communicate with it; this is similar to the approach we've taken in the thread pool sample above. Part 6 also included a version of this server that caches data on a local Redis instance; the goal was to demonstrate the complexity of event-driven code when additional layers of callbacks are added and how async/await can help mitigate that. Here's our Rust version of this server, using the redis crate (that has a tokio component enabled explicitly to support async calls): In conclusion, while Rust provides excellent support for async programming, it doesn't solve its inherent issues like function colors and the need for careful separation between blocking and non-blocking tasks. These issues are typically surmountable with some extra care, and async programming with Tokio in Rust is very popular due to its performance benefits. All the code for this post is available on GitHub . Part 1 - Introduction Part 2 - Threads Part 3 - Event-driven Part 4 - libuv Part 5 - Redis case study Part 6 - Callbacks, Promises and async/await Part 7 - Rust (this part) Because of the function color problem , the redis crate has a connection constructor specifically for async: get_multiplexed_async_connection . Here we have an example of shared state between tokio tasks - the Redis connection. Note that we don't require any particular synchronization because MultiplexedConnection is Clone ; cloning it to different tasks is safe - and in fact that's what we do for each new task. There's no magic here; if you look inside MultiplexedConnection , you'll see that it already has all the synchronization mechanisms implemented internally, as needed. Due to the magic of async/await, the code in serve_client is nice and linear. We simply await on the Redis call, and once it's back we continue with the rest of the handler. Since we're using an async Redis connection, in case waiting is required, control will be ceded to some other task that's not currently blocked on I/O.

0 views
baby steps 3 weeks ago

Cylic trait implementations: motivation

Lately I’ve been thinking about cyclic trait implementations. This is a problem that I’ve been trying to understand for years and years and I finally feel like I’m geting somewhere. I’m going to try to write out a series of blog posts documenting those explorations and, hopefully, culminating in a design that could be RFC’d. In this first post, I want to talk about one of the interesting questions, what I am going to call “internal” vs “external” proofs. I know that this material can seem abstract, so I’m going to try and connect it to “real Rust” as much as possible! This particular blog post is an introduction, explaining the general problem and giving some motivation for why we care. Right now in Rust we require most traits to have non-cyclic , or inductive , implementations. To explain what I mean, let’s consider this trait: Now imagine that we have an impl of this for : A simple impl for and `Option : and finally a recursive type that has an impl as well: If I try to show that , I do that by There’s no cycle here – that is, I didn’t have to use impl L to show that impl L is valid. Now, when I said that “the impl L didn’t have to use the impl L to show that it is valid” that might not have sounded suspicious to you. In fact, it’s a pretty natural idea. After all, generally when you try to establish a logical argument, you aren’t allowed to use cyclic reasoning. That is, you can’t say: I know that Niko likes Rust because Niko likes Rust. So, in the same sense, it seems natural that I should not be able to say “I know that implements because implements ”. But actually, it would sometimes be really useful to say exactly that. One example is so-called “perfect derive”. In our impl above, we had one where-clause, . And if you were to create a custom derive for and write , the impl I showed is typically exactly what you would get. But it’s not necessarily what you want . Consider what you get with : Here, the derive is going to create an impl that requires . But if you look closely, you’ll see that all the fields only use , so in fact, we should be able to clone a even without ! But how is the compiler to know this? You might think that the compiler could do some super smarty-pants analysis on the fields to figure it out. And, in a way, it can: that is what cyclic trait solving is all about. The thing is, while the compiler can do that, the derive cannot – the derive doesn’t have access to the definitions of other types and so forth, and clearly we would need to know things about and to figure out whether is required here. But what we could do is to generate a different impl. Instead of adding for each type parameter, we could add a where-clause for each field type. This makes sense: after all, we are just going to be calling on every field, so it’s quite logical to say that the impl is valid if every field is cloneable: Under this formulation, we can see that all we have to be able to do is to clone an and clone an , neither of which require that . We call this idea [perfect derive][] and it’s been a goal for a while. The thing is, cyclic reasoning is tricky to get right. The example is actually an easy one: that one doesn’t really require cyclic reasoning: But if we use that same “cyclic derive pattern” to generate our impl, things don’t work out so well. Instead of just a bound, our impl now has two bounds: Now imagine we try to show . We begin by applying impl L1, which requires us to show that its where clauses hold: Ugh. Something’s tricky here! Now, maybe you think we can just accept any cycles. And for these examples, it would be fine: but it’s not correct if you consider supertraits . Consider this trait and impl pair: If you are naive, this weird trait-impl pair can be used to prove that any type is , regardless of whether it has a impl. For example: Uh oh, now we did something wrong. We proved that even though there is no impl. Something is fishy. Now clearly we can all see the problem here – the implementation of didn’t really add any information. It was just a tautology, saying that if . It’s not wrong , but implementing was supposed to tell us more than just the fact that there is an impl of , it was supposed to tell us also that the supertrait is implemented. And that’s not true here. But if you think about it, it’s hard to decide why we should reject impl M but accept the impl L1 of for . They both wind up with a cyclic proof. So what’s the difference? This is the question we’ll be exploring over the next few blog posts. This gets at an interesting question: what does it mean for the trait system to be sound . This seems obvious but actually it was a question I found kind of non-obvious for a long time. We’ve found two satisfactory answers to that question. One of them involves converting to dictionary-passing style. Nadri explained that in a blog post . I think that’s a great post to read. I’m going to give another definition here that doesn’t require converting to a dependently typed program 2 My rough definition is this 3 : the trait system is sound if, whenever it accepts some program P, that program cannot have a function that believes some holds for the , but there is no impl of that can be used. So in the case of and , it’s easy to write a program that shows simple cyclic trait solving is unsound: By my definition, any sound type/trait system must reject this program because, if it were to execute, then execution would reach and yet there is no impl that is judged to ber applicable to . Uh oh! As I promised, this post was mostly focused on “setting the scene”. My goal was to explain what the problem is that we are trying to solve – permitting “good cyclic impls” but forbidding bad ones. I didn’t spend a lot of time on the bad ones, but it turns out that there’s a wide variety of unsound things one can do, some of which the compiler currently gets wrong, others of which it would only get wrong if we started permitting cycles. My motivation for getting into this work is a bit complicated. I want perfect derive. But it’s also a loose end in our trait semantics that I really want to see nailed down before we move onto other tasks. Having auto traits (e.g., ) work differently from other traits is clearly a “smell”, and without a strong understanding of the logical underpinnings of our trait system it’s easy to get things wrong when we build extensions. In the next few posts I’ll go a bit deeper into the exploration I and others have been doing. I’ll talk about some of the “false starts” we took along the way and why they don’t work, and then about some of the solutions that are under consideration. Working through this stuff has really helped me to broaden my understanding of various areas of logic. By the time we’re done, we’ll cover 4 coinduction and productivity, modal logic and the later modality, and we’ll see how our techniques might even help us with resolving specialization 5 . I cited it earlier, but if you want to read other tasks on the same subject, I definitely recommend Nadri’s post on dictionary-passing style . Apart from the default bound, I’m ignoring that here  ↩︎ I have found that both the dictionary-passing interpretation and the logic approach I’m using are valuable. In the end, they’re more or less equivalent, which I guess shouldn’t be surprising if you’ve heard of the Curry Howard Correspondence , but I’ll talk about that later perhaps.  ↩︎ I would like to, but haven’t, define a simplified version of Rust that includes trait solving and simple type checkoing and show that it cannot “go wrong” .  ↩︎ In a shallow way, I’m no expert!  ↩︎ Plot twist, bet you didn’t see that coming! I sure didn’t.  ↩︎ Applying “impl L” to show that if Then applying “impl I” to show that To show that we have to show that… , which is easy because doesn’t have any where-clauses 1 uses the impl which requires… , which is again easy To show we use impl L1, which has two where-clauses: , this one is easy because the impl requires that which is true. But is tricky. The impl requires that… We need to prove , and then the impl requires that… We need to prove , but that is what we started with! That’s cyclic logic! Say we want to prove that . We observe that if a type implements , it must implement , so… We begin by proving . We use the impl M, which requires that we show , which is a cycle, so we accept it. Apart from the default bound, I’m ignoring that here  ↩︎ I have found that both the dictionary-passing interpretation and the logic approach I’m using are valuable. In the end, they’re more or less equivalent, which I guess shouldn’t be surprising if you’ve heard of the Curry Howard Correspondence , but I’ll talk about that later perhaps.  ↩︎ I would like to, but haven’t, define a simplified version of Rust that includes trait solving and simple type checkoing and show that it cannot “go wrong” .  ↩︎ In a shallow way, I’m no expert!  ↩︎ Plot twist, bet you didn’t see that coming! I sure didn’t.  ↩︎

0 views
Anton Zhiyanov 3 weeks ago

Relying on Go

Everyone is creating a new programming language these days, often one that's "like Go but with more features" or "like Rust but simpler". Solod , a systems language for C and Go developers, might look like one of those languages, but it takes a different approach. Solod is not "Go-like" in the usual sense, nor is it an attempt to "fix Go's mistakes". At the language level, Solod is literally a subset of Go. Solod reuses much of Go's existing tooling, including syntax highlighting, LSP, linters, and the package management system. Take this quick-start guide, for example: Quick start Install the So command line tool: Create a new Go project and add the Solod dependency to use the So standard library: Write regular Go code, but use Solod packages instead of the standard Go packages: Run without saving the binary: There's nothing new here. It's mostly standard Go workflow, except for , which is a Go program that mimics . Solod also reuses a lot of Go's standard library code and tests. Some of it is taken verbatim from Go's source code, like these two string functions: Of course, Solod retains the Go authors' copyright. Some code requires changes to support the manual memory management with explicit allocators used by Solod: You can probably see the resemblance. Go tools don't know that Solod is a subset of the full Go language, so they won't flag features Solod doesn't support, like function literals or iterators. These diagnostics come from the custom tooling: Also, although a substantial part of Go's standard library is ported verbatim or with minimal changes from the original source, that doesn't mean the code is automatically correct. Solod still needs its own tests, including ones that run under sanitizers and static analyzers. All Solod code is translated to regular C11 and then compiled with GCC or Clang. Solod therefore relies on C tooling and decades of optimization work just as much as on Go's. Solod code: Translated C code: The C version is noisier, of course, especially for more complex programs than this one. But it remains readable. And since there's no runtime, interoperability between Solod and C costs nothing. A new language doesn't necessarily need a new ecosystem. Solod relies heavily on Go, and I see that as a strength, not a weakness. Reusing Go's proven tools and standard library makes Solod more reliable and easier to work with. If you're interested, take a look at Solod's readme — it has everything you need to get started. Or try it online without installing anything.

0 views
Blog System/5 1 months ago

An old-new take on argument parsing in Rust

Over the years, I’ve written tens of command-line applications in many different languages—shell is probably the top contender, believe it or not—and for various ecosystems. Along the way, I’ve developed… let’s say… opinions on how they should behave . But behavior and implementation are different topics, and today I would like to talk a little about the latter in the Rust ecosystem. A big theme behind those opinions is that consistency usually wins: when designing an application, you should target an ecosystem and make sure the tool feels “at home” within it instead of reinventing the way it accepts arguments or presents help. But what is the ecosystem? Is it the language the tool is written in, or… is it the set of tools with which it plays? For example: if you were to write a command-line application in Go, you’d naturally reach for the built-in library to define flags. Doing so would make the tool feel normal to other Go developers and would make it easier to “read” to them—but the end user does not care, dare I say… at all , which language your tool is written in. So if they try to use such a tool in the context of standard Unix tools like those provided by coreutils or textutils, your tool will feel out of place. And that is what matters to me: I develop tools for a certain ecosystem, not for a language, and I want those tools to integrate well no matter which language they are written in. I mentioned Go right above because Go is the prime example of opinionated choices that “leak” in various ways. This article is about Rust, however, so let’s switch languages. But before we do, take a moment to subscribe to Blog System/5 to demonstrate your support. It’s free if you want it to be! When writing Rust command-line applications, the expectation nowadays—or rather, assumption—is that you’ll use the crate to parse options and arguments. Funnily enough, this assumption is so ingrained in the ecosystem that, when I asked a late-2025 coding agent to review a codebase of mine, it hallucinated that I was using even when such crate was nowhere to be found. Here is what a simple -based hello world app looks like: Sample clap-based tool. On the left, the source code. On the right, an invocation without arguments and one invoking help. I will not deny that the resulting app looks nice and that the declarative idiom to define this interface is concise and very powerful. But the result is… out of place with other programs because of all these colors (I know they can be disabled). Also, the code is a bit too magical, as usually happens with -style libraries (I know you can opt out of that). And yet… even with all the bells and whistles, the library doesn’t provide enough mechanisms to define an app “end to end”. You see: Rust’s can return an , which is enough to report success or failure to the caller, but this still leaves the application’s control flow in your hands. Because you have to explicitly call within , there is no guarantee that you do it at “the right time”: you might be tempted to parse config files before parsing arguments and other nasty things like that, which can then lead to weird behavior like not working if the config file is malformed or on an unavailable network drive. isn’t the only game in town though. There are indeed other Rust libraries to parse command lines, with being another popular choice. Some of these are also built around derives, some make different tradeoffs around help text and output style, and some smaller alternatives focus mostly on parsing options . This is all fine, but it still doesn’t give me the small Unix-y framework I wanted: something that treats options and positional arguments as one interface, validates both consistently, and owns the startup sequence from parsing to exit code. For all of the reasons above, I’ve developed “my own ways” to parse options and arguments in Rust so that they align with more traditional Unix-y programs. In doing so, I ended up writing my own library. I initially wrote this library in the context of the EndBOX where I had to implement various system services and wanted: to enforce consistency among them with as little code duplication as possible, and to ensure integration into the host’s ecosystem of Unix-y tools provided by the NetBSD base image. I called that library at the time and, in the fall of 2025, I thought of cleaning it up a little and publishing it. So, today, I want to belatedly announce . Mind you, I had drafted this article back in November but never published it, so today is the day. Better late than never. You might be thinking that is quite a mouthful, and even an ugly name. And you know what? That’s true. But the name is what it is because builds on and extends the ancient getopts , in the tradition of Unix-like systems. is largely unused today in the Rust ecosystem—except for the tiny little fact that itself uses it. OK, OK, if you want me to be perfectly honest… the reason exists at all is because is what I originally picked up in 2016 when I started learning Rust based on my previous knowledge of the POSIX and the GNU libc functions… and I never switched gears. is basically a wrapper over , extending it to offer argument-parsing facilities. Where leaves you with a list of free-form strings to validate by hand, lets you: declare positional arguments with cardinality constraints, validates those constraints for you, prints them as a distinct section in the generated help, and provides helpers for common application metadata such as version, bug-reporting, home page, and manual-page information. As such, its API tries to follow the same interfaces that offers, which means I’ve kept original names intact and modeled my own extensions in a similar fashion. This leads to rather cryptic method names and suboptimal Rust interfaces, but again, I tried to mimic as much as possible. What I’ve changed, however, is the way in which you should use the library. provides an “end-to-end” framework to define the method of an application, and it does so with three pieces. The first is the application , which is a struct that implements the builder pattern to register application metadata, options, and arguments. The second is the command-line , which extends the struct for parsed options with access to parsed arguments. And the third is an macro that facilitates writing the scaffolding for , delegating to a couple of functions. A sample, full-featured program looks like this: And then we can run it in various ways: You might still say: there is too much magic in those macros and builders! And you’re right, so you can also define the same app using imperative code and avoid all of that. To see how, I’ll refer you to the various upstream examples . I do not intend for this crate to replace the nicer or or the other libraries that exist out there. Heck, I do not even expect any of you to want to use it. But I still have a use for something like this in my own programs, and I wanted to factor out the code I had already written into a cohesive standalone piece, so I had to publish the crate. Head to https://github.com/jmmv/getoptsargs/ for more details! Is it the language the tool is written in, or… is it the set of tools with which it plays? Sample clap-based tool. On the left, the source code. On the right, an invocation without arguments and one invoking help. I will not deny that the resulting app looks nice and that the declarative idiom to define this interface is concise and very powerful. But the result is… out of place with other programs because of all these colors (I know they can be disabled). Also, the code is a bit too magical, as usually happens with -style libraries (I know you can opt out of that). And yet… even with all the bells and whistles, the library doesn’t provide enough mechanisms to define an app “end to end”. You see: Rust’s can return an , which is enough to report success or failure to the caller, but this still leaves the application’s control flow in your hands. Because you have to explicitly call within , there is no guarantee that you do it at “the right time”: you might be tempted to parse config files before parsing arguments and other nasty things like that, which can then lead to weird behavior like not working if the config file is malformed or on an unavailable network drive. isn’t the only game in town though. There are indeed other Rust libraries to parse command lines, with being another popular choice. Some of these are also built around derives, some make different tradeoffs around help text and output style, and some smaller alternatives focus mostly on parsing options . This is all fine, but it still doesn’t give me the small Unix-y framework I wanted: something that treats options and positional arguments as one interface, validates both consistently, and owns the startup sequence from parsing to exit code. Enter simpler times For all of the reasons above, I’ve developed “my own ways” to parse options and arguments in Rust so that they align with more traditional Unix-y programs. In doing so, I ended up writing my own library. I initially wrote this library in the context of the EndBOX where I had to implement various system services and wanted: to enforce consistency among them with as little code duplication as possible, and to ensure integration into the host’s ecosystem of Unix-y tools provided by the NetBSD base image. declare positional arguments with cardinality constraints, validates those constraints for you, prints them as a distinct section in the generated help, and provides helpers for common application metadata such as version, bug-reporting, home page, and manual-page information.

0 views
Unsung 1 months ago

“Gravity is worth asking about.”

I’ve enjoyed John Gruber’s posts about ads appearing on an increasing number of Apple surfaces: the App Store, Apple News, and – soon, perhaps – Apple Maps. (Just for reference, here’s an example of such an ad.) = 3x)" srcset="https://unsung.aresluna.org/_media/gravity-is-worth-asking-about/1-framed.1600w.avif" type="image/avif"> In a post earlier this week , Gruber likened ads to stickers on laptops, and shared a fun Steve Jobs story: That’s what those stickers on PCs are: they’re ads. Intel pays for the “Intel Inside” stickers that booger up PC laptop palm rests. Longtime readers will recall that back in August 2007 , Apple held a Town Hall event to introduce new iMacs and some iLife and iWork software updates. In a post-event Q&A (imagine that), Bob Keefe of Cox Newspapers asked “Can you say why you all are not participating in the Intel Inside program, putting the stickers on your new or previous Macs?” This question was so absurd from the perspective of those who covered Apple closely that it prompted outright laughter. […] The 2007 exchange went as follows: Keefe: Why are you not participating in Intel Inside program and not putting stickers on your Macs? Jobs: Uh… what can I say? We like our own stickers better. (In case it’s not clear, this was a joke; Apple didn’t and doesn’t put any such stickers on their products.) In May, Gruber posted about Apple’s ads , too, and brought up the zero-one-infinity rule : I feel like a variation of Zero-One-Infinity is a good rule of thumb for ads, too. From the perspective of users — and probably developers — zero was the best number of ads for Apple to show in App Store search results. One was worse but acceptable. But now that they’re showing more than one, they’re on their way to infinity. They’ve started down the slippery slope. Remember when Google only showed one ad in search results? “Slippery slope” is a perfect term. But I wanted to add something here. In my experience, in the realm of UI, there is no middle notch. I’ve seen it time and time again… the moment you open the door to One, Infinity starts exerting its pull: Here are two examples I’ve been thinking about recently: = 2x) and (width >= 700px)" srcset="https://unsung.aresluna.org/_media/gravity-is-worth-asking-about/2.2096w.avif" type="image/avif"> = 3x) or (width >= 700px)" srcset="https://unsung.aresluna.org/_media/gravity-is-worth-asking-about/2.1600w.avif" type="image/avif"> This – screenshotting in iOS – was originally just one fork: Save or Delete. Now it’s a staggering five options I have to choose from, every time, even if I never touch four of them: Once you wedge one thing in the door, it’s really hard to stop. My theory is that this is because digital interfaces are pretty much all infinitely extensible. There will always be a way to add one more button, one more link, one more setting, one more ad. If something doesn’t fit, you make it smaller. If making it smaller looks bad, you add a scrollbar. If a scrollbar doesn’t feel right, there’s always overflow. Not only it’s very hard to create interfaces that have limitations, but a bad decision is not just precedent – it’s code that can be copied and reused. Existing code always had tons of… well, gravity, even before LLMs. And so, products grow complex without anyone intending them to; a new team adds just one more thing, which in isolation always feels like nothing to worry about. The Hick’s Law , the extra mental load , the complexity all grow in between those moments, in a no-man’s land no team typically feels responsible for. The logic is always circular: Why would the team adding a third option have to do something a team adding a second option didn’t have to do? Why would the team adding the second option worry in advance about option number 5? This is why it’s important to hire and recognize people who will understand that those limitations have to be imposed arbitrarily, and empower them to be able to say, “Let‘s not add this. We like our own stickers better.” = 2x) and (width >= 700px)" srcset="https://unsung.aresluna.org/_media/gravity-is-worth-asking-about/4.2096w.avif" type="image/avif"> = 3x) or (width >= 700px)" srcset="https://unsung.aresluna.org/_media/gravity-is-worth-asking-about/4.1600w.avif" type="image/avif"> (My MacBook does have a sticker, which I bought and put on it since for some reason I find it really funny.) #complexity #system design adding just one setting will send a message that We Do Settings Now and more settings will follow, one uncomfortable exception followed by weeks of deliberations will inevitably open the door to subsequent mindless exceptions, one cheap or lazy approach can spread through the interface like rust, subconsciously telling people “cheap and lazy solutions are okay here.” This right click menu in Chrome started with just one fork (new window or new tab) – now there are three alts that I have to choose between, every single time, even if I only ever use one option: This – screenshotting in iOS – was originally just one fork: Save or Delete. Now it’s a staggering five options I have to choose from, every time, even if I never touch four of them:

0 views
Corrode 1 months ago

Understanding Dyn Compatibility

In Rust, some traits can’t be used as trait objects with . When a trait can’t be used with dynamic dispatch, we say it’s “not dyn compatible.” 1 This has an impact on how you can use these traits in your code. I think that’s one area where the Rust compiler could print a more helpful error message. Fixing the issue is mostly about tradeoffs between compile-time generics and runtime polymorphism and learning when each one fits. Once you understand the concept, you’ll know how to get around the issues by choosing a better design for your trait. If the compiler told you a trait is “not dyn compatible” , your trait can’t be used as because it has a method that can’t go through dynamic dispatch, usually one that returns , takes no , or is generic. To fix it, pick one: Continue reading to understand the tradeoffs between each approach. Here’s an example with code that won’t compile . Say you have a trait that has a method returning a copy of itself: …and there’s a button, which implements : If you tried to compile this code, you’d get an error like this: That all sounds pretty confusing. When you use , Rust creates a trait object . Trait objects use dynamic dispatch to call methods at runtime. Dynamic dispatch just means that the exact method to call is determined at runtime based on the actual type of the object. For dynamic dispatch to work, the trait’s dispatchable API must follow certain rules. These are simplifications: each method-level rule is really “…unless that method opts out with ”, which we’ll see in a moment. Traits also have a few item-level restrictions, such as no associated constants; we’ll summarize the fuller list later. For now, the rough version is enough to build intuition. In our example, we violate the first rule: the method returns , which means “the same type as the implementor of the trait”. When you use , the concrete implementor is hidden behind the trait-object interface. The vtable still points to the right concrete implementation, but the call site has no single concrete return type it can name for . That’s a problem, because the compiler needs to know the size of the return value at compile time, and could be any size . It needs to know the size, because the returned value has to live somewhere : the caller sets aside exactly the right amount of space (usually on the stack) before the call even happens. With a , the concrete type is erased from the caller’s static type, so there’s no single size the compiler could reserve for it. It will become clearer once we look at some fixes. Don’t worry, we won’t have to refactor all our code! All fixes use the same trait example. There are multiple ways to make it dyn compatible. We have a bunch of options: Each approach comes with different tradeoffs. Depending on the kind of dyn-compatibility issue, one might fit better than the others, or you might combine a few. Let’s look at each of these in detail. One common way to fix the problem is to use generics instead of trait objects. Generics resolve to concrete types at compile time , so the compiler knows the size of . The compiler generates a separate copy of the function for each concrete type that implements the trait. Then, at runtime, you no longer need to worry about any dynamic dispatch (which means “figuring out the type at runtime”). The compiler always knows which type it is dealing with, so it can pick the right method to call. Our trait stays the same: But now we change the function which uses the trait to use generics instead of : Note how we changed the function signature to use a generic type parameter that implements the trait. Here we tell Rust: “I have some type that implements , and I want to use it.” Rust then generates the necessary code for each type used. That’s close to using , but not quite the same. The difference is that with generics, the compiler knows the concrete type at compile time, so it can handle correctly. For instance, we might know that is in this case, so returns a . Now the confusion about what means is gone! The downside is that you can’t fully lean on dynamic dispatch anymore, and you might have to refactor a lot of code if you were using trait objects extensively before. Your binary size might also grow because of all the copies of the function that the compiler generates for each concrete type. What’s the benefit of fully leaning on dynamic dispatch? Fair question! Dynamic dispatch has a bunch of really nice properties: Another option is to keep using trait objects but change the problematic method to only work with concrete types. This means “this method can only be called when has a known size at compile time”, which is true for concrete types but not for trait objects. It’s more explicit, since you control how the trait can be used. The catch is that it limits the trait further down the line: some methods won’t be callable on every trait object, and changing the trait later becomes a breaking change. You won’t be able to call on , but you can still call it on concrete types like . So you keep most of the flexibility of trait objects (unlike with generics), as long as you remember that some methods won’t be available through . We can change the return type of the problematic method to return a boxed trait object instead of . This works because has a known size at compile time. It’s a pointer to an object on the heap. It’s actually a fat pointer : two words wide, or 16 bytes on a 64-bit system, because it also stores a pointer to the vtable; more on that later. What matters is that this size is fixed and known at compile time, unlike , which varies based on the concrete type. The downside is that tends to be viral in your codebase. You’ll end up writing more often than you’d like, which gets noisy. On top of that, this fix only works for methods that return . If your trait also has static methods or generic methods, you’ll need to combine this approach with one of the other fixes. Sometimes the best solution is to separate the dyn-compatible methods from the problematic ones into different traits. Maybe your code is silently trying to tell you that you are mixing up two different responsibilities and that they should be untangled. In general, prefer smaller, focused traits over large, monolithic ones. Traits are not interfaces! Instead, we lean on composition and focus on behavior rather than mangling multiple ideas into a single trait. Here’s a more realistic example: separating rendering from widget creation. Factory methods are often static (no parameter), which makes them incompatible with . So we split them off into a separate trait. When you write , you’re creating a trait object . It’s a special kind of value that consists of two pointers (a “fat pointer”): As you can see, a trait object has: The vtable is created at compile time and contains pointers to the methods for the specific type. It is common in many programming languages that support dynamic dispatch, such as C++, C#, or D. When you call a method on a trait object, Rust uses the vtable to look up the correct function to call based on the actual type of the data. For dynamic dispatch to be sound, the vtable-facing methods need stable, concrete function signatures: If a trait has dispatchable methods that return or have generic parameters, there is no single vtable entry with one concrete signature that can represent all possible calls. That is the root cause of dyn compatibility issues. A trait is dyn compatible if it follows a list of rules . The rules boil down to the same core issue: the interface must have a finite, statically-known shape even though the concrete implementor behind it is hidden. A Modern Gotcha: in Traits Since Rust 1.75 , you can write directly in a trait. But there’s a catch: a trait with an is not dyn compatible . An desugars to a regular method that returns , a hidden return-position . The type is called “opaque”, because we don’t know what it is, and the compiler doesn’t expose it to us. Opaque return types aren’t dispatchable (which means we can’t put them in a vtable of functions), so the trait can’t be used behind . If you need dynamic dispatch with async methods today, you have a few options: Dyn compatibility determines if a trait can be used with . The rules exist because: If your trait is not dyn compatible, don’t worry! Many standard library traits ( , , etc.) are also not dyn compatible. As we’ve seen, there are ways to work around these limitations with type erasure, generics, or more fine-grained traits. Which fix to reach for depends on what your trait needs and what you’re willing to give up: In practice you’ll often combine these. For example, splitting a trait and boxing a return value. I find it interesting to see how dyn compatibility evolved over time in Rust. If you do, too, here are some resources to dig deeper: The lang team also wants a “practical path” to call s through natively. It’s on the 2026 project goals , so the async gotcha above should ease over time. The concept used to be called “object safety” until Rust 1.84.0. If you’re reading older resources, they mean the same thing. The name got changed because it was confusing. “Object safety” suggests that Rust has “objects” in the traditional OOP sense and that the term is about “safety”, which is misleading. The new term “dyn compatibility” does a better job of saying that it’s about whether a trait can be used with for dynamic dispatch. I still don’t love either term, but I also can’t think of a better name that is both short and accurate. ↩ add to the offending method return instead of use generics instead of split the trait in two What does “not dyn compatible” mean? Shouldn’t the part take care of it? What’s a “vtable”, and why does the trait need to “allow building” one? What does it have to do with ? Dispatchable methods must not return . Dispatchable methods must have an allowed receiver ( , , , and a few related pointer forms). Plain static methods don’t have one. Dispatchable methods must not have generic type parameters. Use generics instead Opt out problematic methods with Return boxed trait objects instead of Split into two traits It’s very flexible. You can swap out implementations at runtime, which is great for plugins or when you want to change behavior without recompiling. It allows for polymorphism. You can treat different types that implement the same trait uniformly, which can simplify code that needs to work with various types. You could technically do the same with generics, but sometimes you can’t afford the increase in code size or compile times that come with monomorphization. It can lead to cleaner and more maintainable code in certain scenarios, especially when dealing with complex hierarchies of types and behaviors. For example, take a graphics rendering engine where you have different shapes (circles, squares, triangles) that all implement a trait. Using dynamic dispatch, you can store them all in a single collection and call . If you were to try the same with generics, you’d end up with a lot of boilerplate code to handle each shape type separately. A data pointer that points to the actual data (the concrete type implementing the trait) A vtable pointer that points to a table of function pointers for the methods every dispatchable method needs a receiver that leads to the object and its vtable argument and return types must be expressible without knowing the hidden concrete the vtable must contain a finite set of function pointers, known at compile time Box the future yourself and return . Use the crate, which does that boxing for you. Use the crate, which generates a dyn-compatible wrapper for traits with . Trait objects use dynamic dispatch via vtables Vtables are static, compile-time structures, which hold method pointers Type information is erased at runtime to allow polymorphism The compiler must guarantee type safety at all times, even if it can’t see the concrete type 2014-09-22: RFC 255 - Introduced object safety (2014, before Rust 1.0) 2014-11-03: Issue #428 - Object-safety and static methods 2015-01-03: RFC 546 - Removed implied bound on traits 2023-08-24: Rust 1.72 - GATs can be opted out with 2023-12-28: Rust 1.75.0 - Stabilized and return-position in traits (though such traits still aren’t dyn compatible) 2025-01-09: Rust 1.84.0 - The docs had moved from “object safety” to “dyn compatibility” around this release cycle; the tracking issue notes that the rename unfortunately missed the release notes. The concept used to be called “object safety” until Rust 1.84.0. If you’re reading older resources, they mean the same thing. The name got changed because it was confusing. “Object safety” suggests that Rust has “objects” in the traditional OOP sense and that the term is about “safety”, which is misleading. The new term “dyn compatibility” does a better job of saying that it’s about whether a trait can be used with for dynamic dispatch. I still don’t love either term, but I also can’t think of a better name that is both short and accurate. ↩

0 views
<antirez> 1 months ago

Being Linux Torvalds

(This blog post was adapted from the transcription obtained from my YouTube video at https://www.youtube.com/watch?v=l6lxgYeVZqs) When Linus Torvalds developed the first Linux kernel, he had studied the Minix sources, he had studied computer architecture, he had the base knowledge needed, and he was obviously a very brilliant programmer. But that operation of writing a minimal yet working Unix kernel for the 386 (at the beginning Linux was, let's say, mono-architecture) was something within the reach of many other programmers and students. Many in the sense of, I don't know, 0.1%, one in a thousand, one in ten thousand. Obviously most people are not able to do this kind of feat, but a lot of people are. If you look at Hacker News in the latest years, you'll see how many projects of kernels written in C, microkernels implemented from scratch, kernels written in Rust, kernels made in all sauces and manners, small Unix systems created vertically for the Raspberry Pi, operating systems for the ESP32 and so forth. Writing a kernel is not something within everybody's reach, but it is something that many can complete, if they put enough effort into it. Then, of course, not everybody will do it well. He is a genius programmer, without any doubt, so he did it better. And yet, of Linus there is only one. This implementative capacity of his, in fact, would not tell us much about him: what we should focus on, instead, is what happened later. ## He stopped writing code Among the maintainers of the famous open source projects, he was one of the very few that, very early in the history of the development of Linux, almost completely stopped writing code in order to concentrate on the leading of the project. On being the leader, the coordinator, the single mind holding the clarity about what the goals of the project must be, and so on. And this is a rare thing. Many maintainers (myself included, for a long time) continue instead to implement things directly, to not delegate much, and so forth. This also starts from a different idea of software. Linux, necessarily, had to grow immeasurably: it is in the quality itself of a kernel that wants to embrace many devices, platforms, subsystems, and to continuously adapt to the times, to the needs of the new software, to the hardware that comes out little by little. So this was not a mistake. Redis, on the contrary, could remain something self contained. The other day I received a pull request on linenoise from Dr. Richard Hipp of SQLite: he too aimed at stability, at minimalism, at performances, but always keeping the code base very small, and he continued to write code for a very long time. Linus, instead, no. He understood immediately that he had to donate his time to something that was more important, for a project destined to become very big compared to what is the implementative capacity of a single person. So he became the project leader, the one that owns the ideas, the direction. And what is it that Linus does, then? He does not look at every patch line by line, every time. Of course it also happens to him to look deeply into a single implementation, in order to understand what is going on. It happened to him, over the years, to write some new subsystem, or even to rewrite one: I think he did it once with the USB layer, many years ago, and he did it with the virtual file system, that at some point I believe he reimplemented, changing the structure of the inodes and of the inode cache, and he did it for several other reasons. From time to time he continued to program, when he created Git, and so forth. But for the most part he does not look at the patches singularly, in detail, line after line: he communicates with the maintainers of the subsections, and understands if a given feature or a given direction is, or is not, a road to take. So, to say it in Brooks' terms, in Mythical Man Month terms, Linus holds the design concepts of the kernel, and continues to dialogue with everybody below him in the hierarchy of the kernel so that the kernel goes towards a certain direction. So that the developments go towards a certain direction, both from the implementative point of view (how these developments are implemented, what is the quality, what is the implementative idea in the very way the code is written), and from the design point of view: what is it that we want to do, what we don't want, what is the best strategy for the modules, for the scheduler, for the hardware support, for the integration of Rust or not. All this stuff here. Now, I believe that this was the real genius of Linus. He is not just a very brilliant programmer: there are others. He is also a maintainer, an incredible designer, and one capable of handling a huge project ideas and structure in a coherent way, dialoguing with many other people. This thing is not for everybody. ## We are Linus, now Now, when we program with the artificial intelligences, we are exactly that same thing. We are Linus Torvalds, not always with the talent that he has, but the role we should assume, in the projects where we don't do the review of every line of the code, is exactly of that type. It is exactly the role that he has. Only, the thing is simpler to dominate: unless we use a lot of agents in parallel, it is substantially simpler to dominate than a multitude of patches arriving from different ways. But it is much faster. It is as if, instead of interacting with a team composed of many people at human speed, we interacted with a team composed of one, two, three people, based on how many parallel branches of our project we are developing in that moment, but that are much faster, so they give us immediately a much faster feedback. This slightly changes the modality of the work, but in my opinion for the better: it is easier, less context switching, fewer people to deal with, many fewer problems due to the character, the attitude, and so forth. So, if we think that this role is important, we must not think that automatic programming is "I put the prompt, and the thing writes". Vibe coding is a wrong idea of what automatic programming is, and of what automatic programming will be for the majority of people. Vibe coding is a very interesting thing for who does not have technological abilities and wants anyway to have an impact on the construction of their own tools, and so forth: so, welcome, because it democratizes the possibilities. But it is not that. Automatic programming, instead, in the hands of people that are expert technicians, or expert programmers, expert designers, expert software architects, is to assume the role of Linus, with the agents and the LLMs assuming the role of the different maintainers of the different subsystems. And since not everybody is able to do it so well, automatic programming as well has need of talents that talk with the agents, that check the ideas, that know which are the implementations to do and the ones not to do, the way of communicating with the agents in order to make them do the best work, putting there those design hints that a great programmer intuits, that a good programmer intuits and manages to precompute. So automatic programming, when it is done well, means to assume the role of Linus. And this thing can be done well, it can be done badly, it can be understood, or it can instead be debased. And it is also something that needs training, that needs to be learned, exactly as Linus had to learn it: he surely had an innate talent for this, but he passed from "I implement everything" to that capability of handling a symphony, of being the orchestra director. That, for me, is the lesson of Linus, and it is one that should immediately be used as an argument of contrast for those that say that, well, with the LLMs programming is easy for everybody. Comments

0 views
Corrode 1 months ago

Hardening Rust Code For Production

We talked about patterns for defensive programming in Rust before, in which implicit invariants that aren’t enforced by the compiler lead to utter misery. But being careful isn’t enough! Even valid code can fail at runtime in ways that are hard to predict and control. That’s what we’re covering next. This article is for you if you want to… What happens when a Rust program panics? There is no single correct answer because is not a “single behavior.” For starters, there’s a difference between unwind and abort. invokes a closure, which captures the cause of an unwinding panic. But the Rustonomicon has the following to say about unwinding panics: We would encourage you to only do this sparingly . In particular, Rust’s current unwinding implementation is heavily optimized for the “doesn’t unwind” case. If a program doesn’t unwind, there should be no runtime cost for the program being ready to unwind. The alternative to unwinding is aborting the entire process. That does what it says on the tin: the program immediately terminates without unwinding the stack or running destructors. Halt and catch fire. Weirdly enough, that’s often the safer choice, especially when dealing with FFI boundaries or performance-critical code. That’s because unwinding across FFI boundaries is undefined behavior, and unwinding can be expensive in performance-sensitive code. To enable aborting on panic, add the following to your : And even if you did not explicitly configure this, catastrophic panics like stack overflows and out-of-memory errors always abort the process . That’s because unwinding in these situations is unsafe and can lead to undefined behavior. In practice, this shows up in two places: These failures are fundamentally different from ordinary panics in that they cannot be caught or recovered from. To handle them gracefully, you need to know exactly how and where your program will run, and design accordingly. For example, in the case of , avoid unbounded user input that could lead to excessive allocations. Another difference is between thread-level failures and process-level crashes. A common misunderstanding is that terminates the entire program, but in a multi-threaded application, that is not necessarily the case. For example, a background worker thread can panic while the main thread continues running. What sounds like a benefit can leave the system in a partially degraded state. This distinction becomes especially important in long-running systems (servers, workers, async runtimes, …). A panic in a request-handling thread might only abort that one request, while the rest of the service remains available. Here’s a small example using scoped threads ( Playground ): The interesting part of the output is this: Request 2 panics, but requests 1 and 3 still finish. The panic belongs to the worker thread. The main thread gets notified on but keeps running. 1 Whether this is acceptable depends on the system’s invariants. If a panic indicates a violated assumption confined to a small scope, like a single request, letting the process continue may be reasonable. But if it signals a global invariant violation, continuing execution can be outright dangerous. Panic behavior is part of your system’s failure model . Treating all panics as equivalent hides important distinctions and leads to fragile assumptions. Be explicit about whether a failure may take down a single task, a single thread, or the entire process. Never panic in an uncontrolled manner. If you maintain a library, you have less control over where your code runs and what a panic can take down. Consider enabling stricter Clippy lints such as and to catch common panic sources before they become part of your public API. Those lints can be noisy in applications, but they are often useful when panic freedom matters more than convenience. Now that you understand how panics work, let’s talk about operational hardening. When things go wrong, you want to know about it. But by default, Rust panics just print to and disappear into the void. In production systems, that’s not so great. You might prefer crash reporting or centralized failure handling, and that’s where panic hooks come in. A panic hook is a function that gets called whenever a panic occurs, giving you a chance to record the failure before the program terminates or unwinds. It will not make an invalid state safe again. Its job is to capture enough context to debug the failure, alert someone, and shut down cleanly when possible. Here’s a simple example of setting a panic hook: And here’s a panic hook that sends structured JSON data to a crash reporting service: What’s Inside ? The struct contains the panic message (via ) and the source location where the panic occurred (via ). Be aware that both can leak sensitive information: file paths may reveal internal directory structure, and panic messages might contain interpolated user data. And finally, here’s Sentry’s panic hook handler , which is even more sophisticated: Sentry’s panic hook: There’s a lot to learn from these few lines of code! Panic hooks are also your final opportunity to prevent information leaks. The sensitive data can come from two places: the panic payload and the panic location. The payload is whatever your code passed to , , , or an assertion. That means it can contain interpolated user input, internal state from output, request headers, tokens, email addresses, IP addresses, customer IDs, or other identifiers. The location can expose source file paths, workspace names, or CI/build machine directory layouts. A well-designed panic hook sanitizes these messages before they reach logs or crash reports. Better yet, avoid putting secrets or raw user data into panic messages in the first place. Prefer stable error codes, request IDs, or redacted domain types. Regexes can catch obvious patterns like email addresses and bearer tokens. UUIDs and IP addresses can also identify users. Treat those checks as your final fallback. You can look into crates like expunge or veil to automatically redact sensitive information from structs: Before the process terminates, you might want to flush logs, close network connections, or notify other systems that this instance is going down. Setting a hook is a great way to perform such cleanup operations. Panic Hooks Run in a Compromised Environment Be careful: one of the subsystems you want to interact with might be the cause of the panic you’re handling! For example, if your database connection pool panicked, trying to flush pending writes to that same pool will likely fail or hang. Keep cleanup operations fault-tolerant and avoid anything that can panic, block indefinitely, or depend on the subsystem that just failed. Panic hooks only run for unwinding panics. If your program aborts on panic, or if the panic is caused by a stack overflow or out-of-memory condition, your hook won’t execute. Never rely on panic hooks for correctness. They’re purely for observability and graceful degradation; don’t try to recover from logic errors as it is very hard to rely on a system’s fragile underpinnings at this stage. Okay, you handle errors gracefully and you know how your system behaves on panic. Panic behavior isn’t the only runtime failure mode you need to worry about. Here’s some simple recursive code. What is wrong with it? The problem is that recursion can quickly exhaust stack space. If you allow users to call this function with large inputs, it might crash your program. Rust does not guarantee tail-call optimization on stable Rust . Some compilers and languages can turn certain tail-recursive functions into loops, but you should not rely on that transformation in Rust. If recursion depth depends on user input or external data, rewrite the algorithm iteratively or put an explicit bound on the depth. It takes some experience, but for recursive algorithms where you’re not in control of the input size, it’s often safer to use an iterative approach: One of the most dangerous assumptions in Rust development is that debug and release builds are functionally equivalent. They’re not. In many ways, you’re shipping a different program than the one you tested. The most obvious difference is integer overflow behavior. Debug builds panic on overflow, while release builds silently wrap around. We covered that in Pitfalls of Safe Rust . But the differences run deeper than arithmetic. Release builds remove checks, enable optimizations, and may exercise different code paths behind . Unsafe code and FFI boundaries are especially sensitive to this: undefined behavior can appear harmless in debug mode and break only once the optimizer starts relying on Rust’s aliasing and validity rules. Here is a trivial example: In a debug build, trips the . In a release build, the assertion is gone. The subtraction can underflow and wrap around, turning an invalid discount into a huge number. If the check protects a real runtime invariant, use or return a instead of relying on . The fact that tests pass in debug mode does not prove that production behavior is correct. Run normal debug tests as the fast default, and add release-mode tests for critical integration tests, arithmetic-heavy code, unsafe or FFI-heavy code, and anything whose behavior depends on optimization or release-only configuration. Your code is only as safe as your dependencies. You should regularly audit your dependencies for known vulnerabilities. Two helpful tools for that are and . It’s recommended to run those as part of CI. mimalloc is a drop-in global allocator built by Microsoft. What’s special about it is that it also has a secure mode , which adds mitigations like guard pages, randomized allocation, and encrypted free lists to make some heap-corruption bugs harder to exploit. 2 Safe Rust already prevents most use-after-free and buffer-overflow bugs, and a secure allocator does not magically make memory-unsafe code safe. This is mostly defense-in-depth for programs with unsafe code, custom allocators, C/C++ dependencies, or FFI-heavy boundaries. To enable secure mode, put this in : Then use it as your global allocator: Now, all heap allocations in your Rust program will use mimalloc’s secure allocator. Measure the performance impact on your workload before rolling this out broadly; allocator choice can matter a lot for latency-sensitive services, games, packet processing, and other allocation-heavy programs. Even well-written Rust code can be compromised through its dependencies, environment, or C FFI boundaries. The idea is to reduce your blast radius. Now, how you do that depends on your deployment environment, but generally people use Docker and Linux, so I thought I’d share some techniques for those; specifically, how to build minimal container images and filesystem sandboxing. A minimal production image contains exactly what you put in it. Even if your service is compromised, the attacker has very limited tools at their disposal to do further damage. My recommendation is Google’s distroless images , but please do your own research 3 as I’m not an expert on this. Distroless images are minimal Debian-based images stripped of everything unnecessary, while still including TLS certificates and a non-root user. For a typical Rust web service, start with : it includes the C runtime libraries that a normal Debian-built Rust binary may dynamically link against, but no shell or package manager. (Check the latest version in the distroless README .) Here is an example Dockerfile using for dependency caching: Take this Dockerfile as a starting point, but please adapt it to your own project requirements. keeps dependency builds in a separate Docker layer, so changing your application code does not force all dependencies to rebuild. The important details are: use the same Rust version in all build stages, build with , scope workspace builds with when appropriate, and keep , , and editor files out of the build context via . For a deep dive on Docker images and build-time optimization, see Tips For Faster CI Builds . Keep the Debian suffix explicit instead of using the unversioned tag, and pin by digest if reproducible deploys matter to you. If you deliberately build a fully static musl binary, then or even can be a better fit. But don’t mix the two approaches: a glibc-linked binary needs a runtime image that provides the libraries it links against. A Note On Alpine Base Images Alpine base images are a well-known alternative, but they use musl instead of glibc. That can expose differences in DNS resolution, TLS/native dependencies, allocator behavior, and crates that assume a glibc-like environment. ( 1 2 3 ) That doesn’t mean Alpine or musl are wrong; just treat them as a deliberate target and test them like one. If you build on Debian and want a small runtime image, distroless is usually the less surprising default. Even inside a minimal container, your process still has access to any file the container mounts. Landlock is a Linux security module that lets a process restrict its own filesystem access. If your service is ever exploited, the attacker can only reach the files you explicitly allowed. 4 Landlock Is Deployment-Specific Landlock is Linux-only and requires kernel support. It landed in Linux 5.13, but older enterprise kernels, custom cloud images, or container hosts may not enable it. Check your actual deployment target. Also apply the sandbox only after you know which files your process needs. If your service executes helper binaries from , reads timezone data from , loads certificates, opens SQLite files, reads config from , or writes uploads to , those paths must be allowed explicitly. On non-Linux targets, look for equivalent sandboxing mechanisms instead of copying this exact snippet. Call as early as possible in , before spawning threads or accepting connections. The restrictions apply to the entire process from that point forward. The two approaches really go hand in hand: Don’t run as root in production, even inside a container. That’s one reason distroless images provide a user and why the example above uses the tag. If your service only needs to listen for HTTP traffic, prefer a high port like over running as root just to bind to port . Linux capabilities are another useful lever. Instead of giving a process full root privileges, grant only the specific capability it needs, such as for binding to low ports. If a process needs elevated privileges only during startup, drop them before accepting requests. The details vary by platform and orchestrator, so treat Linux containers as one concrete setup. For systemd services, Kubernetes, FreeBSD jails, macOS sandboxing, or Windows services, look up the equivalent least-privilege and sandboxing features for that environment. The big picture is that security hardening is about reducing the surface of things that can go wrong. Every capability your process holds unnecessarily is a liability and everything your code manages that could be delegated to the OS, init system, or container runtime probably should be. Miri is an interpreter for Rust’s mid-level intermediate representation (MIR) that can detect undefined behavior at runtime. It works by executing your Rust code in a special environment that tracks memory accesses, pointer validity, and other low-level details to catch issues that the compiler can’t statically guarantee against. More people should know about Miri, because it is really helpful for hard-to-detect race conditions in multi-threaded or async code; but it can do way more than that, of course. It has already detected a lot of real-world bugs , even in the standard library. Using it is as simple as running: This will run your tests under Miri’s interpreter. The docs also describe how to add miri to CI : (Make sure to check the latest instructions in the Miri repo, as the setup process may change over time.) If you’d like to learn more about Miri, there is a research paper from 2026 that goes into the design and implementation details: Miri: Practical Undefined Behavior Detection for Rust . A hardened service doesn’t just crash. Instead, it shuts down gracefully when asked. Aim to finish in-flight requests, flush your buffers, and release resources cleanly before you exit. The pattern is: listen for shutdown signals, stop accepting new work, drain existing work, then exit. Frameworks like Axum have built-in support for graceful shutdown . Use it! The key is handling signals like (sent by Kubernetes, systemd, or ) and (Ctrl+C). Here’s a minimal example using tokio-graceful-shutdown , which is a crate that provides good signal handling without much boilerplate. It introduces a concept of “subsystems” that can run concurrently and listen for shutdown requests. When an external service (database, API, cache) starts failing, you don’t want to keep hammering it with requests. A circuit breaker tracks failures and “trips” when a threshold is reached. For production use, consider crates like or the more actively maintained , which is based on failsafe. Unbounded resources are a common source of runtime failures. Everybody who was on call for a production service will tell you this. Set explicit limits on everything . SREs will thank you for it! Limits make your service more predictable, and they make misconfigurations obvious sooner. Common things you should limit include: Here are some examples of how to do this in practice: See Axum’s : Bound the number of items in every queue or channel in your system. Every unbounded resource is a potential DoS vector. Explicit limits turn those catastrophic failures into (annoying but harmless) graceful rejections. Ideally, your system should be able to recover from transient failures without human intervention. Health checks let load balancers and orchestrators know when something is wrong, so they can react. A typical setup has two endpoints, a liveness probe and a readiness probe. The liveness probe checks if the process is alive at all, while the readiness probe checks if the process is healthy enough to handle traffic. This could honestly be an entire article on its own, but here’s a quick example using Axum to illustrate the concept: What’s neat about it is that this maps directly to Kubernetes’ health check system: Do we really need both probes? Yes, because they serve different purposes: Finally, here are some more tools that help you catch problems before they hit production: The tools above help catch undefined behavior, memory safety issues, code coverage gaps, and performance bottlenecks. They are dynamic analysis tools that complement Rust’s static guarantees. This only holds for unwinding panics. If you compile with , or hit a stack overflow or out-of-memory failure, the whole process exits and never gets a chance to return . ↩ https://docs.rs/mimalloc-safe/latest/mimalloc_safe/ ↩ Data sources I found useful for this topic include this post and this comparison . ↩ This approach would have prevented a vulnerability in Meta’s crate , a tool for recording and displaying system data like hardware utilization and cgroup information on Linux. ↩ make your code resilient at runtime harden your Rust code for production know how Rust code can fail in unexpected ways and how to recover from that Panic Semantics Are Part of Your API Unwind vs. Abort Thread-Level vs. Process-Level Failures Observing Failures With Panic Hooks Example Panic Hooks Sanitizing Sensitive Data Cleanup Operations Limitations Stack Overflows And Runtime Behavior Release and Debug Builds Are Two Different Programs Testing Release Behavior Supply-Chain Security Secure Allocations With mimalloc Limit Your Runtime Attack Surface Minimal Docker Images Filesystem Sandboxing With Landlock Drop Privileges and Capabilities Miri: Detect Unsafe Code Issues Graceful Shutdown Handling Circuit Breakers for External Dependencies Resource Limits Request Body Size Limits Limit Queue Depth Set Timeouts on Everything External Health Checks and Self-Healing Runtime Hardening Tooling Panics that would unwind across an extern “C” boundary are defined to abort instead of unwinding, because letting unwinding cross that boundary is undefined behavior . And if a fails, it aborts the process . If that’s a problem, you need to proactively check for allocation sizes before allocating or avoid heap allocations altogether. Logs the panic information Preserves the previous panic hook behavior by calling Ensures the hook is only set once using minimal images limit what’s in the container Landlock limits what the process can touch at runtime. Upper bound on any user input (upload file size, parameter bounds, etc.) request body size timeouts on external calls concurrent connections to external services queue depth for background jobs thread count and DB connection pool size Kubernetes stops sending traffic (graceful degradation) if the readiness probe fails. It does not yet kill the pod. Kubernetes restarts your pod if the liveness probe fails (it’s self-healing!) – fuzz testing for Rust code – another fuzzer with Rust support – detects usage of unsafe code – runs Valgrind on Rust code to find memory errors – code coverage via rustc/LLVM source-based instrumentation ( ). It reports line and region coverage, works with and , and is a good default for new projects. – an older Rust coverage tool with strong Cargo and CI ergonomics. On Linux it defaults to a backend ( only); LLVM coverage is available through and is the default on macOS and Windows. Useful if its reports fit your workflow, but expect different platform and test-runner edge cases than . This only holds for unwinding panics. If you compile with , or hit a stack overflow or out-of-memory failure, the whole process exits and never gets a chance to return . ↩ https://docs.rs/mimalloc-safe/latest/mimalloc_safe/ ↩ Data sources I found useful for this topic include this post and this comparison . ↩ This approach would have prevented a vulnerability in Meta’s crate , a tool for recording and displaying system data like hardware utilization and cgroup information on Linux. ↩

0 views
Corrode 1 months ago

The Rust Foundation

Most Rust developers use the language, compiler, package registry, and tooling every day without thinking too much about the organization that helps keep parts of that ecosystem funded and sustainable. This episode is a re-introduction to the Rust Foundation: what it does, what it does not do, how it relates to the Rust Project, and why that distinction matters for teams using Rust professionally. My guests are Rebecca Rumbul, Executive Director and CEO of the Rust Foundation, Lori Lorusso, Director of Outreach at the Rust Foundation, and David Wood, Principal Software Engineer at Arm, Compiler Team Co-Lead in the Rust Project, and a Rust Foundation board member. Together we talk about the practical side of ecosystem stewardship: infrastructure, security, interop, maintainer support, governance, corporate membership, open-source funding, and the pressure new technologies like AI put on language ecosystems. CodeCrafters helps you become proficient in Rust by building real-world, production-grade projects. Learn hands-on by creating your own shell, HTTP server, Redis, Kafka, Git, SQLite, or DNS service from scratch. Start for free today and enjoy 40% off any paid plan by using this link . The Rust Foundation is an independent non-profit organization supporting the success, sustainability, and positive impact of the Rust programming language. Its work includes funding and supporting ecosystem infrastructure, security and interoperability initiatives, maintainer support, project administration, community programs, events, and collaboration with member companies and donors. The Foundation is separate from the Rust Project. The Rust Project governs the language, compiler, standard library, and technical direction through its own teams and decision-making processes. The Foundation provides organizational, financial, legal, and operational support around that work, without owning Rust’s technical roadmap. Rebecca Rumbul is the Executive Director and CEO of the Rust Foundation. She leads the Foundation’s work on organizational strategy, member engagement, sustainability, and support for the broader Rust ecosystem. Lori Lorusso is Director of Outreach at the Rust Foundation. Her work connects the Foundation with the Rust community, member organizations, trainers, contributors, and companies adopting Rust in production. David Wood is a Principal Software Engineer at Arm, CE-SW Rust Team Lead, Compiler Team Co-Lead in the Rust Programming Language Project, and a board member of the Rust Foundation. In this episode, David adds the perspective of someone involved in Rust’s technical work as well as Foundation governance. Mozilla - The first home of the Rust language Python Steering Council - The governing body of the Python Project How to Write a C++ Language Extension Proposal - Bjarne Stroustrup, the inventor of C++, on why C++ needed a standards committee SCRC - The Safety-Critical Rust Consortium FLS - The Ferrocene Language Specification, a specification of the Rust language that is required for certain steps in the certification of Rust for safety-critical applications Foundation Membership Tiers - The different quantifiable benefits from Diamond to Silver and Associate Memberships Rust Commercial Network - A group of organisations that use Rust in production working together with the Rust Project Rust-C++ Interoperability Initiative - An initiative of the Rust Foundation to improve interoperability between Rust and C++ Rust Embedded Working Group - An official working group of the Rust language to improve usability of the language in hardware-constrained environments An AI Security Engineer in Residence for the Rust Ecosystem - Describing the position of the security engineer made possible by funding from Alpha-Omega Rust Foundation Maintainers Fund - The Foundation’s fund to support Rust maintainers Rust Foundation Trusted Training - The Foundation’s accreditation program for Rust training providers Rust Foundation Website Rust Foundation Media Room Rust Foundation on GitHub Rust Foundation on LinkedIn David Wood’s website

0 views
baby steps 1 months ago

Battery packs: Let's talk about crates, baby

This blog post describes an idea I’ve been kicking around called battery packs . Battery packs are a curated set of crates arranged around a common theme. For example, there’s a CLI battery pack that has everything you need to build a great CLI , an opinionated pack for creating a backend web service , and one for embedded development (based on the Embedded Working Group’s Awesome Rust repository ). We’ve also got some smaller ones, such as the error-handling battery pack that shows how to handle errors in Rust. But this is just the beginning – a key part of the battery pack design is that anybody can create one. Battery packs are meant to address one of the most common things I hear from new Rust adopters. Everyone loves the wealth of high-quality crates available on crates.io. And everyone hates having to spend a bunch of time researching and comparing alternatives. Battery packs can serve as a good set of default choices. And they don’t lock you in. At heart, they’re basically just a list of recommended crates, so you can always swap something out if you find an alternative. We’ve got a prototype of the battery pack tool working today, so you can try it out if you’re curious. Just run and then try a few commands! For example, will show you the set of available battery packs, based on a crates.io search (as I’ll explain below, a battery pack is itself packaged and distributed as a crate, but not one that you take a direct dependency on). And will add batteries from a battery pack into your crate, so e.g. would let you select and add common CLI libraries. If you want to see a more involved demo, try out , which is derived from the Awesome Embedded Rust repository. One of the key ideas from battery packs is that anybody can publish one . They are just a crate named ; the dependencies of that crate are your recommendations. Features are designations of common sets of crates frequently used together. The examples are your templates. And so forth. Letting anybody create a battery pack is in contrast to the previous ideas for an “extended standard library for Rust” 1 , and it is intended to address some of Rust’s unique challenges. For one thing, it lets people publish battery packs that are tailored to specific requirements. For example, the CLI and backend service battery packs are targeting a “typical computer”. But I could imagine the Rust embedded working group publishing a battery pack with libraries focused on no-std and binary size optimization. Being open-ended also addresses the “who decides?” question. To my mind, the best people to recommend what libraries you ought to use are other people building systems like yours . This is why I mentioned the Embedded Working Group publishing an Embedded battery pack, for example, as I think they are clearly a set of people who know their space well. But even within the embedded space there are yet smaller groups, and I imagine that sometimes it’ll make sense to get narrower. For example, perhaps a battery pack targeted embassy and its associated ecosystem? Unclear. If you wanted to create a battery pack, how do you do it? One answer is that you just create a new crate. But a better approach is to use the “battery-pack battery pack” 2 , which bundles a template: This will prompt you for the name of the battery pack you want to create and a few other things and make your crate. Then you can just use dependencies to represent the libraries you want to recommend and publish. The “batteries” that you can add to your project aren’t always dependencies. They can also be “recipes” or templates. For example, the CI battery pack 3 can configure your project with the kind of “super neat-o” github actions you’ve always wanted but never wanted to bother configuring. To use it, select one or more of the templates to install: I expect this kind of “actions to improve your crate” to become a rich source of things. Right now we’re using a relatively lightweight template system built on minijinja , but I think we’re going to want to expand on this. Battery Packs also support more than just a flat listing of dependencies/features/templates. You can group dependencies and features into categories and then, for each category, distinguish between “pick at most one” or “pick any number”. For a fun example, try , which is derived from the Awesome Embedded Rust repository. If you run it, you’ll see something like this, which groups the choices thematically and, in some areas like “concurrency framework”, makes it clear that you want to pick one: So why am I so keen on battery packs? It’s largely because I’ve heard so many would-be or recent Rust adopters talk about picking crates as a challenge. But I feel they would help with some other problems as well. What I really want to see is working groups in the Rust Commercial Network banding together to publish battery packs and recommendations. These would cover the dependencies that they’re actually using. One of the reasons I want to have RCN-recognized battery packs is that they are a natural focal point to then prompt RCN members to fund the maintenance of those crates. I am imagining that for each sponsored battery pack vended within the RCN, there is an associated “ecosystem fund”. Companies or individuals could sponsor this fund to get access to early patches, security disclosures, etc or other perks. The money would be used to support the maintainers of those crates, to implement missing features, and so forth. Another value-add from battery packs is the ability to drive interop efforts. I think that as soon as we start talking about standardizing, we’re also going to recognize that there are some places where standardization is hard. For example, early conversations within the network service working group (unsurprisingly) immediately identified that while most people are using tokio , some major companies are using their own runtimes internally. It’s not like the need for “async runtime interop” is news . But right now, every crate winds up effectively implementing their own set of little traits to make it work. Sponsored battery packs offer the possibility of a neutral home for that sort of thing. There are some risks to people using battery packs. The most obvious is that the fact that anybody can publish a battery pack may mean that you just get a ton of battery packs, which doesn’t really help anybody! I’m not so worried about this because I think that there will be a few obvious places that most people go first, and then I think once people are oriented, they’ll get excited to explore what crates.io has to offer and start discovering more niche battery packs. Battery packs are designed to evolve. I’ve seen it happen a number of times that there is a dominant crate for something, often taking a “traditional approach”, but then somebody else comes along and presents an interesting alternative that gradually takes off. I love that and I don’t want to put it at risk. One example of evolution around CLI argument parsing. For a time, docopt was a popular way to parse command-line options. Then clap came along and presented a more structured alternative; that was nice, but then structopt came along and connected clap to an auto-derive, so you could just write your data structure and be done. And that was awesome. (That is now the standard in clap.) I want to be sure that, even if there is a CLI battery pack, there’s room for the next clap to come along. There are a few things about battery pack that I think will help us deal with this. First, they are a “thin abstraction”. You don’t “depend on” a battery pack, you depend on the crates within it. So if a new version comes out that uses clap instead of docopt, that doesn’t impact you at all. Your code keeps working same as it ever did. And of course it helps that anybody can publish a battery pack. You can now have variations on battery packs that are focused around a new approach to help it get started. Done right, I think that standardized battery packs can also help the ecosystem evolve and pivot. As it is now, knowledge of new crates has to spread by word-of-mouth. But if everybody is aligned around a new approach, adopting that new approach within a battery packs sends a clear signal that your group is aligned that something is the new hotness. I see always bet on the ecosystem as a key Rust design axiom. It’s the reason we chose a small standard library and a package manager in the first place. It’s also why battery packs are designed to be published by anyone. But just like plants sometimes need a trellis to grow taller, any successful ecosystem reaches a point where it needs another layer of structure to help it keep growing. Without that, you have this “layer of tacic knowledge” (in the words of a Rust Vision Doc interviewee ) that becomes an obstacle for folks. And I think we’ve reached that point with . I am hopeful that battery packs can provide that next layer of structure. But at the end of the day, if there’s a better approach, that’s fine too, so long as we find a way to help people find ( and fund! ) the crates they need. So let’s talk about it! My first recollection of it was the Rust Platform idea we floated in 2016!  ↩︎ Yo dawg…  ↩︎ Hat tip to Jess Izen, who proposed and developed the CI battery pack. Neat idea.  ↩︎ Oh, and: my apologies to Salt-N-Peppa .  ↩︎ My first recollection of it was the Rust Platform idea we floated in 2016!  ↩︎ Yo dawg…  ↩︎ Hat tip to Jess Izen, who proposed and developed the CI battery pack. Neat idea.  ↩︎ Oh, and: my apologies to Salt-N-Peppa .  ↩︎

0 views
Martin Fowler 1 months ago

Fragments: July 13

Some more of my notes from Thoughtworks Future of Software Development Retreat . When we had our first retreat in Utah early this year, nobody had heard of Harness Engineering . This time we had a whole session on it. When comes to the guide side of harnesses, most of the discussion is about context management. While context windows have increased is size as models get more sophisticated, that doesn’t mean that models will properly focus on the right bits. Models typically only focus attention on part of the context, and to get the best behavior, we need to manage that focus. One attendee keeps their context small, limiting the file to less than 200 lines On the sensor side, we see more attention on computational sensors. Two patterns from one participant was shifting to languages with greater controls, (eg Rust rather than Python) and “leveling up” validation approaches, using more property-based testing and techniques from formal methods. One commented that while they aren’t smart enough to write specifications in a formal specification language, they are smart enough to read it and check it makes sense for their domain. Will our attention on harnesses last long enough for our next retreat? Will the models just get so good that harnesses become unnecessary? Those with some mechanical sympathy for LLMs seem to think not - but are they overly coupled to the current state of technology? I find such speculation tends not to lead anywhere useful, I’ve not seen much success in guessing the future in the past, and with technology as radical as this, I don’t see it being any easier. So for the moment, attention to harnesses pays off. We find it reduces token usage, and also allows weaker models to be useful, supporting such things as local hosting of open-weight models. ❄                ❄ Which naturally segues me to a session on self-hosted models. Increasing token costs have made hosting an open-weight model more attractive, particularly due to the decreasing time for open-weight models to catch up with frontier models. Cost isn’t the only factor, however, many folks find a desire to be independent of the frontier model firms to be the the driving force. After all we’ve seen the U.S. government intervene to deny access to models, increasing the desire for greater model sovereignty. Information security is also something to consider, some attendees just can’t give models necessary data for critical work. Even without that, if someone else hosts the model then their model learns rather than your model. And although recent events have increased interest, several participants worked with companies that had been self-hosting for up to a couple of years. Is this trudging down the same path of self-hosted clouds, which led to lots of folks spending excessive funds on half-arsed private clouds ? The answer hinges upon whether it ends up being simpler to host a model than a cloud, perhaps due to a simpler interaction protocol. The hard part of this may be the talent required to efficiently use the GPUs, managing an inference data center currently isn’t a widely available skill. Even self-hosted models are a cost to operate, capital costs in GPUs, ongoing costs in electricity. The physical design of a data center can affect optimal usage. There’s an opportunity here for professional services firms to help companies manage this. Cost control also involves teaching people to pick the right model for the job. Can we teach engineers, or indeed other users, to pick a less-powerful model? This, of course, could be a job for model itself, acting as a broker, deciding which model is the best choice to tackle certain jobs. Self-hosting may lead to a greater use of fine-tuning. Currently that’s a niche activity, but over time we could well find that models that are fine-tuned to a particular domain need less reasoning, consume less tokens, and thus are cheaper to operate. We are seeing models trained specifically to support programming. As with any topic with this degree of uncertainty, the big win isn’t finding the right answer, but coming up with a strategy that will cope with the inevitable and unpredictable changes. ❄                ❄ After an event like this, many people come up to me and ask me to make some grand summing up. I hate this, because I rarely leave these kinds of event with some grand narrative. Even after mulling on it afterwards (in writing the above notes) I still usually don’t have one, and distrust one that forms, as my skepticism includes attempts to make coherent narratives of an event that’s naturally rather jumbled. However my failings are irrelevant this time, because Kief Morris has put together such a narrative, and it’s a convincing one , even to a narrative-denier like me. The sessions had different titles and different casts, and on the surface they were about different problems. But they weren’t. Nearly every one of them was a different facet of the same argument. How much do we let an agent decide, and how do we stay confident in what it does? He looks at code review, questions whether it matters, but sees that the rigor that many associate with code review shifts to other forms. He describes the disagreements about how much we should trust an agent to identify and fix production incidents. He sees that the contrast between how much leeway teams give to agents depends on the context they are operating Underneath all of these sessions, the operations debate, the wide-remit team, the dark-factory spectrum, the argument about who’s allowed to steer the model, people were making the same handful of choices over and over about a single thing: the unit of work they were prepared to hand to an agent. How big it is. How much of the job it covers. What you do to get it ready to hand over. How you check what comes back. What you put around the agent to keep it inside the lines. Different rooms set those differently, but they were setting the same controls. ❄                ❄ Sam Ruby convened a session called “Bring me a Rock”. The name evokes a particular kind of management dysfunction. The manager tells his underlings to bring him a rock, and then starts rejecting the results without explaining why (“no not that one”, “no not that one”) until eventually one rock matches the unstated expectation. It names a manager who substitutes serial rejection for the work of saying what they want, and makes you pay for their unfinished thinking one rock at a time. Sam had already written why he thought with LLMs, this changed from a slur to a defensible way to work . When its a bunch of tireless machines with endless patience, that return new rocks in minutes rather than days, then an approach like this (using the brainstorming register becomes a defensible way to work. Sam described the discussion : The room pulled it somewhere narrower than I’d framed, and the narrower place was the more interesting one: not how to explore by elimination but who should even be allowed to. Product managers, increasingly people managers, are reaching for these models directly, and seasoned engineers get measurably better results from them than untrained people do — so the worry followed. If expertise is what separates a good outcome from slop, should non-engineers be steering the model at all? It’s a fair question, and I think it’s the wrong one, because it mistakes the act. When a manager reaches for an LLM instead of routing the work to the team that reports to them, they didn’t pick up a tool — they made a hire. And you don’t ask permission to manage your own team; a manager who decides a piece of work is better given to a new participant than to the existing one is doing the most ordinary thing a manager does. Framed that way, the permission question dissolves into an older, better-understood one — the one Drucker named in 1959: when the worker knows more about the specifics than the manager does, you manage by objective, not by method. The non-engineer steering an agent is exactly that manager, out-known by the thing they’re directing, and the slop the room feared is the old danger of managing by method when you should be managing by objective. The question isn’t may they hire? It’s do they know how to manage by objective? — which you can teach, hire for, and hold people to without anyone first becoming an engineer. Sam’s article explores managing an LLM by objective, giving it a goal rather than a task. And Kief’s earlier point about the essence of the discussion still holds: how confident can we be that it’s done the right thing? We can outsource many things, but not the acceptance criteria, at some point there’s a human request and a human judgment on whether that request was properly executed. But the danger lies in important unstated objectives, unstated perhaps because they weren’t even imagined. It’s easy to state objectives around desired functionality. Give me a an application that will examine my emails and form a todo list for today. But behind that simple statement is a thicket of unstated assumptions. We tend to assume The Genie won’t include any undesired functionality, perhaps deleting emails it thinks are unworthy of our attention. We assume it won’t let an email tell it to send private information to [email protected]. We have some hope here - we hear more experiences that suggest that recent models can do an excellent job of finding (and hopefully fixing) security holes. The careful precision of the machine outruns the sloppy if imaginative thinking in squishyware. Perhaps we can assume the genie can take care of some of our unstated objectives. Conformance tests (sensors) are more valuable than specifications (guides), but it’s hard to imagine all the conformance tests that are needed to say what shouldn’t happen. Furthermore, building software is about exploration, finding out how a workflow can evolve as machines are embedded in the process. For a human to guide that process, we need some understanding of it. My sense is that model building is still important, and while I agree that the genie can take an active role in that construction, I don’t think the human can entirely outsource it. Even if the genie builds the model itself, it needs to teach us that model, because the model helps us imagine and communicate the goals, the objectives that we give to the machine. ❄                ❄                ❄                ❄                ❄ If you follow my feeds (which you probably do if you’re reading this), then you’ll know that Birgitta Böckeler has written a couple of memos on working with local models. She first looked the factors that influence how viable they are for programming , and then related some of her recent experiences evaluating such models . As a nice, if accidental, complement to these, Sebastian Raschka wrote a detailed guide to his local model environment . Like Birgitta, he’s found the Qwen 3.6 model to be the current sweet spot for local agentic programming. ❄                ❄                ❄                ❄                ❄ Simon Willison shares a useful tip to save money while using the latest Anthropic Fable model Tell Fable to use other models for smaller tasks, applying its own judgement about which model to use. ❄                ❄                ❄                ❄                ❄ Josh Comeau writes a blog and online courses for developer education, primarily front-end web material. His been successful for most of this decade but has found his online courses have had only ⅓ the sales this year . He attributes this to AI, partly as people worry if it’s worth spending money on a job that may not have a future, but also because AI can provide personalized tutoring. ideally, it shouldn’t cost any money to learn stuff. But I sorta worry about how this is supposed to work, going forwards, if there’s no incentive for people to make high-quality free content. I’ve spoken to a few course creators now, and we’re all seeing the same trend. Revenue down 50%+. Fewer people engaging with our content. People switching to LLMs, which slurp up all of our work and regurgitate it, without consent or compensation. It feels pretty bleak. 😅 ❄                ❄                ❄                ❄                ❄ John Gruber is annoyed that Claude’s desktop app for MacOS in uses Electron . Electron guarantees that an app feels just as wrong on all platforms. He has some tasty invective for the folks at Anthropic with ties to the Electron platform. Finding out that one guy — who is a senior Electron maintainer — has led the teams for the desktop clients for Slack, Notion, and now Claude is like discovering that it was one guy — whose family business was a distillery — who helmed the Titanic, piloted the Hindenburg, and then served as air traffic controller for Amelia Earhart. The deeper question here is whether there should be a future for cross-platform front-ends in the world of agentic programming. There’s lots of evidence that coding agents do a great job of building the same thing in multiple languages and platform ecosystems. That should mean that the days of least-denominator cross-platform UIs are numbered - and that number is small. ❄                ❄                ❄                ❄                ❄ Dan Davies tries to draw a distinction between interactional and contributory expertise . Contributory expertise is that held by people who are doing the work to advance a field of study, interaction expertise is held by folks that spend time talking to contributory experts, building up a decent store of knowledge themselves, but not steeped in the day-to-day of the work. it seems to me that there is an important distinction here, which is not any less important because the dividing line might be difficult to establish empirically, or even if that line turns out to be in a different place from where we guessed it was. As well as difficult cases where it’s not clear, I think we could also come up with cases where the distinction between interactional and contributory expertise would suddenly become very clear and important indeed – the ones where someone who was faking it got “found out”. And so the question that I think is quite important is whether there is a similar kind of distinction between the kind of expertise that it’s possible for a machine to get by industralised consumption and interaction with a much larger corpus of literature than any human being could inhale, and genuine contributory expertise that could apply to entirely new situations outside that literature. As a human, I’d like to think I’m more of a contributor than an interactor (especially given my increasing introversion), and thus relatively safe from being forced into obsolescence by silicon. But I’m also aware that my career is devoid of any original ideas, my skill is only that of someone who is good at selecting and explaining the ideas of others. (As Brian Foote put it more memorably: “an intellectual jackal with good taste in carrion”.) But there’s skill in being a good jackal too - and we don’t really know yet where the real boundaries of the LLMs will lie.

0 views
seated.ro 1 months ago

You fail to learn if you don't learn to fail

If all your time is spent watching output tokens, where do your input tokens come from? Letting an agent rip on full auto is basically doom scrolling. Even worse if you're doom scrolling while the agent runs. We humans love frying our dopamine receptors. This feels great until you realize what you were offloading: the struggle. The part where you fail. Failure is the entire point. You don't make progress in the gym unless you take a set at least close to failure. The muscle only adapts when it's forced to. It is no different for the brain. It is very hard to admit to yourself that your skills have atrophied. It is even harder to admit this to other people. I will admit that over the past several months my brain has gotten smoother (and I wasn't even on Twitter much!). Recently, I had written an abstraction for my diff viewer ( diffy ), an element system with a macro that lets agents write html-like code in rust for native ui (they reason better with this). But it wasn't adopted everywhere in the repo yet, so when I asked for a new feature, the model decided to hand paint it straight to the viewport instead. Every behavior the element system gives you for free was just... missing. Text wasn't selectable. Hover highlights wouldn't go away. And since I wasn't looking closely, it iterated on the slop and produced more slop, more bugs. I just kept saying continue. I lost a whole day untangling it, and the funny part is that once I actually looked at what it had built, every bug was the same bug. When you hit a roadblock and your immediate reaction is to reach for something else (previously, this used to be other people, but now it is a language model) you are essentially skipping the part where you actually learn to solve the problem. It is funny how one of the best "learning tools" has turned out to be the number one cause (anecdotal. sue me) of the lack of learning! It's been a few months since I started writing this, and things have gotten more dire. Several major software services barely work now, grown engineers I once respected are writing somber posts about missing a language model that was banned for a while. Mourning. For model weights. It's all so dystopian. As the agents get better, one is basically expected to produce code at an alarming rate. The timeline to get something done is compressed but the time it takes to come up with solutions to hard problems has not. There are usually a few good abstractions one can come up with that balance the upsides and tradeoffs for most software problems. However it is currently trivial to turn your brain off and let the slop flow. The code will be complex. It might look like it all works, but something always breaks. And the solution to that? More slop. Software quality is collapsing as a result, and the societal expectation that engineers understand what they ship is disappearing. You never understood the code in the first place. So when you need to change it, you're asking the same stateless clanker to modify code it has no memory of writing. All output tokens and zero thinking tokens. A lower barrier of entry to write software doesn't imply the standards for good software must be lowered. The growing trend is to do things because you now can (supposedly), but we used to try and do things because we could not out of sheer stubbornness. Carmack and gang shipped QuakeWorld with client-side prediction over dial-up when the conventional wisdom was that twitch shooters over the internet were unplayable. This only happened because Quake's original netcode was laggy and everyone hated it. (They fixed it in a month.) George Dantzig arrived late to class, mistook two "unsolvable" statistics problems for homework, and solved them. Nobody told him they were impossible, so he just did the work. Andrew Wiles spent seven years alone in his attic working on Fermat's Last Theorem, a problem mathematicians had given up on for 350 years. He announced the proof, a reviewer found a hole in it, and he spent another year fixing that too. Notice that all three of them became who they are because of the struggle, not despite it. The people benefiting most from generative tools today, say Terence Tao or Mitchell Hashimoto, already put in the time, so when they offload work they're just skipping the typing. When people like you and me (if this is not you, then I apologize) offload, we skip the grind itself. With language models, easy tasks got easier, hard tasks stayed hard. The hard part was never the task itself. I don't know, I am figuring this out as I go. The amount of time I have spent actually programming has been dropping month over month this year. I used to have a coding stats section on my website that would track hours I spent writing code split by language, recently I had updated it to this: and it made me quite sad. I do think that sometimes all you need is to realize that the thing you are doing is actually detrimental to your growth. Consistency matters more than one would assume. If you consistently take some time away from these tools and actually use your brain, that alone is already significantly better than offloading your thoughts. Solve the problems yourself. Or at least try, fail, and spend time thinking. There is seemingly no "learning" phase anymore. You are expected to just know things. Learning is fun, don't let anyone take this away from you. I've written about this before . It is probably going to be slow, learning takes time and effort. You will feel stupid (I feel stupid). This is a good feeling, because there exists a world where you are no longer stupid and the path towards it is learning. Books still exist! Libraries are still open, notebooks waiting to be written in. Read more. Write more. If you really do care about improving yourself, be honest and use these models for what they are, highly efficient filters of zettabytes of data (the internet is estimated to be 175-240 zettabytes ( 10^{21} bytes)). It was extremely difficult to identify what one needed to read to learn niche topics even like 2 years ago. I remember asking a good friend of mine to recommend material to dive deep into learning about SIMD, and honestly there wasn't much stuff to read except the Intel Intrinsics Guide. And if you've ever taken a look at that, it is quite cancerous for a first-time reader. Language models are super useful here because you can point them at material and you can ask questions that pertain to the thing you care about and it will simply just tell you the correct things. One good thing in this age of slop is to consume knowledge at an unbelievable pace. I don't necessarily mean using only model output for learning (I don't trust them to learn any topic more than a shallow amount), but rather using them to help sift through the plethora of information available out there and identifying the right things to read. Human slop exists too and using a language model to supplement your learning might help keep you sane (ironically). I like using these models to write code that I tell it to write (outside of work I enjoy doing it myself entirely), and I am largely disinterested in asking it what I should write. There are exceptions of course, because not everyone is working on scaling software services which has largely been solved (but slowly being forgotten), but that would be for you to decide. The best model you have access to (and it has solved continual learning) is, and always has been, the one inside your skull. It's time to scale up its input tokens.

0 views
Ankur Sethi 1 months ago

Data locality (sometimes) beats algorithmic complexity

I've been ECS -curious ever since I learned about it in the Bevy game engine documentation . The ECS architecture predictably improves performance in languages that give you low-level control over memory (C, C++, Rust, Zig, and friends). But how does it fare when used in high-level, dynamic, garbage-collected languages such as JavaScript? This is the question Dan Murphy set out to answer in The Physics of Memory : Is it possible to use an ECS-style architecture in Javascript? And for applicable operations, does that actually do better than objects + V8’s garbage collection? To answer the question, Murphy built a 2D physics simulation of 15,000 balls bouncing around in a box using several different techniques. He found that a JavaScript implementation of the simulation that used ECS outperformed the usual "giant graph of objects" OOP implementation by 24x. He writes: It's also worth noting how the usual OOP implementation creates GC pressure: In OOP, entities are scattered across the heap. As they move and interact, the JavaScript engine’s garbage collector is constantly triggered, and the CPU frequently stalls waiting for pointer lookups. This causes sporadic frame drops (micro-stutter). Because ECS uses pre-allocated, flat TypedArrays, memory access is 100% predictable and GC overhead is zero, guaranteeing perfectly smooth frame delivery. My favorite thing about Murphy's post is that you can run all his benchmarks in your own browser. I love it when technical explanations or benchmarks are accompanied by embedded "apps" you can play around with. I'm surprised at how much data locality matters for performance. An algorithm with worse big-O complexity can outperform one with better complexity if it makes good use of the CPU's L1/L2 caches. Very cool. Cache Locality > Algorithmic Complexity : At 15,000 entities, pointer-chasing and unpredictable tree branching cannot compete with the contiguous L1/L2 cache locality of a flat 1D array sort—even though trees have a better theoretical Big-O complexity. You Don’t Need WASM for ECS Wins : Simply switching your JavaScript codebase to a flat Structure of Arrays (SoA) layout yields up to a  24x speedup  over OOP. WASM is the cherry on top (another 2.5x), not the entry ticket. Pragmatism Wins : While a hand-tuned SoA is the absolute fastest, using a production ECS library like   still gives you a massive  14x speedup  over OOP while providing a clean, scalable API. IMO, for 99% of applications using a library is the correct engineering choice.

0 views

Why I think Rust is Object Oriented

Before we begin, I want to say I don't care that much if you disagree with me. There's no sound precise mathematical definition of Object Oriented, no ISO standard, and no grand arbiter who decides what is and what is not OO (although I believe Casey Muratori may have applied for that position somewhere in his 12 day monologue about the subject). You are unlikely to change my mind, and I am unlikely to change yours, and that's perfectly fine. On with the post! In my heart of hearts, Rust feels like an object oriented language. Practically everything I write in Rust is a datum + set of behaviours intimately associated therewith. I hide my struct fields, give my totally-not-objects equality semantics and string representation. I utilise polymorphism via traits - and while I know that traits are not technically interfaces, that fact occupies the same region of my brain that knows that mandrills are not technically baboons. Almost all my functionality is written in methods, and those methods belong to the data they operate on, in a way that they definitely don't in something like C or OCaml. But what about inheritance? OO-haters often fixate on this as the defining thing about objects, which has always perplexed me. I came up in the JavaMania era and "composition over inheritance" was the accepted wisdom of every programmer who took OO seriously. I scarcely used it during my time in the C# mines. Historically speaking the case is weak as well; neither the first smalltalk, nor the first simula had inheritance. Self was hugely influential (traits originated in Self) and didn't have it either. I won't deny it wasn't a common feature, but it never took center stage in my mind outside the brief "Cat inherits Mammal" phrase we all go through, and it certainly wasn't encouraged in the OO design books I read. So why the disconnect? Why does talking about this not resonate with other Rust programmers? I think because in the Rust culture, "objects" are something very different. They're virtual destructors & inheritance trees. They're a nasty thing C++ had that Rust forwent because dynamic dispatch is slow (except in Zig of course where it's fast now). Of course Rust isn't OO! But for those of us who took object-oriented design seriously, everything you read just reinforced that they're neat little black boxes you call methods on. And of those, rust is full.

0 views
Brain Baking 1 months ago

Favourites of June 2026

The beginning of this month marks the official end of my own company. After just two years of establishing and owning Brain Baking BV , the notary ended it. There have been no professional activities related to the company since I switched back to education in December so for me it made little sense to keep that door open only for the monthly administrative costs to pile up. I hope to build a bit more stability this time around, both on personal and professional level. My statute as lecturer has been extended for a year: so far, so good! Previous month: May 2026 . A few very short ones and one quite big one that I ended up enjoying very much. DreadXP, the developers behind Dread Delusion , also recorded dev diaries on YouTube: Related topics: / metapost / By Wouter Groeneveld on 3 July 2026.  Reply via email . The Aching —a Sierra On-Line-like adventure game weighing less than that runs on any 8086 machine. It also happens to be good, even though it feels more like an introduction of this horrified world. Serious Sam: The First Encounter —I started replaying this two years ago and finally pushed forward a bit more. After endless complete freezes of my Win98 machine I gave up. AAAAAHHHHHH boom . I remember liking this a lot more: it’s…. bland? Dread Delusion —A weird looking game that I was drawn to the first time I laid eyes on screenshots a few years ago. I remembered it and felt the time was right to crack this one open. It’s one of the best games I’ve played in the last years. I recorded a playthrough log to convince you to drop everything and go play it as well! Lucy Dreaming —A lovely classic nineties adventure game that’s perhaps playing it too safe to try to be an homage to Monkey et al. ? I still enjoyed myself but the abrupt ending was a bit of a letdown. Speaking of The Aching , the developer explains their philosophy behind the Gorgon Engine . Interestingly, Gorgon is designed to be small and able to run on older original hardware, while new adventure games that look and feel old like The Telwynium are made with PowerQuest for Unity and require hundreds of megabytes. Nobody really cares, but I do. This ACM paper on a conceptual model for ownership types in Rust sheds new light on how the borrow checker works from an educational point of view. More Rust-y stuff—even though I have yet to touch the language—Michael Neumann investigated how long it takes to compile Rust from source compared to other languages. Hint: looooooonnnnggg. As in lonngggggggggg. James also printed his blog in book form years before I did! He selected all coffee-related articles to create a lovely personal caffeinated hardcover. Games That I Missed documents progress on their pinball machine projects . That old electronic stuff inside the machines is mesmerising. Phil Gyford laboriously kept track of how much money he spent each year on music for the past 30 years (via ) In a timely manner, Miss Booleana wrote about Claire Dederer’s Monsters: What Do We Do With Great Art By Bad People? . I asked myself the same question recently and added the book to my toread list. Andrew Webster publishes a Great Truth on The Verge: The Nintendo DS is still the best gaming handheld for travel . Yup. Another paper that confirms LLM-driven gender bias in citations in academic work . James Pennebaker confirms what I’ve been thinking and feeling: expressive writing can influence thoughts, feelings, and behaviours . The link is a past event but a good starting point to find publications by Pennebaker. Chris Kirk-Nielsen begs us to start playing indie games . Stop that Assassin’s Creed nonsense: scroll up and watch the Dread Delusion dev diary instead! Nic tringali sometimes feels the creative drudgery . A surprise ending is in it for you if you decide to read it. Jeff Gerstmann finally decided to apply Rigorous Science (TM) to compile an exhaustive (!!) list of the best NES games ever released in USA . Number one is NOT Mario nor Zelda! I particularly enjoyed Erik Hane’s piece in Typebar Magazine on fandom strain and the IP illness killing Magic: the Gathering . The magazine really is “An interesting thing to read on the internet”, as their footer claims. In a post called Cultures of making and relating , Konrad Hinsen brings the recent Cultures of Programming book our attention. It’s been an open browser tab ever since. Memray looks like an interesting memory profiler for Python, if I ever would need one. GentleOS is a friendly hobby OS for 32-bit PCs. The Corporate EU Observatory revealed that Big Tech invested almost 50% more in lobbying throwaway money ( !) compared to 2020. Diablo II has a new class: the Warlock . I really wish it was playable without the remaster though. Warp Point is a curated list of indie video game websites and Jefklak’s Codex is in it.

0 views

Summary of reading: April - June 2026

"The Nuremberg Trial" by John Tusa and Ann Tusa - a detailed, meticulously researched account of the Nuremberg Trials. There's not a whole lot of side questing in this book - it's all focused on the trials themselves. Interesting read overall, though somewhat dry and academic. "Things Become Other Things: A Walking Memoir" by Craig Mod - a kind of travelogue of the author walking across Japan's Kii peninsula, mixed with his childhood memories and impressions of life in Japan in general. It's a good book, though I thought I'd find more details about Japan here, whereas it's a much more introspective work about the author himself. "Social Justice Fallacies" by Thomas Sowell - the usual data-driven Sowell fare, using real historical data and statistical analysis to tackle some hot political issues like personal liberties, poverty data and affirmative action. "Focus: The ASML way" by Marc Hijink - I was inspired to read this book about ASML and its EUV technology after watching a fantastic Veritasium video on the topic. The book turned out to be a disappointment, however; I was interested in the technology behind ASML's machines, but the book is 98% focused on the human, political and organizational aspects of the company. If you're interested in the tech, the aforementioned video is a much better use of your time. "Every Living Thing: The Great and Deadly Race to Know All Life" by Jason Roberts - combined biography of Carl Linnaeus and the Comte de Buffon, who were groundbreaking naturalists in the 18th century working to categorize all living thiings. It's a history of the very early days of what was called "natural science", and later evolved into botany, biology, zoology, ecology and related disciplines. The title is hyperbolic, but the book itself is interesting and well written. "Junglekeeper: What It Takes to Change the World" by Paul Rosolie - the author is a conservationist in the Peruvian Amazonia rainforest. This book recounts his adventures on the path to establish Junglekeepers - his organization for preserving the forest and its wildlife. It's a nice read overall, though the writing is overly embellished and tiringly hyperbolic at times. "There Is No Place for Us: Working and Homeless in America" by Brian Goldstone - a poignant account of several families in Atlanta struggling with keeping access to suitable rental housing, circa 2020. It covers the danger zone of having just enough but absolutely no buffer, and how life emergencies affect families. Quite impressive investigative work to be able to produce a book like this; the stories are truly touching. The book does reasonably well to skirt around politics without too much preaching, which is appreciated. "Understanding Software Dynamics" by Richard Sites - from the ground up discussion of software performance analysis with sampling, tracing and understanding the different ways in which CPUs are unable to make progress. A good chunk of the book is dedicated to the author's KUtrace system. I wanted to like this book, but ultimately failed. I found it extremely verbose and tedious to follow, full of walls of text. "A Table for Two" by Amor Towles - a collection of short stories and a Novella. The stories all have some whimsical elements in them, and the writing is very good. That said, this book didn't quite recapture the magic of "A Gentleman in Moscow" for me. "Never Enough" by Jennifer Breheny Wallace - talks about the stress teens are under to excel academically and athletically to improve chances of admission to top universities, and what to do about it. Somewhat similar to "The Price of Privelege" and "Unequal Childhoods", though this one is more popular, in the sense that the author is a journalist, not a scientist, and the book is a collection of anecdata laced with official statistics, rather than experiences from the author's own research. "Thunder Below!" by Eugene B. Fluckey - the story of USS Barb, a submarine in the pacific during the latter part of WWII, written by its commander through 5 different deployments against Japan. Written in an engaging writing style, this book is very informative about how submarine warfare looked back then. I was somewhat shocked at the recklesness (suicidal courage?) of the commander though; he clearly was a very capable captain with a talented team, but surely there was lots of luck involved to be able to survive what he describes. That said, the book was also written 45 years after the events, so it's possible that there's some embellishment involved. "Breakneck: China's Quest to Engineer the Future" by Dan Wang - a very nice book about China's manufacturing superiority in the last few decades, as well as other aspects of its society like the one child policy and the COVID-19 lockdowns. The author contrasts China - "an engineer-driven society" with the USA - "a lawyer-driven society", discussing the effects on industrial capacity, culture and civil rights. Informative and well written. "Good People: A Novel" by Patmeena Sabit - an Afghan refugee family settles in Virginia in the early 2000s; this is a novel / mystery focused on their older children, their assimilation in the USA and what that lead to. Haunting book that will be difficult to get out of one's head, particularly for parents of teenage girls. "The Invention of China" by Bill Hayton - the author's thesis is that much of the national ethos of China - the unity of its peoples, language, territory - was invented about a century ago as part of a political agenda. The book is quite dry and academic, but this is key to its effectiveness, as it relies heavily on historical documents. It mentions a fantastic quote from Mao - "Make the past serve the present" - and I feel like this describes the book's main thesis very well. A fascinating example of the narrative of Orwell's 1948 in real life. "Advanced Hands-on Rust" by Herbert Wolverson - the idea of the book is to help one learn Rust through hands-on projects, by building games that use the Bevy framework. My conclusion is that Bevy is a particularly poor way to learn Rust because it's a massive, opaque and opinionated framework that bends your code to its will and conventions. Actually learning or practicing a language by building these simple games from scratch would be much better, IMO. So if your goal is to practice Bevy, sure, this book isn't too bad; but for Rust, stay away. Other than the Bevy issue, the book is poorly edited, with code samples out of sync with the accompanying code and diffuclt to follow to keep the project buildable. Also, since Bevy changes very quickly, you'll have to stick to the older version the book is using - otherwise things just won't compile. On the brighter side, the book does provide some coverage of Rust tooling that is useful - like benchmarking and creating well-behaved crates. But these topics in themselves are hardly worth a whole book. "Mathematics for Human Flourishing" by Francis Su - a math professor's attempt at defining the effects of doing mathematics on meaning in human life. I wanted to like this book, but unfortnately it's a bit too kumbaya for me. While I appreciate what the author was aiming at here, it just didn't click. "Benjamin Franklin: An American Life" by Walter Isaacson "A Gentleman in Moscow" by Amor Towles

0 views
Cassidy Williams 2 months ago

Whitespace in Astro 7.0

There’s some new default whitespace handling in the latest version of Astro! I noticed that when I updated my blog template (this blog! Right here!) to the new Astro 7.0 , a bunch of words and spacing were broken up in weird ways. Turns out, in the brand new Rust compiler, there’s some very specific JSX changes. Before, if you had two elements one after the other, like so: It would render as “Howdy y’all” on the page. But, in version 7.0, it would render as “Howdyy’all” instead, with no space. If you wanted to fix it, you’d have to do: Which is very JSX-y like React and other similar frameworks. It was a bit of an annoying change for me, because my blog template has lines like this in a few components that were now rendering incorrectly: But! There’s a solution here, if you don’t want to edit all of your components and templates (like me). In your , add the following in : You can see it in context in my template here , if you’d like. I hope this is helpful for ya! Here’s the upgrade guide for more details!

0 views