Posts in Zig (20 found)
Dayvster 5 days ago

Why Bun leaving Zig is Great for Zig

Why Anthropics million line AI rewrite is a massive gamble for Bun, a marketing stunt for Claude, and a quiet win for Zig.

0 views

Async I/O in Zig 0.16, today

Zig 0.16 shipped last month with , a cross-platform interface for I/O and concurrency. This is a big step for the ecosystem. Libraries can now be written against a standard I/O abstraction, independent of the runtime, and application developers can plug in whatever implementation they want. The only usable implementation shipped with 0.16 is , which uses a thread pool. When you spawn concurrent tasks, it creates OS threads to run them. Let’s see how it works with a simple example: This spawns 10,000 concurrent tasks, each sleeping for 10 seconds. On my machine, it completes in about 20 seconds: The overhead comes from spawning OS threads. If you try increasing this to 50,000 tasks, it will likely fail on most systems due to thread limits ( on Linux). This isn’t just an arbitrary benchmark. Asynchronous I/O exists to solve a real problem: network servers with many connected clients. You don’t want to spawn an OS thread for every client connection. That’s why we have event loops, coroutines, and async I/O. There is in the standard library, which is meant to use io_uring on Linux and kqueue on BSD/macOS. It’s still a work in progress though, missing many functions and doesn’t currently compile. I’ve written about zio before , and I’ve just released version 0.11 with a full implementation. It uses stackful coroutines and asynchronous OS-level I/O APIs (io_uring or epoll on Linux, kqueue on BSD/macOS, IOCP on Windows). Here’s the same example using zio: The code is almost identical. You just initialize a zio runtime and use its method to get the interface. With zio, the same 10,000 tasks complete in about 10 seconds: That’s the expected time, since all tasks run truly concurrently. You can increase this to 50,000 or more tasks and it will continue to work, limited only by available memory. You can use this instance for anything you’d use for. To write an HTTP server with , for example, just pass zio’s and it will work the same way. If you want to use async I/O in Zig 0.16 with the standard APIs, you don’t need to wait for to be ready. Zio’s implementation is still new, so if you hit any problems, please reach out on GitHub and I’ll be happy to help.

0 views
matklad 3 weeks ago

Steering Zig Fmt

Two tips on using effectively. Read this if you are writing Zig, or if you are implementing a code formatter. For me, is better than any other formatter I used: , the one in IntelliJ, . is steerable. For every syntactic construct, it has several variations for how it might be laid out. The variation used is selected by looking at what’s currently in a file. Easier to show a pair of examples: Depending on the trailing comma, function call is formatted on a single line, or with one argument per line. The way this plays out in practice is that you decide how you want to lay out the code, add a couple of , hit the reformat shortcut ( , p is mine ), and does the rest. For me, this works better than the alternative of the formatter guessing. 90% of great formatting are blank lines between logical blocks and tasteful choice of intermediate variables, so you might as well lean into key choices, rather than eliminate them. I know of one non-trivial formatting customization point: columnar layout for arrays: One would think that trailing comma would lead to a number-per-line layout, but, for arrays, also takes note of the first line break. In this case, the line break comes after the first three items, so we get three numbers per line, aligned: How cool is that! Furthermore, with judicious use of ( array concatenation ), you can vary the number of items per line. When I need to pass pairs to subprocess, I often go for formatting like this:

0 views
Anton Zhiyanov 3 weeks ago

Solod v0.1: Go ergonomics, practical stdlib, native C interop

Solod ( So ) is a system-level language with Go syntax and zero runtime. It's designed for two main audiences: The initial version (let's call it v0) was focused on picking a subset of Go and translating it to C. The next logical step was to port Go's standard library and make it easier to interop with C. That's what the v0.1 release I'm presenting today is all about. Standard library • SQLite bindings • Persistent map • Store and retrieve • Command-line interface • Performance • Wrapping up Solod v0.1 ships with the following stdlib packages ported from Go: And a couple of its own packages: Stdlib documentation In the following sections, I'll demonstrate some of the v0.1 features using a simple example: a persistent key-value store backed by SQLite. Since So doesn't provide yet, we'll call SQLite directly through its C API. To do this, let's import the necessary headers with the directive and generate extern declarations using the sobind tool: The directive is required for constants ( ) and types ( ). As for functions ( ), we can just declare them without a body — the transpiler will treat them as extern declarations even without . With the SQLite API in place, let's implement a key-value type that wraps the database connection: Add a constructor that connects to an SQLite database and creates a table to store the items: As you can see, this So code looks a lot like regular Go code. However, there are some key differences: First, let's implement the method: No surprises here, just a bunch of SQLite API calls. The method is more interesting: The pointer returned by is managed by SQLite. It becomes invalid after calling (which does before returning). Because of this, we need to allocate a copy of the returned value, using in this case. So's approach to memory allocation is similar to Zig's — all heap allocations must be done explicitly by providing a specific instance of the interface. The caller, of course, must free the allocated string: Here, is a specific allocator that uses libc's and . Alternatively, we could use or any other implementation of the interface: With the type in place, let's create a simple CLI using the package: Then add command routing: Again, no surprises here — the package works just as it does in Go. Solod isn't trying to outperform hand-tuned C. Still, performance matters: the code is benchmarked and optimized to run reasonably fast. Since So compiles to plain C and then to native code with full optimizations, the results are sometimes better than Go's. Here are some highlights from the benchmarks: There're no GC pauses and no Cgo bridge cost when calling C libraries. The tradeoff is that you have to handle memory yourself, but as the SQLite example above shows, So's allocator interface makes that pretty manageable. Solod vs. Go benchmarks Solod is still in its early days, but with the v0.1 release, it's ready for hobby projects. The already-ported parts of the Go standard library make it easy to write command-line tools (check out the , , , and examples ). Plus, with native C interop, you can build just about anything else you need. The next release (v0.2) will likely focus on networking, concurrency, or both — along with more stdlib packages. If you're interested, take a look at So's readme — it has all the information you need to get started. Or try So online without installing anything. Go developers who want low-level control and zero-cost C interop, without having to learn a new language or standard library. C developers who like Go's style. , , and — Abstractions and types for general-purpose I/O. , , , and — Common byte and text operations. and — Generic heap-allocated data structures. and — Generating random data. , , and — Working with the command line and files. — Structured logging. — Measuring and displaying time. — Memory allocation with a pluggable allocator interface. — Low-level C interop helpers. When compiled, the code is first translated to plain C, then compiled into a native binary using GCC or Clang. Unlike Go, there is no runtime (no automatic heap memory allocation, no garbage collection, no goroutine scheduler). There is no overhead when calling C functions, unlike Go's Cgo. The interop syntax is a bit cleaner. For example, Go's ( in the call) automatically decays to C's . Buffered I/O is 3x faster than Go. String and byte operations are up to 2.5x faster. Maps are 1.5x faster for modifications. Integer formatting is 2x faster.

0 views
matklad 4 weeks ago

Minimal Viable Zig Error Contexts

Out of the box, Zig provides minimal and sufficient facilities for error handling — strongly-typed error codes . Error reporting is left to the user. Idiomatic solution is to pass a out parameter (“sink”) to materialize human-readable strings as needed. Diagnostics pattern works well for “production” code, but for more script-y code it adds too much friction relative to the default option of a plain , which of course gives a less than ideal message on failure: Error trace is helpful, but knowing which file is the problem is even more so. The first attempt at finding a middle ground between fully-fledged diagnostics sink pattern and a plain try is something like this: Unsatisfactory. The friction is high, you need to come up with a reasonably-sounding error message, the “happy path” of the code is obscured, and you need to repeat this for every fallible operation. A worse-is-better version of the above code is That is, just log error context as pairs, guarded by . The result is not pretty, but passable: The friction is reduced a lot: There’s one huge drawback though — the error message is logged, even if the error is subsequently handled. This is especially important in Zig 0.16, where cancelation ( serendipitous-success ) is a possible error for any IO-ing operation, and which is intended to be handled, rather than reported. Generalizing: This does feel like a better error management strategy than decorating errors individually, when they happen. I wonder which language features facilitate this style? No need to come up with any error messages beyond existing variable names. No need to change any of the s. The context is set per-block. If a function does several fallible operations on a file, the path needs to be specified only once. The context is “telescopic” every function in the call-stack can add its own context. Happy path adds context to all operations in-progress. Errors materialize current context.

0 views
Armin Ronacher 1 months ago

Before GitHub

GitHub was not the first home of my Open Source software. SourceForge was . Before GitHub, I had my own Trac installation. I had Subversion repositories, tickets, tarballs, and documentation on infrastructure I controlled. Later I moved projects to Bitbucket, back when Bitbucket still felt like a serious alternative place for Open Source projects, especially for people who were not all-in on Git yet. And then, eventually, GitHub became the place, and I moved all of it there. It is hard for me to overstate how important GitHub became in my life. A large part of my Open Source identity formed there. Projects I worked on found users there. People found me there, and I found other people there. Many professional relationships and many friendships started because some repository, issue, pull request, or comment thread made two people aware of each other. That is why I find what is happening to GitHub today so sad and so disappointing. I do not look at it as just the folks at Microsoft making product decisions I dislike. GitHub was part of the social infrastructure of Open Source for a very long time. For many of us, it was not merely where the code lived; it was where a large part of the community lived. So when I think about GitHub’s decline, I also think about what came before it, and what might come after it. I have written a few times over the years about dependencies, and in particular about the problem of micro dependencies . In my mind, GitHub gave life to that phenomenon. It was something I definitely did not completely support, but it also made Open Source more inclusive. GitHub changed how Open Source feels, and later npm and other systems changed how dependencies feel. Put them together and you get a world in which publishing code is almost frictionless, consuming code is almost frictionless, and the number of projects in the world explodes. That has many upsides. But it is worth remembering that Open Source did not always work this way. Before GitHub, Open Source was a much smaller world. Not necessarily in the number of people who cared about it, but in the number of projects most of us could realistically depend on. There were well-known projects, maintained over long periods of time by a comparatively small number of people. You knew the names . You knew the mailing lists. You knew who had been around for years and who had earned trust. That trust was not perfect, and the old world had plenty of gatekeeping, but reputation mattered in a very direct way. We took pride (and got frustrated) when the Debian folks came and told us our licensing stuff was murky or the copyright headers were not up to snuff, because they packaged things up. A dependency was not just a package name. It was a project with a history, a website, a maintainer, a release process, a lot of friction, and often a place in a larger community. You did not add dependencies casually, because the act of depending on something usually meant you had to understand where it came from. Not all of this was necessarily intentional, but because these projects were comparatively large, they also needed to bring their own infrastructure. Small projects might run on a university server, and many of them were on SourceForge, but the larger ones ran their own show. They grouped together into larger collectives to make it work. My first Open Source projects lived on infrastructure I ran myself. There was a Trac installation, Subversion repositories, tarballs, documentation, and release files served from my own machines or from servers under my control. That was normal. If you wanted to publish software, you often also became a small-time system administrator. Georg and I ran our own collective for our Open Source projects: Pocoo . We shared server costs and the burden of maintaining Subversion and Trac, mailing lists and more. Subversion in particular made this “running your own forge” natural. It was centralized: you needed a server, and somebody had to operate it. The project had a home, and that home was usually quite literal: a hostname, a directory, a Trac instance, a mailing list archive. When Mercurial and Git arrived, they were philosophically the opposite. Both were distributed. Everybody could have the full repository. Everybody could have their own copy, their own branches, their own history. In principle, those distributed version control systems should have reduced the need for a single center. But despite all of this, GitHub became the center. That is one of the great ironies of modern Open Source. The distributed version control system won, and then the world standardized on one enormous centralized service for hosting it. It is easy now to talk only about GitHub’s failures, of which there are currently many, but that would be unfair: GitHub was, and continues to be, a tremendous gift to Open Source. It made creating a project easy and it made discovering projects easy. It made contributing understandable to people who had never subscribed to a development mailing list in their life. It gave projects issue trackers, pull requests, release pages, wikis, organization pages, API access, webhooks, and later CI. It normalized the idea that Open Source happens in the open, with visible history and visible collaboration. And it was an excellent and reasonable default choice for a decade. But maybe the most underappreciated thing GitHub did was archival work: GitHub became a library. It became an index of a huge part of the software commons because even abandoned projects remained findable. You could find forks, and old issues and discussions all stayed online. For all the complaints one can make about centralization, that centralization also created discoverable memory. The leaders there once cared a lot about keeping GitHub available even in countries that were sanctioned by the US. I know what the alternative looks like, because I was living it. Some of my earliest Open Source projects are technically still on PyPI , but the actual packages are gone. The metadata points to my old server, and that server has long stopped serving those files. That was normal before the large platforms. A personal domain expired, a VPS was shut down, a developer passed away, and with them went the services they paid for. The web was once full of little software homes, and many of them are gone 1 . The micro-dependency problem was not just that people published very small packages. The hosted infrastructure of GitHub and npm made it feel as if there was no cost to create, publish, discover, install, and depend on them. In the pre-GitHub world, reputation and longevity were part of the dependency selection process almost by necessity, and it often required vendoring. Plenty of our early dependencies were just vendored into our own Subversion trees by default, in part because we could not even rely on other services being up when we needed them and because maintaining scripts that fetched them, in the pre-API days, was painful. The implied friction forced some reflection, and it resulted in different developer behavior. With npm-style ecosystems, the package graph can grow faster than anybody’s ability to reason about it. The problem that this type of thinking created also meant that solutions had to be found along the way. GitHub helped compensate for the accountability problem and it helped with licensing. At one point, the newfound influx of developers and merged pull requests left a lot of open questions about what the state of licenses actually was. GitHub even attempted to rectify this with their terms of service. The thinking for many years was that if I am going to depend on some tiny package, I at least want to see its repository. I want to see whether the maintainer exists, whether there are issues, whether there were recent changes, whether other projects use it, whether the code is what the package claims it is. GitHub became part of the system that provides trust, and more recently it has even become one of the few systems that can publish packages to npm and other registries with trusted publishing. That means when trust in GitHub erodes, the problem is not isolated to source hosting. It affects the whole supply chain culture that formed around it. GitHub is currently losing some of what made it feel inevitable. Maybe that’s just the life and death of large centralized platforms: they always disappoint eventually. Right now people are tired of the instability, the product churn, the Copilot AI noise, the unclear leadership, and the feeling that the platform is no longer primarily designed for the community that made it valuable. Obviously, GitHub also finds itself in the midst of the agentic coding revolution and that causes enormous pressure on the folks over there. But the site has no leadership! It’s a miracle that things are going as well as they are. For a while, leaving GitHub felt like a symbolic move mostly made by smaller projects or by people with strong views about software freedom. I definitely cringed when Zig moved to Codeberg! But I now see people with real weight and signal talking about leaving GitHub. The most obvious one is Mitchell Hashimoto, who announced that Ghostty will move . Where it will move is not clear, but it’s a strong signal. But there are others, too. Strudel moved to Codeberg and so did Tenacity . Will they cause enough of a shift? Probably not, but I find myself on non-GitHub properties more frequently again compared to just a year ago. One can argue that this is good: it is healthy for Open Source to stop pretending that one company should be the default home of everything. Git itself was designed for a world with many homes. Going back to many forges, many servers, many small homes, and many independent communities will increase decentralization, and in many ways it will force systems to adapt. This can restore autonomy and make projects less dependent on the whims of Microsoft leadership. It can also allow different communities to choose different workflows. What’s happening in Pi ‘s issue tracker currently is largely a result of GitHub’s product choices not working in the present-day world of Open Source. It was built for engagement, not for maintainer sanity. It can also make the web forget again. I quite like software that forgets because it has a cleansing element. Maybe the real risk of loss will make us reflect more on actually taking advantage of a distributed version control system. But if projects move to something more akin to self-hosted forges, to their own self-hosted Mercurial or cgit servers, we run the risk of losing things that we don’t want to lose. The code might be distributed in theory, but the social context often is not. Issues, reviews, design discussions, release notes, security advisories, and old tarballs are fragile. They disappear much more easily than we like to admit. Mailing lists, which carried a lot of this in earlier years, have not kept up with the needs of today, and are largely a user experience disaster. As much as I like the idea of things fading out of existence, we absolutely need libraries and archives. Regardless of whether GitHub is here to stay or projects find new homes, what I would like to see is some public, boring, well-funded archive for Open Source software. Something with the power of an endowment or public funding to keep it afloat. Something whose job is not to win the developer productivity market but just to make sure that the most important things we create do not disappear. The bells and whistles can be someone else’s problem, but source archives, release artifacts, metadata, and enough project context to understand what happened should be preserved somewhere that is not tied to the business model or leadership mood of a single company. GitHub accidentally became that archive because it became the center of Open Source activity. Once that no longer holds, we should not assume some magic archival function will emerge or that GitHub will continue to function as such. We have already seen what happens when project homes are just personal servers and good intentions, and we have seen what happened to Google Code and Bitbucket. I hope GitHub recovers, I really do, in part because a lot of history lives there and because the people still working on it inherited something genuinely important. But I no longer think it is responsible to let the continued memory of Open Source depend on GitHub remaining a healthy product. The world before GitHub had more autonomy and more loss, and in some ways, we’re probably going to move back there, at least for a while. Whatever people want to start building next should try to keep the memory and lose the dependence. It should be easier to move projects, easier to mirror their social context, easier to preserve releases, and harder for one company’s drift to become a cultural crisis for everyone else. I do not want to go back to the old web of broken tarball links and abandoned Trac instances. I also do not want Open Source to pretend that the last twenty years were normal or permanent. GitHub wrote a remarkable chapter of Open Source, and if that chapter is ending, the next one should learn from it and also from what came before. This is also a good reminder that we rely so very much on the Internet Archive for many projects of the time. ↩ This is also a good reminder that we rely so very much on the Internet Archive for many projects of the time. ↩

0 views
matklad 1 months ago

256 Lines or Less: Test Case Minimization

Property Based Testing and fuzzing are a deep and science-intensive topic. There are enough advanced techniques there for a couple of PhDs, a PBT daemon, and a client-server architecture . But I have this weird parlor-trick PBT library, implementable in a couple of hundred lines of code in one sitting. This week I’ve been thinking about a cool variation of a consensus algorithm. I implemented it on the weekend. And it took just a couple of hours to write a PBT library itself first, and then a test, that showed a deep algorithmic flaw in my thinking (after a dozen trivial flaws in my coding). So, I don’t get to write more about consensus yet, but I at least can write about the library. It is very simple, simplistic even. To use an old Soviet joke about Babel and Bebel , it’s Gogol rather than Hegel. But for just 256 lines, it’s one of the highest power-to-weight ratio tools in my toolbox. Read this post if: Zig works well here because it, too, is exceptional in its power-to-weight. The implementation is a single file, , because the core abstraction here is a Finite Random Number Generator — a PRNG where all numbers are pre-generated, and can run out. We start with standard boilerplate: In Zig, files are structs: you obviously need structs, and the language becomes simpler if structs are re-used for what files are. In the above assigns a conventional name to the file struct, and declares instance fields (only one here). and are “static” (container level) declarations. The only field we have is just a slice of raw bytes, our pre-generated random numbers. And the only error condition we can raise is . The simplest thing we can generate is a slice of bytes. Typically, API for this takes a mutable slice as an out parameter: But, due to pre-generated nature of FRNG, we can return the slice directly, provided that we have enough entropy. This is going to be our (sole) basis function, everything else is going to be a convenience helper on top: The next simplest thing is an array (a slice with a fixed size): Notice how Zig goes from runtime-known slice length, to comptime known array type. Because is a constant, slicing with returns a pointer to array, . We can re-interpret a 4-byte array into . But, because this is Zig, we can trivially generalize the function to work for any integer type, by passing in comptime parameter of type : This function is monomorphised for every type, so becomes a compile-time constant we can pass to . Production code would be endian-clean here, but, for simplicity, we encode our endianness assumption as a compile-time assertion. Note how Zig communicates information about endianness to the program. There isn’t any kind of side-channel or extra input to compilation, like flags. Instead, the compiler materializes all information about target CPU as Zig code. There’s a file somewhere in the compiler caches directory that contains This file can be accessed via and all the constants inspected at compile time. We can make an integer, and a boolean is even easier: Strictly speaking, we only need one bit, not one byte, but tracking individual bits is too much of a hassle. From an arbitrary int, we can generate an int in range. As per Random Numbers Included , we use a closed range, which makes the API infailable and is usually more convenient at the call-site: As a bit of PRNG trivia, while this could be implemented as , the result will be biased (not uniform). Consider the case where , and a call like . The numbers in are going to be twice as likely as the numbers in , because the last quarter of 256 range will be aliased with the first one. Generating an unbiased number is tricky and might require drawing arbitrary number of bytes from entropy. Refer to https://www.pcg-random.org/posts/bounded-rands.html for details. I didn’t, and copy-pasted code from the Zig standard library. Use at your own risk! Now we can generate an int bounded from above and below: Another common operation is picking a random element from a slice. If you want to return a pointer to a element, you’ll need a and versions of the function. A simpler and more general solution is to return an index: At the call site, doesn’t look too bad, is appropriately -polymorphic, and is also usable for multiple parallel arrays. So far, we’ve spent about 40% of our line budget implementing a worse random number generator that can fail with at any point in time. What is it good for? We use it to feed our system under test with random inputs, see how it reacts, and check that it does not crash. If we code our system to crash if anything unexpected happens and our random inputs cover the space of all possible inputs, we get a measure of confidence that bugs will be detected in testing. For my consensus simulation, I have a struct that holds a and a set of replicas: has methods like: I then select which method to call at random: Here, is another FRNG helper that selects an action at random, proportional to its weight. This helper needs quite a bit more reflection machinery than we’ve seen so far: is compile-time duck-typing. It means that our function is callable with any type, and each specific type creates a new monomorphised instance of a function. While we don’t explicitly name the type of , we can get it as . is a type-level function that takes a struct type: and turns it into an enum type, with a variant per-field, exactly what we want for the return type: Tip: if you want to quickly learn Zig’s reflection capabilities, study the implementation of and in Zig’s standard library. The built-in function accesses a field given field name. It’s exactly like Python’s / with an extra restriction that it must be evaluated at compile time. To add one more twist here, I always find it hard to figure out which weights are reasonable, and like to generate the weights themselves at random at the start of the test: (If you feel confused here, check out Swarm Testing Data Structures ) Now we have enough machinery to describe the shape of test overall: A test needs an (which ultimately determines the outcome) and an General Purpose Allocator for the . We start by creating a simulated with random action weights. If entropy is very low, we can run out of entropy even at this stage. We assume that the code is innocent until proven guilty — if we don’t have enough entropy to find a bug, this particular test returns success. Don’t worry, we’ll make sure that we have enough entropy elsewhere. We use to peel off error. I find that, whenever I handle errors in Zig, very often I want to discharge just a single error from the error set. I wish I could use parenthesis with a : Anyway, having created the , we step through it while we still have entropy left. If any step detects an internal inconsistency, the entire crashes with an assertion failure. If we got to the end of loop, we know that at least that particular slice of entropy didn’t uncover anything suspicious. Notice what isn’t there. We aren’t generating a complete list of actions up-front. Rather, we make random decisions as we go, and can freely use the current state of the to construct a menu of possible choices (e.g., when sending a message, we can consider only not currently crashed replicas). And here we can finally see the reason why we bothered writing a custom Finite PRNG, rather than using an off-the-shelf one. The amount of entropy in FRNG defines the complexity of the test. The fewer random bytes we start with, the faster we exit the step loop. And this gives us an ability to minimize test cases essentially for free. Suppose you know that a particular entropy slice makes the test fail (cluster enters split brain at the millionth step). Let’s say that the slice was 16KiB. The obvious next step is to see if just 8KiB would be enough to crash it. And, if 8KiB isn’t, than perhaps 12KiB? You can binary search the minimal amount of entropy that’s enough for the test to fail. And this works for any test, it doesn’t have to be a distributed system. If you can write the code to generate your inputs randomly, you can measure complexity of each particular input by measuring how many random bytes were drawn in its construction. And now the hilarious part — of course it seems that the way to minimize entropy is to start with a particular failing slice and apply genetic-algorithm mutations to it. But a much simpler approach seems to work in practice — just generated a fresh, shorter entropy slice. If you found some failure at random, then you should be able to randomly stumble into a smaller failing example, if one exists — there are much fewer small examples, so finding a failing one becomes easier when the goes down! The problem with binary searching for failing entropy is that a tripped assertion crashes the program. There’s no unwinding in Zig. For this reason, we’ll move the search code to a different process. So a single test will be a binary with a function, that takes entropy on . Zig’s new juicy main makes writing this easier than in any previous versions of Zig :D Main gets as an argument, which provides access to things like command line arguments, default allocator and a default implementation. These days, Zig eschews global ambient IO capabilities, and requires threading an Io instance whenever we need to make a syscall. Here, we need Io to read stdin. Now we will implement a harness to call this main. This will be : It will be spawning external processes, so it’ll need an . We also need a path to an executable with a test main function, a System Under Test. And we’ll need a buffer to hold the entropy. This driver will be communicating successes and failures to the users, so we also prepare a for textual output. How we get entropy to feed into ? Because we are only interested in entropy size, we won’t be storing the actual entropy bytes, and instead will generate it from a seed. In other words, just two numbres, entropy size and seed, are needed to reproduce a single run of the test: We use default deterministic PRNG to expand our short seed into entropy slice of the required size. Then we spawn proces, feeding the resulting entropy via stdin. Closing child’s stdin signals the end of entropy. We then return either or depending on child’s exit code. So, both explicit errors and crashes will be recognized as failures. Next, we implement the logic for checking if a particular seed size is sufficient to find a failure. Of course, we won’t be able to say that for sure in a finite amount of time, so we’ll settle for some user-specified amount of retries: The user passes us the number of to make, and we return if they all were successfull, or a specific failing seed if we found one: To generate a real seed we need “true” cryptographic non-deterministic randomness, which is provided by . Finally, the search for the size: Here, we are going to find a smallest entropy size that crashes . If we succeed, we return the seed and the size. The upper bound for the size is the space available in the pre-allocated entropy buffer. The search loop is essentially a binary search, with a twist — rather than using dichotomy on the directly, we will be doubling a we use to change the size between iterations. That is, we start with a small size and step, and, on every iteration, double the step and add it to the size, until we hit a failure (or run out of buffer for the entropy). Once we found a failure, we continue the serach in the other direction — halving the step and subtracting it from the , keeping the smaller if it still fails. On each step, we log the current size and outcome, and report the smallest failing size at the end. Finally, we wrap Driver’s functionality into main that works in two modes — either reproduces a given failure from seed and size, or searches for a minimal failure: Running the search routine looks like this in a terminal: Those final seed&size can then be used for , giving you a minimal reproducible failure for debugging! This … of course doesn’t look too exciting without visualizing a specific bug we can find this way, but the problem there is that interesting examples of systems to test in this way usually take more than 256 lines to implement. So I’ll leave it to your imagination, but you get the idea: if you can make a system fail under a “random” input, you can also systematically search the space of all inputs for the smallest counter-example, without adding knowledge about the system to the searcher. This article also provides a concrete (but somewhat verbose) example. Here’s the full code: https://gist.github.com/matklad/343d13547c8bfe9af310e2ca2fbfe109 You want to stretch your generative testing muscles. You are a do-it-yourself type, and wouldn’t want to pull a ginormous PBT library off the shelf. You would pull a library, but want to have a more informed opinion about available options, about essential and accidental complexity. You want some self-contained real-world Zig examples :P

0 views
マリウス 2 months ago

Updates 2026/Q1

This post includes personal updates and some open source project updates. 안녕하세요 and greetings from Asia! Right now I’m in Seoul, Korea. I’ll start this update with a few IRL experiences regarding my time here and some mechanical keyboard related things. If you’re primarily here for the technical stuff, you can skip forward or even skip all of the personal things and jump straight to the open source projects . With that said, let’s dive straight into it. Seoul has been one of the few places that I genuinely love coming back to. I cannot pinpoint why that is, but there’s a particular rhythm to the capital that’s hard to explain until you’ve lived in it for a while. Not the tourist rhythm, where you tick off palaces and night markets to “complete your bucket list” but the deeper, slower one that makes the city truly enjoyable. The rhythm of picking a neighborhood, learning its backstreets, finding your morning coffee spot, and then finding a different one the following week. I spent my time here doing exactly that, and what follows are some honest reflections on a city that continues to surprise me. As some of you might know by now, I’m basically the Mark Wiens of coffee, because I travel for coffee , except that I don’t film myself and put it online. But I’ve surely had a lot of coffee, in a lot of cities. However, Seoul’s coffee scene operates on a completely different level. The sheer density of independently run coffee shops is staggering. Within a fifteen-minute walk in neighborhoods like Mangwon , Hapjeong , or Sangsu , you can pass dozens of places where someone is carefully dialing in their espresso, roasting their own beans, and serving a beautifully made Americano for usually around three or four thousand KRW . That’s roughly two to three US dollars for a genuinely excellent cup of coffee, which is a pretty solid value proposition. I’ve been in Seoul before, multiple times actually, and I had the chance to find genuinely great cafes which I kept on my list of places to revisit whenever I would happen to come back. And so I did. But as life moves forward, places change or, in more unfortunate circumstances, even close down for good. das ist PROBAT is one of the places that sadly closed just a few days before I arrived. In its spot is now a new Ramen restaurant that seemed fairly popular. A few other places I’d loved on previous visits and that are still operating left me genuinely disappointed this time around. Compile Coffee was one of the sharper letdowns. Two years ago, it was a highlight. This time, however, the experience felt rushed and careless. The barista hurried through the ordering process, despite no one else waiting in line, and the cappuccino that followed was a spectacle for all the wrong reasons. The milk was frothed to an almost comical extreme, the liquid poured in first, then the foam scooped in one spoonful at a time, and finally a thick layer of chocolate powder on top that I hadn’t asked for. It felt like watching a car accident happening slowly enough for every detail to remain stuck in one’s head, yet too fast to articulate anything about it. I gave the place another try a few weeks after this incident only to experience a similarly rushed and somewhat unloving execution. Another change that I hadn’t seen coming was Bean Brothers in Hapjeong . The coffee house converted from their old industrial-style space to a noticeably more polished and… well, “posh” one. The new spot is nice enough, but the vibe has shifted towards a more upscale, less alternative one. In addition, they also opened up a new location in Sangsu , which leans further in that direction, with wait times for walk-ins that suggest a clientele they’re specifically courting. Bean Brothers seems to be evolving into a streamlined, upscale chain, and while that’s not inherently bad, it’s a different thing from what originally made it special. And last but not least, there’s Anthracite Coffee Roasters , specifically the Seogyo location , which had been one of my absolute favorite spots back in 2023. It pains me to say this, but the place has become a ripoff, with this specific location charging eight thousand KRW for a hot (drip coffee) Americano to go. For context, the healthy food chain Preppers serves a full meal consisting of a big portion of rice and a protein, as well as some greenery, for 8,900 KRW. The cup of drip coffee at Anthracite is only halfway full, and most of the time it arrives already lukewarm, which makes it essentially useless as a to-go option, unless all you want is to gulp down around 120ml of coffee. You’d think a place charging premium prices would at least discount a thousand Won for takeaways, as many Seoul cafes do. The Seogyo location’s commitment to drip coffee not only makes it feel somewhat pretentious considering the prices, but also adds a whole other layer of issues. During peak hours, the wait is considerable, and the coffee menu is limited to a small rotation of options that, more often than not, skew toward the acidic side of the spectrum. If that’s your preference, there’s nothing wrong with that. But when combined with the pricing, the lukewarm temperatures, and the half-filled cups, the experience increasingly feels like you’re paying for a brand name rather than a good cup of coffee. However, the beautiful thing about Seoul’s coffee culture is that for every established spot that drifts toward becoming another Starbucks experience, ten new places pop up that more than make up for it. The ecosystem is relentlessly self-renewing. In the same neighborhood as Anthracite ’s Seogyo location, I discovered a handful of places that are not only better in the cup, but dramatically more affordable: These are only a handful of places that I think of off the top of my head, but rest assured that there are plenty more. The quiet confidence of people who care about the craft without needing to perform it is what makes these places special. No gimmicks, no inflated prices justified by whatever interior design. Just friendly people and good coffee that’s made well and respects the customer. The time in Seoul reinforced what I already knew from past visits. This city is one of the best places in the world to simply be in. The neighborhoods are endlessly walkable, the infrastructure works beautifully (with the exception of traffic lights and escalators, but more on that in a bit), and the coffee culture, despite the occasional disappointment from places that have lost their way, remains one of the richest and most dynamic I’ve encountered anywhere. The disappointments, if anything, make the discoveries sweeter. The food also deserves a mention. Seoul is one of those cities where even a quick, unremarkable lunch tends to be delicious and more often than not at a sane price, judging from a global perspective. Compared to other capital cities like London or, worse, Madrid , in which food prices are frankly absurd, especially when taking the generally low quality into account, the cost of food in Seoul still strikes me as overall reasonable. Unlike for example Madrid , which is an almost homogenous food scene, Seoul offers incredibly diverse options, ranging from traditional Korean food, all the way to Japanese, Thai, Vietnamese and even European and Latin American food. And while the Italian pasta in many places in Seoul might not convince an actual Italian gourmet, it suddenly becomes a very high bar to complain about dishes that originate as far as twelve thousand kilometers/seven thousand miles away and that have almost no local cultural influence . Another beautiful thing about Seoul, at least for keyboard -enthusiasts like I am, is the availability of actual brick-and-mortar keyboard stores. Seoul is home to three enthusiast keyboard shops: Funkeys , SwagKeys , and NuPhy . The first two are local vendors that have physical locations across Seoul, the latter is a Hong Kong-based manufacturer of entry-level enthusiast boards that just opened a showroom in Mapo-gu . I took the time to try to visit each of them and I even scooped up some new hardware. The Funkeys store is located in the Tongsan district, on the second floor of a commercial space. The store is relatively big and stocks primarily FL-Esports , AULA , and 80Retros boards, keycaps and switches, but you can also find a few more exclusive items like the Angry Miao CyberBoard . I seized the opportunity to test (and snap up) some 80Retros switches, but more on that further down below. SwagKeys is probably a name that many people in the keyboard enthusiast community have stumbled upon at least once. They are located in the Bucheon area and they used to have a showroom, which I tried to visit. Sadly, it wasn’t clear to me that the showroom was temporarily (permanently?) closed, so I basically ended up standing in front of locked doors of an otherwise empty space. Luckily, however, SwagKeys have popup stores in different malls, which I have visited as well. Unfortunately in those popup stores they only seem to offer entry-level items; Enthusiast products are solely available through their web shop and cannot be ordered and picked-up at any of their pop-up locations. I was curious to test and maybe get the PBS Modern Abacus , which SwagKeys had in stock at that time, but none of the pop-ups had it available. Exclusive SwagKeys pop-up. This is a shared space with plenty of other brands to choose from. The NuPhy showroom in the Mapo-gu area is a small space packed with almost all the products the brand offers, from keyboards, over switches and keycaps all the way to accessories and folios /bags. However, the showroom is exactly that: A showroom. There’s no way to purchase any of the hardware. As with almost everything in Seoul, your best bet is to order it from NuPhy’s official Korean store, which accepts Naver Pay . Apart from Funkeys , SwagKeys and NuPhy , there are various brands (like Keychron , Razer and Logitech ) that can be found across in-store pop-ups in different malls. It’s interesting to see a society like the one in Seoul, that has largely moved away from offline-shopping for almost everything but fashion (more on this in a moment) having that many shops and pop-ups selling entry-level mechanical keyboards. I guess with keyboards being something in which haptics and personal preference play a big role, it makes sense to have places for people to test the various boards and switches, even if most of them will ultimately only sell the traditional Cherry profiles. Speaking of mechanical keyboards, I happened to be in the right place at the right time this year to visit the Seoul Mechanical Keyboard Expo 2026 at the Seoul Trade Exhibition Center ( SETEC ) in the Gangnam area. It was an interesting experience despite being less of a traditional enthusiast community event and more of a manufacturer trade fair targeting average users. Because yes, the average user in Korea does indeed seem to have a soft-spot for mechanical keyboards. This, however, meant that most vendors would primarily showcase the typical mainstream products, like Cherry profile keycaps and boards that are more affordable. For example while Angry Miao were around, their Hatsu board was nowhere to be seen. And it made sense: Every vendor had little signs with QR codes that would lead to their store’s product page for people to purchase it right away. Clearly, the event was geared more toward the average consumer than the curious enthusiast. It was nevertheless interesting to see an event like this happening in the wild . Getting around is different in Seoul than it is in other cities. If you’re navigating Seoul with Google Maps , you’re doing it wrong. Naver Map is simply superior in every way that matters for daily life here, although this might soon change . Not only does Naver show you where the crosswalks are, something you don’t realize you need until you’ve jaywalked across six lanes of traffic because Google told you the entrance was “right there” , but it also shows last order times for restaurants and cafes, saving you from going to places only to find out they’re not serving anymore. And public transit arrival times? Accurate to a degree that feels almost unsettling. You trust Naver , because it earns that trust. Clearly, however, me being me , I only used Naver without an account and on a separate profile on my GrapheneOS phone . Also, I mostly use it for finding places and public transit; For everything else CoMaps works perfectly fine, and I take care to contribute to OSM whenever I can. Note: The jaywalking example isn’t too far-fetched. You’re very tempted to cross at red lights simply because traffic light intervals in Seoul are frankly terrible. As a pedestrian you age significantly waiting for the stoplight to finally turn green. If you’re unlucky, you’re at a large crossing that is followed by smaller crossings, which for reasons I cannot comprehend turn green for pedestrians at the exact same time. Unless you are Usain Bolt there is no way to make it across multiple crossings in one go, leading you to have to stop at every crossing for around three minutes. That doesn’t sound like much, until you’re out at -15°C/5°F. Seoul has too many pedestrian crossings with traffic lights, and too few simple marked crosswalks. This is however probably due to drivers often not giving a damn about traffic rules and almost running over people trying to cross at regular marked crossings. My gut feeling tells me that, because of the indifference of drivers, the government decided to punish every traffic participant by building traffic lights at almost every corner. However, this didn’t have the (supposedly) intended effect, as especially scooters, but also regular cars often couldn’t care less about their bright red stop light. Considering the amount of CCTVs (more on this in just a second) one could assume that traffic violations are being enforced strictly. However, judging by the negligence of drivers towards traffic rules I would guess that this is probably not happening. Circling back to the painfully long waiting times at crossings, that are only outrivalled by painfully slow escalators literally everywhere, a route for which CoMaps estimates 10 minutes can hence easily become a 20 minute walk. Naver , however, appears to be making time estimations based on average waiting times at crossings, leading to it being more accurate than CoMaps in many cases. With Naver being independent of Google , it works without any of the Google Play Services bs that apps often require for anything related to location. And don’t get me wrong, Naver is just as much of an E Corp as Google , but there’s something worth appreciating on a broader level here. Korea built and maintains its own mapping platform rather than ceding that ground to US big tech, and it shows. Naver Map is designed by people who actually navigate Korean cities, and that local knowledge is baked into every interaction. I would love to see more countries doing the same, especially European ones. While there is Nokia HERE Maps HERE WeGo in Europe, it’s as bad for public transport as you might expect from a joint venture between Audi , BMW and Mercedes-Benz , and it is not at all comparable to Naver Maps , let alone Naver as a whole. One big caveat with Naver , however, is that it will drain your battery like a Mojito on a hot summer evening, so it’s essential to carry a power bank . Even on a Pixel 8 , the app feels terribly clunky and slow. In addition, the swiping recognition more often than not mistakes horizontal swipes (for scrolling through photos of a place) for vertical swiping, making it really cumbersome to use. I assume that on more modern Samsung and Apple devices the app probably works significantly better, as the Korean market appears to be absolutely dominated by these two brands. As a matter of fact, the Google Pixel is not even being sold in Korea, which brings me to one important aspect of life in Seoul that might be interesting for the average reader of this site. As much as I enjoy Seoul, it is an absolute privacy disaster. CCTV cameras in Seoul are everywhere and the city government actively expands and upgrades them as part of its public-safety and smart city initiatives. The systems are “AI” -enabled and can automatically detect unusual behavior or safety risks . It’s hard to find a definitive number, but it’s estimated that Seoul is covered with around 110,000 to 160,000 surveillance cameras, with an ongoing expansion of the network. This makes Seoul one of the most surveilled major cities in the world. In addition to CCTV surveillance, Seoul is also almost completely cashless. Most places only accept card/NFC payments with cash payments being a highly unusual thing to do. While there are still ATMs around, getting banknotes is almost pointless. You can top up your transit card using cash, and you might be thinking that at least this way nobody knows who owns the card and you cannot be tracked, but with the amount of “AI” cameras everywhere, there’s no need to track people using an identifier as primitive as a transit card. Speaking of which, mobile connectivity is another thing. In Korea SIM cards are registered using an ID/Passport. From what I have found, there’s no way to get even just a pre-paid SIM without handing over your ID. In addition, with everything being cashless, your payment details are also connected to the SIM card. You could of course try to only use the publicly available WiFi to get around and spare yourself the need for a SIM card. However, the moment you’d want to order something online, you will need a (preferably Korean) phone number that can retrieve verification SMS and you might even need to verify your account with an ID. You might think that this doesn’t really matter because online shopping isn’t something vital that you have to do. But with Seoul being almost completely online in terms of shopping you cannot find even the most basic things easily in brick-and-mortar stores. For example, I was looking to upgrade my power brick from the UGREEN X757 15202 Nexode Pro GaN 100W 3-Port charger that I’ve been using for the past year to the vastly more powerful UGREEN 55474 Nexode 300W GaN 5-Port charger. I bought the 3-Port Nexode last year during my time in Japan , in a Bic Camera . However, in Seoul it was impossible to find any UGREEN product. In fact, I could not find any household name products, like Anker or Belkin , regardless of where I looked. Everyone kept telling me to look online, on Naver or Coupang . Short story long, to be able to live a normal life in Seoul you will unfortunately have to hand over your details at every corner. Note : Only one day before publishing this update, the popular Canadian YouTuber Linus Tech Tips uploaded a video titled “Shopping in Korea’s Abandoned Tech Mall” , which perfectly captures the sad state of offline tech stores in Seoul. What I found more shocking than this, however, is that it doesn’t seem like privacy concerns are part of the public discourse. The dystopian picture that people in the Western hemisphere paint in literature and movies, in which conglomerates run large parts of society and the general population are merely an efficient workforce and consumers isn’t far off from how society here appears to be working. At the end of February I ran into an issue that I had seen before : Back then, I attributed it to either alpha particles or cosmic rays, as I was unable to reproduce the issue nor reliably find bad regions in the RAM. This time, however, my laptop was crashing periodically, for seemingly no reason at all. After running the whole playbook of and to verify the filesystem, as well as multiple rounds of the , I found several RAM addresses that were reported faulty. I decided to seize the opportunity and publish a post on BadRAM . At this point, I removed one of the two 32GB RAM sticks and it appears to have helped at least somewhat: The device now only crashes every few hours rather than every twenty or so minutes. But with RAM and SSD prices being what they are, I’m not even going to attempt to actually fix the issue. After all, it might well be that whatever is causing the buzzing sound I’ve been hearing on my Star Labs StarBook has also had an impact on the RAM modules or even the logic board. I’m going to hold on to this hardware for as long as possible, but I’ve also realized that the StarBook has aged quicker than I anticipated. I have therefore been glancing at alternatives for quite a while now. I love what Star Labs has done with the StarBook Mk VI AMD in terms of form factor and Linux support. Back when I bought it , the Zen 3 Ryzen 7 5800U had already been on the market for almost 4 years and wasn’t exactly modern anymore. However, its maturity gave me hope that Linux support would be flawless (which is the case) and that Star Labs would eventually be able to deliver on their promises. When I purchased the device, Star Labs had advertised an upcoming upgrade from its American Megatrends EFI (“BIOS”) to Coreboot , an open-source alternative. Years later, however, this upgrade is still nowhere to be seen . At this point it is highly unlikely, that Coreboot on the AMD StarBook will ever materialize. As already hinted exactly one year ago I’m done waiting for Star Labs and I am definitely not going to look into any of their other (largely obsolete) AMD offerings, especially considering the outrageous prices. I’m also not going to consider any of their StarBook iterations, whether it’s the regular version, or the Horizon , given that none of them come with AMD CPUs any longer, and, more importantly, that their Intel processors are far too outdated for their price tags. Let alone all the quirks the Star Labs hardware appears to be having, and the firmware features that sometimes make me wonder what the actual f… the Star Labs people are smoking. Note : The firmware update lists the following update: * Remove the power button debounce (double press is no longer required) “Power button debounce” is what Star Labs calls the requirement to double-press the power button in order to power on the laptop when it is not connected to power. It is mind-boggling that this feature made it into the firmware to begin with. Who in their right mind thought “Hey, how about we introduce a new feature with the coming firmware update which we won’t communicate anywhere, which requires the user to press the power button quickly twice in a row for their device to power on, but only when no power cable is connected? And how about if they only press it once when no power cord is attached the device simply won’t boot, but it will nevertheless produce a short audible sound to make it seem like it tried to boot, but in reality it won’t boot?” …? Because this is exactly what the “power button debounce” was about. I believe it got introduced sometime around , but I can’t really tell, because Star Labs didn’t mention it anywhere. Short story long, instead of spending more money on obsolete and quirky Star Labs hardware, I have identified the ASUS ExpertBook Ultra as a potential successor. The ExpertBook Ultra is supposed to be released in Q2 in its highest performance variant, featuring the Intel Core Ultra X9 Series 3 388H “Panther Lake” processor, running at 50W TDP and sporting up to 64 GB LPDDR5x memory, which is the model that I’m interested in. I will wait out the reviews, specifically for Linux, but unless major issues are to be expected I’ll likely upgrade to it. “Wait, aren’t you Team Red?” , you might be wondering. And, yes, for the past decade I’ve been solely purchasing AMD CPUs and GPUs, with one exception that was a MacBook with Intel CPU. However, at this point I’m giving up on ever finding an AMD-based laptop that fits my specs, because sadly with AMD laptops it’s always something : Either the port selection sucks, or there’s no USB4 port at all, or if there is it’s only on one specific side, or the display and/or display resolution sucks, or the battery life is bad, or you can only get some low-TDP U variant, or the device is an absolute chonker, or or or. It feels like with an AMD laptop I always have to make compromises at a price point at which I simply don’t want to have to make these compromises anymore. So unless AMD and the manufacturers – looking specifically at you, Lenovo! – finally get their sh#t together to build hardware that doesn’t feel like it’s artificially choked, I’m going back to Team Blue . “Panther Lake” seems to have made enough of a splash, TDP-performance-wise, that it is worth considering Intel again, despite the company’s history of monopolistic business tactics, its anti-consumer behavior, its major security flaws, its quality control issues, and its general douchebag attitude towards everything and everyone. The ASUS ExpertBook Ultra appears to feature the performance that I want, with all the connectivity that I need, packaged in a form factor that I find aesthetically pleasing and lightweight enough to travel with. If the Intel Core Ultra X9 388H notably exceeds the preliminary benchmarks and reviews of the Intel Core Ultra X7 358H version of the ExpertBook Ultra , then I’m “happy” to pay the current market premium for a device that will hopefully hold up for much longer and with fewer quirks than I’ve experienced with the StarBook . With a Speedometer 3.1 rating of around 30 and reporting 11:25:05 hours for on my current device, however, I’m fairly certain that even the X7 358H will be a significant improvement. “Did you hear about the latest XPS 14 & 16 from Dell? They also come with Panther Lake!” , I hear you say. See here and there on why those are seemingly disappointing options. The tl;dr is that Dell only feeds them 25W (14") / 35W (16"), instead of the 45W that ASUS runs the CPU at. I can’t tell for sure how long I’ll be able to continue working on the StarBook . While I can do the most critical things, the looming threat of data-corruption and -loss is frightening. The continuous crashes also introduce unnecessary overhead. I’m hoping for ASUS to make the ExpertBook Ultra available rather sooner than later, but if there’s no clarity on availability soon I might have to go with a different option. Ultrabook Review luckily has a full list of Panther Lake laptops to help with finding alternatives. What’s the second best thing that can happen when your computer starts failing? Exactly: Your phone (slowly) dying. It appears that the infamous Pixel 8 green-screen-of-death hit my GrapheneOS device, making it almost impossible to use it. Not only does the display glitch terribly, but it appears that the lower bottom part of the phone gets abnormally hot. When the glitching began, it would be sufficient to literally slap the bottom part of the phone and it would temporarily stop glitching. Sadly, the effectiveness of this workaround has decreased so much over time that now I basically need to squeeze the bottom part of the phone for the glitching to stop. The moment I decrease force, the screen starts glitching again. My plan was to keep the Google Pixel 8 for the next few years and eventually move to a postmarketOS /Linux phone as soon as there will be a viable option. Sadly it seems that I’m going to have to spend more money on Google’s bs hardware to get another GrapheneOS device for the time being. Unfortunately Google is not selling the Pixel devices across Asia, making it hard to find an adequate replacement for the phone right now. I might just have to suck it up and wait until I’ll pass by a region in which Pixel devices are more widely available. Of course, I luckily brought backups , although those run malware and are hence less than ideal options. My Anker Soundcore Space Q45 have died on me during a flight, for absolutely no reason at all. I purchased them back at the end of May 2024 and now, after not even 2 years it appears that the electronics inside of them broke in a way in which the headphones cannot be turned off or on again. They seem to be in a sort-of odd state in between, in which pressing e.g. the ANC button does something and makes the LED light up, but there’s no Bluetooth connectivity whatsoever. When connecting them via USB-C to power or to another device, the LED changes dozens of times per second between white and red. Holding the power button makes the LED turn on (white) but nothing else. The moment the power button is let go, the LED turns back off. This is yet another Anker product that broke only shortly after its warranty expired and I’m starting to see a common theme here. Hence, I will avoid Anker products going forward, especially given the tedious support that I had experienced in the past with one of their faulty power banks. I still use the Soundcore headphones via audio jack, as this luckily works independently of the other electronics. To avoid anything bad happening, especially during flights, I opened the left earcup and removed the integrated battery. The USI 2.0 stylus that I had bought back in mid September of 2024 from the brand Renaisser is another hardware item that has pretty much died. It seems like the integrated battery is done, hence the pen doesn’t turn on anymore unless a USB-C cable is connected to it to power it externally. While I’m still using it, it is slightly inconvenient to have a relatively stiff USB-C cable pull on the upper end of the pen while writing or editing photos, which is what I use the pen primarily for. As mentioned in the Seoul part, I picked up a handful of mechanical keyboard-related items, namely MX switches for my keyboard(s) . KTT x 80Retros GAME 1989 Orange , 40g (22mm KOS single-stage extended, bag lubed with Krytox 105 ), lubed with Krytox 205G0 . 80Retros x HMX Monochrome , 42g (48g bottom out), LY stem, PA12 top housing, HMX P2 bottom housing, 22mm spring, factory lubed, 2mm pre-travel, 3.5mm total. I invested quite some time in pursuing my open source projects in the past quarter, hence there are a few updates to share. This quarter I have finally found the time to also update my feature and make it work with the latest version of Ghostty , the cross-platform terminal emulator written in Zig. You can use this commit if you want to patch your version of Ghostty with this feature. It is unlikely that the Ghostty team is ever going to include this feature in their official release, yet I’m happy to keep maintaining it as it’s not a lot of code. I have updated and it now supports a new flag (that does not support), which makes it possible to build a complete power management policy directly through command-line arguments. I have documented it in detail in the repository , but the idea is that the flag allows executing arbitrary shell commands when the battery reaches a specific percentage, either by charging or discharging. The flag takes three arguments: For , the command fires when the battery percentage drops to or below the given value. For , it fires when the percentage reaches or exceeds it. The command fires once when the condition is met and will only fire again after the condition has cleared and been met again. Additionally, the flag can be specified multiple times to define different rules. This makes it possible to build a complete power management policy, from low-battery warnings to automatic shutdown, without any external scripts or configuration files. The benefit this has over, let’s say, rules, is that script execution as the current user is significantly easier, less hacky and poses fewer overall security risks, as does not need to (read: should not ) be run in privileged mode. Another one of my Zig tools that got a major update is , the command line tool for getting answers to everyday questions like or more importantly . The new version has received an update to work with Zig 0.15.0+ and its command line arguments parser logic was rewritten from scratch to be able to handle more complex cases. In addition, is now able to do a handful of velocity conversions, e.g. . As a quick side note, alongside the Breadth-first search implementation that it is using, , has also been updated to support Zig 0.15.0+. I had some fun a while ago building an XMPP bot that’s connected to any OpenAI API (e.g. ) and is able to respond when mentioned and respond to private messages. It preserves a single context across all messages, which might not be ideal in terms of privacy, but it is definitely fun in a multi-user chat – hey, btw, come join ours! The code is relatively crude and simple. Again, this was a just a two-evening fun thing, but you can easily run the bot yourself, check the README and the example configuration for more info. The work on my new project, ▓▓▓▓▓▓▓▓▓▓▓, which I had announced in my previous status update sadly didn’t progress as quickly as I was expecting it to, due to (amongst other things) the RAM issues that I’ve had to deal with. It also turns out that when writing software in 2026, everyone seems to expect instant results, given all the Codexes and Claudes that are usually being employed these days to allow even inexperienced developers to vibe code full-blown Discord alternatives within shorts periods of time. However, because I don’t intend to go down that path with ▓▓▓▓▓▓▓▓▓▓▓, it will sadly take some more time for me to have a first alpha ready. To everyone who reached out to offer their help with alpha testing: You will be the first ones to get access as soon as it’s ready. Kauf Roasters : A roastery with a clear focus on simplicity and quality without pretension. Identity Coffee Lab : This one stunned me. A hot Americano to go for 3,000 KRW. That’s almost a third of what Anthracite charges. And the coffee isn’t just cheaper, it is significantly better! It’s a bigger cup, it’s notably less acidic, and, here’s the part that really got me, it comes out steaming hot and stays that way for a good twenty minutes. You can actually walk around and sip it casually, even in freezing cold temperatures, just the way a to-go coffee is meant to be enjoyed, instead of gulping it down before it turns into cold brew. Oscar Coffee Booth : This became a personal favorite. Another spot where the coffee is serious, the price is fair, and nobody is trying to impress you with anything other than a well-made drink. On top of that the owner is a genuinely kind person. : Either (aliases: , ) or (aliases: , ) : The battery level (number from 0 to 100) : The shell command to execute

0 views
Evan Hahn 3 months ago

Introducing gzpeek, a tool to parse gzip metadata

In short: gzip streams contain metadata, like the operating system that did the compression. I built a tool to read this metadata. I love reading specifications for file formats. They always have little surprises. I had assumed that the gzip format was strictly used for compression. My guess was: a few bytes of bookkeeping, the compressed data, and maybe a checksum. But then I read the spec . The gzip header holds more than I expected! In addition to two bytes identifying the data as gzip, there’s also: The operating system that did the compression. This was super surprising to me! There’s a single byte that identifies the compressor’s OS: for Windows, for the Amiga, for Unix, and many others I’d never heard of. Compressors can also set for an “unknown” OS. Different tools set this value differently. zlib, the most popular gzip library, changes the flag based on the operating system . (It even defines some OSes that aren’t in the spec, like for BeOS.) Many other libraries build atop zlib and inherit this behavior, such as .NET’s , Ruby’s , and PHP’s . Java’s , JavaScript’s , and Go’s set the OS to “unknown” regardless of operating system. Some, like Zopfli and Apache’s , hard-code it to “Unix” no matter what. All that to say: in practice, you can’t rely on this flag to determine the source OS, but it can give you a hint. Modification time for the data. This can be the time that compression started or the modification time of the file. It can also be set to if you don’t want to communicate a time. This is represented as an unsigned 32-bit integer in the Unix format. That means it can represent any moment between January 1, 1970 and February 7, 2106. I hope we devise a better compression format in the next ~80 years, because we can only represent dates in that range. In my testing, many implementations set this to . A few set it to the current time or the file’s modification time—the command is one of these. FTEXT , a boolean flag vaguely indicating that the data is “probably ASCII text”. When I say vaguely, I mean it: the spec “deliberately [does] not specify the algorithm used to set this”. This is apparently for systems which have different storage formats for ASCII and binary data. In all my testing, nobody sets this flag to anything but . An extra flag indicating how hard the compressor worked. signals that it was compressed with max compression (e.g., ), for the fastest algorithm, and for everything else. In practice, zlib and many others set this correctly per the spec, but some tools hard-code it to . And as far as I can tell, this byte is not used during decompression, so it doesn’t really matter. The original file name . For example, when I run , the name is set to . This field is optional, so many tools don’t set it, but the command line tool does. You can disable that with . A comment . This optional field is seldom used, and many decompressors ignore it. But you could add a little comment if you want. Extra arbitrary data . If the other metadata wasn’t enough, you can stuff whatever you want into arbitrary subfields. Each subfield has a two-byte identifier and then 0 or more bytes of additional info. That’s way more info than I expected! I was intrigued by this metadata and I’ve been wanting to learn Zig , so I wrote gzpeek . gzpeek is a command-line tool that lets you inspect the metadata of gzip streams. Here’s how to read metadata from a gzipped file: It extracts everything I listed above: the operating system, original file name, modification time, and more. I used it a bunch when surveying different gzip implementations. Give it a try, and let me know what gzip metadata you find. The operating system that did the compression. This was super surprising to me! There’s a single byte that identifies the compressor’s OS: for Windows, for the Amiga, for Unix, and many others I’d never heard of. Compressors can also set for an “unknown” OS. Different tools set this value differently. zlib, the most popular gzip library, changes the flag based on the operating system . (It even defines some OSes that aren’t in the spec, like for BeOS.) Many other libraries build atop zlib and inherit this behavior, such as .NET’s , Ruby’s , and PHP’s . Java’s , JavaScript’s , and Go’s set the OS to “unknown” regardless of operating system. Some, like Zopfli and Apache’s , hard-code it to “Unix” no matter what. All that to say: in practice, you can’t rely on this flag to determine the source OS, but it can give you a hint. Modification time for the data. This can be the time that compression started or the modification time of the file. It can also be set to if you don’t want to communicate a time. This is represented as an unsigned 32-bit integer in the Unix format. That means it can represent any moment between January 1, 1970 and February 7, 2106. I hope we devise a better compression format in the next ~80 years, because we can only represent dates in that range. In my testing, many implementations set this to . A few set it to the current time or the file’s modification time—the command is one of these. FTEXT , a boolean flag vaguely indicating that the data is “probably ASCII text”. When I say vaguely, I mean it: the spec “deliberately [does] not specify the algorithm used to set this”. This is apparently for systems which have different storage formats for ASCII and binary data. In all my testing, nobody sets this flag to anything but . An extra flag indicating how hard the compressor worked. signals that it was compressed with max compression (e.g., ), for the fastest algorithm, and for everything else. In practice, zlib and many others set this correctly per the spec, but some tools hard-code it to . And as far as I can tell, this byte is not used during decompression, so it doesn’t really matter. The original file name . For example, when I run , the name is set to . This field is optional, so many tools don’t set it, but the command line tool does. You can disable that with . A comment . This optional field is seldom used, and many decompressors ignore it. But you could add a little comment if you want. Extra arbitrary data . If the other metadata wasn’t enough, you can stuff whatever you want into arbitrary subfields. Each subfield has a two-byte identifier and then 0 or more bytes of additional info.

0 views
daniel.haxx.se 3 months ago

curl security moves again

tldr: curl goes back to Hackerone. When we announced the end of the curl bug-bounty at the end of January 2026, we simultaneously moved over and started accepting curl security reports on GitHub instead of its previous platform. This move turns out to have been a mistake and we are now undoing that part of the decision. The reward money is still gone, there is no bug-bounty , no money for vulnerability reports, but we return to accepting and handling curl vulnerability and security reports on Hackerone . Starting March 1st 2026, this is now (again) the official place to report security problems to the curl project. This zig-zagging is unfortunate but we do it with the best of intentions. In the curl security team we were naively thinking that since so many projects are already using this setup it should be good enough for us too since we don’t have any particular special requirements. We wrongly thought . Now I instead question how other Open Source projects can use this. It feels like an area and use case for Open Source projects that is under-focused: proper, secure and efficient vulnerability reporting without bug-bounty. To illustrate what we are looking for, I made a little list that should show that we’re not looking for overly crazy things. Here is a list of nits and missing features we fell over on GitHub that, had we figured them out ahead of time, possibly would have made us go about this a different way. This list might interest fellow maintainers having the same thoughts and ideas we had. I have provided this feedback to GitHub as well – to make sure they know . Sure, we could switch to handling them all over email but that also has its set of challenges. Including: Since we dropped the bounty, the inflow tsunami has dried out substantially . Perhaps partly because of our switch over to GitHub? Perhaps it just takes a while for all the sloptimists to figure out where to send the reports now and perhaps by going back to Hackerone we again open the gates for them? We just have to see what happens. We will keep iterating and tweaking the program, the settings and the hosting providers going forward to improve. To make sure we ship a robust and secure set of products and that the team doing so can do that If you suspect a security problem in curl or libcurl, report it here: https://hackerone.com/curl Gitlab, Codeberg and others are GitHub alternatives and competitors, but few of them offer this kind of security reporting feature. That makes them bad alternatives or replacements for us for this particular service. Incoming submissions are reports that identify security problems . The reporter needs an account on the system. Submissions start private; only accessible to the reporter and the curl security team All submissions must be disclosed and made public once dealt with. Both correct and incorrect ones. This is important. We are Open Source. Maximum transparency is key. There should be a way to discuss the problem amongst security team members, the reporter and per-report invited guests. It should be possible to post security-team-only messages that the reporter and invited guests cannot see For confirmed vulnerabilities, an advisory will be produced that the system could help facilitate If there’s a field for CVE, make it possible to provide our own. We are after all our own CNA. Closed and disclosed reports should be clearly marked as invalid/valid etc Reports should have a tagging system so that they can be marked as “AI slop” or other terms for statistical and metric reasons Abusive users should be possible to ban/block from this program Additional (customizable) requirements for the privilege of submitting reports is appreciated (rate limit, time since account creation, etc) GitHub sends the whole report over email/notification with no way to disable this. SMTP and email is known for being insecure and cannot assure end to end protection. This risks leaking secrets early to the entire email chain. We can’t disclose invalid reports (and make them clearly marked as such) Per-repository default collaborators on GitHub Security Advisories is annoying to manage, as we now have to manually add the security team for each advisory or have a rather quirky workflow scripting it. https://github.com/orgs/community/discussions/63041 We can’t edit the CVE number field! We are a CNA, we mint our own CVE records so this is frustrating. This adds confusion. We want to (optionally) get rid of the CVSS score + calculator in the form as we actively discourage using those in curl CVE records No CI jobs working in private forks is going to make us effectively not use such forks, but is not a big obstacle for us because of our vulnerability working process. https://github.com/orgs/community/discussions/35165 No “quote” in the discussions? That looks… like an omission. We want to use GitHub’s security advisories as the report to the project, not the final advisory (as we write that ourselves) which might get confusing, as even for the confirmed ones, the project advisories (hosted elsewhere) are the official ones, not the ones on GitHub No number of advisories count is displayed next to “security” up in the tabs, like for issues and Pull requests. This makes it hard to see progress/updates. When looking at an individual advisory, there is no direct button/link to go back to the list of current advisories In an advisory, you can only “report content”, there is no direct “block user” option like for issues There is no way to add private comments for the team-only, as when discussing abuse or details not intended for the reporter or other invited persons in the issue There is a lack of short (internal) identifier or name per issue, which makes it annoying and hard to refer to specific reports when discussing them in the security team. The existing identifiers are long and hard to differentiate from each other. You quite weirdly cannot get completion help for in comments to address people that were added into the advisory thanks to them being in a team you added to the issue? There are no labels, like for issues and pull requests, which makes it impossible for us to for example mark the AI slop ones or other things, for statistics, metrics and future research Hard to keep track of the state of each current issue when a number of them are managed in parallel. Even just to see how many cases are still currently open or in need of attention. Hard to publish and disclose the invalid ones, as they never cause an advisory to get written and we rather want the initial report and the full follow-up discussion published. Hard to adapt to or use a reputation system beyond just the boolean “these people are banned”. I suspect that we over time need to use more crowdsourced knowledge or reputation based on how the reporters have behaved previously or in relation to other projects.

0 views
Anton Zhiyanov 3 months ago

(Un)portable defer in C

Modern system programming languages, from Hare to Zig, seem to agree that is a must-have feature. It's hard to argue with that, because makes it much easier to free memory and other resources correctly, which is crucial in languages without garbage collection. The situation in C is different. There was a N2895 proposal by Jens Gustedt and Robert Seacord in 2021, but it was not accepted for C23. Now, there's another N3734 proposal by JeanHeyd Meneide, which will probably be accepted in the next standard version. Since isn't part of the standard, people have created lots of different implementations. Let's take a quick look at them and see if we can find the best one. C23/GCC  • C11/GCC  • GCC/Clang  • MSVC  • Long jump  • STC  • Stack  • Simplified GCC/Clang  • Final thoughts Jens Gustedt offers this brief version: Usage example: This approach combines C23 attribute syntax ( ) with GCC-specific features: nested functions ( ) and the attribute. It also uses the non-standard macro (supported by GCC, Clang, and MSVC), which expands to an automatically increasing integer value. Nested functions and cleanup in GCC A nested function (also known as a local function) is a function defined inside another function: Nested functions can access variables from the enclosing scope, similar to closures in other languages, but they are not first-class citizens and cannot be passed around like function pointers. The attribute runs a function when the variable goes out of scope: The function should take one parameter, which is a pointer to a type that's compatible with the variable. If the function returns a value, it will be ignored. On the plus side, this version works just like you'd expect to work. On the downside, it's only available in C23+ and only works with GCC (not even Clang supports it, because of the nested function). We can easily adapt the above version to use C11: Usage example: The main downside remains: it's GCC-only. Clang fully supports the attribute, but it doesn't support nested functions. Instead, it offers the blocks extension, which works somewhat similar: We can use Clang blocks to make a version that works with both GCC and Clang: Usage example: Now it works with Clang, but there are several things to be aware of: On the plus side, this implementation works with both GCC and Clang. The downside is that it's still not standard C, and won't work with other compilers like MSVC. MSVC, of course, doesn't support the cleanup attribute. But it provides "structured exception handling" with the and keywords: The code in the block will always run, no matter how the block exits — whether it finishes normally, returns early, or crashes (for example, from a null pointer dereference). This isn't the we're looking for, but it's a decent alternative if you're only programming for Windows. There are well-known implementations by Jens Gustedt and moon-chilled that use and . I'm mentioning them for completeness, but honestly, I would never use them in production. The first one is extremely large, and the second one is extremely hacky. Also, I'd rather not use long jumps unless it's absolutely necessary. Still, here's a usage example from Gustedt's library: Here, all deferred statements run at the end of the guarded block, no matter how we exit the block (normally or through ). The stc library probably has the simplest implementation ever: Usage example: Here, the deferred statement is passed as and is used as the loop increment. The "defer-aware" block of code is the loop body. Since the increment runs after the body, the deferred statement executes after the main code. This approach works with all mainstream compilers, but it falls apart if you try to exit early with or : Dmitriy Kubyshkin provides a implementation that adds a "stack frame" of deferred calls to any function that needs them. Here's a simplified version: Usage example: This version works with all mainstream compilers. Also, unlike the STC version, defers run correctly in case of early exit: Unfortunately, there are some drawbacks: The Stack version above doesn't support deferring code blocks. In my opinion, that's not a problem, since most defers are just "free this resource" actions, which only need a single function call with one argument. If we accept this limitation, we can simplify the GCC/Clang version by dropping GCC's nested functions and Clang's blocks: Works like a charm: Personally, I like the simpler GCC/Clang version better. Not having MSVC support isn't a big deal, since we can run GCC on Windows or use the Zig compiler, which works just fine. But if I really need to support GCC, Clang, and MSVC — I'd probably go with the Stack version. Anyway, I don't think we need to wait for to be added to the C standard. We already have at home! We must compile with . We must put a after the closing brace in the deferred block: . If we need to modify a variable inside the block, the variable must be declared with : Defer only supports single-function calls, not code blocks. We always have to call at the start of the function and exit using . In the original implementation, Dmitriy overrides the keyword, but this won't compile with strict compile flags (which I think we should always use). The deferred function runs before the return value is evaluated, not after.

0 views
Anton Zhiyanov 5 months ago

'Better C' playgrounds

I have a soft spot for the "better C" family of languages: C3, Hare, Odin, V, and Zig. I'm not saying these languages are actually better than C — they're just different. But I needed to come up with an umbrella term for them, and "better C" was the only thing that came to mind. I believe playgrounds and interactive documentation make programming languages easier for more people to learn. That's why I created online sandboxes for these langs. You can try them out below, embed them on your own website, or self-host and customize them. If you're already familiar with one of these languages, maybe you could even create an interactive guide for it? I'm happy to help if you want to give it a try. C3  • Hare  • Odin  • V  • Zig  • Editors An ergonomic, safe, and familiar evolution of C. ⛫  homepage • αω  tutorial • ⚘  community A systems programming language designed to be simple, stable, and robust. ⛫  homepage • αω  tutorial • ⚘  community A high-performance, data-oriented systems programming language. ⛫  homepage • αω  tutorial • ⚘  community A language with C-level performance and rapid compilation speeds. ⛫  homepage • αω  tutorial • ⚘  community A language designed for performance and explicit control with powerful metaprogramming. ⛫  homepage • αω  tutorial • ⚘  community If you want to do more than just "hello world," there are also full-size online editors . They're pretty basic, but still can be useful.

0 views
matklad 5 months ago

Static Allocation For Compilers

TigerBeetle famously uses “static allocation” . Infamously, the use of the term is idiosyncratic: what is meant is not arrays, as found in embedded development, but rather a weaker “no allocation after startup” form. The amount of memory TigerBeetle process uses is not hard-coded into the Elf binary. It depends on the runtime command line arguments. However, all allocation happens at startup, and there’s no deallocation. The long-lived event loop goes round and round happily without . I’ve wondered for years if a similar technique is applicable to compilers. It seemed impossible, but today I’ve managed to extract something actionable from this idea? Static allocation depends on the physics of the underlying problem. And distributed databases have surprisingly simple physics, at least in the case of TigerBeetle. The only inputs and outputs of the system are messages. Each message is finite in size (1MiB). The actual data of the system is stored on disk and can be arbitrarily large. But the diff applied by a single message is finite. And, if your input is finite, and your output is finite, it’s actually quite hard to need to allocate extra memory! This is worth emphasizing — it might seem like doing static allocation is tough and requires constant vigilance and manual accounting for resources. In practice, I learned that it is surprisingly compositional. As long as inputs and outputs of a system are finite, non-allocating processing is easy. And you can put two such systems together without much trouble. routing.zig is a good example of such an isolated subsystem. The only issue here is that there isn’t a physical limit on how many messages can arrive at the same time. Obviously, you can’t process arbitrary many messages simultaneously. But in the context of a distributed system over an unreliable network, a safe move is to drop a message on the floor if the required processing resources are not available. Counter-intuitively, not allocating is simpler than allocating, provided that you can pull it off! Alas, it seems impossible to pull it off for compilers. You could say something like “hey, the largest program will have at most one million functions”, but that will lead to both wasted memory and poor user experience. You could also use a single yolo arena of a fixed size, like I did in Hard Mode Rust , but that isn’t at all similar to “static allocation”. With arenas, the size is fixed explicitly, but you can OOM. With static allocation it is the opposite — no OOM, but you don’t know how much memory you’ll need until startup finishes! The “problem size” for a compiler isn’t fixed — both the input (source code) and the output (executable) can be arbitrarily large. But that is also the case for TigerBeetle — the size of the database is not fixed, it’s just that TigerBeetle gets to cheat and store it on disk, rather than in RAM. And TigerBeetle doesn’t do “static allocation” on disk, it can fail with at runtime, and it includes a dynamic block allocator to avoid that as long as possible by re-using no longer relevant sectors. So what we could say is that a compiler consumes arbitrarily large input, and produces arbitrarily large output, but those “do not count” for the purpose of static memory allocation. At the start, we set aside an “output arena” for storing finished, immutable results of compiler’s work. We then say that this output is accumulated after processing a sequence of chunks, where chunk size is strictly finite. While limiting the total size of the code-base is unreasonable, limiting a single file to, say, 4 MiB (runtime-overridable) is fine. Compiling then essentially becomes a “stream processing” problem, where both inputs and outputs are arbitrary large, but the filter program itself must execute in O(1) memory. With this setup, it is natural to use indexes rather than pointers for “output data”, which then makes it easy to persist it to disk between changes. And it’s also natural to think about “chunks of changes” not only spatially (compiler sees a new file), but also temporally (compiler sees a new version of an old file). Is there any practical benefits here? I don’t know! But seems worth playing around with! I feel that a strict separation between O(N) compiler output and O(1) intermediate processing artifacts can clarify compiler’s architecture, and I won’t be too surprised if O(1) processing in compilers would lead to simpler code the same way it does for databases?

0 views
Michael Lynch 5 months ago

Refactoring English: Month 12

Hi, I’m Michael. I’m a software developer and founder of small, indie tech businesses. I’m currently working on a book called Refactoring English: Effective Writing for Software Developers . Every month, I publish a retrospective like this one to share how things are going with my book and my professional life overall. At the start of each month, I declare what I’d like to accomplish. Here’s how I did against those goals: I’ve gotten stuck on my design docs chapter. There’s a lot I want to cover, and I’m having trouble articulating some of it and deciding how much of it belongs in the book. Part of the problem is that the chapter is so long that it feels overwhelming to tackle all at once. My new plan is to break the chapter into smaller sections and focus on those one at a time. I think this is my last “hard” chapter, as I have a better sense of what I want to say in the remaining chapters. I keep procrastinating on this even though I enjoy doing it and get useful responses. I keep automating more of the logistical work in the hopes that reducing initial friction will motivate me to do it more. 3,508 people read the post, so it was somewhat successful at attracting new readers. Bob Nystrom, the author I was writing about, liked my article , which was gratifying. I figured even if my article flopped, at least it would let Bob Nystrom know how much I appreciated his work. November was a good month in terms of visits and sales. Visits were down slightly from October, but it was still one of the strongest months of the year. I did a Black Friday discount for 30% off. I only advertised it to readers on my mailing list, as I always feel strange spamming a sale everywhere. But the announcement was successful, as 18 customers purchased for a total of $359.41. Peter Spiess-Knafl , co-founder of zeitkapsl , cited Refactoring English in a blog post , which reached #1 on Lobsters . I was glad to see Peter’s post, as my plan for the book has always been for it to help readers write successful blog posts and be happy enough about the book that they recommend it. I read Hacker News so often that I feel like I’d be good at predicting which stories will reach the front page, but I’ve never tested this belief rigorously. So, I made a game to test my accuracy. The game shows me the newest submissions to Hacker News, and the player predicts whether or not they’ll reach the front page: The biggest problem with the game is that a story can take up to 24 hours to reach the front page. Waiting 24 hours for results sucks the fun out of the game. I tried changing the rules so that you’re predicting whether an article will reach the front page in its first 30 minutes, but 30 minutes still feels painfully slow. My new idea is to make a tentative call 10 minutes after a story has been submitted. Given the story’s age, upvotes, and comment count, I can calculate some rough probability of whether it has a chance of hitting the front page. So, if you predicted a story would reach the front page, but 10 minutes later, it still has no upvotes or comments, the game will tentatively tell you that you got it wrong, but you can still get the points back if the story makes a miraculous comeback in the next 24 hours. I thought about making a version of the game where you guess the results of past stories. That way, I could give instant feedback because the answer is already available, but that feels less fun, as other people have made similar games. Plus, for the HN diehards I’m hoping this game appeals to, past data ruins it because you kind of remember what was on the front page and what wasn’t. My wife and I had our first child last year , so we wanted a way to share baby photos with our family privately. Some of my friends had used apps like this, but they were all ad-supported. I hate the idea of companies slapping ads on photos of my child, so I looked for other options. When I came across TinyBeans, I thought I’d found a winner. They had a paid version that disabled ads, and privacy was the main feature they advertised: perfect! Then, I started using TinyBeans, and there were ads everywhere. “Buy our photo books!” “Give us more personal information!” I opened the app just now and had to dismiss three separate ads to see photos of my own child. TinyBeans shows me three huge ads when I open the app, even though I’m a paying customer and have dismissed these exact ads dozens of times before. It also turns out that my family members receive even more ads than I see, including for third-party services. Here’s a recent one that encourages my family to invest in some scammy AI company: When TinyBeans sends emails to my family, they stick spammy ads like these in between photos of my son. The “no ads” promise of the paid tier is limited to me and my wife; TinyBeans bombards everyone else in my family with ads and upsells. I wanted to ditch TinyBeans early on, but I was too busy with new parent stuff to find a new app and migrate my whole family to it. So, each month, I begrudgingly give TinyBeans my $9. Then, Black Friday happened. TinyBeans sent me an email patting themselves on the back for not cluttering my inbox with Black Friday deals because all the deals would be in the app. TinyBeans sends me a pointless email to boast about not cluttering my inbox with pointless emails. Great, an email congratulating yourself about how little you’ll email me. But that wasn’t even true! TinyBeans proceeded to send me four more emails telling me to check my app for Black Friday deals: After promising not to bombard me with Black Friday promotions, TinyBeans emailed me five Black Friday promotions. That pushed me over the edge, and now I’m on a spite mission to create my own TinyBeans replacement and stop giving TinyBeans my money. “And what are your reasons for wanting to create an app to share baby photos?” The only functionality I care about in TinyBeans is: How hard could that be? 20 hours of dev work? The TinyBeans web and Android apps suck anyway, so I’ll be glad to move away from them. And because the experience is mostly email-based, I can replace TinyBeans with my own app without my family having to do any work as part of the migration. I’m not starting a company to compete with TinyBeans. I just want to make a web app that replaces TinyBeans’ functionality. One of my shameful secrets as a developer is that I’m bad at managing windows on my screen. I compensate by overusing my mouse, even though that’s slow and inefficient. Last year, I switched from Windows to Linux and got a 49" ultrawide monitor . While Windows was designed for mouse-happy users like me, Linux desktops are much more keyboard-focused, so my lack of keyboard discipline began catching up with me. I’d keep opening windows and never close them, so I’d end up with 10+ VS Code windows, 10+ Firefox windows, and 5 different instances of the calculator app for one-off calculations. They were all in one big pile in the middle of my desktop. At that point, it was obvious I was wasting tons of screen real estate and burning time locating my windows. I tried a few different window managers, but I kept running into issues. Like I couldn’t get lockscreens to work, or they’d fail to use my monitor’s full 5120x1440 resolution. The fastest person I’ve ever seen navigate their computer is my friend okay zed . I asked him for advice, and he explained his approach to window management . His strategy is to use many virtual desktops where windows are almost always full screen within the desktop. He uses xmonad, but he suggested I try Awesome Window Manager. I liked okay’s philosophy of single-purpose virtual desktops, so I created an Awesome window manager configuration around it. So, I have a dedicated desktop for my blog, a dedicated desktop for my book, one for email, etc. I try to limit myself to 1-2 windows per desktop, but sometimes I’ll pull up a third or fourth while looking something up. Here’s what my blog desktop looks like, which is pretty typical: one VS Code window for editing, one Firefox window for viewing the result, and sometimes a second Firefox window for looking stuff up: I didn’t like any of the default desktop modes, so I had to roll my own . It gives each window 25% of my screen’s width, and if I open more than four, it squishes everything to fit. I can also manually expand or contract windows with Shift+Win+H and Shift+Win+L. Except sometimes I accidentally lock myself out because Win+L is my hotkey for locking the screen. Based on a few weeks with Awesome, here’s how I’m feeling: I was talking to LGUG2Z on Mastodon about how annoying it is to embed tweets on my blog. If the user deletes their tweet, I end up with dead content in my post. Even when it works, my readers have to load trackers from Twitter. I’ve been working around it by just screenshotting tweets, but that’s an ugly solution. I want to embed tweets in Hugo (the static site generator I use for this blog) with a shortcode like , and then Hugo could fetch the tweet data and store it under source control so that I don’t have an ongoing dependency on Twitter. LGUG2Z explored this idea and implemented support for it on his Zola blog. He runs a script to pre-download data once from external sources (like tweets), and then he can embed the content in his blog without re-retrieving it at blog build time or reader visit time. I tried to adapt LGUG2Z’s solution for Hugo, but it got too complicated . I wrote a standalone script that downloads data from Twitter and then I’d render it in a tweet-like UI . Regular text tweets worked okay, but once I got to tweets with embedded media or retweets, it felt like I was building too much on shaky foundation. I used to store all of my photos on Google Photos. Despite my privacy concerns, Google Photos was just so much better than anything else that I held my nose and just gave them all my photos. I’ve since become more privacy sensitive and distrustful of Google, so I stopped uploading new photos to Google Photos, but I haven’t found a replacement. I’ve heard good things about Immich and Ente, so I was glad to see this detailed writeup from Michael Stapelberg about his experience setting up an Immich server using NixOS . Firefox recently improved their Enhanced Tracking Protection , a feature I didn’t realize existed. I turned it on, and it blocks trackers that uBlock was allowing and hasn’t had any false positives. I just discovered “Rich Friend, Poor Friend” from 2022 and the follow up from a few weeks ago. I definitely relate to hiring professionals instead of asking my friends for help (e.g., hiring movers instead of asking friends). I’m maybe in the worst part of the curve where I’m wealthy enough to not want to ask friends to help me move but not so wealthy that I have a separate guest house to make it easy to host them. The Deel corporate espionage story is getting surprisingly little attention in my bubble. In March 2025, Rippling revealed that they discovered one of their employees was actually a corporate spy working for their competitor, Deel. When they caught the spy, he ran into the bathroom and tried to flush his phone down the toilet. Rippling posted an update in November that they found banking records showing that Deel had routed payments to the spy through the wife of Deel’s COO. The wife was, coincidentally, a compliance lead at Robinhood, another company known for its scummy ethics . As an unhappy former Deel customer, I’m happy to see them get their comeuppance. I’m working on a game to predict which posts will reach the front page of Hacker News. I’m creating a family photo sharing app out of spite. I switched to a keyboard-first window manager. Result : Published one new chapter Result : I only reached out to two readers (one responded). Result : Published “What Makes the Intro to Crafting Interpreters so Good?” My family can browse the baby photos and videos I’ve uploaded. My family members can subscribe to receive new photos and videos via email. My family members can comment or give emoji reactions to photos. What I like Encourages me to keep single-purpose desktops for better focus. Encourages me to navigate via keyboard hotkeys rather than mouse clicks. Doesn’t crash on suspend 2% of the time like Gnome did. What I dislike Everything is implemented in and configured through Lua, a language I don’t know. I’m using LLMs to write all my configs. The configuration is fairly low-level, so you have to write your own logic for things like filling the viewport without overflowing it. I don’t like any of the default desktop modes, so I had to roll my own. The documentation is all text, which feels bizarre for software designed specifically around graphics. If you accidentally define conflicting hotkeys, Awesome doesn’t warn you. If I click a link outside of Firefox, sometimes it loads the link in a browser that isn’t on my current desktop. I’m guessing it loads it on whatever Firefox window I most recently touched. What I still need to figure out How to implement “scratchpad” functionality. Like if I want to pull up my password manager as a floating window or summon the calculator for a quick calculation, then dismiss it. How to put more widgets into the status bar like network connectivity and resource usage. Published “What Makes the Intro to Crafting Interpreters so Good?” Published “My First Impressions of MeshCore Off-Grid Messaging” . Published “Add a VLAN to OPNsense in Just 26 Clicks Across 6 Screens” Created a tiny Zig utility called count-clicks to count clicks and keystrokes on an x11 system. Got Awesome Window Manager working. Quick feedback is important in creating a fun game. TinyBeans actually has a lot of ads, even on the paid version. The Awesome window manager is a better fit for my needs than Gnome. Publish a game that attracts people to the Refactoring English website. Publish two chapters of Refactoring English . Write a design doc for a just-for-fun family photo sharing app. If you’re interested in beta testing the “Will it Hit the Front Page?” game, reach out .

1 views
Lukáš Lalinský 7 months ago

How I turned Zig into my favorite language to write network programs in

I’ve been watching the Zig language for a while now, given that it was created for writing audio software (low-level, no allocations, real time). I never paid too much attention though, it seemed a little weird to me and I didn’t see the real need. Then I saw a post from Andrew Kelley (creator of the language) on Hacker News, about how he reimplemented my Chromaprint algorithm in Zig, and that got me really interested. I’ve been planning to rewrite AcoustID’s inverted index for a long time, I had a couple of prototypes, but none of the approaches felt right. I was going through some rough times, wanted to learn something new, so I decided to use the project as an opportunity to learn Zig. And it was great, writing Zig is a joy. The new version was faster and more scalable than the previous C++ one. I was happy, until I wanted to add a server interface. In the previous C++ version, I used Qt , which might seem very strange for a server software, but I wanted a nice way of doing asynchronous I/O and Qt allowed me to do that. It was callback-based, but Qt has a lot of support for making callbacks usable. In the newer prototypes, I used Go, specifically for the ease of networking and concurrency. With Zig, I was stuck. There are some Zig HTTP servers, so I could use those. I wanted to implement my legacy TCP server as well, and that’s a lot harder, unless I want to spawn a lot of threads. Then I made a crazy decision, to use Zig also for implementing a clustered layer on top of my server, using NATS as a messaging system, so I wrote a Zig NATS client , and that gave me a lot of experience with Zig’s networking capabilities. Fast forward to today, I’m happy to introduce Zio, an asynchronous I/O and concurrency library for Zig . If you look at the examples, you will not really see where is the asynchronous I/O, but it’s there, in the background and that’s the point. Writing asynchronous code with callbacks is a pain. Not only that, it requires a lot of allocations, because you need state to survive across callbacks. Zio is an implementation of Go style concurrency, but limited to what’s possible in Zig. Zio tasks are stackful coroutines with fixed-size stacks. When you run , this will initiate the I/O operation in the background and then suspend the current task until the I/O operation is done. When it’s done, the task will be resumed, and the result will be returned. That gives you the illusion of synchronous code, allowing for much simpler state management. Zio support fully asynchronous network and file I/O, has synchronization primitives (mutexes, condition variables, etc.) that work with the cooperative runtime, has Go-style channels, OS signal watches and more. Tasks can run in single-threaded mode, or multi-threaded, in which case they can migrate from thread to thread for lower latency and better load balancing. And it’s FAST. I don’t want to be posting benchmarks here, maybe later when I have more complex ones, but the single-threaded mode is beating any framework I’ve tried so far. It’s much faster than both Go and Rust’s Tokio. Context switching is virtually free, comparable to a function call. The multi-threaded mode, while still not being as robust as Go/Tokio, has comparable performance. It’s still a bit faster than either of them, but that performance might go down as I add more fairness features. Because it implements the standard interfaces for reader/writer, you can actually use external libraries that are unaware they are running within Zio. Here is an example of a HTTP server: When I started working with Zig, I really thought it’s going to be a niche language to write the fast code in, and then I’ll need a layer on top of that in a different language. With Zio, that changed. The next step for me is to update my NATS client to use Zio internally. And after that, I’m going to work on a HTTP client/server library based on Zio.

0 views