Posts in Rust (20 found)
baby steps 3 days ago

But then again...maybe alias?

Hmm, as I re-read the post I literally just posted a few minutes ago, I got to thinking. Maybe the right name is indeed , and not . The rationale is simple: alias can serve as both a noun and a verb. It hits that sweet spot of “common enough you know what it means, but weird enough that it can be Rust Jargon for something quite specific”. In the same way that we talk about “passing a clone of ” we can talk about “passing an alias to ” or an “alias of ”. Food for thought! I’m going to try on for size in future posts and see how it feels.

0 views
baby steps 3 days ago

Bikeshedding `Handle` and other follow-up thoughts

There have been two major sets of responses to my proposal for a trait. The first is that the trait seems useful but doesn’t over all the cases where one would like to be able to ergonomically clone things. The second is that the name doesn’t seem to fit with our Rust conventions for trait names, which emphasize short verbs over nouns. The TL;DR of my response is that (1) I agree, this is why I think we should work to make ergonomic as well as ; and (2) I agree with that too, which is why I think we should find another name. At the moment I prefer , with coming in second. The first concern with the trait is that, while it gives a clear semantic basis for when to implement the trait, it does not cover all the cases where calling is annoying. In other words, if we opt to use , and then we make creating new handles very ergonomic, but calling remains painful, there will be a temptation to use the when it is not appropriate. In one of our lang team design meetings, TC raised the point that, for many applications, even an “expensive” clone isn’t really a big deal. For example, when writing CLI tools and things, I regularly clone strings and vectors of strings and hashmaps and whatever else; I could put them in an Rc or Arc but I know it just doens’t matter. My solution here is simple: let’s make solutions that apply to both and . Given that I think we need a proposal that allows for handles that are both ergonomic and explicit, it’s not hard to say that we should extend that solution to include the option for clone. The explicit capture clause post already fits this design. I explicitly chose a design that allowed for users to write or , and hence works equally well (or equally not well…) with both traits A number of people have pointed out doesn’t fit the Rust naming conventions for traits like this, which aim for short verbs. You can interpret as a verb, but it doesn’t mean what we want. Fair enough. I like the name because it gives a noun we can use to talk about, well, handles , but I agree that the trait name doesn’t seem right. There was a lot of bikeshedding on possible options but I think I’ve come back to preferring Jack Huey’s original proposal, (with a method ). I think and is my second favorite. Both of them are short, relatively common verbs. I originally felt that was a bit too generic and overly associated with sharing across threads – but then I at least always call a shared reference 1 , and an would implement , so it all seems to work well. Hat tip to Ariel Ben-Yehuda for pushing me on this particular name. The flurry of posts in this series have been an attempt to survey all the discussions that have taken place in this area. I’m not yet aiming to write a final proposal – I think what will come out of this is a series of multiple RFCs. My current feeling is that we should add the , uh, trait. I also think we should add explicit capture clauses . However, while explicit capture clauses are clearly “low-level enough for a kernel”, I don’t really think they are “usable enough for a GUI” . The next post will explore another idea that I think might bring us closer to that ultimate ergonomic and explicit goal. A lot of people say immutable reference but that is simply accurate: an is not immutable. I think that the term shared reference is better.  ↩︎ A lot of people say immutable reference but that is simply accurate: an is not immutable. I think that the term shared reference is better.  ↩︎

0 views
sunshowers 4 days ago

`SocketAddrV6` is not roundtrip serializable

A few weeks ago at Oxide , we encountered a bug where a particular, somewhat large, data structure was erroring on serialization to JSON via . The problem was that JSON only supports map keys that are strings or numbers, and the data structure had an infrequently-populated map with keys that were more complex than that 1 . We fixed the bug, but a concern still remained: what if some other map that was empty most of the time had a complex key in it? The easiest way to guard against this is by generating random instances of the data structure and attempting to serialize them, checking that this operation doesn’t panic. The most straightforward way to do this is with property-based testing , where you define: Modern property-based testing frameworks like , which we use at Oxide, combine these two algorithms into a single strategy , through a technique known as integrated shrinking . (For a more detailed overview, see my monad tutorial , where I talk about the undesirable performance characteristics of monadic composition when it comes to integrated shrinking.) The library has a notion of a canonical strategy for a type, expressed via the trait . The easiest way to define instances for large, complex types is to use a derive macro . Annotate your type with the macro: As long as all the fields have defined for them—and the library defines the trait for most types in the standard library—your type has a working random generator and shrinker associated with it. It’s pretty neat! I put together an implementation for our very complex type, then wrote a property-based test to ensure that it serializes properly: And, running it: The test passed! But while we’re here, surely we should also be able to deserialize a , and then ensure that we get the same value back, right? We’ve already done the hard part, so let’s go ahead and add this test: The roundtrip test failed! Why in the world did the test fail? My first idea was to try and do a textual diff of the outputs of the two data structures. In this case, I tried out the library, with something like: And the output I got was: There’s nothing in the output! No or as would typically be printed. It’s as if there wasn’t a difference at all, and yet the assertion failing indicated the before and after values just weren’t the same. We have one clue to go by: the integrated shrinking algorithm in tries to shrink maps down to empty ones. But it looks like the map is non-empty . This means that something in either the key or the value was suspicious. A is defined as: Most of these types were pretty simple. The only one that looked even remotely suspicious was the , which ostensibly represents an IPv6 address plus a port number. What’s going on with the ? Does the implementation for it do something weird? Well, let’s look at it : Like a lot of abstracted-out library code it looks a bit strange, but at its core it seems to be simple enough: The is self-explanatory, and the is probably the port number. But what are these last two values? Let’s look at the constructor : What in the world are these two and values? They look mighty suspicious. A thing that caught my eye was the “Textual representation” section of the , which defined the representation as: Note what’s missing from this representation: the field! We finally have a theory for what’s going on: Why did this not show up in the textual diff of the values? For most types in Rust, the representation breaks out all the fields and their values. But for , the implementation (quite reasonably) forwards to the implementation . So the field is completely hidden, and the only way to look at it is through the method . Whoops. How can we test this theory? The easiest way is to generate random values of where is always set to zero, and see if that passes our roundtrip tests. The ecosystem has pretty good support for generating and using this kind of non-canonical strategy. Let’s try it out: Pretty straightforward, and similar to how lets you provide custom implementations through . Let’s test it out again: All right, looks like our theory is confirmed! We can now merrily be on our way… right? This little adventure left us with more questions than answers, though: The best place to start looking is in the IETF Request for Comments (RFCs) 2 that specify IPv6. The Rust documentation for helpfully links to RFC 2460, section 6 and section 7 . The field is actually a combination of two fields that are part of every IPv6 packet: Section 6 of the RFC says: Flow Labels The 20-bit Flow Label field in the IPv6 header may be used by a source to label sequences of packets for which it requests special handling by the IPv6 routers, such as non-default quality of service or “real-time” service. This aspect of IPv6 is, at the time of writing, still experimental and subject to change as the requirements for flow support in the Internet become clearer. […] And section 7: Traffic Classes The 8-bit Traffic Class field in the IPv6 header is available for use by originating nodes and/or forwarding routers to identify and distinguish between different classes or priorities of IPv6 packets. At the point in time at which this specification is being written, there are a number of experiments underway in the use of the IPv4 Type of Service and/or Precedence bits to provide various forms of “differentiated service” for IP packets […]. Let’s look at the Traffic Class field first. This field is similar to IPv4’s differentiated services code point (DSCP) , and is meant to provide quality of service (QoS) over the network. (For example, prioritizing low-latency gaming and video conferencing packets over bulk downloads.) The DSCP field in IPv4 is not part of a , but the Traffic Class—through the field—is part of a . Why is that the case? Rust’s definition of mirrors the defined by RFC 2553, section 3.3 : Similarly, Rust’s mirrors the struct. There isn’t a similar RFC for ; the de facto standard is Berkeley sockets , designed in 1983. The Linux man page for defines it as: So , which includes the Traffic Class, is part of , but the very similar DSCP field is not part of . Why? I’m not entirely sure about this, but here’s an attempt to reconstruct a history: (Even if could be extended to have this field, would it be a good idea to do so? Put a pin in this for now.) RFC 2460 says that the Flow Label is “experimental and subject to change”. The RFC was written back in 1998, over a quarter-century ago—has anyone found a use for it since then? RFC 6437 , published in 2011, attempts to specify semantics for IPv6 Flow Labels. Section 2 of the RFC says: The 20-bit Flow Label field in the IPv6 header [RFC2460] is used by a node to label packets of a flow. […] Packet classifiers can use the triplet of Flow Label, Source Address, and Destination Address fields to identify the flow to which a particular packet belongs. The RFC says that Flow Labels can potentially be used by routers for load balancing, where they can use the triplet source address, destination address, flow label to figure out that a series of packets are all associated with each other. But this is an internal implementation detail generated by the source program, and not something IPv6 users copy/pasting an address generally have to think about. So it makes sense that it isn’t part of the textual representation. RFC 6294 surveys Flow Label use cases, and some of the ones mentioned are: But this Stack Exchange answer by Andrei Korshikov says: Nowadays […] there [are] no clear advantages of additional 20-bit QoS field over existent Traffic Class (Differentiated Class of Service) field. So “Flow Label” is still waiting for its meaningful usage. In my view, putting in was an understandable choice given the optimism around QoS in 1998, but it was a bit of a mistake in hindsight. The Flow Label field never found widespread adoption, and the Traffic Class field is more of an application-level concern. In general, I think there should be a separation between types that are losslessly serializable and types that are not, and violates this expectation. Making the Traffic Class (QoS) a socket option, like in IPv4, avoids these serialization issues. What about the other additional field, ? What does it mean, and why does it not have to be zeroed out? The documentation for a says that in its textual representation, the scope identifier is included after the IPv6 address and a character, within square brackets. So, for example, the following code sample: prints out . What does this field mean? The reason exists has to do with link-local addressing . Imagine you connect two computers directly to each other via, say, an Ethernet cable. There isn’t a central server telling the computers which addresses to use, or anything similar—in this situation, how can the two computers talk to each other? To address this issue, OS vendors came up with the idea to just assign random addresses on each end of the link. The behavior is defined in RFC 3927, section 2.1 : When a host wishes to configure an IPv4 Link-Local address, it selects an address using a pseudo-random number generator with a uniform distribution in the range from 169.254.1.0 to 169.254.254.255 inclusive. (You might have seen these 169.254 addresses on your home computers if your router is down. Those are link-local addresses.) Sounds simple enough, right? But there is a pretty big problem with this approach: what if a computer has more than one interface on which a link-local address has been established? When a program tries to send some data over the network, the computer has to know which interface to send the data out on. But with multiple link-local interfaces, the outbound one becomes ambiguous. This is described in section 6.3 of the RFC: Address Ambiguity Application software run on a multi-homed host that supports IPv4 Link-Local address configuration on more than one interface may fail. This is because application software assumes that an IPv4 address is unambiguous, that it can refer to only one host. IPv4 Link-Local addresses are unique only on a single link. A host attached to multiple links can easily encounter a situation where the same address is present on more than one interface, or first on one interface, later on another; in any case associated with more than one host. […] The IPv6 protocol designers took this lesson to heart. Every time an IPv6-capable computer connects to a network, it establishes a link-local address starting with . (You should be able to see this address via on Linux, or your OS’s equivalent.) But if you’re connected to multiple networks, all of them will have addresses beginning with . Now if an application wants to establish a connection to a computer in this range, how can it tell the OS which interface to use? That’s exactly where comes in: it allows the to specify which network interface to use. Each interface has an index associated with it, which you can see on Linux with . When I run that command, I see: The , , and listed here are all the indexes that can be used as the scope ID. Let’s try pinging our address: Aha! The warning tells us that for a link-local address, the scope ID needs to be specified. Let’s try that using the syntax: Success! What if we try a different scope ID? This makes sense: the address is only valid for scope ID 2 (the interface). When we told to use a different scope, 3, the address was no longer reachable. This neatly solves the 169.254 problem with IPv4 addresses. Since scope IDs can help disambiguate the interface on which a connection ought to be made, it does make sense to include this field in , as well as in its textual representation. The keen-eyed among you may have noticed that the commands above printed out an alternate representation: . The at the end is the network interface that corresponds to the numeric scope ID. Many programs can handle this representation, but Rust’s can’t. Another thing you might have noticed is that the scope ID only makes sense on a particular computer. A scope ID such as means different things on different computers. So the scope ID is roundtrip serializable, but not portable across machines. In this post we started off by looking at a somewhat strange inconsistency and ended up deep in the IPv6 specification. In our case, the instances were always for internal services talking to each other without any QoS considerations, so was always zero. Given that knowledge, we were okay adjusting the property-based tests to always generate instances where was set to zero. ( Here’s the PR as landed .) Still, it raises questions: Should we wrap in a newtype that enforces this constraint? Should provide a non-standard alternate serializer that also includes the field? Should not forward to when hides fields? Should Rust have had separate types from the start? (Probably too late now.) And should Berkeley sockets not have included at all, given that it makes the type impossible to represent as text without loss? The lesson it really drives home for me is how important the principle of least surprise can be. Both and have lossless textual representations, and does as well. By analogy it would seem like would, too, and yet it does not! IPv6 learned so much from IPv4’s mistakes, and yet its designers couldn’t help but make some mistakes of their own. This makes sense: the designers could only see the problems they were solving then, just as we can only see those we’re solving now—and just as we encounter problems with their solutions, future generations will encounter problems with ours. Thanks to Fiona , and several of my colleagues at Oxide, for reviewing drafts of this post. Discuss on Hacker News and Lobsters . This is why our Rust map crate where keys can borrow from values, , serializes its maps as lists or sequences.  ↩︎ The Requests for Discussion we use at Oxide are inspired by RFCs, though we use a slightly different term (RFD) to convey the fact that our documents are less set in stone than IETF RFCs are.  ↩︎ The two fields sum up to 28 bits, and the field is a , so there’s four bits remaining. I couldn’t find documentation for these four bits anywhere—they appear to be unused padding in the . If you know about these bits, please let me know!  ↩︎ a way to generate random instances of a particular type, and given a failing input, a way to shrink it down to a minimal failing value. generate four values: an , a , a , and another then pass them in to . A left square bracket ( ) The textual representation of an IPv6 address Optionally , a percent sign ( ) followed by the scope identifier encoded as a decimal integer A right square bracket ( ) A colon ( ) The port, encoded as a decimal integer. generated a with a non-zero field. When we went to serialize this field as JSON, we used the textual representation, which dropped the field. When we deserialized it, the field was set to zero. As a result, the before and after values were no longer equal. What does this field mean? A is just an plus a port ; why is a different? Why is the not part of the textual representation? , , and are all roundtrip serializable. Why is not? Also: what is the field? a 20-bit Flow Label, and an 8-bit Traffic Class 3 . QoS was not originally part of the 1980s Berkeley sockets specification. DSCP came about much later ( RFC 2474 , 1998). Because C structs do not provide encapsulation, the definition was set in stone and couldn’t be changed. So instead, the DSCP field is set as an option on the socket, via . By the time IPv6 came around, it was pretty clear that QoS was important, so the Traffic Class was baked into the struct. as a pseudo-random value that can be used as part of a hash key for load balancing, or as extra QoS bits on top of the 8 bits provided by the Traffic Class field. This is why our Rust map crate where keys can borrow from values, , serializes its maps as lists or sequences.  ↩︎ The Requests for Discussion we use at Oxide are inspired by RFCs, though we use a slightly different term (RFD) to convey the fact that our documents are less set in stone than IETF RFCs are.  ↩︎ The two fields sum up to 28 bits, and the field is a , so there’s four bits remaining. I couldn’t find documentation for these four bits anywhere—they appear to be unused padding in the . If you know about these bits, please let me know!  ↩︎

0 views
Corrode 6 days ago

Patterns for Defensive Programming in Rust

I have a hobby. Whenever I see the comment in code, I try to find out the exact conditions under which it could happen. And in 90% of cases, I find a way to do just that. More often than not, the developer just hasn’t considered all edge cases or future code changes. In fact, the reason why I like this comment so much is that it often marks the exact spot where strong guarantees fall apart. Often, violating implicit invariants that aren’t enforced by the compiler are the root cause. Yes, the compiler prevents memory safety issues, and the standard library is best-in-class. But even the standard library has its warts and bugs in business logic can still happen. All we can work with are hard-learned patterns to write more defensive Rust code, learned throughout years of shipping Rust code to production. I’m not talking about design patterns here, but rather small idioms, which are rarely documented, but make a big difference in the overall code quality. Here’s some innocent-looking code: This code works for now, but what if you refactor it and forget to keep the length check? That’s our first implicit invariant that’s not enforced by the compiler. The problem is that indexing into a vector is decoupled from checking its length: these are two separate operations, which can be changed independently without the compiler ringing the alarm. If we use slice pattern matching, we’ll only get access to the element if the arm is executed. Note how this automatically uncovered one more edge case: what if the list is empty? We hadn’t considered this case before. The compiler-enforced pattern matching forces us to think about all possible states! This is a common pattern throughout robust Rust code, the attempt to put the compiler in charge of enforcing invariants. When initializing an object with many fields, it’s tempting to use to fill in the rest. In practice, this is a common source of bugs. You might forget to explicitly set a new field later when you add it to the struct (thus using the default value instead, which might not be what you want), or you might not be aware of all the fields that are being set to default values. Instead of this: Yes, it’s slightly more verbose, but what you gain is that the compiler will force you to handle all fields explicitly. Now when you add a new field to , the compiler will remind you to set it here as well and reflect on which value makes sense. Let’s say you’re building a pizza ordering system and have an order type like this: For your order tracking system, you want to compare orders based on what’s actually on the pizza - the , , and . The timestamp shouldn’t affect whether two orders are considered the same. Here’s the problem with the obvious approach: Now imagine your team adds a field for customization options: Your implementation still compiles, but is it correct? Should be part of the equality check? Probably yes - a pizza with extra cheese is a different order! But you’ll never know because the compiler won’t remind you to think about it. Here’s the defensive approach using destructuring: Now when someone adds the field, this code won’t compile anymore. The compiler forces you to decide: should be included in the comparison or explicitly ignored with ? This pattern works for any trait implementation where you need to handle struct fields: , , , etc. It’s especially valuable in codebases where structs evolve frequently as requirements change. Sometimes there’s no conversion that will work 100% of the time. That’s fine. When that’s the case, resist the temptation to offer a implementation out of habit; use instead. Here’s an example of in disguise: The is a hint that this conversion can fail in some way. We set a default value instead, but is it really the right thing to do for all callers? This should be a implementation instead, making the fallible nature explicit. We fail fast instead of continuing with a potentially flawed business logic. It’s tempting to use in combination with a catch-all pattern like , but this can haunt you later. The problem is that you might forget to handle a new case that was added later. Instead of: By spelling out all variants explicitly, the compiler will warn you when a new variant is added, forcing you to handle it. Another case of putting the compiler to work. If the code for two variants is the same, you can group them: Using as a placeholder for unused variables can lead to confusion. For example, you might get confused about which variable was skipped. That’s especially true for boolean flags: In the above example, it’s not clear which variables were skipped and why. Better to use descriptive names for the variables that are not used: Even if you don’t use the variables, it’s clear what they represent and the code becomes more readable and easier to review without inline type hints. If you only want your data to be mutable temporarily, make that explicit. This pattern is often called “temporary mutability” and helps prevent accidental modifications after initialization. See the Rust unofficial patterns book for more details. Let’s say you had a simple type like the following: Now you want to make invalid states unrepresentable. One pattern is to return a from the constructor. But nothing stops someone from creating an instance of directly: This should not be possible! One way to prevent this is to make the struct non-exhaustive: Now the struct cannot be instantiated directly outside of the module. However, what about the module itself? One way to prevent this is to add a hidden field: Now the struct cannot be instantiated directly even inside the module. You have to go through the constructor, which enforces the validation logic. The attribute is often neglected. That’s sad, because it’s such a simple yet powerful mechanism to prevent callers from accidentally ignoring important return values. Now if someone creates a but forgets to use it, the compiler will warn them: This is especially useful for guard types that need to be held for their lifetime and results from operations that must be checked. The standard library uses this extensively. For example, is marked with , which is why you get warnings if you don’t handle errors. Boolean parameters make code hard to read at the call site and are error-prone. We all know the scenario where we’re sure this will be the last boolean parameter we’ll ever add to a function. It’s impossible to understand what this code does without looking at the function signature. Even worse, it’s easy to accidentally swap the boolean values. Instead, use enums to make the intent explicit: This is much more readable and the compiler will catch mistakes if you pass the wrong enum type. You will notice that the enum variants can be more descriptive than just or . And more often than not, there are more than two meaningful options; especially for programs which grow over time. For functions with many options, you can configure them using a parameter struct: This approach scales much better as your function evolves. Adding new parameters doesn’t break existing call sites, and you can easily add defaults or make certain fields optional. The preset methods also document common use cases and make it easy to use the right configuration for different scenarios. Rust is often criticized for not having named parameters, but using a parameter struct is arguably even better for larger functions with many options. Many of these patterns can be enforced automatically using Clippy lints. Here are the most relevant ones: You can enable these in your project by adding them to your or at the top of your crate, e.g. Defensive programming in Rust is about leveraging the type system and compiler to catch bugs before they happen. By following these patterns, you can: It’s a skill that doesn’t come naturally and it’s not covered in most Rust books, but knowing these patterns can make the difference between code that works but is brittle, and code that is robust and maintainable for years to come. Remember: if you find yourself writing , take a step back and ask how the compiler could enforce that invariant for you instead. The best bug is the one that never compiles in the first place. Make implicit invariants explicit and compiler-checked Future-proof your code against refactoring mistakes Reduce the surface area for bugs

0 views
fasterthanli.me 1 weeks ago

Engineering a Rust optimization quiz

There are several Rust quizzes online, including one that’s literally called the “Unfair Rust Quiz” at https://this.quiz.is.fckn.gay/ , but when I was given the opportunity to record an episode of the Self-Directed Research podcast live on the main stage of EuroRust 2025 , I thought I’d come up with something special. The unfair rust quiz really deserves its name. It is best passed with a knowledgeable friend by your side.

0 views
Matthias Endler 1 weeks ago

Building Up And Sanding Down

Over the years, I’ve gravitated toward two complementary ways to build robust software systems: building up and sanding down. Building up means starting with a tiny core and gradually adding functionality. Sanding down means starting with a very rough idea and refining it over time. Neither approach is inherently better; it’s almost a stylistic decision that depends on team dynamics and familiarity with the problem domain. On top of that, my thoughts on the topic are not particularly novel, but I wanted to summarize what I’ve learned over the years. Building Up Working on a solid stone block in ancient Egypt Source: Wikimedia Public Domain Building up focuses on creating a solid foundation first. I like to use it when working on systems I know well or when there is a clear specification I can refer to. For example, I use it for implementing protocols or when emulating hardware such as for my MOS 6502 emulator . I prefer “building up” over “bottom-up” as the former evokes construction and upward growth. “Bottom-up” is more abstract and directional. Also “bottom-up” always felt like jargon while “building up” is more intuitive and very visual, so it could help communicate the idea to non-technical stakeholders. There are a few rules I try to follow when building up: When I collaborate with highly analytical people, this approach works well. People who have a background in formal methods or mathematics tend to think in terms of “building blocks” and proofs. I also found that functional programmers tend to prefer this approach. In languages like Rust, the type system can help enforce invariants and make it easier to build up complex systems from simple components. Also, Rust’s trait system encourages composition, which aligns well with that line of thinking. The downside of the “build up” approach is that you end up spending a lot of time on the foundational layers before you can see any tangible results. It can be slow to get to an MVP this way. Some people also find this approach too rigid and inflexible, as it can be hard to pivot or change direction once you’ve committed to a certain architecture. For example, say you’re building a web framework. There are a ton of questions at the beginning of the project: In a building-up approach, you would start by answering these questions and designing the core abstractions first. Foundational components like the request and response types, the router, and the middleware system are the backbone of the framework and have to be rock solid. Only after you’ve pinned down the core data structures and their interactions would you move on to building the public API. This can lead to a very robust and well-designed system, but it can also take a long time to get there. For instance, here is the struct from the popular crate: There are quite a few clever design decisions in this short piece of code: With the exception of extensions, this design has stood the test of time. It has remained largely unchanged since the very first version in 2017. Sanding Down Drawing of the part of wall painting in the tomb of Rekhmire Source: Wikimedia Public Domain The alternative approach, which I found to work equally well, is “sanding down.” In this approach, you start with a rough prototype (or vertical slice) and refine it over time. You “sand down” the rough edges over and over again, until you are happy with the result. It feels a bit like woodworking, where you start with a rough piece of wood and gradually refine it into a work of art. (Not that I have any idea what woodworking is like, but I imagine it’s something like that.) Crucially, this is similar but not identical to prototyping. The difference is that you don’t plan on throwing away the code you write. Instead, you’re trying to exploit the iterative nature of the problem and purposefully work on “drafts” until you get to the final version. At any point in time you can stop and ship the current version if needed. I find that this approach works well when working on creative projects which require experimentation and quick iteration. People with a background in game development or scripting languages tend to prefer this approach, as they are used to working in a more exploratory way. When using this approach, I try to follow these rules: This approach makes it easy to throw code away and try something new. I found that it can be frustrating for people who like to plan ahead and are very organized and methodical. The “chaos” seems to be off-putting for some people. As an example, say you’re writing a game in Rust. You might want to tweak all aspects of the game and quickly iterate on the gameplay mechanics until they feel “just right.” In order to do so, you might start with a skeleton of the game loop and nothing else. Then you add a player character that can move around the screen. You tweak the jump height and movement speed until it feels good . There is very little abstraction between you and the game logic at this point. You might have a lot of duplicated code and hardcoded values, but that’s okay for now. Once the core gameplay mechanics are pinned down, you can start refactoring the code. I think Rust can get in the way if you use Bevy or other frameworks early on in the game design process. The entity component system can feel quite heavy and hinder rapid iteration. (At least that’s how I felt when I tried Bevy last time.) I had a much better experience creating my own window and rendering loop using macroquad . Yes, the entire code was in one file and no, there were no tests. There also wasn’t any architecture to speak of. And yet… working on the game felt amazing! I knew that I could always refactor the code later, but I wanted to stay in the moment and get the gameplay right first. Here’s my game loop , which was extremely imperative and didn’t require learning a big framework to get started: You don’t have to be a Rust expert to understand this code. In every loop iteration, I simply: It’s a very typical design for that type of work. If I wanted to, I could now sand down the code and refactor it into a more modular design until it’s production-ready. I could introduce a “listener/callback” system to separate input handling from player logic or a scene graph to manage multiple game objects or an ontology system to manage game entities and their components. But why bother? For now, I care about the game mechanics, not the architecture. Finding the Right Balance Both variants can lead to correct, maintainable, and efficient systems. There is no better or worse approach. I found that most people gravitate toward one approach or the other. However, it helps to be familiar with both approaches and know when to apply which mode. Choose wisely, because switching between the two approaches is quite tricky as you start from different ends of the problem. Focus on atomic building blocks that are easily composable and testable. Build up powerful guarantees from simple, verifiable properties. Focus on correctness, not performance. Write the documentation along with the code to test your reasoning. Nail the abstractions before moving on to the next layer. Will it be synchronous or asynchronous? How will the request routing work? Will there be middleware? How? How will the response generation work? How will error handling be done? The struct is generic over the body type , allowing for flexibility in how the body is represented (e.g., as a byte stream, a string, etc.). The struct is separated from the struct, allowing for easy access to the request metadata without needing to deal with the body. can be used to store extra data derived from the underlying protocol. The field is a zero-sized type used to prevent external code from constructing directly. It enforces the use of the provided constructors and ensures that the invariants of the struct are maintained. Switch off your inner perfectionist. Don’t edit while writing the first draft. Code duplication is strictly allowed. Refactor, refactor, refactor. Defer testing until after the first draft is done. Focus on the outermost API first; nail that, then polish the internals. get the inputs update the player state draw the player wait for the next frame

0 views
Steve Klabnik 2 weeks ago

I see a future in jj

In December of 2012, I was home for Christmas, reading Hacker News. And that’s when I saw “ Rust 0.5 released ."" I’m a big fan of programming languages, so I decided to check it out. At the time, I was working on Ruby and Rails, but in college, I had wanted to focus on compilers, and my friends were all very much into systems stuff. So I decided to give Rust a try. And I liked it! But, for other reasons I won’t get into here, I was thinking about a lot of things in that moment. I was looking to shake things up a bit. So I asked myself: is Rust going to be A Thing? So, I thought about it. What does a programming language need to be successful? It needs some sort of market fit. It needs to have people willing to work on it, as bringing a new language into the world is a lot of work. And it needs users. When I considered all of these things, here’s what I saw with Rust: Market fit: there was basically no credible alternatives to C and C++. I had been involved in the D community a bit, but it was clear that it wasn’t going to take off. Go was a few years old, and hit 1.0 earlier that year, but for the kinds of work that C and C++ are uniquely able to do, I saw the same problem that I did with D: garbage collection. This doesn’t mean Go isn’t a good language, or that it’s not popular, but I didn’t see it as being able to credibly challenge C and C++ in their strongholds. Rust, on the other hand, had a novel approach to these problems: memory safety without garbage collection. Now, I also need to mention that Rust back in those days was much closer to Go than it even is today, but again, I had just learned about it for a few hours, I didn’t really have a deep understanding of it yet. If I had, I actually might have also dismissed it as well, as it wasn’t really GC that was the issue, but a significant runtime. But again: I hadn’t really come to that understanding yet. Point is: low-level programming was a space where there hadn’t been much innovation in a very long time, and I thought that meant that Rust had a chance. Check. For a team: well, Mozilla was backing it. This is a big deal. It meant that there were folks whose job it was to work on the language. There’s so much that you need to do to make a new language, and that means a ton of work, which means that if you’re going to be able to get it done in a reasonable amount of time, having paid folks working on it is certainly better than the alternative. Check. And finally, how does this translate into users? Well, Mozilla was planning on using it in Firefox. This is huge. Firefox is a major project, and if they could manage to use Rust in it, that would prove that Rust was capable of doing real work. And, more importantly, it would mean that there would be a lot of folks who would need to learn Rust to work on Firefox. This would create a base of users, which would help the language grow. Check. Finally, even though it wasn’t part of my initial assessment, I just really liked the Rust folks. I had joined IRC and chatted with people, and unlike many IRC rooms, they were actually really nice. I wanted to be around them more. And if I did, other people probably would too. So that was also a plus. So, I started learning Rust. I decided to write a tutorial for it, “Rust for Rubyists,” because I’m a sucker for alliteration. And I eventually joined the team, co-authored The Book, and if you’re reading this post, you probably know the rest of the story. For some background, jj is a new version control system (VCS), not a programming language. It is written in Rust though! While I talked about how I decided to get involved with Rust above, my approach here generalizes to other kinds of software projects, not just programming languages. I have a rule of thumb: if Rain likes something, I will probably like that thing, as we have similar technical tastes. So when I heard her talk about jj, I put that on my list of things to spend some time with at some point. I was especially intrigued because Rain had worked at Meta on their source control team. So if she’s recommending something related to source control, that’s a huge green flag. It took me a while, but one Saturday morning, I woke up a bit early, and thought to myself, “I have nothing to do today. Let’s take a look at jj.” So I did . You’ll note that link goes to a commit starting a book about jj. Since it worked for me with Rust, it probably would work for me for jj as well. Writing about something really helps clarify my thinking about it, and what better time to write something for a beginner than when you’re also a beginner? Anyway, people seem to really like my tutorial, and I’m thankful for that. So, what do I see in jj? Well, a lot of it kind of eerily mirrors what I saw in Rust: a good market fit, a solid team, and a potential user base. But the market fit is interesting. Git has clearly won, it has all of the mindshare, but since you can use jj to work on Git repositories, it can be adopted incrementally. At Oxide , Rain started using jj, and more of us did, and now we’ve got a chat channel dedicated to it. This is, in my opinion, the only viable way to introduce a new VCS: it has to be able to be partially adopted. Google is using jj, and so that is a bit different than Mozilla, but the same basic idea. I have more to say about Google’s relationship to jj, but that’s going to be a follow-up blog post. What I will say in this post is that at the first ever jj conference a few weeks ago, Martin (the creator of jj) said that internal adoption is going really well. I’m burying the lede a bit here, because the video isn’t up yet, and I don’t want to get the details of some of the more exciting news incorrect in this post. I also don’t mean to imply that everyone at Google is using jj, but the contingent feels significant to me, given how hard it is to introduce a new VCS inside a company of that size. Well, in this case, it’s using Piper as the backend, so you could argue about some of the details here, but the point is: jj is being used in projects as small as individual developers and as large as one of the largest monorepos in the world. That’s a big deal. It can show the social proof needed for others to give jj a chance. Outside of Google, a lot of people say that there’s a bit of a learning curve, but once you get over that, people really like it. Sound familiar? I think jj is different from Rust in this regard in that it’s also very easy to learn if you aren’t someone who really knows a ton about Git. It’s folks that really know Git internals and have put time and care into their workflows that can struggle a bit with jj, because jj is different. But for people who just want to get work done, jj is really easy to pick up. And when people do, they often tend to like it. jj has developed a bit of a reputation for having a passionate fanbase. People are adopting it in a skunkworks way. This is a great sign for a new tool. And finally, the team. Martin is very dedicated to jj, and has been working on it for a long time. There’s also a small group of folks working on it with him. It recently moved out from his personal GitHub account to its own organization, and has started a more formal governance. The team is full of people who have a deep history of working on source control tools, and they know what they’re doing. The burgeoning jj community reminds me of that early Rust community: a bunch of nice folks who are excited about something and eager to help it grow. Basically, to me, jj’s future looks very bright. It reminds me of Rust in all of the best ways. Speaking of burying the lede… I’ve decided to leave Oxide. Oxide is the best job I’ve ever had, and I love the people I work with. I was employee 17. I think the business will do fantastic in the future, and honestly it’s a bit of a strange time to decide to leave, since things are going so well. But at the same time, some of my friends have started a new company, ERSC , which is going to be building a new platform for developer collaboration on top of jj. Don’t worry, “errssk” isn’t going to be the name of the product. It’s kind of like how GitHub was incorporated as Logical Awesome , but nobody calls it that. This won’t be happening until next month, I have some stuff to wrap up at Oxide, and I’m going to take a week off before starting. But as sad as I am to be leaving Oxide, I’m also really excited to be able to spend more time working in the jj community, and helping build out this new platform. For those of you who’ve been asking me to finish my tutorial, well, now I’ll have the time to actually do that! I’m sorry it’s taken so long! You’ll see me talking about jj even more, spending even more time in the Discord , and generally being more involved in the community. And I’ll be writing more posts about it here as well, of course. I’m really excited about this next chapter. 2025 has been a very good year for me so far, for a number of reasons, and I am grateful to be able to take a chance on something that I’m truly passionate about. Here’s my post about this post on BlueSky: I see a future in #jj-vcs : steveklabnik.com/writing/i-se... I see a future in jj Blog post: I see a future in jj by Steve Klabnik

0 views
baby steps 2 weeks ago

Move, Destruct, Forget, and Rust

This post presents a proposal to extend Rust to support a number of different kinds of destructors. This means we could async drop, but also prevent “forgetting” (leaking) values, enabling async scoped tasks that run in parallel à la rayon/libstd. We’d also be able to have types whose “destructors” require arguments. This proposal – an evolution of “must move” that I’ll call “controlled destruction” – is, I think, needed for Rust to live up to its goal of giving safe versions of critical patterns in systems programming. As such, it is needed to complete the “async dream”, in which async Rust and sync Rust work roughly the same. Nothing this good comes for free. The big catch of the proposal is that it introduces more “core splits” into Rust’s types. I believe these splits are well motivated and reasonable – they reflect inherent complexity , in other words, but they are something we’ll want to think carefully about nonetheless. The TL;DR of the proposal is that we should: In a talk I gave some years back at Rust LATAM in Uruguay 1 , I said this : Rust currently does a pretty good job with preventing parts of your program from interfering with one another, but we don’t do as good a job when it comes to guaranteeing that cleaup happens 2 . We have destructors, of course, but they have two critical limitations: That motivation was fairly abstract, so let me give some concrete examples of things that tie back to this limitation: The goal of this post is to outline an approach that could solve all of the above problems and which is backwards compatible with Rust today. The core problem is that Rust today assumes that every value can be moved, dropped, and forgotten: The way I see, most methods are “opt-in” – they don’t execute unless you call them. But destructors are different. They are effectively a method that runs by default – unless you opt-out, e.g., by calling . But the ability to opt-out means that they don’t fundamentally add any power over regular methods, they just make for a more ergonomic API. The implication is that the only way in Rust today to guarantee that a destructor will run is to retain ownership of the value. This can be important to unsafe code – APIs that permit scoped threads, for example, need to guarantee that those parallel threads will be joined before the function returns. The only way they have to do that is to use a closure which gives -borrowed access to a : Because the API nevers gives up ownership of the scope, it can ensure that it is never “forgotten” and thus that its destructor runs. The scoped thread approach works for sync code, but it doesn’t work for async code. The problem is that async functions return a future, which is a value. Users can therefore decide to “forget” this value, just like any other value, and thus the destructor may never run. When you start poking around, you find that guaranteed destructors turn up quite a bit in systems programming. Scoped APIs in futures are one example, but DMA (direct memory access) is another. Many embedded devices have a mode where you begin a DMA transfer that causes memory to be written into memory asynchronously. But you need to ensure that this DMA is terminated before that memory is freed. If that memory is on your stack, that means you need a destructor that will either cancel or block until the DMA finishes. 4 This situation is very analogous to the challenge of revisiting the default bound, and I think the same basic approach that I outlined in [this blog post][sized] will work. The core of the idea is simple: have a “special” set of traits arranged in a hierarchy: By default, generic parameters get a bound, so is equivalent to . But if the parameter opts in to a weaker bound, then the default is suppressed, so means that is assumed by “destructible” but not forgettable. And indicates that can only be moved. Let me explain briefly how these bounds would work. Given a default type , or one that writes explicitly, the function can do anything that is possible today: The function would require as well: This means that if you have only , the function can only drop or move, it can’t “forget”: We would modify the function to require only : We would also extend the borrow checker so that when it sees a value being dropped (i.e., because it went out of scope), it would require the bound. That means that if you have a value whose type is only , you cannot “drop” it: This means that if you have only a bound, you must move anything you own if you want to return from the function. For example: If you have a function that does not move, you’ll get an error: It’s worth pointing out that this will be annoying as all get out in the face of panics: I’m ok with this, but it is going to put pressure on better ways to rule out panics statically. In fact, we are already doing something much like this destruct check for const functions. Right now if you have a const fn and you try to drop a value, you get an error: Compiling that gives you the error: This check is not presently taking place in borrow check but it could be. The final part of the check would be requiring that “moved” values implement : You might think that having types that are would replace the need for pin, but this is not the case. A pinned value is one that can never move again , whereas a value that is not can never be moved in the first place – at least once it is stored into a place. I’m not sure if this part of the proposal makes sense, we could start by just having all types be , , or (the default) . The other part of the proposal is that you should be able to explicit “opt out” from being forgettable, e.g. by doing Doing this will limit the generics that can accept your type, of course. The tough part with these “default bound” proposals is always associated type bounds. For backwards compatibility, we’d have to default to but a lot of associated types that exist in the wild today shouldn’t really require . For example a trait like should really just require for its return type: I am basically not too worried about this. It’s possible that we can weaken these bounds over time or through editions. Or, perhaps, add in some kind of edition-specific “alias” like where is implemented for everything that implements . I am not sure exactly how to manage it, but we’ll figure it out – and in the meantime, most of the types that should not be forgettable are really just “guard” types that don’t have to flow through quite so many places. The one place that I think it is really imporatnt that we weaken the associated type bounds is with closures– and, fortunately, that’s a place we can get away with due to the way our “closure trait bound” syntax works. I feel like I wrote a post on this before, but I can’t find it now, but the short version is that, today, when you write , that means that the closure must return . If you write , then this type must have been declared somewhere else, and so will (independently from the associated type of the trait) get a default bound. So since the associated type is not independently nameable in stable Rust, we can change its bounds, and code like this would continue to work unchanged: Recently I was pointed at this internals thread for a “substructural type system” which likely has very similar capabilities. To be totally honest, though, I haven’t had time to read and digest it yet! I had this blog post like 95% done though so I figured I’d post it first and then go try and compare. So, the system as I described would allow for ‘unmoveable’ types (i.e., a struct that opts out from everything and only permits ), but such a struct would only really be something you could store in a static memory location. You couldn’t put it on the stack because the stack must eventually get popped. And you couldn’t move it from place to place because, well, it’s immobile. This seems like something that could be useful – e.g., to model “video RAM” or something that lives in a specific location in memory and cannot live anywhere else – but it’s not a widespread need. I imagine something like this: With this setup, any function that owns a must eventually invoke . This is because no values of this type can be dropped, so they must be moved. This setup provides attacks a key problem that has blocked async drop in my mind, which is that types that are “async drop” do not have to implement “sync drop”. This gives the type system the ability to prevent them from being dropped in sync code, then, and it would mean that they can only be dropped in async drop. But there’s still lots of design work to be done there. This comes from the const generifs work. I don’t love it. But there is a logic to it. Right now, when you drop a struct or other value, that actually does a whole sequence of things, only one of which is running any impl – it also (for example) drops all the fields in the struct recursively, etc. The idea is that “destruct” refers to this whole sequence. I…don’t actually think it would be very hard. I’ve thought somewhat about it and all of the changes seem pretty straightforward. I would be keen to support a lang-team experiment on this. The whole topic of destructors and leaks and so forth datesback to approximately Rust 1.0, when we discovered that, in fact, our abstraction for threads was unsound when combined with cyclic ref-counted boxes. Before that we hadn’t fully internalized that destructors are “opt-out methods”. You can read this blog post I wrote at the time . At the time, the primary idea was to have some kind of bounds and it was tied to the idea of references (so that all data was assumed to be “leakable”, and hence something you could put into an ). I… mostly think we made the right call at the time. I think it’s good that most of the ecosystem is interoperable and that doesn’t require bounds, and certainly I think it’s good that we moved to 1.0 with minimal disruption. In any case, though, I rather prefer this design to the ones that were under discussion at the time, in part because it also addresses the need for different kinds of destructors and for destructors with many arguments and so forth, which wasn’t something we thought about then. I think that specifying the bounds you want is inherently better than today’s design, both because it’s easier to understand and because it allows us to backwards compatibly add traits in between in ways that are not possible with the design. However, I do see that having mean that does not hold is subtle. I wonder if we should adopt some kind of sigil or convention on these traits, like or something. I don’t know! Something to consider. That was a great conference. Also, interestingly, this is one of my favorite of all my talks, but for some reason, I rarely reuse this material. I should change that.  ↩︎ Academics distinguish “safety” from “liveness properties”, where safety means “bad things don’t happen” and “liveness” means “good things eventually happen”. Another way of saying this is that Rust’s type system helps with a lot of safety properties but struggles with liveness properties.  ↩︎ Uh, citation needed. I know this is true but I can’t find the relevant WebAssembly issue where it is discussed. Help, internet!  ↩︎ Really the DMA problem is the same as scoped threads. If you think about it, the embedded device writing to memory is basically the same as a parallel thread writing to memory.  ↩︎ Introduce a new “default trait bound” and an associated trait hierarchy: , representing values that can be forgotten , representing values with a destructor , representing values that can be moved , the base trait that represents any value Use the “opt-in to weaker defaults” scheme proposed for sizedness by RFC #3729 (Hierarchy of Sized Traits) So defaults to “a that can be forgotten/destructed/moved” And means “a that can be destructed, but not necessarily forgotten” And means “a that can be moved, but not necessarily forgotten” …and so forth. Integrate and enforce the new traits: The bound on will already require , so that’s good. Borrow check can enforce that any dropped value must implement ; in fact, we already do this to enforce bounds in . Borrow check can be extended to require a bound on any moved value. Adjust the trait bound on closures (luckily this works out fairly nicely) It’s easy to expose a high-performance API. But it’s hard to help users control it – and this is what Rust’s type system does. All destructors must meet the same signature, , which isn’t always adequate. There is no way to guarantee a destructor once you give up ownership of a value. The ability to have or drop, both of which require a distinct drop signature. The ability to have a “drop” operation that takes arguments, such as e.g. a message that must be sent, or a result code that must be provided before the program terminates. The ability to have async scopes that can access the stack, which requires a way to guarantee that a parallel thread will be joined even in an async context. The ability to integrate at maximum efficiency with WebAssembly async tasks, which require guaranteed cleanup. 3 That was a great conference. Also, interestingly, this is one of my favorite of all my talks, but for some reason, I rarely reuse this material. I should change that.  ↩︎ Academics distinguish “safety” from “liveness properties”, where safety means “bad things don’t happen” and “liveness” means “good things eventually happen”. Another way of saying this is that Rust’s type system helps with a lot of safety properties but struggles with liveness properties.  ↩︎ Uh, citation needed. I know this is true but I can’t find the relevant WebAssembly issue where it is discussed. Help, internet!  ↩︎ Really the DMA problem is the same as scoped threads. If you think about it, the embedded device writing to memory is basically the same as a parallel thread writing to memory.  ↩︎

0 views
Brain Baking 2 weeks ago

The Crazy Shotguns In Boomer Shooters

Emberheart’s recent Wizordum rekindled my interest in retro-inspired First Person Shooters (FPS) also known as boomer shooters . Some are offended by the term, but I quite like it: it not only denotes the DOOM clones of the early nineties as the boomer generation of FPS gaming but also perfectly defines what a boomer shooter is: things that go boom . That’s it. And boy, do things go boom in these games, thanks to the crazy amount of weaponry at the player’s disposal. Combine that with emphasis on movement and speed—remember circle strafing? That’s just the bare minimum now—and you’ve got yourself a hundred different ways to murder, shred, and rip your enemies apart. To stay true to their DOOM roots, boomer shooters are usually a bloody affair. I’ve always been fascinated with the shotguns in these games: the rapid BOOM TSJK BOOM TSJK BOOM TSJK of Quake , the heavy KABOOM click clack KABOOM click clack of the super shotgun in DOOM II . Somehow along the way, the shotgun (and double barrel one) became an indispensable part of any boomer shooter. That’s why I’d like to take a closer look at the craziness involved in these retro-inspired shooters. Or more specifically, what’s bound behind key number 3. Assuming 1 is the melee weapon and 2 is the pistol, of course. It’s impossible to talk about shotguns in shooters without mentioning DOOM —which I already did three times, but they, one more time can’t hurt. In 1993, id Software not only started the gory Binary Space Partitioning revolution, but also iterated on Wolfenstein 3D ’s rather boring weaponry line-up: the pistol, the automatic rifle, and the mini gun. DOOM gave us a plethora of new stuff to play with, including some sweet sweet pump action. Yet that digitized child toy won at fairs can hardly be called crazy by modern standards. Enter DOOM II ’s double barrel “super” shotgun: double the barrels, double the fun! Thirty-one years later, those two barrels still pack a mighty punch, up to the point that most other weapons in the game are obsolete. According to various weapon damage tables , the super shotgun has a mean damage output of as much as the rocket launcher! Rocking my super shotgun in a slimy sewer hallway the Legacy of Rust expansion. The deep sound that accompanies the shotgun is still an instant nostalgia trigger. You’ll immediately recognize it. Let’s put it to the test: for this article, I randomly compiled 11 different shotgun sounds into a single audio file. It’s up to you to identify the games and shotguns: If that’s too difficult for you, the following hint will spoil the games but not the order: So where do you go from there? What can possibly topple DOOM II ’s super shotgun? Nothing, really, but developers have been giving it a damn good try since then anyway. There are quite a few almost as iconic double barrel shotguns. In DUSK , we see the protagonist getting attached to their favourite killing machine. When it’s temporarily taken away and then returned a few levels later, we whisper welcome back, friend , lovingly stroke its long barrels, and happily resume the rampage. In Serous Sam , the BOOM sound the weapon emits is almost as majestic as the huge open spaces between the pyramids that are infested with AAAAAAAAAHHHH screaming beheaded kamikazes. How about reskinning the shotgun into a crossbow firing three green projectiles ( Heretic )? Not cool enough? Okay, I get it, we need to step up our game. How about modding our double barrels? Sawing them off, perhaps? In Outlaws , there are three (!) shotguns mapped to your keypads: a single barrel, a double barrel, and a sawn-off one, although to this day I am puzzled by the difference in function as they even sound alike. In Project Warlock , being a more modern retro-inspired shooter, you can upgrade your weapons after collecting enough skill points. That single barrel can become an automatic and that double barrel lovingly called the Boom Stick can gain alternate firing modes. Project Warlock's Doom Stick has a very satisfying 'boom sound' to it. Speaking of mods, DOOM Eternal ’s super shotgun Meat Hook attachment is one of the most genius ideas ever: pulling yourself closer to your enemies before unloading those two barrels ups the fun (and gore) dramatically. I believe you can also inject incendiary rounds. In DOOM 2016 , you can tinker with your shotgun by swapping out pieces. Tadaa, now it’s a shotgun gatling gun! Still not crazy enough, I hear ya. What about Forgive Me Father then, where the unlocked upgrades gradually push more and more crazy (literally) into the weapon designs by merging with the Cthulhu mythos. The Abyssal Shotgun features more bullets per shot and has an increased firing speed, essentially making it an automatic double barrel? What about dual wielding instead? In DUSK , beyond the trusty double barrel, you can dual wield two regular shotguns and pump out that lead at a demonic speed (no wait wrong game). In Nightmare Reaper , the reflection power-up allows you to temporarily dual wield your current load-out that can already be pretty wild as the modifiers are random. I saw someone unloading 100+ shots at once. How’s that for a boomer shooter. The idea is not new though: Blood allowed us to temporarily dual wield sawn-off shotguns in as early as 1997. If that’s not impressive enough, F.E.A.R. not (get it? 1 ): if two barrels aren’t enough, then how about three instead? The game INCISION will congratulate you with the message “Ludicrous Gibs!!” after firing off that bad boy. But we can do even better: the hand cannon in Prodeus features a whopping four barrels that can be fired individually or all at once, turning anything on screen into ketchup. KABOOM click clack. I first thought Prodeus invented that but Shadow Warrior —yet another crazy Build Engine game from 1997 with even crazier weapons—technically already featured a four-barreled shotgun that rapidly rotates as you shoot. I don’t think you can unload everything at once though. Or how about another rotating barrel that can also eject grenades? That’s Shelly’s Disperser from Ion Fury . Guess what, Ion Fury runs on the Build Engine. No coincidence there. Shelly's Disperser might not look sexy but the hybrid weapon can rapidly fire off 6 shots and launch as many grenades! But perhaps the craziest of them all must be the projectile boosting mechanic in ULTRAKILL : after firing off those shotgun shells, you can hit them with your fists to increase their speed? I have no idea how that works. I skipped that game because the trailers induced motion sickness. I can tolerate a crazy amount of crazy but that’s a bit too much. From a pump action toy to a boom stick, quad shotgun, rapid firing abyssal shotgun or disperser. From a regular buckshot shell to incendiary rounds, grenades, and meat hooks. I love these kinds of games because they have the creative freedom to bend all the rules—especially when it comes to the weaponry. And yet, we stay true to our DOOM-like roots: you can’t release a successful retro-inspired shooter without the presence of a (super) shotgun. If you’re interested in my opinion on many of the games mentioned here, be sure to check out my reviews on these retro shooters . The game F.E.A.R. , although not a boomer shooter, is revered for its excellent VK-12 combat shotgun that chews through enemies rather quickly.  ↩︎ Related topics: / games / boomer shooters / By Wouter Groeneveld on 20 October 2025.  Reply via email . DOOM II (obviously) DOOM Eternal Outlaws (2x) Project Warlock Redneck Rampage Serious Sam The game F.E.A.R. , although not a boomer shooter, is revered for its excellent VK-12 combat shotgun that chews through enemies rather quickly.  ↩︎

0 views
underlap 3 weeks ago

IPC channel multiplexing: what next?

About three months ago, I posted IPC channel multiplexing: next steps . Since then I’ve taken a complete break from the project to make the most of the summer and to go on holiday. As I consider how to proceed, I think those next steps still make sense, but that’s not the whole story. The basic problem the multiplexing prototype is trying to solve is as follows. If an IPC channel endpoint is sent over another IPC channel, when it is received, it consumes a file descriptor (at least on Unix variants). A new file descriptor is consumed even if the same IPC channel endpoint is received multiple times. This can crash the receiving process if it runs out of file descriptors. The thing that has changed in the intervening gap is my motivation. I really enjoyed implementing multiplexing of IPC channels as it was relatively self-contained. Extending the API to support more Servo usecases does not feel so fun. Also, I would like more assurance that if I invest the effort to make IPC channel multiplexing suitable for adoption by Servo, that there’s a reasonable chance it will actually be adopted. There seem to be relatively few Servo developers who understand IPC channel well enough to engage with adopting multiplexing. Plus they are likely to be very busy with other things. So there may simply be a lack of traction. Also, multiplexing isn’t a simple piece of code, so merging it into IPC channel will increase the project’s size and complexity and therefore its maintenance cost. There may be performance or usability issues in adopting multiplexing. I’m not aware of any such issues and I don’t anticipate these being significant if they crop up, but there’s still a risk. Currently, I’m working in isolation from the Servo team and I’d like some reassurance that the direction I’m heading in is likely to be adopted. The advantages of continuing are: The disadvantages of continuing are: On balance, I think I’ll continue. It would be possible to move the multiplexing prototype to a separate repository and crate which on the IPC channel crate. The advantages of this are: One possible disadvantage is that it would not be possible to reuse IPC channel internals. For example, if one of the missing features for multiplexing was essentially the same as that for vanilla IPC channel, I couldn’t just generalise the code and share it. I think the most effective way forward is to test the Servo team’s willingness to adopt multiplexing by focussing on a usecase that is known to exhibit the bug, reproducing the bug in isolation, showing that multiplexing fixes the bug, and proposing a fix for Servo. So I’ll start by looking at the bug reports, picking one, and looking at the IPC channel usecase in Servo which hits the bug. I’ll defer the decision of whether to package the prototype as a separate repository until I start to touch the prototype code again. This is contrary to the sunk cost fallacy. ↩︎ I’m not sure what else I would prefer to do with my spare mental capacity. ↩︎ I really dislike Microsoft’s policy of trawling github.com to build AI models. I’m also shocked about Microsoft’s willingness to create e-waste by dead-ending Windows 10 and not supporting older hardware with Windows 11, although they have delayed the deadline with the Windows 10 Extended Security Updates (ESU) programme . (On the other hand, maybe this move will push more people to adopt Linux. See END OF 10 .) ↩︎ Unfortunately, and it’s a big unfortunately, this still requires the repository to be mirrored to github.com . See Non-Github account creation . ↩︎ Capitalising on the effort already expended. [1] Potentially fixing the bug. Overcoming the difficulties involved would give a greater sense of achievement. I enjoy solving difficult problems and it would keep my brain active. Potentially wasting more effort. Now may be an opportunity to retire properly from my career in software development. [2] It could increase the profile of the prototype. I could host this on codeberg.org rather than github.com [3] Ease of code navigation, since the code would be pure Rust rather than multiplatform. Ease of CI: Linux only. Ease of promotion of changes, since it wouldn’t require the involvement of IPC channel committers. Publication to crates.io for ease of consumption by Servo. [4] Documentation could be centred on multiplexing. This is contrary to the sunk cost fallacy. ↩︎ I’m not sure what else I would prefer to do with my spare mental capacity. ↩︎ I really dislike Microsoft’s policy of trawling github.com to build AI models. I’m also shocked about Microsoft’s willingness to create e-waste by dead-ending Windows 10 and not supporting older hardware with Windows 11, although they have delayed the deadline with the Windows 10 Extended Security Updates (ESU) programme . (On the other hand, maybe this move will push more people to adopt Linux. See END OF 10 .) ↩︎ Unfortunately, and it’s a big unfortunately, this still requires the repository to be mirrored to github.com . See Non-Github account creation . ↩︎

0 views
baby steps 3 weeks ago

We need (at least) ergonomic, explicit handles

Continuing my discussion on Ergonomic RC, I want to focus on the core question: should users have to explicitly invoke handle/clone, or not? This whole “Ergonomic RC” work was originally proposed by Dioxus and their answer is simple: definitely not . For the kind of high-level GUI applications they are building, having to call to clone a ref-counted value is pure noise. For that matter, for a lot of Rust apps, even cloning a string or a vector is no big deal. On the other hand, for a lot of applications, the answer is definitely yes – knowing where handles are created can impact performance, memory usage, and even correctness (don’t worry, I’ll give examples later in the post). So how do we reconcile this? This blog argues that we should make it ergonomic to be explicit . This wasn’t always my position, but after an impactful conversation with Josh Triplett, I’ve come around. I think it aligns with what I once called the soul of Rust : we want to be ergonomic, yes, but we want to be ergonomic while giving control 1 . I like Tyler Mandry’s Clarity of purpose contruction, “Great code brings only the important characteristics of your application to your attention” . The key point is that there is great code in which cloning and handles are important characteristics , so we need to make that code possible to express nicely. This is particularly true since Rust is one of the very few languages that really targets that kind of low-level, foundational code. This does not mean we cannot (later) support automatic clones and handles. It’s inarguable that this would benefit clarity of purpose for a lot of Rust code. But I think we should focus first on the harder case, the case where explicitness is needed, and get that as nice as we can ; then we can circle back and decide whether to also support something automatic. One of the questions for me, in fact, is whether we can get “fully explicit” to be nice enough that we don’t really need the automatic version. There are benefits from having “one Rust”, where all code follows roughly the same patterns, where those patterns are perfect some of the time, and don’t suck too bad 2 when they’re overkill. I mentioned this blog post resulted from a long conversation with Josh Triplett 3 . The key phrase that stuck with me from that conversation was: Rust should not surprise you . The way I think of it is like this. Every programmer knows what its like to have a marathon debugging session – to sit and state at code for days and think, but… how is this even POSSIBLE? Those kind of bug hunts can end in a few different ways. Occasionally you uncover a deeply satisfying, subtle bug in your logic. More often, you find that you wrote and not . And occasionally you find out that your language was doing something that you didn’t expect. That some simple-looking code concealed a subltle, complex interaction. People often call this kind of a footgun . Overall, Rust is remarkably good at avoiding footguns 4 . And part of how we’ve achieved that is by making sure that things you might need to know are visible – like, explicit in the source. Every time you see a Rust match, you don’t have to ask yourself “what cases might be missing here” – the compiler guarantees you they are all there. And when you see a call to a Rust function, you don’t have to ask yourself if it is fallible – you’ll see a if it is. 5 So I guess the question is: would you ever have to know about a ref-count increment ? The trick part is that the answer here is application dependent. For some low-level applications, definitely yes: an atomic reference count is a measurable cost. To be honest, I would wager that the set of applications where this is true are vanishingly small. And even in those applications, Rust already improves on the state of the art by giving you the ability to choose between and and then proving that you don’t mess it up . But there are other reasons you might want to track reference counts, and those are less easy to dismiss. One of them is memory leaks. Rust, unlike GC’d languages, has deterministic destruction . This is cool, because it means that you can leverage destructors to manage all kinds of resources, as Yehuda wrote about long ago in his classic ode-to- RAII entitled “Rust means never having to close a socket” . But although the points where handles are created and destroyed is deterministic, the nature of reference-counting can make it much harder to predict when the underlying resource will actually get freed. And if those increments are not visible in your code, it is that much harder to track them down. Just recently, I was debugging Symposium , which is written in Swift. Somehow I had two instances when I only expected one, and each of them was responding to every IPC message, wreaking havoc. Poking around I found stray references floating around in some surprising places, which was causing the problem. Would this bug have still occurred if I had to write explicitly to increment the ref count? Definitely, yes. Would it have been easier to find after the fact? Also yes. 6 Josh gave me a similar example from the “bytes” crate . A type is a handle to a slice of some underlying memory buffer. When you clone that handle, it will keep the entire backing buffer around. Sometimes you might prefer to copy your slice out into a separate buffer so that the underlying buffer can be freed. It’s not that hard for me to imagine trying to hunt down an errant handle that is keeping some large buffer alive and being very frustrated that I can’t see explicitly in the where those handles are created. A similar case occurs with APIs like like 7 . takes an and, if the ref-count is 1, returns an . This lets you take a shareable handle that you know is not actually being shared and recover uniqueness. This kind of API is not frequently used – but when you need it, it’s so nice it’s there. Entering the conversation with Josh, I was leaning towards a design where you had some form of automated cloning of handles and an allow-by-default lint that would let crates which don’t want that turn it off. But Josh convinced me that there is a significant class of applications that want handle creation to be ergonomic AND visible (i.e., explicit in the source). Low-level network services and even things like Rust For Linux likely fit this description, but any Rust application that uses or might also. And this reminded me of something Alex Crichton once said to me. Unlike the other quotes here, it wasn’t in the context of ergonomic ref-counting, but rather when I was working on my first attempt at the “Rustacean Principles” . Alex was saying that he loved how Rust was great for low-level code but also worked well high-level stuff like CLI tools and simple scripts. I feel like you can interpret Alex’s quote in two ways, depending on what you choose to emphasize. You could hear it as, “It’s important that Rust is good for high-level use cases”. That is true, and it is what leads us to ask whether we should even make handles visible at all. But you can also read Alex’s quote as, “It’s important that there’s one language that works well enough for both ” – and I think that’s true too. The “true Rust gestalt” is when we manage to simultaneously give you the low-level control that grungy code needs but wrapped in a high-level package. This is the promise of zero-cost abstractions, of course, and Rust (in its best moments) delivers. Let’s be honest. High-level GUI programming is not Rust’s bread-and-butter, and it never will be; users will never confuse Rust for TypeScript. But then, TypeScript will never be in the Linux kernel. The goal of Rust is to be a single language that can, by and large, be “good enough” for both extremes. The goal is make enough low-level details visible for kernel hackers but do so in a way that is usable enough for a GUI. It ain’t easy, but it’s the job. This isn’t the first time that Josh has pulled me back to this realization. The last time was in the context of async fn in dyn traits, and it led to a blog post talking about the “soul of Rust” and a followup going into greater detail . I think the catchphrase “low-level enough for a Kernel, usable enough for a GUI” kind of captures it. There is a slight caveat I want to add. I think another part of Rust’s soul is preferring nuance to artificial simplicity (“as simple as possible, but no simpler”, as they say). And I think the reality is that there’s a huge set of applications that make new handles left-and-right (particularly but not exclusively in async land 8 ) and where explicitly creating new handles is noise, not signal. This is why e.g. Swift 9 makes ref-count increments invisible – and they get a big lift out of that! 10 I’d wager most Swift users don’t even realize that Swift is not garbage-collected 11 . But the key thing here is that even if we do add some way to make handle creation automatic, we ALSO want a mode where it is explicit and visible. So we might as well do that one first. OK, I think I’ve made this point 3 ways from Sunday now, so I’ll stop. The next few blog posts in the series will dive into (at least) two options for how we might make handle creation and closures more ergonomic while retaining explicitness. I see a potential candidate for a design axiom… rubs hands with an evil-sounding cackle and a look of glee   ↩︎ It’s an industry term .  ↩︎ Actually, by the standards of the conversations Josh and I often have, it was’t really all that long – an hour at most.  ↩︎ Well, at least sync Rust is. I think async Rust has more than its share, particularly around cancellation, but that’s a topic for another blog post.  ↩︎ Modulo panics, of course – and no surprise that accounting for panics is a major pain point for some Rust users.  ↩︎ In this particular case, it was fairly easy for me to find regardless, but this application is very simple. I can definitely imagine ripgrep’ing around a codebase to find all increments being useful, and that would be much harder to do without an explicit signal they are occurring.  ↩︎ Or , which is one of my favorite APIs. It takes an and gives you back mutable (i.e., unique) access to the internals, always! How is that possible, given that the ref count may not be 1? Answer: if the ref-count is not 1, then it clones it. This is perfect for copy-on-write-style code. So beautiful. 😍  ↩︎ My experience is that, due to language limitations we really should fix, many async constructs force you into bounds which in turn force you into and where you’d otherwise have been able to use .  ↩︎ I’ve been writing more Swift and digging it. I have to say, I love how they are not afraid to “go big”. I admire the ambition I see in designs like SwiftUI and their approach to async. I don’t think they bat 100, but it’s cool they’re swinging for the stands. I want Rust to dare to ask for more !  ↩︎ Well, not only that. They also allow class fields to be assigned when aliased which, to avoid stale references and iterator invalidation, means you have to move everything into ref-counted boxes and adopt persistent collections, which in turn comes at a performance cost and makes Swift a harder sell for lower-level foundational systems (though by no means a non-starter, in my opinion).  ↩︎ Though I’d also wager that many eventually find themselves scratching their heads about a ref-count cycle. I’ve not dug into how Swift handles those, but I see references to “weak handles” flying around, so I assume they’ve not (yet?) adopted a cycle collector. To be clear, you can get a ref-count cycle in Rust too! It’s harder to do since we discourage interior mutability, but not that hard.  ↩︎ I see a potential candidate for a design axiom… rubs hands with an evil-sounding cackle and a look of glee   ↩︎ It’s an industry term .  ↩︎ Actually, by the standards of the conversations Josh and I often have, it was’t really all that long – an hour at most.  ↩︎ Well, at least sync Rust is. I think async Rust has more than its share, particularly around cancellation, but that’s a topic for another blog post.  ↩︎ Modulo panics, of course – and no surprise that accounting for panics is a major pain point for some Rust users.  ↩︎ In this particular case, it was fairly easy for me to find regardless, but this application is very simple. I can definitely imagine ripgrep’ing around a codebase to find all increments being useful, and that would be much harder to do without an explicit signal they are occurring.  ↩︎ Or , which is one of my favorite APIs. It takes an and gives you back mutable (i.e., unique) access to the internals, always! How is that possible, given that the ref count may not be 1? Answer: if the ref-count is not 1, then it clones it. This is perfect for copy-on-write-style code. So beautiful. 😍  ↩︎ My experience is that, due to language limitations we really should fix, many async constructs force you into bounds which in turn force you into and where you’d otherwise have been able to use .  ↩︎ I’ve been writing more Swift and digging it. I have to say, I love how they are not afraid to “go big”. I admire the ambition I see in designs like SwiftUI and their approach to async. I don’t think they bat 100, but it’s cool they’re swinging for the stands. I want Rust to dare to ask for more !  ↩︎ Well, not only that. They also allow class fields to be assigned when aliased which, to avoid stale references and iterator invalidation, means you have to move everything into ref-counted boxes and adopt persistent collections, which in turn comes at a performance cost and makes Swift a harder sell for lower-level foundational systems (though by no means a non-starter, in my opinion).  ↩︎ Though I’d also wager that many eventually find themselves scratching their heads about a ref-count cycle. I’ve not dug into how Swift handles those, but I see references to “weak handles” flying around, so I assume they’ve not (yet?) adopted a cycle collector. To be clear, you can get a ref-count cycle in Rust too! It’s harder to do since we discourage interior mutability, but not that hard.  ↩︎

0 views
NorikiTech 4 weeks ago

Rust’s lack of constructors

Part of the “ Rustober ” series. I found it interesting that Rust doesn’t have initializers. (It doesn’t have classes either and those may be related.) You either set all the fields on a struct directly, create a function which acts like a memberwise initializer and is named by convention, or use a builder pattern : Here’s an example of a function. Rust helps us a bit here by allowing to use parameter names directly in place of matching field names. Library struct fields are often private (like above) and the only way to initialize one is through an associated function (= static method = no parameter). Of course, the parameters can be arbitrary and don’t have to match the struct fields — it’s simply a function, not an initializer. According to the “ Constructors ” chapter in Rustonomicon: There is exactly one way to create an instance of a user-defined type: name it, and initialize all its fields at once. That’s it. Every other way you make an instance of a type is just calling a totally vanilla function that does some stuff and eventually bottoms out to The One True Constructor. … The reasons for this are varied, but it largely boils down to Rust’s philosophy of being explicit . … In concrete contexts, a type will provide a static method for any kind of “default” constructor. This has no relation to in other languages and has no special meaning. It’s just a naming convention.

0 views
NorikiTech 4 weeks ago

Rust struct field order

Part of the “ Rustober ” series. One of the Rust quirks is that when initializing a struct, the named fields can be in any order: In Swift, this is an error. However, looking at the rules for C initialization , it seems the C behavior is the same, called “designated initializer” and has been available since C99. Possibly, this also has to deal with Rust’s struct update syntax where you can initialize a struct based on another instance, in which case the set of field names would be incomplete, so their order does not really matter since they are named:

0 views
NorikiTech 1 months ago

Finished the Rust Book!

Part of the “ Rustober ” series. Just finished my paper copy of “The Rust Programming Language” (2 nd edition covering Rust 2021), the same one that I shared in a previous post . I thought earlier that I’d finish about half, up to more advanced topics, and I’d be ready to work on my side project. However, looking at all the advanced syntax and concepts the external libraries are using , the choice to finish reading the book was worth it. Towards the end, even though the density of information became higher, the book showed how to organize and work with a larger amount of Rust code, and what principles multithreading and macros were based on. Now, looking at some of the function signatures, it’s not entirely new territory. The current, Rust 2024 edition of the book — and the upcoming 3 rd edition of the paper book coming out next year — now has an additional chapter about asynchronous programming that didn’t exist before: “ Fundamentals of Asynchronous Programming: Async, Await, Futures, and Streams ”. I’ll have a cursory look before looking at Axum’s docs…

0 views
NorikiTech 1 months ago

Cargo dependency tree

Part of the “ Rustober ” series. Rust’s tool is quite pleasant to use. I know that Rust projects generally use many dependencies in total (dozens) and I thought: how do I get a dependency tree? Turns out, has a [built-in command]. The output for my project that I described in “ A Rust web service ” is a bit scary! The stars mean that this dependency is present somewhere else in the list, so its dependencies are omitted. Looking at all the different versions, I immediately thought: what happens if two dependencies use different versions of the same crate? Cargo usually resolves the issue by bumping one version (if possible) to another, but if the versions are SemVer-incompatible (i.e. 1.0 and 2.0), then both are included separately. Of course, this does not always work smoothly , but I don’t expect to run into this kind of problem for my project (and even then there are ways of resolving it). What’s interesting is that in the latter case you will get an error similar to this ( source ): Cargo has specific recommendations on how libraries should follow SemVer to reduce the possibility of such problems.

0 views
NorikiTech 1 months ago

Rules of ownership and borrowing in Rust

Part of the “ Rustober ” series. Coming from any practical programming language to Rust, we need to learn its system of memory ownership which is different to what we’re used to. Even if some concepts are familiar from languages with manual memory management (like C and C++), or the application of them is familiar like in languages with a garbage collector, Rust will surprise you. I like how simple and elegant the rules of memory ownership are. There are only three: The second rule, I think, is the most important and gives Rust its power to enforce correctness. In C, you can allocate memory and it’s just there, you “own” it as long as you have a pointer. You can use the memory throughout its lifetime, but it’s an open question who owns it at any one time. When you don’t need the memory anymore, you it. Because single ownership can’t be enforced, you can try to use memory somewhere else as if it’s still yours — that is a SEGFAULT. After you’ve freed the memory, you can try to free it again somewhere else — that is, too, a SEGFAULT. Finally, you can choose to not free the memory at all, which is a memory leak but it’s fairly safe because your program doesn’t crash. Rust also allows borrowing a value to use its memory without changing the owner. That comes with its own set of rules : The rules above prevent the value from being changed from two places at once, or let it be changes from under you. A classic example of that in C is resizing an array. You have a pointer to it in one place and consider it stable and safe (nothing in C is safe). Elsewhere, you resize the array by calling to give it more memory. You get a new pointer to the array and the “stable and safe” pointer immediately becomes invalid. As I like to say, it is now a pointer to a SEGFAULT. Even worse is resizing it because something doesn’t fit and then losing the new pointer. Tracking ownership is enabled by lifetimes — everything has a lifetime scope, the longest being , the life of the program. Early in Rust development, lifetime annotations were required in most places and that made the code really ugly (I remember my initial impressions of looking at early Rust). However, since then lifetime elision rules have been adopted and now lifetime annotations are needed much less often. Lifetime elision is also very simple (but took a long time to get right!): For example, a clear violation of these rules is when you pass two different things into a function and return one of them. The compiler cannot tell automatically which one of them will pass its lifetime to the output, so you must tell it by annotating the parameter and output lifetimes. What I found interesting is that lifetimes are a generic parameter like generic type parameters which means you can do to them things you do to generics e.g. applying trait bounds (especially it comes into play with trait objects). And more importantly, you cannot assign lifetimes arbitrarily, you can only reveal (=annotate) relationships to the compiler between the lifetimes that already exist. Each value has an owner. There can only be one owner at a time. When the owner goes out of scope, the value will be dropped. At any given time, you can have either one mutable reference or any number of immutable references. References must always be valid. Each elided lifetime in the function or closure parameters becomes a distinct lifetime parameter. If there is exactly one lifetime used in the parameters (elided or not), that lifetime is assigned to all elided output lifetimes. If the receiver has type or (i.e. it’s a method) , then the lifetime of that reference to is assigned to all elided output lifetime parameters.

0 views
baby steps 1 months ago

The Handle trait

There’s been a lot of discussion lately around ergonomic ref-counting. We had a lang-team design meeting and then a quite impactful discussion at the RustConf Unconf. I’ve been working for weeks on a follow-up post but today I realized what should’ve been obvious from the start – that if I’m taking that long to write a post, it means the post is too damned long. So I’m going to work through a series of smaller posts focused on individual takeaways and thoughts. And for the first one, I want to (a) bring back some of the context and (b) talk about an interesting question, what should we call the trait . My proposal, as the title suggests, is – but I get ahead of myself. For those of you who haven’t been following, there’s been an ongoing discussion about how best to have ergonomic ref counting: The focus of this blog post is on one particular question: what should we call “The Trait”. In virtually every design, there has been some kind of trait that is meant to identify something . But it’s been hard to get a handle 1 on what precisely that something is. What is this trait for and what types should implement it? Some things are clear: whatever The Trait is, and should implement it, for example, but that’s about it. My original proposal was for a trait named that was meant to convey a “lightweight clone” – but really the trait was meant to replace as the definition of which clones ought to be explicit 2 . Jonathan Kelley had a similar proposal but called it . In RFC #3680 the proposal was to call the trait . The details and intent varied, but all of these attempts had one thing in common: they were very operational . That is, the trait was always being defined in terms of what it does (or doesn’t do) but not why it does it. And that I think will always be a weak grounding for a trait like this, prone to confusion and different interpretations. For example, what is a “lightweight” clone? Is it O(1)? But what about things that are O(1) with very high probability? And of course, O(1) doesn’t mean cheap – it might copy 22GB of data every call. That’s O(1). What you want is a trait where it’s fairly clear when it should and should not be implemented and not based on taste or subjective criteria. And and friends did not meet the bar: in the Unconf, several new Rust users spoke up and said they found it very hard, based on my explanations, to judge whether their types ought to implement The Trait (whatever we call it). That has also been a persitent theme from the RFC and elsewhere. But really there is a semantic underpinning here, and it was Jack Huey who first suggested it. Consider this question. What are the differences between cloning a and a ? One difference, of course, is cost. Cloning the will deep-clone the vector, cloning the will just increment a referece count. But the more important difference is what I call “entanglement” . When you clone the , you don’t get a new value – you get back a second handle to the same value . 3 Knowing which values are “entangled” is key to understanding what your program does. A big part of how the borrow checker 4 achieves reliability is by reducing “entaglement”, since it becomes a relative pain to work with in Rust. Consider the following code. What will be the value of and ? The answer, of course, is “depends on the type of ”. If is a , then . But if is, say, a struct like this one: There are many types that act like a : it’s true for and , of course, but also for things like and channel endpoints like . All of these are examples of “handles” to underlying values and, when you clone them, you get back a second handle that is indistinguishable from the first one. Jack’s insight was that we should focus on the semantic concept (sharing) and not on the operational details (how it’s implemented). This makes it clear when the trait ought to be implemented. I liked this idea a lot, although I eventually decided I didn’t like the name . The word isn’t specific enough, I felt, and users might not realize it referred to a specific concept: “shareable types” doesn’t really sound right. But in fact there is a name already in common use for this concept: handles (see e.g. ). This is how I arrived at my proposed name and definition for The Trait, which is : 5 The trait includes a method which is always equivalent to . The purpose of this method is to signal to the reader that the result is a second handle to the same underlying value. Once the trait exists, we should lint on calls to when the receiver is known to implement and encourage folks to call instead: Compare the above to the version that the lint suggests, using , and I think you will get an idea for how increases clarity of what is happening: The defining characteristic of a handle is that it, when cloned, results in a second value that accesses the same underlying value. This means that the two handles are “entangled”, with interior mutation that affects one handle showing up in the other. Reflecting this, most handles have APIs that consist exclusively or almost exclusively of methods, since having unique access to the handle does not necessarily give you unique access to the value . Handles are generally only significant, semantically, when interior mutability is involved. There’s nothing wrong with having two handles to an immutable value, but it’s not generally distinguishable from two copies of the same value. This makes persistent collections an interesting grey area: I would probably implement for something like , particularly since something like a would make entaglement visible, but I think there’s an argument against it. In the stdlib, handle would be implemented for exactly one type (the others are values): It would be implemented for ref-counted pointers (but not ): And it would be implemented for types like channel endpoints, that are implemented with a ref-counted value under the hood: OK, I’m going to stop there with this “byte-sized” blog post. More to come! But before I go, let me layout what I believe to be a useful “design axiom” that we should adopt for this design: Expose entanglement. Understanding the difference between a handle to an underlying value and the value itself is necessary to understand how Rust works. The phrasing feels a bit awkward, but I think it is the key bit anyway. That. my friends, is foreshadowing . Damn I’m good.  ↩︎ I described as a kind of “lightweight clone” but in the Unconf someone pointed out that “heavyweight copy” was probably a better description of what I was going for.  ↩︎ And, not coincidentally, the types where cloning leads to entanglement tend to also be the types where cloning is cheap.  ↩︎ and functional programming…  ↩︎ The “final” keyword was proposed by Josh Triplett in RFC 3678. It means that impls cannot change the definition of . There’s been some back-and-forth on whether it ought to be renamed or made more general or what have you; all I know is, I find it an incredibly useful concept for cases like this, where you want users to be able to opt-in to a method being available but not be able to change what it does. You can do this in other ways, they’re just weirder.  ↩︎ It began with the first Rust Project Goals program in 2024H2, where Jonathan Kelley from Dioxus wrote a thoughtful blog post about a path to high-level Rust that eventually became a 2024H2 project goal towards ergonomic ref-counting . I wrote a series of blog posts about a trait I called . Josh and I talked and Josh opened RFC #3680 , which proposed a keyword and closures. Reception, I would say, was mixed; yes, this is tackling a real problem, but there were lots of concerns on the approach. I summarized the key points here . Santiago implemented experimental support for (a variant of) RFC #3680 as part of the 2025H1 project goal . I authored a 2025H2 project goal proposing that we create an alternative RFC focused on higher-level use-cases which prompted Josh and I have to have a long and fruitful conversation in which he convinced me that this was not the right approach. We had a lang-team design meeting on 2025-08-27 in which I presented this survey and summary of the work done thus far . And then at the RustConf 2025 Unconf we had a big group discussion on the topic that I found very fruitful, as well as various follow-up conversations with smaller groups. That. my friends, is foreshadowing . Damn I’m good.  ↩︎ I described as a kind of “lightweight clone” but in the Unconf someone pointed out that “heavyweight copy” was probably a better description of what I was going for.  ↩︎ And, not coincidentally, the types where cloning leads to entanglement tend to also be the types where cloning is cheap.  ↩︎ and functional programming…  ↩︎ The “final” keyword was proposed by Josh Triplett in RFC 3678. It means that impls cannot change the definition of . There’s been some back-and-forth on whether it ought to be renamed or made more general or what have you; all I know is, I find it an incredibly useful concept for cases like this, where you want users to be able to opt-in to a method being available but not be able to change what it does. You can do this in other ways, they’re just weirder.  ↩︎

0 views
NorikiTech 1 months ago

A Rust web service

Part of the “ Rustober ” series. A web service in Rust is much more than it seems. Whereas in Go you can just and , in Rust you have to bring your own async runtime. I remember when async was very new in Rust, and apparently since then the project leadership never settled on a specific runtime. Now you can pick and choose (good), but also you have to pick and choose (not so good). In practical, real-world terms it means you use Tokio . And . And . And . And . I think that’s it? My default intention is to use as few dependencies as possible — preferably none. For Typingvania I wrote the engine from scratch in C and the only library I ship with the game is SDL . DDPub that runs this website only really needs Markdown support. However, with Rust I want to try something different and commit to the existing ecosystem. I expect there will be plenty of challenges as it is. So how does all of the above relate to each other and why is it necessary? In short, as far as I understand it now: Because all of the libraries are so integrated with each other, it enables (hopefully) a fairly ergonomic experience where I don’t have to painstakingly plug things together. They are all mature libraries tested in production, so even though it’s a lot of API surface, I consider it a worthwhile time investment: my project will work, and more likely than not I’ll have to work with them again if/when I get to write Rust in a commercial setting — or just work on more side projects. , being the source (all three of the others are subprojects) provides the runtime I mentioned above and defines how everything else on top of it works. provides the trait that defines how data flows around, with abstractions and utilities around it, but is protocol-agnostic. Here’s the trait: implements HTTP on top of ’s trait, but as a building block, not as a framework. is a framework for building web applications — an abstraction over and . Effectively, I will be using mostly facilities implemented using the other three libraries. Finally, is an asynchronous SQL toolkit using directly.

0 views
NorikiTech 1 months ago

Rust traits vs Swift protocols

Part of the “ Rustober ” series. As I said in the first post of the series , parts of Rust are remarkably similar to Swift, such as and . Let’s try to compare Rust traits to Swift protocols. I’m very new to Rust and I’m not aiming for completeness, so take it with a grain of salt. Looking at them both, Swift leans towards developer ergonomics (many things are implicit, less strict rules around what can be defined where) and Rust leans towards compile-time guarantees: there’s less flexibility but also less ambiguity. For example, in Swift you can add multiple protocol conformances at once, and the compiler will pick up any types that are named the same as associated types: And in Rust: Even this short example shows how flexible Swift is — and we haven’t even seen generics yet. I’m convinced Rust generics in traits are better done than in Swift partly because they are more granular. Whenever I tried to compose anything complicated out of Swift protocols, I always ran into problems either with “Self or associated type requirements” (when a protocol can only be used a generic constraint) or existential types. Here’s a real example where Swift couldn’t help me constrain an associated type on a protocol, so I had to leave it simply as an associated type without additional conformance. The idea is to have a service that would be able to swap between multiple instances of concrete providers, all conforming to several different types and ultimately descending (in the sense, not sense) from one common ancestor. Here’s similar code in Rust which does not have this problem: I’m looking forward to exploring the differences (and similarities) (and bashing my head on the wall) when I get to write some actual Rust code.

0 views
NorikiTech 1 months ago

Rust on Linux

Part of the “ Rustober ” series. What surpised me the most in the 2024 State of Rust Survey results was how many people were actually developing on Linux, and it clearly leading the chart for several years running. Truly, the year of Linux on the desktop. The presence of WSL implies that people could be developing in a Linux container, but the percentage is still too high. This is surprising to me, because over my career I’ve never worked in companies where Linux was viable to use by developers, simply because there were either Windows or macOS requirements most of time. It would be interesting to work at a shop that encouraged or allowed Linux. I’ll use this opportunity to drop a Linux hardware tier list I found on Reddit . I usually have an underpowered Linux nettop or laptop that I use for writing notes, but recent AMD chips look promising enough that I might be tempted.

0 views