2025 Recap: so many projects
Thanks to my sponsors: Romain Ruetschi, Scott Sanderson, Stephan Buys, Lawrence Bethlenfalvy, Nicolas Coulange, compwhizii, Chris Thackrey, Andy Gocke, Marcus Griep, Eugene Bulkin, Benjamin Röjder Delnavaz, Justin Ossevoort, The0x539, Gorazd Brumen, Julian Schmid, Chris Sims, Christian Bourjau, Zachary Myers, Cole Tobin, Jan-Stefan Janetzky and 263 more
I’ve been working on so many projects in 2025, I thought it was important for me to make a recap, if only just to clear my head.
There are many, many, many things to go through and we don’t have a sponsor today, so I’m gonna start right away with facet!
facet
facet is a project that I started working on in March of this year — that’s right, it’s only been ten months, yet it feels like an eternity.
In the beginning
I think the initial driving force for facet for me was: “I’m sick and tired of waiting for things in the Serde Cinematic Universe to compile.”
I’ve spent many, many moons looking for opportunities to make builds faster, I’ve written custom tooling for it…
I am in the top results every time I search for CI optimization, which is not a good place to be. Do not gaze into the abyss lest the abyss starts texting you at 4am asking if you’re up.
Basically, the idea was that serde is highly generic: Deriving the Serialize
or Deserialize trait for a Rust type already generates a bunch of code, but
you don’t really pay for it yet, because it’s generic code.
It’s only when you actually call serde_json’s from_str with your type that the
generic code gets instantiated and then rustc and LLVM do their best to optimize
all this and that can take a long time.
My first attempt at tackling this, merde, had serde-like traits but mine were dyn-compatible so that you could do dynamic dispatch instead of monomorphizing everything.
That’s what you call instantiating generic types with concrete types, and that’s what can generate a lot of code and make builds reaaal slow—
However, reading this today I realize I just made a shittier version of erased_serde, so… that was a waste of time.
facet is my second attempt, and it’s based on the realization that it’s much nicer to implement serialization on top of reflection than the other way around.
Instead of having these visitor patterns that drive serialization in serde, you get access to an associated const called “SHAPE” for every type that implements Facet.
That's it, that's the trait
In that shape you have information about what kind of type it is. Is it an enum? Is it a struct? You have information about which traits are implemented and how to call different methods.
You also have vtables for lists, maps, and sets, along with information about fields, their offsets, and their own shape.
The first golden age
From there, things kind of snowballed. It’s like, okay, we have the information,
we must have a nice API to read from existing values: I called that Peek —
that gives us serialization. But also we must be able to build values from
scratch using reflection.
And that one’s super tricky because you’re dealing with a partially initialized object. You’re dealing with states of like… if it’s an enum, have you selected a variant yet? Have you initialized some of the fields of the variant payload? what happens to those fields if you switch to a different variant now?
Borrowing is hard okay
It’s a minefield of undefined behavior, potential memory corruption etc. — I would never have embarked on this journey if it weren’t for miri, which catches a lot of undefined behaviour and some defined behaviour as well.
Sanitizers are like cats: capricious and temperamenta— uhh you can never have too many.
But the good news is once you’ve written that unsafe code you can write so much stuff on top of it.
Now, deriving Facet on a struct is just a bunch of statics, and maybe a bunch
of… I wanna say trampolines or adapter functions for the vtables.
And each format like facet-json, facet-yaml, facet-postcard, is just one crate
that works with every type that implements Facet. There’s no generics involved.
It’s all just reading the shape of types at runtime and acting accordingly.
Disappointment
Now, I knew this was gonna have a cost in terms of runtime performance, obviously, but I didn’t know exactly how much. Initial tests showed facet-json to be between five to seven times slower than serde_json.
That product has a very good name, but have you ever heard of flaky benchmarks? Yeahhh.
And I was okay with that, you know, it’s still the same order of magnitude. I was still hopeful that it would actually be faster to compile. You would gain in build times, you would gain in binary size, etcetera.
And then I measured build times and binary sizes, etcetera.
And it turns out that not only was it slower at runtime, it was also slower to compile and bigger in binary sizes.
So when I made the video of the announcement of facet, I was like, well, things are not exactly what I would like them to be right now, but I have to say the truth.
Like, you have to share the numbers, right? Otherwise what are we doing here? Knowing the situation is the first step towards improving it.
I'm not owned, I'm not owned, I repeat, as I go deeper and deeper into my side project that has come to consume my entire life.
But that kinda ate into my enthusiasm and for a while there I was just focused on other projects I think.
Refocus
And in October of 2025, someone started porting their codebase, big proprietary codebase, from serde to facet. And they encountered a million bugs, which I told them to report individually on the issue tracker. And so we had I think a couple weeks of back and forth, like, oh here are these four new issues and me fixing them as fast as I could.
And one of the issues was build times. Build times got worse switching over to facet. Part of the reason is that facet generates a lot of code. And part of the reason is that it’s really hard to completely switch away from serde and syn and other crates like that because they are so prevalent. So you might still be paying for them somewhere else.
Maybe tracing pulls them in. Maybe you have a derive macro somewhere. Maybe some crate uses serde_json internally just to have a value type. So now facet, which isn’t free, is on top of the ecosystem you were trying to move away from.
And it’s during those two weeks that I decided, you know what, we’re not gonna try to be the smallest, fastest to compile, fastest at runtime crate. We’re just gonna try to be the nicest, in terms of developer experience (DX).
I started focusing on adding features that would be nice to have, like best of class error reporting for parsing errors using miette, streaming deserializing from AsyncRead, etc.
I started working on facet-solver, which gives you the best error messages for untagged enums and flattened structs and everything, things that are really hard to resolve. You need a complete view of the shape of the types you’re trying to deserialize and you need to know everything that happened up to this point.
There's a bit of redundancy here that happened when we improved the display implementation. I'll fix it next year.
And that’s exactly what facet-solver does, and it does it for all the format crates! Not just for JSON!
Because we have so many format crates, we have JSON, YAML, TOML, Postcard, MsgPack, XML, KDL, but also now SVG, HTML, CSV, XDR, query strings, command line arguments, ASN.1 — features tended to drift between crates. Like you implement something about untagged enums in JSON and suddenly YAML doesn’t support it, so you have to fix all crates individually.
You think it's fun maintaining all this? Well it... kind of is actually.
To combat this, I introduced facet-format, which is the successor of facet-serialize and facet-deserialize, and it’s the basis that all format crates are today based on. Yes, that meant rewriting all the format crates and then renaming the old crates to legacy and the new crates back to the old names and then deleting a hundred thousand lines of code in one PR.
It was glorious.
(And there’s probably a hundred regressions I haven’t found yet, despite careful planning and execution.)
Volte-face
And in the middle of like adding a lot of features to facet to make it the nicest thing ever, I decided you know what? No, it was supposed to be lighter. It’s bullshit that it generates more code than serde and that it’s actually slower to compile. I don’t like that.
So I started working on reducing the amount of code generated: There’s a handful of tools you can use to do that. One of them is cargo-llvm-lines, but you can also use the “-Zmacro-stats” unstable rustc flag, and of course you can do rustc self-profiling.
And using those I was able to get to a point where a bloat benchmark using generated struct and enum types was actually faster to compile with facet than with serde.
That was a couple months ago, but I checked just before writing this, and apart from a couple dependencies that slipped in by mistake, things are pretty even still.
More importantly, there is now tooling to compare facet against its past self in terms of lines of LLVM intermediate representation generated, compile times, compile size, etcetera. I made a little text user interface using ratatui to look at all the records which are tracked in the repository :)
The measurements are done on a synthetic code base, but I’ve gotten reports that it does translate to similar results on real-world codebases. It is a moving target though and things evolve as we add more features.
Just-in-time for the new year
Another thing I said in my “trying to be optimistic” announcement of facet initially was like, well, okay, it’s.. it’s slower at runtime, but maybe we can do JIT, just in time compilation, and then it might even be faster!
And for a long time I kept using that as a kind of shield of like, yeah, it’s slow, but that’s because we haven’t tried doing the real thing yet. And I felt like it was dishonest and I was tired of using it as an excuse and I decided to just make it happen, with cranelift.
Well, when you say it like that, it doesn't sound too complicated.
And at first I made what I’m calling tier one JIT that all formats can benefit from. It’s: instead of assigning fields etc. via reflection, you just generate code that does it directly for you.
And you get some decent performance benefits just from doing that, but it gets even better if you go to tier-two JIT, which is the format crate, like facet-json, knows how to parse JSON, so you just emit instructions on how to parse JSON and on how to assign fields like construct those types from the source material at the same time, in the same code.
And then that gives you a lot more performance. There are caveats of course,
We don’t get the auto-vectorization that LLVM can do. We don’t get to inline
calls into the standard library, so we have to be smart and do things like use
staging buffers and then do Vec::from_raw_parts, from hash maps you can build a
slice of key/value pairs and then build the map in one shot with from_iter.
There’s a ton of little tricks that went into this, but as of today I’m happy to report that if you’re okay with somehow depending on cranelift at runtime, with having code generated that is nigh-impossible to debug, might contain undefined behavior and crash your program and whatnot, you can now beat serde while staying in the facet ecosystem, at least for JSON.
I know, because I made a performance dashboard that tracks facet-json versus serde_json using divan for benchmarking time and gungraun for benchmarking instructions.
I would not necessarily trust these results. I think we need to verify them. 0.03x seems suspiciously good — there might be some zero-copy apples-to-oranges shenanigans going on there.
It’s not on there, but I also enabled it for facet-postcard just for fun, and it is indeed faster than the reference postcard implementation. That said, should you use it, like I said, there’s a bunch of caveats.
For me, I don’t mind. I think it’s funny. I think most programs would be like, we can take the performance hit of reflection, or like, we’ll just stay using serde, or a thing I haven’t explored yet, but I know would work is doing codegen from facet information.
You would depend on a “types” crate as a build dependency and generate serialization and deserialization code from a build script, much like the derived macros of serde does. That’s an option I’ve only really explored for one specific use case that I’m getting to.
But enough about facet.
arborium
In the meantime, I’m working on lots of Rust crates. I’m working on lots of different formats. I notice in my Rust docs that blocks of KDL or TOML or whatever are not highlighted on docs.rs, and that makes me very sad.
Meanwhile, I’m also working on some private proprietary projects that I’m not going to talk about, and I need to do syntax highlighting using tree-sitter, which is great, but I always have to go chase grammars that work, and I always run into problems compiling them to Wasm with a Rust toolchain.
There’s a bunch of linker hacks, I’m trying out Buck2, which doesn’t help things at all. It’s a nightmare.
I decided to start working on the definitive Rust distribution of tree-sitter and tree-sitter grammars called arborium. So I go and find 96 grammars, I figure out a good API, I make sure that they all have syntax highlighting queries, I bundle a bunch of themes, I make a nice little landing page for it.
And it doubles as a history lesson!
I make sure they’re able to compile to WebAssembly by faking all the C functions they claim they need. And I spend forever automating CI so that I can actually release updates to the grammars and the themes and the crates. I make sure that license information and attribution is still there.
I get in touch with the crates.io team saying, I’m sorry, I’m going to be publishing 100 crates tomorrow. Is that okay? And they’re like, yeah, you’re already on the list of people who can do that stuff which means, somehow you’ve done weird things before.
And tada, arborium. I’m super, super happy about this release. It’s already useful to a bunch of people. I hope it becomes useful to a bunch more people in the future. But for now, it’s just so satisfying to comprehensively solve a problem and just never have to think about it again.
I say that as knowing that still, in the back of my brain somewhere, I’m like wouldn’t it be nice to have a pure Rust version of tree-sitter instead of having generated parsers in C and a C core and everything?
It might, it would also be slower to compile. It would also be like a lot of work. It might be reasonable if you’re willing to give up some performance and give up all the incrementality of tree-sitter, then maybe. But I don’t need that, so I’m not doing it.
dodeca
Meanwhile, I’m working on facet. I decide we need a proper website with proper documentation on there. What are the options for a static website? Zola. Everybody loves Zola. It’s Rust, it’s our Émile Zola to their Victor Hugo.
Well, I have very very strong opinions when it comes to making websites, both the experience of making the website and what the results should look like.
So you become aggravated with little things, little paper cuts, little developer experience problems. And the fact that it’s harder than it should be to just make a plugin, right? I’m just looking around for an SSG that will just let me make plugins. And in Rust, that does not exist.
In other languages, sure. In JavaScript just fucking eval it straight into my veins. In Ruby, require it, in Python, good luck with all those paths. But in Rust, nope! Largely, nope.
That discussion has actually been moved to discourse, but then it died a year later. Because what are you going to do, honestly? Make an entire RPC system just for this?
And I’m reminded that if I somehow forked it and added everything I wanted, I’m reminded that even if I did all of that, then it still would be pretty average at caching.
Just like pretty much every static site generator, it would cache things that are stale, and it would fail to cache things that it should be (by using conservative HTTP headers). And that just makes me aggravated. So I decided to make my own, named dodeca after the dodecahedron, a nice shape.
And you know, how hard can it be? It’s “just” turning Markdown into HTML.
That's the attitude that got me through the whole year.
That part’s easy, pulldown-cmark, want syntax highlighting? I just made arborium. Super. I want minification for HTML, JavaScript and CSS built-in, there are crates for all that. I want cache-busting, of course of course — now I need to rewrite HTML so link, script and img tags point to cache-busted URLs.
I want image processing built-in: PNGs go in, JPEG-XL, AVIF and WebP are served to browsers — you better believe we’ve got either pure Rust implementations or wrappers for the original C/C++ implementations.
The phrasing in the first post is so off lmao, I would welcome... a house with a pool right now!
And at some point I’m 1200 dependencies in…
And iterating becomes really painful.
I’m reminded of the time I made rubicon to enable dynamic linking even if you’re using crates with thread-local statics like tokio, tracing, etc.
I'm still really fond of this logo by Misia.
But I don’t want to have anything to do with dynamic linking anymore.
So what’s the next best thing? IPC.
rapace
IPC is just RPC at home, so I named my thing rapace, which is RPC with extra letters, and is French for “bird of prey”.
And I have again, strong opinions about how to do RPC. I’ve been doing it for a while. There are things that I like and things that I don’t like. For example, gRPC, I don’t like. Mostly because of protobufs, which have all the downsides of Go with none of the charm.
And I figured I have all these different patterns that I want. First off, to make iteration easier, I’m going to have the central app, the hub, be its own binary. And every cell around it be its own binary. And then you put like HTML modification in one cell.
You put image compression in one cell. You put even HTTP serving in one cell. Like text user interface in one cell. Everything is a cell. I have 18 cells right now in dodeca.
And yeah, I want to do this over shared memory because I’m giving up on dynamic linking, but I’m not giving up on performance, you know. If you have to compress a large image, it’s nice if you don’t actually have to make several copies of it over the RPC system.
So to do this right, you have to set up your shared memory as kind of an allocator buffer pool. You have to keep track of which buffer is owned by whom.
There's a couple of unsafe implementations for Send and Sync just out of frame. Spooky stuff.
When sending the uncompressed image payload, like, all the pixels, between two IPC peers, you can do that in a zero-copy fashion if you treat it as a reference or a handle to the allocated memory (and if you used a special memory allocator, which takes from the shared memory area).
I’m not exactly there yet, but I have eliminated quite a few copies and I’m doing zero copy deserialization, where you get the frame back and then you deserialize borrowing from the frame and then you just carry the frame and the deserialized payload together, like you would do with the yoke crate, but without using the yoke crate, because it relies on syn!
Obviously, rapace uses facet-postcard for serialization and deserialization and that makes it super easy to discover services dynamically and use them, call their endpoints without even knowing about them at compile time. So you can make dashboards that explore services, and there is one in the examples of rapace.
So I did this whole complicated shared memory design. But, you know, once you have RPC semantics, it’s tempting to try other transports. For example, why shouldn’t I use that to have the dodeca dev tools talk to the dodeca dev server to get things like, you know, hot module replacement, except instead of modules, it’s paragraphs of markdown getting rendered to HTML.
Dodeca DevTools are a thing, by the way, but they're in the middle of being rewritten, so they only do live reload for now instead of inspecting the template expansion environment.
Why shouldn’t I use the same thing for different services on my Kubernetes cluster to talk to each other? And so next thing you know, you have a shared memory transport, of course, but also a WebSocket transport, a generic stream transport, an in-memory transport for testing, of course.
Again, there was kind of a rapid growth era where I just added rapace to everything I could think of, and it worked more or less, and then I started thinking about implementations for other languages and I figured okay I need a proper specification. Enough with just winging it.
tracey
And that reminded me of something that James, my podcast co-host on self-directed research, taught me about this year, which is traceability. You want to have a specification and you want to have an implementation and you want to have links between the two. You want to have like every requirement in the specification has a unique identifier.
And then you annotate your code to say this implements that requirement.
And then you cross-reference it. You can go through the entire spec and see if everything is implemented in the code.
And you can do the reverse too! You can go through the code and see if all the code is covered by requirements.
And you know what, this is very much in the spirit of my year 2025. But I remember James saying there’s no great tooling for it. And I didn’t even check. I just went and made my own immediately, which I called tracey, mostly so that people who are looking for the Tracy profiler get confused and use my software instead.
And you know, now I have a great interface to let me know that ther apace implementation is very far from being spec compliant. And also to tell me that the tracey application itself is very far from being fully specified.
I have added support for the tracey requirement syntax to dodeca so that you can embed the specification on your website and just have it be clickable and refer to it, which means you can have IDE tooling that refer to a spec requirement and that links directly to the website.
picante
Speaking of dodeca, another very important part of it is the query system. I’ve been obsessed with salsa ever since I’ve learned about it — basically, when you write something like a compiler, you have a bunch of inputs and you have a bunch of queries which can read from those inputs or from other queries, which themselves can… you get the idea.
Snacking on the shoulders of giants
And the goal is super simple: be as lazy as possible. Don’t recompute a query unless anything that goes into it, one of the inputs to it, has actually changed. And of course, only evaluate the queries that you actually need, that someone actually requested the result of.
salsa is used in rust-analyzer and the laziness is a blessing and a curse. They had to implement pre-warming because if you don’t query anything, it’s not doing anything. So the first time you ask for a completion, it’s like, whoa, buddy, I have to analyze this entire code base for the first time ever.
As of fairly recently, salsa is able to save a cache to disk, which can be, again, a blessing and a curse, because what if the cache is huge and now loading it from disk is extremely costly as well.
Because I wanted perfect caching in dodeca, I started using salsa! But I fairly quickly ran into the problem that most of my operations are asynchronous. Even just compressing an image is making an async RPC over shared memory to a different cell. Therefore, salsa doesn’t work for me because all the queries are supposed to be synchronous.
And that is how I started working on picante, which is not a fork or anything. It’s just like the same ideas as salsa, but async first.
I had to make a bunch of different choices there. It uses facet for everything,
of course, including equality comparison, even if your types don’t implement
PartialEq, it just does structural equality, which is nice.
I did also implement persisting the cache to disk and even incrementally persisting the cache to disk, so you don’t have a big save phase at the end.
I’m sure there’s still a lot of bugs in picante, but overall it’s been giving me what I wanted: track absolutely everything. In dodeca, there’s very little difference between the production build of a website and the development build of a website. The main difference is that we inject a script tag for DevTools.
But we do minification of JavaScript, HTML, and CSS by default. We do, of course, image compression on the fly, depending on what you request, which is to say, depending on what your browser supports…
…and we do something that I’ve always dreamed of doing: codepoint-accurate font subsetting, which requires parsing styles and interpreting them and collecting Unicode sets, codepoint sets from all the pages on your website.
So there’s a query for like rendering all the markdown to HTML. There’s a query for extracting all the code points from HTML and putting them into one set. There’s a query for taking the uncompressed font and the set of all the code points for a certain style together and doing the font subsetting.
And it’s all lazy. Like it only recalculates when you use a character you’ve never used before. Like suddenly you paste in something that has Unicode box drawing characters and yeah it needs to add them from the original font.
This is how I imagine the people in my head poking holes in my articles as I write them.
But Amos, isn’t font-subsetting expensive? You need to shell out to Python to use pyftsubset. No, it’s not, because I released woofwoof, which is just a build of the WOFF2 C++ implementation. But I packaged it nicely. I made sure that it builds in CI for Linux, Mac and Windows.
And I released fontcull, which is a Rust version of what was my favorite tool for that up until that point called… glyphhanger. The problem is that glyphhanger hadn’t been updated in five years and was using a very, very old version of Playwright, old enough that it couldn’t download browsers anymore. And I just decided, fuck it, let’s do it all in Rust. We have the crates.
Or do we? Because I’ve been keeping an eye on this for a while, and for font-subsetting, there was only something that worked for PDF, which doesn’t need the full font information. So it couldn’t be used to subset fonts and then use them in browsers. But I also knew that some people at Google were working on a bunch of Rust crates around fonts called fontations.
And the good news is that this year they came close enough that you can use their crates to subset fonts and use them in browsers. And I know that because I vendored their code straight from Git, they’re not released on crates.io yet, except as part of fontcull.
And the result is that if you go on facet.rs, which does use dodeca for the docs, and you look in the network tab and you filter by fonts, you will see that the Iosevka font being served is 10 kilobytes down from the original 2MB (which includes NerdFonts etc.)
pikru
Speaking of dodeca, since I knew I was going to use it for specifications and technical documentation, I wanted to have a way to make diagrams, but I didn’t want to use Mermaid because I don’t like client-side rendering. It’s bad for page load time, it’s bad for accessibility, it’s bad for page shift.
I just hate it.
I looked around for alternatives, I found D2 which is made in Go, but it’s made in Go. I found Typst which I bundled for a while to make Open Graph previews for pages automatically, but dear lord it was my heaviest dependency by far.
Eventually someone pointed me to pikchr, a diagramming solution that I didn’t know at all, and that had a completely self-contained C implementation, a very good candidate for a Rust port. The thing is I didn’t feel like porting it myself. So I essentially set off Claude to port it by giving it the tools to compare a hundred test cases, the rendering between the C implementation and the Rust implementation.
Some of these are no joke.
I had to generate a comparison HTML to show test coverage, and for each test, a visual comparison side-by-side, onion skin, etc. That’s for me, that’s for humans. And then for it, I made it make for itself an MCP that runs a single test and then renders two SVGs to PNG because those models have vision capabilities.
So sometimes it’s able to look at the thing and go, I see that the lines are in the wrong place. Whereas if it were to compare SVG, it would just be drowned in all the markup. Speaking of comparing SVG, the Rust implementation also uses facet-svg, which is just a bunch of types defined on top of facet-xml, which I made just for this.
And I worked on a bunch of tree diffing algorithms just to produce diffs good enough so that the agent was able to tell, oh, this is what’s wrong with the rendering.
Eventually the Rust port reached 100% parity with the C implementation — not test coverage, actual output parity — all the tests passed. I published the comparison HTML on GitHub Pages and named my implementation pikru.
aasvg-rs
It’s at that point that I discovered that actually Claude and GPT both suck at writing PIK diagrams. So unless I make the diagrams myself, which I don’t really want to (there’s not a lot of auto layout going on in PIK), I decided to port another diagramming solution.
I didn’t know that something like svgbob existed (and already has a Rust crate…). Instead, my research found aasvg, which is based on a client-side markdown implementation called markdeep.
Before...
And after!
My port is called aasvg-rs, unimaginatively, And it also has parity with the original and it’s doing that nice thing that I did in both my ports where it’s using CSS variables to get light-dark support in SVG.
Little caveat, this is absolutely not supported by Safari Mobile, which is the current Internet Explorer.
facet keeps growing
More things have happened in the facet ecosystem that I haven’t really talked about. As I was writing this, I worked on the facet benchmarks, and I was like, oh, the JavaScript code for the benchmark browser view keeps getting out of sync with the format generated by the benchmarks.
If only we could use TypeScript types to validate the frontend, and if only we could use json-schema to make sure that the backend is producing what we think it is, and if the source of truth was just a bunch of Rust types, Wouldn’t that be fantastic?
Well, obviously, there’s existing solutions, existing derived macros like schemars, But at some point, I promise that Facet would be the last derive macro you’d need. And in this case, it works! I quickly threw together facet-typescript and facet-json-schema, which made iterating on the benchmark dashboard a lot easier.
Knock knock! Who is it? Room! Room who? Room for improvement.
Other things I was interested in included an alternative to thiserror, but based on facet, or something like displaydoc or derive for the miette crate, so you can implement diagnostic without either doing a manual implementation or using something that depends, again, on syn.
And the problem with all these is that you actually have to act like a macro.
You have to generate additional code. It’s not just, oh, you derived Facet,
so you can do whatever you want at runtime. You have to implement Error.
You have to implement Display. You have to implement Diagnostic. Therefore, you
have to generate code. Therefore, we needed to come up with some sort of plug-in
system for facet. something that would reuse the result of parsing your type
definitions that facet macros already does, but that is able to use templates to
generate different implementations. And we did exactly just that.
It’s not entirely final and the templates are pretty simple for now, but it works. Like, you don’t need another derive macro, you don’t need syn. I have no idea what the performance is like. I haven’t actually measured build times on any of this, but the idea is pretty simple.
We shouldn’t need too many of these because there’s a finite amount of traits that you really want to implement: I barely bother implementing Debug anymore, because if I want to see what’s inside a type, I just use facet-pretty: Every trait where performance is not paramount and that can be re-implemented just using reflection is savings in terms of code gen, build times, final binary size, etc.
The Debug trait especially; even if you’re not planning on using facet, look
at how much code debug is generating for large structs etc. It can make up a
solid chunk of your binary.
fs-kitty
One very cool application of rapace that I made recently is fs-kitty which has to do with virtual file systems.
The SSD that’s on your laptop, has a real file system on it. Maybe it’s ext4, maybe it’s btrfs, maybe it’s ZFS if you’re the good kind of nerd. Or, you know, APFS or NTFS, whatever.
Sometimes you want to access files over the network and then you would use something like Samba or NFS. And sometimes you just want to kind of make up files like pretend you mounted a zip file for example. And for that you need a VFS, a virtual file system.
On Linux, if you want to make a virtual file system, you can use FUSE, which means file system in user space. And on Mac, you could make kernel extensions but of course anything that runs in the kernel must never crash, otherwise everything crashes because monolithic kernels won the war.
Therefore, Apple has been trying to kill kernel extensions for as long as they’ve been a thing, and they’ve introduced things piecemeal to kind of let companies like Dropbox have their virtual file system without touching the kernel as much.
The last piece being FSKit, which lets you implement the file system entirely in user space, communicating to the kernel over XPC, another form of RPC. Which is great, except you have to package it up as a file system extension, as an .appex bundle, which registers in system settings when you open the associated regular .app bundle: It’s not really designed for command line tools.
But I saw some people made something called FSKitBridge, which adds another layer of RPC (they used protobuf). That way you can implement your file system in any language. And their file system extension connects to your binary over TCP. And, you know, it’s a little layer cake of RPC that works.
And you don’t have to worry about the terrible Apple requirements. You don’t have to ship an app yourself or sign it. But it’s an extension that, I don’t know, I just didn’t trust these guys to install a file system extension from them, even though it’s fully in userspace. I don’t know, it felt wrong.
So I made my own, fs-kitty, but using rapace for RPC.
So all the app itself and most of the app extension is in Swift. And initially, I just compiled a bit of Rust code and linked it, called it from Swift using swift-bridge, which does support async.
And eventually I thought, you know, I’m getting hangs, I’m getting crashes. Wouldn’t it be easier to just implement everything in Swift? That’s when I started writing rapace implementations in other languages, just like, you know, Rust on the front end is fine. But sometimes I just want to do Svelte and TypeScript and be done with it. Wouldn’t it be nice to just have like a native TypeScript implementation of the rapace protocol?
As I’m writing this, it’s at a stage where… it used to work at some point, and then I started reworking all the dependencies. Therefore, it’s broken right now, but it’s going to work again in the future, let me tell you.
Because I need it for the project that is probably the most exciting to me of 2025, and it’s going to carry on into 2026.
vixen
Porting my entire monorepo from cargo to buck2 was an eye-opening experience. It’s not just me. Cargo is pretty bad at caching, at least right now. Things are always being improved, but the fact of the matter is it is simply not designed like a proper build system should be.
“Proper” build system is a loaded term, but I have a very specific idea of what it should be. So it’s a personal take on this.
On my monorepo a cold build with cargo is 35 seconds, on buck2 it’s 25 seconds. A no-up build is almost a second with cargo and 0.06 seconds with buck2. As for changing a single line in a function deep in my dependency tree, it’s 21 seconds under cargo and 8.5 seconds in buck2.
The numbers speak for themselves.
However, it is a major pain in the ass to maintain BUCK files for all of your crates, especially if you have like a hundred of them like me, plus maintain fix-ups for all your dependencies.
If only there was a build tool that had the properties of buck2 with the ergonomics of cargo. If only you didn’t have to use a separate tool to generate build files for your dependencies. If only, if only, if only, if only it was designed from scratch to be friendly to the Rust ecosystem.
But not only, because you do need to own C/C++ compilation. And while you’re at it, why not also own things like JavaScript bundling, any sort of manipulation you want to do on assets, container image construction? Why not?
And then if you’re doing this, why not do it with continuous integration in mind, of course, because that’s where caching matters most. I look at workflows that takes 1.5 minutes for 10 seconds’ worth of compilation and I become the joker.
I know, I know—I’m complaining, people have hour-long CI pipelines. I don’t care, my pipelines should take seconds because that’s how much CPU time I know is necessary to prove that the change is valid.
I have great plans for this. It is a hugely ambitious project. It requires hubris, which I have again, now that we’ve found the right meds, It is absolutely not capable of building anything except the most trivial hello world right now. But I’m going for it. I don’t know what else to tell you.
I genuinely believe it is possible to get the best of both worlds. The most likely outcome is that I just burned out and never make anything useful with it. But damn it, I’m going to keep trying because I’ve been doing hacks around CI build times for a long time.
It's always a good sign when a bug only occurs 25% of the time. It's what you want.
I have a CLI tool called timelord that saves and restores timestamps to try to make cargo stop rebuilding things. And it’s broken because of nanosecond resolution problems on certain environments. I’ve reached a point of I’m just not even going to bother anymore. I’m just going to make my own build tool.
I don’t need anyone else telling me that it’s stupid and that I’m never going to make it. I need people to get excited and make their own. Like, why should I be the only one trying?
Also, I have no illusion, no intention of replacing cargo: cargo is going to be there for as long as Rust is. It has the constraint of having to work for absolutely everyone on absolutely every platform and strong backwards compatibility. I get it.
This is why it’s exciting to be able to do a clean implementation of something like, let’s take all these ideas and properties that are nice and try to put them all together.
It’s a bit like cooking! I like it!
I’m building it with remote execution and a content-addressable store day one, (which is why it’s hard to get even the most trivial builds to run, because there’s so many moving parts).
But that means you’re suddenly not worried about target directories. You’re suddenly not worried about.. if you rebuild with or without a cargo feature enabled, is it going to overwrite part of the target directory and cause rebuilds?
J'ai donné un talk au Meetup Rust de Lyon en décembre et on s'est bien amusé.
And if your CI is running on the same platform that you are, then by the time your changes reach CI, it’s a no-op. Because you’ve already done it. In fact, the entire CI pipeline, you can just run “locally”, as your local vixen command line dispatches tasks to remote executors.
You don’t need several different jobs defined in YAML. You don’t need to temporarily upload artifacts to an artifact store and then download them in the next stage because everything is just in the content addressable store. Every executor has its own memory and disk cache.
You don’t need to worry about dividing builds into several CI jobs so they run in parallel because it’s all part of the same build graph, and the orchestrator will build as much as possible in parallel.
Fan out... inhales ...and fan in.
Of course, for that to work, you need to have your build be hermetic for real. For example, for C/C++, I’m grabbing toolchains from Zig. For rustc, I’m actually downloading toolchains directly from static.rust-lang.org. I’m not going through rustup at all.
For example, with cargo, if you build on the rustup stable channel or the
rustup 1.92 channel, even though they’re exactly the same toolchains, it’s
going to rebuild because the path changed.
That’s not a thing if you have true hermeticity because the toolchain is mounted somewhere, it’s accessible through the virtual file system. And if it has the same hash, it has the same hash. It’s part of the inputs. Inputs didn’t change. No need to rebuild!
(For the last paragraph to work you need to read it out loud in the voice of Sil from The Sopranos. This is not a script note I forgot to remove, it is a note for you, the reader).
The content addressable store is not just a cute gimmick, it’s also: “Oh, you want to have the last 16 Rust toolchains?” Okay, there’s a lot of deduplication you can do in there. They don’t rewrite the standard library every time, you know. There’s LLVM tools that don’t change every release. There’s a lot of things that can be reused.
Ah and also they’re now forever in a server that’s close to you, stored as individual entries that can be streamed quickly and concurrently, as opposed to a tarball you have to decompress sequentially.
For build scripts, with buck2, you have to pretty much either patch them out because they’re doing something naughty, or you can run them if it’s fine. Like if they don’t reach for the network or something, you have to make sure that their inputs are in there. You have to manually declare them.
There’s a lot of things in buck2 that you have to explicitly specify. And I don’t like that. Yes, the build actions should be hermetic. You should know exactly their inputs and outputs. But also sometimes you can just infer that.
Me when I think about writing BUCK files and repeating stuff the build system could 100% discover on its own.
Looking at a build script, if the build script is only calling the cc crate, I’m going to patch the cc crate. I don’t care. I’m going to substitute a version of the cc crate that does not actually build, but instead creates actions to be dispatched by the orchestrator later. That just makes sense to me.
There are so many things that, so many patterns in Rust crates that we recognize that a build system could know about, if it cared to look. And there’s always going to be the odd crate out, like sqlx or rustls, that needs special treatment (sqlx for network access, rustls for assembly),
And instead of relying on someone maintaining a GitHub repo of all the fixups, maybe there’s a package manager built into the freaking build system. Maybe, you know, maybe you can just make life comfortable for yourself. What a novel idea.
Duel licensed? EN GARDE!
And of course, having dependency tracking that’s rigorous to the point where you get perfect caching is extremely useful. Because then you get to see the build graph. You get to debug why something rebuilt, which is something buck2 does well with its explain command — I wanna steal all of that, and again expand it beyond just the build but to like… CI, even deployment, why not?
I’m definitely at the “everything looks like a nail” stage of this, but my hammer is fucking awesome.
conclusion
Conclusion? I had a ton of fun this year. I’m going to have even more next year. I’m at that stage where I’m dogfooding like there’s no tomorrow. Every one of my crates uses another one of my crates.
And it is absolutely great because I get the developer experience that I want. Unless things broke. And then it’s on me to go fix it.
But you know, one of my favorite things to say is, I hope it’s my fault that this broke, because if it’s my fault I know I can go in and fix it. But if it’s someone else’s fault, who knows how long until they fix it.
All the shit that I’ve talked about here is open source, under the bearcove github org — which means you are free to go and play with it all. Don’t expect much stability except for facet: generally if something becomes usable, I’m going to start making noise about it. I’m going to have an official announcement on my blog. I’m going to make a video about it.
If I haven’t yet, there’s a reason.
I’m also happy with what I did regarding videos. This year I worked with two different video editors, Sekun and Vlad, thanks to them for helping me along this journey, there’s more coming next year since videos pay for themselves with sponsorship.
Thanks also to AWS for a large donation towards the development of Facet, to Depot for all the CI build minutes, to Zed for the free credits. If you’re a company who wants to help sponsor some of these development efforts, definitely reach out, my email is on my website’s about page.
I’m looking forward to next year, which is not the way I felt every year for the past 10 years. You know, it’s nice when things are good. I hope things are good for you too.
Take care, and I’ll see you next y-I’ll see you very soon.
Did you know I also make videos? Check them out on PeerTube and also YouTube!
Here's another article just for you:
The bottom emoji breaks rust-analyzer
Some bugs are merely fun. Others are simply delicious!
Today’s pick is the latter.
Reproducing the issue, part 1
(It may be tempting to skip that section, but reproducing an issue is an important part of figuring it out, so.)
I’ve never used Emacs before, so let’s install it. I do most of my computing on an era-appropriate Ubuntu, today it’s Ubuntu 22.10, so I just need to: