Full Width [alt+shift+f] Shortcuts [alt+shift+k]
Sign Up [alt+shift+s] Log In [alt+shift+l]
26
The dream has come true. It’s now possible to build fast, modern web applications without transpiling or bundling either JavaScript or CSS. I’ve been working towards this personal nirvana ever since we begrudgingly started transpiling and bundling assets in the late 2000s. Browsers just weren’t good enough back then to avoid it. But they are now. Here’s a chart of the 68 individual JavaScript files that we load in ONCE #1 via import maps. The waterfall is vertical. HTTP/2 ensures we scarcely pay any penalty for sending so many individual files, yet reap huge rewards with fine-grained cache expiration: I know you have to be a real nerd to think this is pretty, but I think this is pretty. Because I know what’s behind it. I know that looking at any of those individual files via View Source reveals exactly the same source code that I see in my editor. It’s been a long time since JavaScript developers and designers could say that. Now it’s possible. And yes, this is...
a year ago

Improve your reading experience

Logged in users get linked directly to articles resulting in a better reading experience. Please login for free, it takes less than 1 minute.

More from David Heinemeier Hansson

The parental dead end of consent morality

Consent morality is the idea that there are no higher values or virtues than allowing consenting adults to do whatever they please. As long as they're not hurting anyone, it's all good, and whoever might have a problem with that is by definition a bigot.  This was the overriding morality I picked up as a child of the 90s. From TV, movies, music, and popular culture. Fly your freak! Whatever feels right is right! It doesn't seem like much has changed since then. What a moral dead end. I first heard the term consent morality as part of Louise Perry's critique of the sexual revolution. That in the context of hook-up culture, situationships, and falling birthrates, we have to wrestle with the fact that the sexual revolution — and it's insistence that, say, a sky-high body count mustn't be taboo — has led society to screwy dating market in the internet age that few people are actually happy with. But the application of consent morality that I actually find even more troubling is towards parenthood. As is widely acknowledged now, we're in a bit of a birthrate crisis all over the world. And I think consent morality can help explain part of it. I was reminded of this when I posted a cute video of a young girl so over-the-moon excited for her dad getting off work to argue that you'd be crazy to trade that for some nebulous concept of "personal freedom". Predictably, consent morality immediately appeared in the comments: Some people just don't want children and that's TOTALLY OKAY and you're actually bad for suggesting they should! No. It's the role of a well-functioning culture to guide people towards The Good Life. Not force, but guide. Nobody wants to be convinced by the morality police at the pointy end of a bayonet, but giving up on the whole idea of objective higher values and virtues is a nihilistic and cowardly alternative. Humans are deeply mimetic creatures. It's imperative that we celebrate what's good, true, and beautiful, such that these ideals become collective markers for morality. Such that they guide behavior. I don't think we've done a good job at doing that with parenthood in the last thirty-plus years. In fact, I'd argue we've done just about everything to undermine the cultural appeal of the simple yet divine satisfaction of child rearing (and by extension maligned the square family unit with mom, dad, and a few kids). Partly out of a coordinated campaign against the family unit as some sort of trad (possibly fascist!) identity marker in a long-waged culture war, but perhaps just as much out of the banal denigration of how boring and limiting it must be to carry such simple burdens as being a father or a mother in modern society. It's no wonder that if you incessantly focus on how expensive it is, how little sleep you get, how terrifying the responsibility is, and how much stress is involved with parenthood that it doesn't seem all that appealing! This is where Jordan Peterson does his best work. In advocating for the deeper meaning of embracing burden and responsibility. In diagnosing that much of our modern malaise does not come from carrying too much, but from carrying too little. That a myopic focus on personal freedom — the nights out, the "me time", the money saved — is a spiritual mirage: You think you want the paradise of nothing ever being asked of you, but it turns out to be the hell of nobody ever needing you. Whatever the cause, I think part of the cure is for our culture to reembrace the virtue and the value of parenthood without reservation. To stop centering the margins and their pathologies. To start centering the overwhelming middle where most people make for good parents, and will come to see that role as the most meaningful part they've played in their time on this planet. But this requires giving up on consent morality as the only way to find our path to The Good Life. It involves taking a moral stance that some ways of living are better than other ways of living for the broad many. That parenthood is good, that we need more children both for the literal survival of civilization, but also for the collective motivation to guard against the bad, the false, and the ugly. There's more to life than what you feel like doing in the moment. The worst thing in the world is not to have others ask more of you. Giving up on the total freedom of the unmoored life is a small price to pay for finding the deeper meaning in a tethered relationship with continuing a bloodline that's been drawn for hundreds of thousands of years before it came to you. You're never going to be "ready" before you take the leap. If you keep waiting, you'll wait until the window has closed, and all you see is regret. Summon a bit of bravery, don't overthink it, and do your part for the future of the world. It's 2.1 or bust, baby!

4 days ago 4 votes
Self-driving is finally happening

I still remember how the car industry got all excited back in 2017 about how steering wheels would soon be obsolete. Every concept car then was a living room on wheels, seats facing inwards. The self-driving revolution was imminent, they said. Well, it wasn't... but now it actually is! Humans have a hard time with scenarios like this. If you promise them the moon in eight months, but don't end up delivering until eight years later, most will justifiably be skeptical that it's actually here — even in the face of gushing anecdotes and video evidence. That's the problem with delayed promises. So when Jason told me Tesla's self-driving tech was finally ready and real, I was indeed skeptical. I tried FSD as late as last year, and I didn't enjoy it much. Impressive in many ways, but too jerky. Too many interventions. How much could it really have improved in nine months or so? A lot, it turns out. We started the drive from Jason's house, and I watched him not once touch the pedals or steering wheel while we drove half an hour to the other end of town. Then repeated the feat on the way back. But that wasn't even the most impressive part. What really blew my mind was how dramatically better the fluidity of driving with FSD has become. His new Model Y anticipated the red light with the manners of a drives-for-the-queen-level chauffeur. And the way it knew exactly how to slow down to prevent a jerky movement when taking an incline into an elevated parking lot? Sublime. Elon, that son of a bitch, seems to have done it again! Proven everyone wrong. Proven me wrong. The self-driving dream has flipped from vaporware to credible near-term reality. All without LiDAR. AI really delivering on this one.

6 days ago 8 votes
Omarchy is out

My latest love letter to Linux has been published. It's called Omarchy, and it's an opinionated setup of the Arch Linux distribution and the Hyprland tiling window manager. With everything configured out-of-the-box to give you exactly the same setup that I now run every day. My Platonic ideal of what a developer environment should look like. It's not for everyone, though. Arch has a reputation for being difficult, but while I think that's vastly overstated, I still think it's fair to say that Ubuntu is an easier landing for someone new to Linux. And that's why this exists as a sister project to Omakub — my opinionated setup for Ubuntu — and not a replacement of it. Because I do think that Hyprland deserves its reputation of being difficult! Not because the core tiling window manager is hard, but because it comes incredibly bare-boned in the box. You have to figure out everything yourself. Even how to get a lock screen or idle timing or a menu bar or bluetooth setting or... you get the idea. Omarchy is an attempt to solve for all that. To give you a default set of great, beautiful configurations for Hyprland, and installing all the common tooling you'd normally want. You could setup this, not change a thing, and you'll have exactly what I run every day. But you can also just use this as a paved path into the glorious world of Linux ricing. The flip side of Hyprland being so atomized is that it's infinitely configurable. You can really, really make it yours. No wonder its the preferred platform for r/unixporn, and even what PewDiePie picked up for his amazing Russian nuclear core build. I don't know when we'll literally get "The Year of Linux on the Desktop", but I've never been as convinced that its coming as I am now. There's enough dissent in the water. Enough dissatisfaction with both Apple and Microsoft.  And between Valve going all-in on Steam on Linux (the Steamdeck runs Arch!), major creators (like PewDiePie) switching to Linux, and incredible projects like Hyprland — which offer not just a cheap visual copy of the two major commercial operating systems, but something much more unique and compelling — I think all the factors are in place for a big switch. At least among developers. But broad adoption or not, I'm in love with Linux, and thrilled to share my work to make it easier to enjoy.

a week ago 13 votes
Gender and Sexuality Alliances in primary school at CIS?!

The Copenhagen International School is a wonderful private school located in the North Harbor of the city. It's home to over 900 students from around the world. This is where ambassadors, international executives, and other expats send their kids to get a great education in English while stationed in Denmark. As a result, it's perhaps the most diverse, inclusive school in all of Copenhagen. Lovely. What's less lovely is the fact that CIS seems to have caught some of the same gender-ideology obsession that has ravaged many schools in America. We thought Copenhagen would offer a respite from the woke nonsense that's been plaguing California — where some schools in our social circle ended up with a quarter or more of the student body identifying as trans or gender nonconformative — but it seems ideological contagions travel as fast as airplanes these days. It started last week, when the primary school, which includes kindergarten, declared its intention to spend every morning meeting for the entire week focused on gender dysphoria, transgenderism, they/them pronoun protocols, and coloring pride flags. That just sounded a bit odd and a bit much at first, but after reviewing the associated material, it actually looked downright devious. Just look at this example: Draw yourself in the mirror, then adorn it with trans colors? And the guiding example is a boy who sees himself as a girl? As you can imagine, many parents at the school were mortified by the idea of their children participating in this kind of overt indoctrination activities, and some of them let the school know. That's when the revisions started rolling out.  First, the program was revised to no longer apply to kindergarten and first grade, just second through fifth. Then the "draw yourself in the mirror and use trans colors to decorate it" activity was pulled from the program. Then the schedule was reduced from all week to just a single session this Monday while the rest of the material is being "reconsidered". And that's where it stands today. But that's not all. After talking to a number of other parents, I learned that CIS has other highly objectionable programs in this sphere. Like "Gender and Sexuality Alliances" where primary school students in G3-5, meaning kids as young as eight, are invited to join in lunch and recess meetings to talk more about gender, sexuality, and how to become a good ally to the 2SLGBTQIA+ community. According to one parent I spoke to (who's considering pulling their kids out over this), CIS hasn't wanted to disclose all specifics about the staff conducting these lunch and recess meetings with the children. Because while it's billed as "student led" on their website, the sessions are actually facilitated by CIS staff on campus.  I've asked the same question of the school administration, including what qualifications these individuals might have, and have not received an answer either. But ultimately, it shouldn't even matter, because this shouldn't even be happening! There's simply no responsible explanation for having kids as young as eight, or even as old as 11, in lunch and recess meetings with CIS staff to discuss gender and sexuality on school campus. It's preposterous, if not outright creepy. The school's mission is no cover either. The commitment to an inclusive school does not offer a license to indulge in this kind of overt indoctrination or inappropriate lunch meetings where minors discuss gender and sexuality with school staff. And it has to stop. CIS, like any other school, should not be a subsidiary of any specific interest organization. We don't want our kids to get their information about climate change from either Extinction Rebellion or fossil-fuel lobbyists. We expect our school to stay politically neutral on the international conflicts, like the one in Gaza. In higher grades where these topics are appropriate, they should be discussed in a context that also includes things like the Cass Review and the recent UK Supreme Court ruling. It's the same reason Copenhagen Pride Week saw a massive loss of sponsorship after trying to cajole major companies into a position on Gaza last year. Novo, Maersk, Google, and many others rejected this organization (and they're not returning this year either) for their partisan politics. It's bizarre that those same companies now have the children of their employees programmed by this organization's agenda at school.  CIS needs to return to its high-level mission of focusing on giving kids an excellent education, teaching them objectively about the world, and upholding general standards for kindness and caring. Not coloring partisan flags during school programs, not facilitating inappropriate meeting forums about gender and sexuality between staff and children.

4 weeks ago 12 votes
Denmark gets more serious about digital sovereignty

The recent disconnection of the ICC's chief prosecutor, at the behest of the American administration, could not have come at a worse time for Microsoft. Just a month prior, the folks from Redmond tried to assure Europe that all was well. That any speculation Europeans could get cut off from critical digital infrastructure was just fear, doubt, and uncertainty. Then everything Europeans worried could happen happened in Hague. Oops! Microsoft's assurances met reality and reality won. That reality is that all American administrations have the power to disconnect any individual, company, or foreign government from digital infrastructure provided by American Big Tech. So in that sense, it's pointless to blame Microsoft for the sanctioning power vested in the Oval Office. But we certainly can blame them for gaslighting Europe about the risk. What's more important than apportioning blame, though, is getting out of the bind that Europe is in. The continent is hopelessly dependent on American Big Tech for even the most basic of digital infrastructure. If this American administration, or the next, decides to use its sanctioning power again, Europe is in a real pickle. And with the actions taken against the ICC in Haag, Europe would be negligent to ignore the threat. Denmark even more so. It's no secret that tensions between Denmark and the US are at a historic high. Trump keeps repeating a desire to take over Greenland by fuzzy means possible. The American intelligence services have been directed to increase their spying on Denmark and Greenland. Naturally, the Danes are spooked. They should be! Regardless of what happens with Greenland, trade negotiations, or geopolitical disagreements, though, it would suit Europe well to become digitally sovereign. That doesn't mean cutting off all American tech, but it does mean rejecting any services that can be turned off from Washington. So in terms of Microsoft, it means no more Microsoft 365, no more Teams, no more Azure. And that's exactly what the two biggest counties in Denmark have announced plans to do. Copenhagen and Aarhus just declared that they're going to get rid of Microsoft products for all their workers. The Copenhagen county is the largest employer in Denmark with over 40,000 employees. So this is a big deal! The chairman of the Copenhagen committee who pushed this forward made this comment to Danish media:  If, theoretically, the relationship to the US gets worse, we could fear that Microsoft would be forced to shut everything down. That possibility exists. And if we suddenly can't access our emails or communicate via our systems, we'll be challenged. That's an understatement. Denmark is one of the most highly digitalized countries in the world. It's also one of the most Microsoft dependent. In fact, Microsoft is by far and away the single biggest dependency, so it makes perfect sense to start the quest for digital sovereignty there. But Denmark is also full of unambitious, defeatist bureaucrats who can't imagine a world without Microsoft. Just today, the IT director for The Capital Region declared it to utopian to think Denmark could ever achieve digital sovereignty or meaningfully replace Microsoft. Not even a decade would make a dent, says the director, while recognizing that if we'd done something 15 years ago, we wouldn't be in this pickle. A remarkable illustration of cognitive dissonance! Sadly, this is not an uncommon conclusion from people who work inside the belly of bureaucracies for too long. Whatever has always done too often seems like the only thing that ever could be done. But, as Mandela said, it always seems impossible until it's done. So let's get it done. Digital sovereignty isn't easy, but neither was securing a sovereign energy supply. Nor will it be to rebuild a credible defensive military. Europe needs all of it, yesterday. The bureaucrats who aren't interested in making it happen should find employment elsewhere.

4 weeks ago 12 votes

More in programming

Logical Quantifiers in Software

I realize that for all I've talked about Logic for Programmers in this newsletter, I never once explained basic logical quantifiers. They're both simple and incredibly useful, so let's do that this week! Sets and quantifiers A set is a collection of unordered, unique elements. {1, 2, 3, …} is a set, as are "every programming language", "every programming language's Wikipedia page", and "every function ever defined in any programming language's standard library". You can put whatever you want in a set, with some very specific limitations to avoid certain paradoxes.2 Once we have a set, we can ask "is something true for all elements of the set" and "is something true for at least one element of the set?" IE, is it true that every programming language has a set collection type in the core language? We would write it like this: # all of them all l in ProgrammingLanguages: HasSetType(l) # at least one some l in ProgrammingLanguages: HasSetType(l) This is the notation I use in the book because it's easy to read, type, and search for. Mathematicians historically had a few different formats; the one I grew up with was ∀x ∈ set: P(x) to mean all x in set, and ∃ to mean some. I use these when writing for just myself, but find them confusing to programmers when communicating. "All" and "some" are respectively referred to as "universal" and "existential" quantifiers. Some cool properties We can simplify expressions with quantifiers, in the same way that we can simplify !(x && y) to !x || !y. First of all, quantifiers are commutative with themselves. some x: some y: P(x,y) is the same as some y: some x: P(x, y). For this reason we can write some x, y: P(x,y) as shorthand. We can even do this when quantifying over different sets, writing some x, x' in X, y in Y instead of some x, x' in X: some y in Y. We can not do this with "alternating quantifiers": all p in Person: some m in Person: Mother(m, p) says that every person has a mother. some m in Person: all p in Person: Mother(m, p) says that someone is every person's mother. Second, existentials distribute over || while universals distribute over &&. "There is some url which returns a 403 or 404" is the same as "there is some url which returns a 403 or some url that returns a 404", and "all PRs pass the linter and the test suites" is the same as "all PRs pass the linter and all PRs pass the test suites". Finally, some and all are duals: some x: P(x) == !(all x: !P(x)), and vice-versa. Intuitively: if some file is malicious, it's not true that all files are benign. All these rules together mean we can manipulate quantifiers almost as easily as we can manipulate regular booleans, putting them in whatever form is easiest to use in programming. Speaking of which, how do we use this in in programming? How we use this in programming First of all, people clearly have a need for directly using quantifiers in code. If we have something of the form: for x in list: if P(x): return true return false That's just some x in list: P(x). And this is a prevalent pattern, as you can see by using GitHub code search. It finds over 500k examples of this pattern in Python alone! That can be simplified via using the language's built-in quantifiers: the Python would be any(P(x) for x in list). (Note this is not quantifying over sets but iterables. But the idea translates cleanly enough.) More generally, quantifiers are a key way we express higher-level properties of software. What does it mean for a list to be sorted in ascending order? That all i, j in 0..<len(l): if i < j then l[i] <= l[j]. When should a ratchet test fail? When some f in functions - exceptions: Uses(f, bad_function). Should the image classifier work upside down? all i in images: classify(i) == classify(rotate(i, 180)). These are the properties we verify with tests and types and MISU and whatnot;1 it helps to be able to make them explicit! One cool use case that'll be in the book's next version: database invariants are universal statements over the set of all records, like all a in accounts: a.balance > 0. That's enforceable with a CHECK constraint. But what about something like all i, i' in intervals: NoOverlap(i, i')? That isn't covered by CHECK, since it spans two rows. Quantifier duality to the rescue! The invariant is equivalent to !(some i, i' in intervals: Overlap(i, i')), so is preserved if the query SELECT COUNT(*) FROM intervals CROSS JOIN intervals … returns 0 rows. This means we can test it via a database trigger.3 There are a lot more use cases for quantifiers, but this is enough to introduce the ideas! Next week's the one year anniversary of the book entering early access, so I'll be writing a bit about that experience and how the book changed. It's crazy how crude v0.1 was compared to the current version. MISU ("make illegal states unrepresentable") means using data representations that rule out invalid values. For example, if you have a location -> Optional(item) lookup and want to make sure that each item is in exactly one location, consider instead changing the map to item -> location. This is a means of implementing the property all i in item, l, l' in location: if ItemIn(i, l) && l != l' then !ItemIn(i, l'). ↩ Specifically, a set can't be an element of itself, which rules out constructing things like "the set of all sets" or "the set of sets that don't contain themselves". ↩ Though note that when you're inserting or updating an interval, you already have that row's fields in the trigger's NEW keyword. So you can just query !(some i in intervals: Overlap(new, i')), which is more efficient. ↩

9 hours ago 2 votes
The missing part of Espressif’s reset circuit

In the previous article, we peeked at the reset circuit of ESP-Prog with an oscilloscope, and reproduced it with basic components. We observed that it did not behave quite as expected. In this article, we’ll look into the missing pieces. An incomplete circuit For a hint, we’ll first look a bit more closely at the … Continue reading The missing part of Espressif’s reset circuit → The post The missing part of Espressif’s reset circuit appeared first on Quentin Santos.

10 hours ago 2 votes
Setting Element Ordering With HTML Rewriter Using CSS

After shipping my work transforming HTML with Netlify’s edge functions I realized I have a little bug: the order of the icons specified in the URL doesn’t match the order in which they are displayed on screen. Why’s this happening? I have a bunch of links in my HTML document, like this: <icon-list> <a href="/1/">…</a> <a href="/2/">…</a> <a href="/3/">…</a> <!-- 2000+ more --> </icon-list> I use html-rewriter in my edge function to strip out the HTML for icons not specified in the URL. So for a request to: /lookup?id=1&id=2 My HTML will be transformed like so: <icon-list> <!-- Parser keeps these two --> <a href="/1/">…</a> <a href="/2/">…</a> <!-- But removes this one --> <a href="/3/">…</a> </icon-list> Resulting in less HTML over the wire to the client. But what about the order of the IDs in the URL? What if the request is to: /lookup?id=2&id=1 Instead of: /lookup?id=1&id=2 In the source HTML document containing all the icons, they’re marked up in reverse chronological order. But the request for this page may specify a different order for icons in the URL. So how do I rewrite the HTML to match the URL’s ordering? The problem is that html-rewriter doesn’t give me a fully-parsed DOM to work with. I can’t do things like “move this node to the top” or “move this node to position x”. With html-rewriter, you only “see” each element as it streams past. Once it passes by, your chance at modifying it is gone. (It seems that’s just the way these edge function tools are designed to work, keeps them lean and performant and I can’t shoot myself in the foot). So how do I change the icon’s display order to match what’s in the URL if I can’t modify the order of the elements in the HTML? CSS to the rescue! Because my markup is just a bunch of <a> tags inside a custom element and I’m using CSS grid for layout, I can use the order property in CSS! All the IDs are in the URL, and their position as parameters has meaning, so I assign their ordering to each element as it passes by html-rewriter. Here’s some pseudo code: // Get all the IDs in the URL const ids = url.searchParams.getAll("id"); // Select all the icons in the HTML rewriter.on("icon-list a", { element: (element) => { // Get the ID const id = element.getAttribute('id'); // If it's in our list, set it's order // position from the URL if (ids.includes(id)) { const order = ids.indexOf(id); element.setAttribute( "style", `order: ${order}` ); // Otherwise, remove it } else { element.remove(); } }, }); Boom! I didn’t have to change the order in the source HTML document, but I can still get the displaying ordering to match what’s in the URL. I love shifty little workarounds like this! Email · Mastodon · Bluesky

10 hours ago 2 votes
clamp / median / range

Here are a few tangentially-related ideas vaguely near the theme of comparison operators. comparison style clamp style clamp is median clamp in range range style style clash? comparison style Some languages such as BCPL, Icon, Python have chained comparison operators, like if min <= x <= max: ... In languages without chained comparison, I like to write comparisons as if they were chained, like, if min <= x && x <= max { // ... } A rule of thumb is to prefer less than (or equal) operators and avoid greater than. In a sequence of comparisons, order values from (expected) least to greatest. clamp style The clamp() function ensures a value is between some min and max, def clamp(min, x, max): if x < min: return min if max < x: return max return x I like to order its arguments matching the expected order of the values, following my rule of thumb for comparisons. (I used that flavour of clamp() in my article about GCRA.) But I seem to be unusual in this preference, based on a few examples I have seen recently. clamp is median Last month, Fabian Giesen pointed out a way to resolve this difference of opinion: A function that returns the median of three values is equivalent to a clamp() function that doesn’t care about the order of its arguments. This version is written so that it returns NaN if any of its arguments is NaN. (When an argument is NaN, both of its comparisons will be false.) fn med3(a: f64, b: f64, c: f64) -> f64 { match (a <= b, b <= c, c <= a) { (false, false, false) => f64::NAN, (false, false, true) => b, // a > b > c (false, true, false) => a, // c > a > b (false, true, true) => c, // b <= c <= a (true, false, false) => c, // b > c > a (true, false, true) => a, // c <= a <= b (true, true, false) => b, // a <= b <= c (true, true, true) => b, // a == b == c } } When two of its arguments are constant, med3() should compile to the same code as a simple clamp(); but med3()’s misuse-resistance comes at a small cost when the arguments are not known at compile time. clamp in range If your language has proper range types, there is a nicer way to make clamp() resistant to misuse: fn clamp(x: f64, r: RangeInclusive<f64>) -> f64 { let (&min,&max) = (r.start(), r.end()); if x < min { return min } if max < x { return max } return x; } let x = clamp(x, MIN..=MAX); range style For a long time I have been fond of the idea of a simple counting for loop that matches the syntax of chained comparisons, like for min <= x <= max: ... By itself this is silly: too cute and too ad-hoc. I’m also dissatisfied with the range or slice syntax in basically every programming language I’ve seen. I thought it might be nice if the cute comparison and iteration syntaxes were aspects of a more generally useful range syntax, but I couldn’t make it work. Until recently when I realised I could make use of prefix or mixfix syntax, instead of confining myself to infix. So now my fantasy pet range syntax looks like >= min < max // half-open >= min <= max // inclusive And you might use it in a pattern match if x is >= min < max { // ... } Or as an iterator for x in >= min < max { // ... } Or to take a slice xs[>= min < max] style clash? It’s kind of ironic that these range examples don’t follow the left-to-right, lesser-to-greater rule of thumb that this post started off with. (x is not lexically between min and max!) But that rule of thumb is really intended for languages such as C that don’t have ranges. Careful stylistic conventions can help to avoid mistakes in nontrivial conditional expressions. It’s much better if language and library features reduce the need for nontrivial conditions and catch mistakes automatically.

yesterday 2 votes
C++ engineering decision in SumatraPDF code

SumatraPDF is a medium size (120k+ loc, not counting dependencies) Windows GUI (win32) C++ code base started by me and written by mostly 2 people. The goals of SumatraPDF are to be: fast small packed with features and yet with thoughtfully minimal UI It’s not just a matter of pride in craftsmanship of writing code. I believe being fast and small are a big reason for SumatraPDF’s success. People notice when an app starts in an instant because that’s sadly not the norm in modern software. The engineering goals of SumatraPDF are: reliable (no crashes) fast compilation to enable fast iteration SumatraPDF has been successful achieving those objectives so I’m writing up my C++ implementation decisions. I know those decisions are controversial. Maybe not Terry Davis level of controversial but still. You probably won’t adopt them. Even if you wanted to, you probably couldn’t. There’s no way code like this would pass Google review. Not because it’s bad but becaues it’s different. Diverging from mainstream this much is only feasible if you have total control: it’s your company or your own open-source project. If my ideas were just like everyone else’s ideas, there would be little point in writing about them, would it? Use UTF8 strings internally My app only runs on Windows and a string native to Windows is WCHAR* where each character consumes 2 bytes. Despite that I mostly use char* assumed to be utf8-encoded. I only decided on that after lots of code was written so it was a refactoring oddysey that is still ongoing. My initial impetus was to be able to compile non-GUI parts under Linux and Mac. I abandoned that goal but I think that’s a good idea anyway. WCHAR* strings are 2x larger than char*. That’s more memory used which also makes the app slower. Binaries are bigger if string constants are WCHAR*. The implementation rule is simple: I only convert to WCHAR* when calling Windows API. When Windows API returns WCHA* I convert it to utf-8. No exceptions Do you want to hear a joke? “Zero-cost exceptions”. Throwing and catching exceptions generate bloated code. Exceptions are a non-local control flow that makes it hard to reason about program. Every memory allocation becomes a potential leak. But RAII, you protest. RAII is a “solution” to a problem created by exceptions. How about I don’t create the problem in the first place. Hard core #include discipline I wrote about it in depth. My objects are not shy I don’t bother with private and protected. struct is just class with guts exposed by default, so I use that. While intellectually I understand the reasoning behind hiding implementation details in practices it becomes busy work of typing noise and then even more typing when you change your mind about visibility. I’m the only person working on the code so I don’t need to force those of lesser intellect to write the code properly. My objects are shy At the same time I minimize what goes into a class, especially methods. The smaller the class, the faster the build. A common problem is adding too many methods to a class. You have a StrVec class for array of strings. A lesser programmer is tempted to add Join(const char* sep) method to StrVec. A wise programmer makes it a stand-alone function: Join(const StrVec& v, const char* sep). This is enabled by making everything in a class public. If you limit visibility you then have to use friendto allow Join() function access what it needs. Another example of “solution” to self-inflicted problems. Minimize #ifdef #ifdef is problematic because it creates code paths that I don’t always build. I provide arm64, intel 32-bit and 64-bit builds but typically only develop with 64-bit intel build. Every #ifdef that branches on architecture introduces potential for compilation error which I’ll only know about when my daily ci build fails. Consider 2 possible implementations of IsProcess64Bit(): Bad: bool IsProcess64Bit() { #ifdef _WIN64 return true; #else return false; #endif } Good: bool IsProcess64Bit() { return sizeof(uintptr_t) == 8; } The bad version has a bug: it was correct when I was only doing intel builds but became buggy when I added arm64 builds. This conflicts with the goal of smallest possible size but it’s worth it. Stress testing SumatraPDF supports a lot of very complex document and image formats. Complex format require complex code that is likely to have bugs. I also have lots of files in those formats. I’ve added stress testing functionality where I point SumatraPDF to a folder with files and tell it to render all of them. For greater coverage, I also simulate some of the possible UI actions users can take like searching, switching view modes etc. Crash reporting I wrote about it in depth. Heavy use of CrashIf() C/C++ programmers are familiar with assert() macro. CrashIf() is my version of that, tailored to my needs. The purpose of assert / CrashIf is to add checks to detect incorrect use of APIs or invalid states in the program. For example, if the code tries to access an element of an array at an invalid index (negative or larger than size of the array), it indicates a bug in the program. I want to be notified about such bugs both when I test SumatraPDF and when it runs on user’s computers. As the name implies, it’ll crash (by de-referencing null pointer) and therefore generate a crash report. It’s enabled in debug and pre-release builds but not in release builds. Release builds have many, many users so I worry about too many crash reports. premake to generate Visual Studio solution Visual Studio uses XML files as a list of files in the project and build format. The format is impossible to work with in a text editor so you have no choice but to use Visual Studio to edit the project / solution. To add a new file: find the right UI element, click here, click there, pick a file using file picker, click again. To change a compilation setting of a project or a file? Find the right UI element, click here, click there, type this, confirm that. You accidentally changed compilation settings of 1 file out of a hundred? Good luck figuring out which one. Go over all files in UI one by one. In other words: managing project files using Visual Studio UI is a nightmare. Premake is a solution. It’s a meta-build system. You define your build using lua scripts, which look like test configuration files. Premake then can generate Visual Studio projects, XCode project, makefiles etc. That’s the meta part. It was truly a life server on project with lots of files (SumatraPDF’s own are over 300, many times more for third party libraries). Using /analyze and cppcheck cppcheck and /analyze flag in cl.exe are tools to find bugs in C++ code via static analysis. They are like a C++ compiler but instead of generating code, they analyze control flow in a program to find potential programs. It’s a cheap way to find some bugs, so there’s no excuse to not run them from time to time on your code. Using asan builds Address Sanitizer (asan) is a compiler flag /fsanitize=address that instruments the code with checks for common memory-related bugs like using an object after freeing it, over-writing values on the stack, freeing an object twice, writing past allocated memory. The downside of this instrumentation is that the code is much slower due to overhead of instrumentation. I’ve created a project for release build with asan and run it occasionally, especially in stress test. Write for the debugger Programmers love to code golf i.e. put us much code on one line as possible. As if lines of code were expensive. Many would write: Bad: // ... return (char*)(start + offset); I write: Good: // ... char* s = (char*)(start + offset); return s; Why? Imagine you’re in a debugger stepping through a debug build of your code. The second version makes it trivial to set a breakpoint at return s line and look at the value of s. The first doesn’t. I don’t optimize for smallest number of lines of code but for how easy it is to inspect the state of the program in the debugger. In practice it means that I intentionally create intermediary variables like s in the example above. Do it yourself standard library I’m not using STL. Yes, I wrote my own string and vector class. There are several reasons for that. Historical reason When I started SumatraPDF over 15 years ago STL was crappy. Bad APIs Today STL is still crappy. STL implementations improved greatly but the APIs still suck. There’s no API to insert something in the middle of a string or a vector. I understand the intent of separation of data structures and algorithms but I’m a pragmatist and to my pragmatist eyes v.insert (v.begin(), myarray, myarray+3); is just stupid compared to v.inert(3, el). Code bloat STL is bloated. Heavy use of templates leads to lots of generated code i.e. surprisingly large binaries for supposedly low-level language. That bloat is invisible i.e. you won’t know unless you inspect generated binaries, which no one does. The bloat is out of my control. Even if I notice, I can’t fix STL classes. All I can do is to write my non-bloaty alternative, which is what I did. Slow compilation times Compilation of C code is not fast but it feels zippy compared to compilation of C++ code. Heavy use of templates is big part of it. STL implementations are over-templetized and need to provide all the C++ support code (operators, iterators etc.). As a pragmatist, I only implement the absolute minimum functionality I use in my code. I minimize use of templates. For example Str and WStr could be a single template but are 2 implementations. I don’t understand C++ I understand the subset of C++ I use but the whole of C++ is impossibly complicated. For example I’ve read a bunch about std::move() and I’m not confident I know how to use it correctly and that’s just one of many complicated things in C++. C++ is too subtle and I don’t want my code to be a puzzle. Possibility of optimized implementations I wrote a StrVec class that is optimized for storing vector of strings. It’s more efficient than std::vector<std::string> by a large margin and I use it extensively. Temporary allocator and pool allocators I use temporary allocators heavily. They make the code faster and smaller. Technically STL has support for non-standard allocators but the API is so bad that I would rather not. My temporary allocator and pool allocators are very small and simple and I can add support for them only when beneficial. Minimize unsigned int STL and standard C library like to use size_t and other unsigned integers. I think it was a mistake. Go shows that you can just use int. Having two types leads to cast-apalooza. I don’t like visual noise in my code. Unsigned are also more dangerous. When you substract you can end up with a bigger value. Indexing from end is subtle, for (int i = n; i >= 0; i--) is buggy because i >= 0 is always true for unsigned. Sadly I only realized this recently so there’s a lot of code still to refactor to change use of size_t to int. Mostly raw pointers No std::unique_ptr for me. Warnings are errors C++ makes a distinction between compilation errors and compilation warnings. I don’t like sloppy code and polluting build output with warning messages so for my own code I use a compiler flag that turns warnings into errors, which forces me to fix the warnings.

yesterday 2 votes