4020 stories
·
4 followers

Blurry before beautiful: image previews for the web

1 Share

Abstract illustration of a multi-color blur with a line passing through it, extending past the blur, with colored dots along it.

You've probably already seen this common pattern in action: blurry versions of the images of a site appear immediately and, moments later, their full versions replace them.

This pattern has become so common that many image CDNs, frameworks, and libraries support it out of the box. But every implementation has to do the same thing:

  • Fetch both the preview and the final images.
  • Display the preview while the final image loads.
  • Swap the preview and final images.
  • Clean up after the transition.

Below is an example of the effect, which shows a blurry preview image. Click the button below to load and display the final image:

The Louvre pyramid in Paris.

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum volutpat accumsan tristique. Phasellus sit amet mauris odio. Aenean urna felis, laoreet vel ipsum ut, dignissim fermentum mi. Fusce posuere efficitur laoreet. Donec lacinia massa cursus eros ultrices maximus. Proin sit amet vestibulum nibh. Nulla luctus eleifend nisl. Aliquam vel orci a nisl maximus bibendum ut sit amet turpis. Curabitur sodales risus placerat nulla congue bibendum. Aliquam ut mi et dui efficitur semper. Donec tempus nibh eget est aliquet bibendum. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas.

We are proposing to make this a built-in capability of the web, and we'd love to know if this is worth pursuing and what the exact scope should be.

Before diving into the proposal, let's take a look at how this pattern is typically implemented today.

How to do this today?

There's currently no standard way to provide a preview for an image, so the solution depends on the framework or library you're using.

For example, Next.js can generate a blurred placeholder when an image is imported, by using the placeholder=blur property:

import mountain from "./mountain.jpg";

<Image src={mountain} alt="A mountain" placeholder="blur" />

Libraries such as blurhash encode a tiny preview as a short string, which your application can then decode to show the preview while the real image is loading:

// First, create blurhash strings for your images.
import { encode } from "blurhash";
async function encodeImageToBlurhash(width, height, imagePixelData) {
  return encode(imagePixelData, width, height, 4, 4);
};
// At runtime, while the image is loading, decode the string to display a preview.
import { decode } from "blurhash";
const pixels = decode("<the blurhash string>", 32, 32);
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
const imageData = ctx.createImageData(width, height);
imageData.data.set(pixels);
ctx.putImageData(imageData, 0, 0);
document.body.append(canvas);

You can also use write your own custom implementation, for example by creating image previews at build-time, and sending them as data URLs used in CSS background images, while the final image is loading:

<div class="image" style="background-image: url(data:image/png;base64,....)">
  <img src="image.jpg" alt="A mountain">
</div>

These approaches differ, but they all have to coordinate the preview and final image themselves.

Why a native solution?

This preview effect is well established, and for good reasons. It allows your UI to come to life much sooner, giving the impression of a more responsive site which lets users access the rest of the content while images are still loading.

We don't want to change this pattern, but instead implement it in the browser so that you don't need to write, maintain, and run as much code. With a solution that's built-in, you let the browser handle most of the complexity.

What's in the proposal?

For now, our proposal focuses only on loading, displaying, and replacing the preview image. To achieve this, we propose to introduce a new attribute for the <img> element called previewsrc:

<img previewsrc="tiny-blurry-preview.png" src="full-image.avif">

With the previewsrc attribute, the browser would:

  • Load the preview and final images, always giving priority to the final image.
  • Display the preview.
  • Replace the preview with the final image when it becomes ready.

This already simplifies the process of handling image previews a lot because you don't need to use or write code to load the preview, handle cases where the preview doesn't exist or fails to load before the final image is ready, and handle the image swap yourself.

We'd love your feedback on this initial proposal: do you consider this a valuable addition to the web platform? Is it enough on its own for you to use it?

Possible future enhancements

If you're using a library or framework to handle previews today, your solution might also support the following features:

  1. Customize the transition between the preview and the final image, for example by adding a fade effect, to make the swap look nicer.
  2. Support compact formats such as blurhash directly, without needing to write additional code.

Our initial proposal doesn't support these features. When the final image becomes ready to paint, the browser directly replaces the preview with the final image, with no customizable transition. Also, it doesn't natively support compact formats such as blurhash.

We're currently considering them as optional enhancements that could be added in the future, but your feedback on their importance would help us prioritize them: would you use previewsrc alone or would you require support for these optional enhancements (and if so, which ones) before adopting it?

Let us know!

Let us know how you feel about this proposal.

Is the problem worth solving at the web platform level? What would consitute the minimal viable solution for you to adopt the API?

Please send your feedback by opening a new issue on our GitHub repository.


Common questions

You might be wondering about the following questions, so let me provide some answers below.

Doesn't a preview image create a second request?

Sure, if you set previewsrc to another image URL, then an additional network request will be made, which potentially will consume bandwidth. However, consider this:

  • The final image loading would never be delayed by the preview.
  • The browser would always treat preview loading as "best effort".
  • The browser would assign a lower priority to preview fetches and decodes.
  • The browser would, in fact, completely skip previews when they are unlikely to be useful.
  • And you could still continue to use blurhash strings or data URLs for previews, avoiding a second network request altogether.

It's important to keep in mind that this is not a replacement for proper image optimization, and not a performance feature.

Does this avoid abuse?

It doesn't. There is actually no enforcement mechanism in the proposal for ensuring that developers use the previewsrc attribute responsibly.

The proposal depends on developer discipline, the same discipline they apply when using existing image preview techniques.

However, this is important: the browser may skip the preview altogether if it determines that it is unlikely to be useful before the final image is loaded or if resources are constrained. That's definitely a plus compared to today's solutions.

Why not use progressive image delivery instead then?

JPEG images can be progressively delivered, whereby the image is loaded in multiple passes, gradually increasing in quality. This allows users to see a low-quality version of the image almost immediately, while waiting for the full-quality image to load.

In the end, this depends on your use case. A progressive JPEG may be preferable for sites where more fidelity is required, where seeing the real image, even if in low quality, is more important than having an instant, but very blurry preview.

Generally speaking, blurred placeholders seem to be favored for providing a quick visual indication, responsiveness, and layout stability. I feel like developers have mostly moved away from progressive images.

But also, not all sites generate progressive image versions at all.

What about responsive images?

They keep working as before.

An <img> inside a <picture> element can still use the previewsrc attribute. The existing <picture>, srcset, and sizes algorithms continue selecting the final image.

Why not just display the preview as a background-image of the <img> instead?

Using CSS to paint a background-image on the <img> element while it loads is a common technique, as described in How to fake Progressive WebP Images.

The advantage of a browser-provided previewsrc attribute over this are:

  • Both the preview and final URLs are directly associated with the <img> element.
  • The previewsrc attribute would get reflected as an HTMLImageElement property.
  • The browser would manage error handling, sizing and positioning, synchronizing updates, and best-effort scheduling
Read the whole story
emrox
3 hours ago
reply
Hamburg, Germany
Share this story
Delete

The rise and fall of Jev hype

1 Share

The rise and fall of Jev hype

What can we learn about the hype engine that declared war on rational and critical thinking?

This is a quick note on how the industry’s reaction to Jev reveals the desperate state of AI. I’ll dissect:

  • What is Jev, how was it hyped and why it spread like mass hysteria? Jev isn’t dead but its hype cycle is. Decision models are on the rise.

  • What are the noteworthy innovations since ChatGPT brought “AI” to the mainstream?

  • Why is the industry starved from real innovation and what high impact problems are still unsolved after 4 years of revolution incremental improvements?

The point is to use Jev’s hype cycle to call out the manipulative marketing that are too familiar with at this point.

Jev

You probably don’t need an intro to Jev at this time because since its introduction 33 days ago, it has been all the rage!

But if you were clever enough to ignore it, here’s what actually matters:

Jev is basically a general purpose classifier (e.g. quick judgement). What makes it interesting is that you don’t have to train it with a lot of data (although that lack of special-purpose training negatively impacts its utility).

Calling it a “decision model” is a bit far fetched especially since it cannot even see (rival open-weight models have multi-modal features) or reason (that’s how it’s so cheap).

The idea behind Jev isn’t new! The idea of non-autoregressive decision models with RL goes back more than a year ago. The original author is pissed but as we’ll see drama is an inseparable part of AI hype.

Hype

I want to walk you through the emotional roller-coaster I went through when I read their announcement via hackernews:

At OpenAI, I helped build the methods that made language models useful at following instructions and talking with people. That work ended up as the research behind ChatGPT.

😎 So the founder was behind ChatGPT, one of the fastest-growing products in history. I’m listening…

At the time, I thought maybe chat models would lead to AGI, but despite the hype it became obvious to me that there was something really big missing.

😃 Oh, my god! This is it! ChatGPT is missing something big and he’s about to reveal it! We’re one step closer to AGI if we haven’t reached it already! I’m thrilled to read more…

After two years in stealth, countless technical challenges, and research breakthroughs… I am beyond excited to announce

😱 Spit it out! I’m beyond excited to read this…

TypeSafe AI is releasing our first System One Model

😯 Wait what?!! “System one”?

Do you understand what you’re talking about? Here’s my recollection of how Daniel Kahneman defined the two systems:

  1. System one: fast, cheap (in terms of computational power), emotional, unconscious and judgmental.

  2. System two: slow, expensive (takes more brain power), logical, conscious, and more elaborate.

If Jev is system one then LM is system two? It can’t even handle logical reasoning!

Most importantly Jev came with another interesting framing:

can’t hallucinate

Hallucination is one of the biggest problems with the current generation of language models but no hallucination doesn’t mean the response is logically correct. In Jev’s context, it just means that the output matches the schema.

Technically conventional language models can gain this ability by bolting a control loop (e.g., type checker, JSON schema parser) on top. Jev does this much faster and without reasoning. It’s in the name:

We named Jev after William Stanley Jevons. We expect machine intelligence to follow a similar path to coal, after steam-engine efficiency led to an increase in demand.

I don’t know Jev’s architecture because in the true ex-"Open”AI employee tradition, not much is released (probably in the name of “safety” or “moat”). But that doesn’t stop them from dropping jargons:

We built a new […] training method we call Reinforcement Learning for Calibrated Decisions (RLCD).

Note: RLCD rhymes with the “traditional” methods:

  • RLHF: Reinforcement Learning with Human Feedback

  • RLVR: Reinforcement Learning with Verifiable Rewards

They even claim:

Jev is neither small nor an LLM, hence being off the intelligence Pareto curve.

Fortunately, there’s no shortage of similar models, some of them with even better performance (Laya, Kev, Nimble, NanoJev, etc.)

So let me sum up the marketing strategy:

  • Name dropping: former ChatGPT creator, Jevons’ paradox, Pareto curve, System one

  • Big claim: cannot hallucinate, exponentially faster and cheaper, entirely new stack, amazing benchmarks and demos

  • Ambiguity and jargon: no useful implementation details on the “new training algorithm” or RLCD dataset

  • Drama: Laya showed this idea in 1.5 years prior to Jev’s launch

OK, I think at this point we’ve seen this pattern enough to recognize it. I actually think Jev’s team kicked it up a notch to a comical level. Whether their marketing is written by LLM or not, they nailed it.

Fortunately, Jev’s initial noise died off almost at the same speed that it blew up. I can only speculate that as more people experimented with it, we realized:

  1. It cannot hallucinate, but it can still be wrong. It is even more wrong than conventional language models, and in that sense “system one” is the right naming. Although I consider all current generations of language models system one (even the reasoning models) because they do not really reason in the sense that’s expected of system two.

  2. It is cheaper, but there are open-weight models that beat it in performance/cost ratio (depending on the task). What makes Jev different than frontier-level LLMs is the fact that you can actually run it at the edge (e.g. locally) with matching quality.

Jev is not completely hype, we just went from “Peak of Inflated Expectations” to the “Trough of Disillusionment” in record time:

It’s not dead. Some companies use it while I believe the majority of use cases are addressed by open-weight rivals.

I really don’t want to bash Jev. Instead I want to use it as an example of where we are, as an industry.

Incremental improvements

I was one of the lucky few who got access to GPT-2 when it was invitation-only.

As a senior tech lead, I allocate part of my time to scout new tech and experiment in order to stay relevant. Prior to that, I had experienced IBM Watson and a bunch of other products so it wasn’t a big deal for me personally.

I was actually underwhelmed by GPT-2 because at the time it could:

  • Summarize text

  • Predict what comes after a piece of text

  • … that was it really! 😄 I don’t remember more than that. So I moved on…

Then I saw someone create a turn-based dialogue from one of these models and to spice it up, they added an avatar to it. I thought it was creepy and funny.

Then Blake Lemoine came out claiming Google’s language model named LaMDA was conscious. Unsurprisingly, this started a philosophical discussion that’s still ongoing on the side. Again, I didn’t give it too much thought.

BUT, Sam Altman, having spent a few years at the startup accelerator (Y Combinator), saw an opportunity. Specifically the chat application seems to have got his attention and soon ChatGPT was announced.

At the time, I thought it was a cool way to turn their token prediction model into a user-friendly product for the masses.

Personally, I couldn’t use it seriously because:

  1. Poor quality: it didn’t know what it didn’t know, so it just winged it (AKA hallucination). Although the rise of “prompt engineering” and “reasoning models” promised to reduce this issue.

  2. Privacy: I had been working with computers for 2+ decades at the time and live by the mantra that “if it’s possible, assume it happened”. Meaning if OpenAI is technically capable of storing my conversation, I cannot trust it enough to share any company IP or private information with it.

I vividly remember the backlash Google got for their multi-modal demo when people realized it wasn’t recorded in real-time! Those were the days when even a small deception would be a conspiracy.

When llama.cpp came out I got a gaming machine with an NVIDIA card (AI machines weren’t a thing back then) to build with AI.

Meanwhile people discovered that language models can generate code It was tedious to copy/paste it so we got the early versions of a harness.

Quickly after, we got “function calls” which allowed the models to do stuff!

I was using VS Code at the time and the hype was hard to ignore even though my company at the time had a strict policy against using AI tools (yes! hard to believe that just 3-4 years ago companies prioritized IP over velocity).

I remember paying for a Copilot subscription out of pocket and using VS Code insider on my open source projects.

I was so frustrated that I shared this meme on the company Slack:

Meanwhile there was drama at OpenAI (Ilya was part of a coup that fired their CEO, then Satya was playing 4D chess while Elon was lawyering up). It was a sh*tshow which is exactly how it managed to capture the interest of non-techies, a trend that continues to today with “almighty rogue AI” hacking others while whistle-blowers and alarmists rise speak of existential threat!

Simultaneously premature AI products were creating dramas of their own (assisting crimes, cheating, etc.) and AI bros anthropomorphize the tool as if it had desires of its own (my read: they want to judge accountability).

There’s also circular investment, pentagon drama, chip wars, autonomous killer drones, etc. but in the spirit of brevity, let’s stick to the technical side of things.

Then Peter Steinberger, a veteran developer who was away from computers due to burnout, created an AI companion, which brought the Agents (LM that can do stuff) to the mainstream.

So far I’ve covered:

  • Instruction-tuned models

  • Function calls

  • Coding harnesses

  • AI agents

Let’s speed through the other incremental improvements:

  • Larger parameter size unlocks emergent properties that are hard to explain by analyzing the micro level architecture (mostly leading to sophisticated perceived behavior but also jagged alignment).

  • Larger context windows improve accuracy (although the practical limit is much smaller than the theoretical size).

  • Multimodal models enable generating voice, music, pictures, and videos while increasingly more models can understand these modalities.

  • MCP abstracts away the implementation and enables different AI products to use the tools needed to do productive work.

  • ACP, sub-agents and various orchestration techniques allow larger tasks to be planned and broken into smaller pieces.

  • Sandboxes help improve security and isolate unwanted side effects of agentic workflows and when they fall short, there’s HITL (but we still haven’t solved the approval fatigue).

  • Skills, web search and web access allow agents to access information that’s outside their training data or knowledge cut-off date.

  • Various memory architectures and consolidation techniques enable the models to recall what they learn.

  • Chinese labs showed us more efficient ways to train and run the models.

  • Feedback loops and scratchpads improve the model’s perceived quality by running part of the verification closer to the model.

This is good progress, no question about it. But I think a part of the reason AI is a hot topic is all the drama surrounding it:

  • Geopolitics: unlike the US economy which is primarily service-based, China is a manufacturing behemoth. Up until DeepSeek discovered that it can manipulate stock and AI investment by diffusing the compute equation, I don’t think China had a solid AI plan. Now they do. Good old American sanctions target their chip import but China (being run by engineers) made huge strides recently both in chip manufacturing but also in model architecture. Meanwhile AI is used in warfare from drones to intelligence (and mass surveillance back home). In the best seller How the mighty falls, Collins lists 5 stages of demise: a) Hubris Born of Success, b) Undisciplined Pursuit of More, c) Denial of Risk and Peril, d) Grasping for Salvation, e) Capitulation to Irrelevance or Death. Those same stages apply to a falling empires and in this case US has bet big on AI while China diffuses US’s massive bet with open-weight models. Consumers win for now.

  • Debt bubble: You can read more on Bloomberg but it basically boils down to a series of interdependent bets that is so large it impacts the national GDP. It has to work regardless of how much IP has to be stolen and how many lies have to be told. The stakes are too high at this point.

  • Safety: we got used to the “rogue AI” narrative where the AI vendors aren’t held accountable for breaking into other companies and government institutes. Why would we? It’s an open secret that the training data wasn’t legally obtained anyway (and there are multiple class action lawsuits to prove that). On top of it, there have been multiple cases where people lost their lives due to AI — and I’m not even counting “vibe war”. (yes, I actually put an em-dash there!) 🙃

  • Impact on jobs: software engineering is low-hanging fruit for AI automation due to its nature (there’s plenty of training data, and the tools are often on a language model’s home turf). There have been discussions of UBI (universal basic income) and UHI (universal high income) but looking at the benevolence of the billionaires running the show, I don’t buy it at all.

AI is hot, because it can potentially impact all of us. But we have to remember one simple fact:

Humans are fundamentally flawed when it comes to predicting the future.

Most notoriously, we have difficulty understanding the S-curve (meaning the pace of change is slow at the beginning and end).

And with prominent scientists leaving their lucrative positions to slow down the pace of development, we really cannot be sure about how long it will take for AI to really manifest the changes, if ever.

What’s missing?

With all the incremental improvements of the past few years, we still haven’t solved some fundamental problems.

Here’s a short list of what I consider truly game changing:

  • Abstract reasoning: neural networks (not just language models) are prone to making stuff up (hallucination). The state of the industry has improved a lot since hands had 5+ fingers and videos were easy to detect, but the closest we’ve come to a true system 2 is to pair the models with code and still the feedback loops and human intervention are necessary.

  • Continuous learning: auxiliary context (e.g. skills, grounding, search and various model architectures) has come a long way but we’re far from the state where the models learn in real time. By extension, the next generation of AI should learn from fewer samples, data and repetitions.

  • Embodiment: while there have been great improvements in robotic body form factor, we’re still not at a point where AI is rapid and efficient enough to be put in a robot body and react to the sensory stimuli in real time or expose fine motoric control and navigate the 3D world with enough prevision to take over “simple” human chores.

  • Economics of running AI: Jensen Huang talks about installing “Gigawatts” of compute as if it’s normal to measure temperature in kilometers/hour!!! The truth is: our brain delivers AGI level intelligence at 20 Watts and until we solve that problem, there is not enough water, power and data centers that can realistically make massive change in workforce. Unfortunately, we live in a post-truth world where hysteria, personalized propaganda and mass psychosis have an oversized impact on the dominant discourse so we’re all supposed to be scared and distracted. The economics makes even less sense if we expect embodied AI (robots) to replace even a fraction of human population. Unlike bots, our organic bodies return to nature and participate in the nature’s recycling system much faster than metals and plastic that build a robot. Sometimes I feel we forgot that we’re living on a tiny rock with finite resources which get increasingly harder to extract.

All of this, is not to say that AI won’t be used to:

  • Robosoldiers and robocops

  • Generate new diseases

  • Connect mass surveillance to personalized propaganda

  • Corporate espionage and mass penetration into computer systems

  • etc.

The truth is: you don’t need AGI-level of intelligence to cause destruction. The current generation of AI, paired with harness, tools and skills is plenty capable of causing damage in the wrong hands.

And if you think this is not happening already, I know a guy who gives free ice cream in his basement! 🙃

Recap

I think Jev is a good example of how great marketing fizzed out in the face of reality.

  • Is it useful? Yes.

  • Is it relevant? Yes.

  • Is it dead? Just its hype.

  • Is it revolutionary? Depends on your cost, quality, speed triangle.

  • Is it as big as it was claimed? I hope you have the answer by now.

If I had a magic wand, I would want us all to step back and see the holistic arc of recent events and calm down a little. The red flags are easy to see:

  • Name dropping: builds a bridge between the new unknown to the trusted known.

  • Jargon: makes you feel stupid for not having the context.

  • Ambiguity: lets your brain fill in the blanks and if you’re an optimist, you’re at risk of getting overly excited and eventually disappointed

  • Inflate expectations: big claims require big evidence. Don’t trust the isolated benchmarks that do not resemble how YOU would actually use it, especially if the benchmark is coming from the same entity that has a conflict of interest.

  • Drama: engages human emotion to what would otherwise be a logical technical decision. “All publicity is good publicity”.

Unfortunately, I think that ship might have already sailed and in the age of mass-produced synthetic content and personalized propaganda, maybe we’re too stressed to think straight.

Maybe LLMs are “system two”, after all, if we collectively lose our critical thinking and let our emotions be manipulated with the kind of marketing and manipulation we’ve seen too much lately.

Someone shared this on Hackernews (thank you 🙌) and it went all the way up to 2nd position. The early comments came at agentic speed but apparently didn’t bother to read through.

Generally I find HN comments to be quite useful but there seems to be a botnet actively trying to take it personal and derail the conversation to marketing Jev even further. Weird. 🤔

If you found this piece insightful, please share it in your circles and on social media to inspire others.

Share

My monetization strategy is to give away most content for free but these posts take anywhere from a few hours to a few days to draft, edit, research, illustrate, and publish. I pull these hours from my private time, vacation days and weekends. The simplest way to support this work is to like, subscribe and share it. If you really want to support me in lifting our community, you can consider a paid subscription. If you want to save, you can get 20% off via this link. As a token of appreciation, subscribers get full access to the Pro-Tips sections and my online book Reliability Engineering Mindset. Your contribution also funds my open-source products like Service Level Calculator. You can also invite your friends to gain free access or save via a group subscription.

And to those of you who already support me, thank you for sponsoring this content for the others. 🙌 If you have questions or feedback, or you want me to dig deeper into something, please let me know in the comments.

Subscribe to Alex Ewerlöf Notes

Technical Leadership, Reliability Engineering, Growth

Discussion about this post

Ready for more?

1Password menu is available. Press down arrow to select.
Send this story to NewsBlur
Shared stories are on their way...
Read the whole story
emrox
5 hours ago
reply
Hamburg, Germany
Share this story
Delete

GTA.js, a reimplementation of the 1997 Grand Theft Auto engine in JavaScript and WebGL

1 Share

Here’s a little sideproject I’ve been spending my tokens on: https://gtajs.wtf/

It’s a reimplementation of the 1997 Grand Theft Auto engine in JavaScript and WebGL.

The project is not entirely finished, but since I am leaving on vacation right now (I am currently at the airport), I might as well just publish what I have — and it’s already quite a lot I think 😀

Note that because this is a reimplementation (it’s not a DOSBox wrapper / emulator / decompiled version / …) there are differences compared to the original 1997 game in certain physics behaviors, animations, etc.

The mission engine is accurate though, as that is publicly documented (the almighty mtd.doc) 😊

And yes, I totally asked Gemini to implement most things, referring Gemini to open source reimplementations, public documentation and community notes, or by simply telling it how I recall the game from playing it (and editing it!) a lot back in the day.

Still a lot of things to do (cops!) but it’s already playable. Enjoy!

Read the whole story
emrox
10 hours ago
reply
Hamburg, Germany
Share this story
Delete

Railpass

1 Share

Railpass, made by Sami Smith

Bad Schandau–Meißen

Read the whole story
emrox
10 hours ago
reply
Hamburg, Germany
Share this story
Delete

Google's Rust Style Guide

1 Share

Google Style Guides

This document is a style guide for the usage of the Rust programming language at Google. In this context, “style” refers to more than just formatting; it includes guidance on API and implementation design.

Rust is used in many places, including google3, Android, ChromeOS, Fuchsia, and Chrome, and we’d like for Rust usage at Google to be consistent both within and across codebases. In addition to bringing consistency to our Rust code, style guidance also helps establish a standard of quality.

Background

Scope

The current scope of authority of the rules outlined in this guide:

  • In Android, Chromium, and other codebases: This style guide has been developed in consultation with other codebases, and they intend to start applying it. These codebases may have their own rules about how strictly this is expected to be applied. Expect more information here as these codebases adopt this guide.

Goals of the style guide

By and large, we have this document for the same reasons we have a C++ style guide.

Most style rules come from one or more of the goals listed below.

Style rules should prioritize readability and maintainability over writeability

We expect our code to be around for long periods of time, and we consider a little bit of extra effort applied during code writing to be worth it in the long run if the style rule improves readability.

Style rules should favor better automation

Large-Scale Changes (LSCs) are common at Google, and consistency helps make LSCs easier to carry out.

Style rules should be consistent with the wider Rust ecosystem where possible

The broader Rust ecosystem already has a pretty good amount of consistency in style, and we should rely on that where possible to reduce surprise and make it less work to learn Google’s Rust style.

Style rules should pull their weight

Every Rust developer and reviewer at Google has to keep these style rules in mind: as such, we should be careful to include rules that have a tangible benefit.

Style rules may help avoid minor style disagreements

There are many cases where reasonable people may disagree about a style, and we expect that disagreement to show up often on CLs. Having a style rule helps avoid having to rehash this each time. Rules in this category that do not have other benefits should be kept to a minimum.

Follow public Rust style rules

Follow the official public Rust style rules to the extent they don’t conflict with this style guide.

A “conflict” would be a case where this style guide suggests a pattern that is discouraged, or counter to the primary encouraged pattern in the Rust style guide.

Why does the guidance in this style guide deviate from regular Rust style rules?

Most style guides have three goals: consistency, readability, and encouraging good API design. Regardless of the specific choices made, it helps everyone to have these choices made beforehand to limit bikeshedding.

This style guide shares those goals, but it also has an additional goal not seen as often: smoothing the way for Large-Scale Changes (LSCs). Large codebases like many of the ones at Google occasionally need to run codebase-spanning refactorings that are assisted by tooling: for this to work well, the code should be something tooling has an easy time working with. Consistency helps for this, but so does having deliberate refactor-friendly patterns.

Apply API design rules in context

When applying rules that relate to API design, you should distinguish two types of code:

  • Library code

    • Intended for use by other projects (applications or other libraries).
    • Solves a relatively general problem.
  • Application code

    • Intended for use in a single application.
    • Solves a more specific problem.

For library code, style guide rules that concern API design are binding.

For application code, the rules on API design are a strong recommendation. As you apply them, keep in mind that since application code is written to solve a specific problem, it should generally favor simplicity and directness over broad reusability. Avoid premature generalization and abstraction; focus on what is required to meet the immediate need.

Code organization

Terminology:

  • A crate is a unit of compilation. A crate always contains a nameless Rust module, the “root module”.

  • A Rust module is a unit of code organization within a crate. Rust modules are used to control visibility of names. Modules can be nested. They can also be used to split code across files and directories.

The following sections convey that in Chromium, it is preferable to not make use of all possibilities of splitting modules across files.

Prefer small crates [chrome]

Prefer small crates in Chromium; ideally, but not necessarily, a single file (focusing on one abstraction). If your code can be split up into small crates, do so.

A crate corresponds to a single rust_static_library (chrome) rule, which may contain one or more .rs files.

Additional considerations:

  • The root module is allowed to re-export public APIs from leaf submodules, making submodules an implementation detail of the crate (as users don’t need to know about these submodules).

  • This rule about small crates applies equally to crates with and without submodules. First, evaluate whether the APIs need to be in the same crate, and only then consider whether submodules could help organize the APIs.

  • If you decide to use submodules, follow the expected file structure.

Larger crates are allowed when trait impl coherence rules force multiple types and traits to be included in one library, or if you’re building a large foundational library.

Rationale

Fine-grained library targets are recommended in Chromium due to the massive size of our codebase, non-local effects of adding dependencies in the build graph, and to improve performance of parallel compilation and build caching.

Use flat file layout for rust_library sources [chrome]

In Chromium .rs source files belonging to one rust_library (one Rust crate) must reside in a single directory.

Don’t use subdirectories to organize files that belong to a single crate. If organizing one crate’s files into subdirectories feels necessary, this is a strong indication that the crate has grown too large and should be split.

This crate layout is intentionally different from the conventional Cargo directory structure typically used in open-source repositories.

Exception: Rust code in //third_party that is also built with Cargo should follow the conventional Cargo directory structure.

Rationale

The approach in Chromium is driven by the following factors:

  • Based on the preference for small crates, most rust_library targets will only contain one source file.
  • Because of the above point, creating a directory for each rust_library target would result in most directories containing only one source file.
  • Multi-file rust_library targets should not create dedicated directories for submodules either; this ensures a consistent approach.

Name source files and leaf modules after the primary user-defined item

When a leaf module has a primary user-defined item (e.g., Foo), the module should be named after that item (e.g., mod foo and foo.rs).

Leaf modules that are not primarily about any particular abstraction can be named as desired.

Rationale

This makes it easy to identify the content/purpose of a source file or leaf module at a glance.

Don’t implement APIs in non-leaf modules

The only items in non-leaf modules should be module declarations (not definitions) and re-exports.

Note that the definition of leaf modules allows the following sub-modules: mod test and cross-language binding modules like #[cxx::bridge].

Exceptions:

  • Having a small amount of core “ties things together” implementation code in a non-leaf module may be fine. The goal is to not have a lot of code in these modules.

Rationale

This makes it easier to find things, and avoids mixing structure and contents.

Don’t use glob imports

Don’t use glob imports, except in the following situations where you may optionally do so at your own discretion:

  • use super::* for inline modules
  • use enum::* in method bodies of enums, and functions that are primarily dealing with that enum
  • pub use submodule::* for non-leaf modules
  • C++ interop: Since the same namespace (in C++) is a submodule in different Rust crates (in Rust), you may merge the namespaces into a private copy using glob imports. For example:

    mod absl {
        pub use status::absl::*;
        pub use strings::absl::*;
    }
    

    In this hypothetical example, you can use absl::Status and absl::StrCat from Rust, the same way you would from C++, instead of spelling them as status::absl::Status and strings::absl::StrCat.

    This must only be used for merging C++ namespaces, and the resulting merged module must not be public.

If using third-party crates that encourage a prelude, it is up to the reviewer’s discretion to allow it.

This also means that defining preludes should be avoided.

Rationale

Glob imports increase toil for code janitors running LSCs, and make it harder to know where a type comes from in contexts without xrefs.

A use statement should import items from one module

This rule corresponds to the rustfmt setting imports_granularity = Module.

Basically, there should not be any ::’s inside the {..} block of a use statement. For example:

use foo::Foo;
use foo::bar::Bar;
use foo::bar::Baz;
use foo::Foo;
use foo::bar::{Bar, Baz};
use foo::{Foo, bar::Bar};

Prefer locally-scoped trait imports

Import traits within function bodies, unless the trait’s methods are used extensively throughout the file.

fn foo(x: u8) {
    // Import the trait within a function, not at the module scope.
    use std::str::FromStr;

    let string: String = x.parse();
}

Qualify calls to top-level functions with a module name

To use top-level functions defined in other modules or crates, import their module and call them with modulename::functionname(). For example:

use std::mem;

let x = mem::transmute(y);
use std::mem::transmute;

let x = transmute(y);

Exceptions:

  • When importing functions with sufficiently disambiguated names, e.g., functions from C++ (where it is typical to call top-level (free) functions directly).
  • In Chromium, when importing functions from small, single-purpose first-party crates.

  • When the module name itself is prone to collisions. For example, when the module name is a C++ namespace name, and multiple crates define the same submodules.

Rationale

Many Rust codebases consistently keep a style such that unqualified lowercase names are assumed local, so foo() can be assumed to be from the same module. This rule largely exists to avoid reviewer disagreements, the benefit is mostly consistency and a minor edge on reading code without xrefs.

Type names should be imported and used unqualified

By default, types and traits should be referenced in code without using their module path, e.g., code should use Regex, not regex::Regex, with a use regex::Regex at the top of the file or potentially in the relevant function.

Exceptions:

  • In cases where there are ambiguities, it is acceptable to directly reference types with common names like io::Error. It is also acceptable to rename (use std::io::Error as IoError).
  • In cases where you have a large “group” of types, e.g. the hir module in rustc which contains a large number of HIR types.
  • In macros and generated code; where requiring a use is counterproductive.
  • In code with lots of dependencies, where importing everything would lead to a very large import block with little benefit.

Rationale

This rule largely exists to avoid reviewer disagreements.

Use the appropriate import mechanism [chrome]

  • Chrome Use chromium::import!().

Code formatting and aesthetics

Use the appropriate formatting tool

Use the appropriate formatting tool (e.g., git cl format in Chromium).

Configuration (for style guide authors)

A codebase may choose to not apply the following rustfmt options if they care about building on stable and the option is unavailable on their stable Rust:

  • Maximum of 100 columns per line: max_width = 100

  • Usage of lowercase alphabets in hex literals (0xab123cdef as opposed to 0xAB123CDEF): hex_literal_case = Lower

  • Macro matcher formatting: format_macro_matchers = true

  • Usage of escapes for invisible characters in literals. E.g., if you wish to have text containing a Zero-Width joiner, use \u{200D}.

Opting out of automatic formatting

You may opt out of automatic formatting using #[rustfmt::skip], but only in the following situations:

  • Manual alignment – If you have a situation-specific reason to have a snippet of code line up in a particular way (e.g., multiple similar lines of code that you want to remain aligned for readability, or a matrix-constructing function).
  • Generated code (ideally, use a genrule instead).

Important: Formatting should not be skipped for personal style choices.

Ownership and borrowing

Avoid Rc and Arc

Prefer unique ownership and borrowing over Rc and Arc for managing memory.

Exception:

  • Use Rc and Arc when the alternative ownership and borrowing structure would be unreasonably complex to write, understand, or maintain. This is a judgment call to be discussed during code review.

Rationale

Generally, Rust allows representing object graphs without using shared ownership smart pointers. These pointers add runtime overhead and their own unique complexity. Engineers coming from other languages might tend to use Rc and Arc too quickly.

Prefer borrows over deep copies

Use .clone() when you genuinely need a deep copy of an object.

Avoid using .clone() as a substitute for borrowing. Design code that works naturally with Rust’s ownership and borrowing rules. However, if the borrowing relationships become excessively complex, .clone() can be a pragmatic choice.

Consider the performance needs of the code while choosing between cloning and borrowing.

Note: This advice does not apply to cloning Rc or Arc pointers. Cloning reference-counted pointers does not create a deep copy, and is an expected way to use them.

Rationale

Deep copies can be expensive to make due to memory allocation and data copying.

Function inputs and outputs

Accept flexible references for immutable data

When functions accept borrowed, immutable data, use a reference type that affords the most flexibility. Pass references to the data itself instead of references to smart pointers, containers, or wrappers.

Use Instead of Why?
&T &Box<T> Borrow the data inside the box
&[T] &Vec<T> Accept any slice instead of specific containers
&str &String Accept a string slice instead of references to owned strings
Option<&T> &Option<T> Borrow the data inside the Option

Exceptions:

  • When expecting non-linear character data such as Rope/Cord.

For context, see Clippy lints borrowed_box and ptr_arg.

Rationale

Accepting reference types that don’t mention specific smart pointers, containers, or wrappers makes functions more versatile and performant: callers can pass data held in a variety of ways without copying it first.

Iterators vs. owned collections in return types

Prefer returning iterators (either concrete types or impl Iterator) where possible over returning Vec<T>.

Note: In application code, returning a Vec<T> is fine if it simplifies the code and if flexibility and performance advantages of iterators are not clearly needed.

Exceptions:

  • Most callers will need to construct an owned vector.
  • The function’s implementation naturally constructs a vector.
  • The returned value will likely need to be randomly indexed.
  • The algorithm works better as a for loop and the forced allocation doesn’t matter too much.
  • The function’s intended purpose is creating a new owned collection.
  • The function is returning a pre-existing vector.

Rationale

Highly reusable library code generally should strive to provide more flexible and performant APIs. Returning an iterator allows callers to choose how to consume the data: collect it into a vector, process it in a loop, or chain further iterator adapters. Some of these options allow users to avoid unnecessary allocations.

In application code, the usage patterns for a function are often easier to predict. If it is clear that performance is not a concern, or that data would be collected into a vector anyway, returning a vector makes for a simpler API.

Iterators vs. slices in function parameters

Highly reusable library code should accept iterators or impl IntoIterator if it only needs to iterate over the input.

Note: In application code, accepting a slice &[T] is fine if it simplifies the code and if flexibility and performance advantages of iterators are not clearly needed.

Rationale

Highly reusable library code generally should strive to provide more flexible and performant APIs. Functions that accept iterators offer callers:

  • More flexibility (data can be supplied from any collection)
  • Efficiency (data does not need to be copied into a contiguous piece of memory)
  • Composability (the function works better with iterator chains)

In application code, the usage patterns for a function are often easier to predict. If it is clear that performance is not a concern, or that callers would always have a vector anyway, accepting a slice makes for a simpler API.

Accepting Into

When designing library APIs that accept a T but users are likely to have various types, consider accepting impl Into<T> instead of T if call-site ergonomics are paramount.

Tip: This should be balanced against code bloat concerns; it is not a requirement to use lots of Into<T> in your code.

Note: Application code should almost never use impl Into<T> parameters.

Rationale

Using impl Into<T> allows a function to directly accept various arguments of various compatible types. For example, a function accepting impl Into<String> could be called with a String or a &str literal directly.

The tradeoff here is balancing the convenience of avoiding a .into() at the callsite against the cost of generics (more complex API signature, increased code size, sometimes even type inference difficulties).

Prefer enum over bool in APIs

Use enum function parameters instead of bool in cases where the purpose of the parameter is not obvious.

If you do use bool, consider using an inline comment to document the purpose of the bool in an argument list.

Reporting errors

Choose the error reporting mechanism that fits your API contract: Option, Result, or panic!.

Use Option for simple failures where the cause is obvious and the caller would not benefit from further details. This is best for standard, anticipated outcomes like a missing value or a parsing mismatch.

// Good: The cause of the failure is obvious (integer overflow). No further
// details could be reasonably provided.
fn checked_add(a: u32, b: u32) -> Option<u32> { ... }

Use Result for recoverable failures where the caller needs context to handle the error. These are often “unavoidable” errors caused by external factors (like I/O or network) that even correctly implemented code cannot prevent with checks ahead of time. Nevertheless, the caller can reasonably anticipate the kinds of errors (a file-related function can fail with an I/O error). The error details allow the caller to distinguish failure modes and recover appropriately.

// Good: The caller needs to know *why* it failed (NotFound vs PermissionDenied).
fn open_file(path: &Path) -> Result<File, io::Error> { ... }

Use panic! to report bugs and violated invariants. These errors are avoidable: the program is missing a check, and is calling an operation without fulfilling a precondition. These defects must be fixed in the code, not handled at runtime, although coarse-grained recovery might be possible with catch_unwind.

// Good: Panics if the invariant (index must be in bounds) is violated.
// This indicates a bug in the caller's logic.
impl<T> MyVec<T> {
    pub fn remove(&mut self, index: usize) -> T {
        if index >= self.len() {
            panic!("index out of bounds");
        }
        // ...
    }
}

Note: Some codebases may choose to avoid panics entirely, and require even broken invariants to be reported via Result.

Exceptions:

  • Developer-facing binaries may use panics for non-fatal errors.
  • Tests may use panics for assertions.

Types

Newtypes versus type aliases

Use a newtype instead of a type alias in the following cases:

  • If the type you are defining has notable semantic differences.
  • For types that warrant an API of their own.
  • If you want the flexibility to change the underlying type later, especially if you don’t control all usages of your type.
  • For type safety, if it’s easy for the types to get mixed up. For example, a Date type may use newtypes to distinguish Day, Month, and Year numbers in its internal calculations.

Prefer named fields over tuples and non-newtype tuple structs

Prefer types with named fields over tuples, as tuple indexes (.0, .1, …) quickly become confusing.

Exceptions:

  • Newtype tuple structs (tuple structs with only one field).
  • In very local contexts where there is unlikely to be any confusion.

Traits

Add trait implementations when they become necessary

As a general rule, low-level library code should anticipate users’ needs proactively, while application code should add trait impls as needed.

Implement known standard library traits instead of custom methods

If a method matches the purpose and behavior of a standard library trait, implement that trait. For example, instead of writing fn from_str(&str) -> Self on Foo, you should impl FromStr for Foo.

It is acceptable to have both the method and the trait impl for discoverability reasons.

For context, see Clippy lint should_implement_trait (list of traits).

Use #[derive()] if possible

Manual implementations should be avoided if they can be expressed as a #[derive()].

Generics versus dyn Trait

In libraries, prefer accepting generics (T: Trait or impl Trait) in function parameters over dyn Trait.

Exceptions:

  • Cases where code size matters.

Rationale

  • Efficiency

    • Additional indirection: Creating a dyn Trait requires setting up a fat pointer, and a vtable lookup is required for every call of a function on the trait.
    • If a function takes a dyn Trait, the compiler can’t perform certain optimizations that it could if the function was generic, such as inlining calls to functions of the trait.
  • Some traits are not dyn compatible. For example, a trait is not dyn compatible if it has generic functions, associated functions without a self parameter, or functions that return Self.

When to define a trait versus an inherent implementation

Define a trait only for the following purposes:

  • To enable generic programming (to use T: Trait in constraints of generic functions and types).

  • To accept or return an impl Trait or dyn Trait.

  • To define an extension trait (but see also the caveats in the section on extension traits).

In all other cases where you want to implement functionality for a type, use an inherent implementation.

Don’t use traits merely to organize code.

Extension traits

An extension trait is a trait that adds methods to a type from a different crate.

What differentiates an extension trait from other traits is that its sole purpose is to add methods to the type; it is not intended for use in generics (T: Trait, impl Trait) or dynamic dispatch (dyn Trait). To indicate this, the name of an extension trait should carry the suffix ~Ext (for example, ForeignTypeExt).

In general, the team that owns the original type should also own the extension trait.

Exceptions:

  • Extension methods that are expected to become well known and provide significant value, for example, in foundational libraries / frameworks.

Note: This rule is specifically about extension traits. It is relatively common to define a trait as an input trait (e.g., DateLike) and implement it on a bunch of foreign types.

Examples

extern crate foreign_crate;
use foreign_crate::ForeignType;

trait ForeignTypeExt {
  fn additional_method(&self);
}

impl ForeignTypeExt for ForeignType {
  fn additional_method(&self) { ... }
}

It is also possible to define an extension trait for every type that implements some foreign trait. Effectively, this is equivalent to adding default methods to the foreign trait:

extern crate foreign_crate;
use foreign_crate::ForeignTrait;

trait ForeignTraitExt {
  fn additional_method(&self);
}

impl<T: ForeignTrait> ForeignTraitExt for T {
  fn additional_method(&self) { ... }
}

Rationale

When the same team owns the extension trait and the type being extended, this helps avoid method name conflicts between the base type and the extension trait or between two extension traits.

Specific guidance for important traits

  • Types with public fields: Consider implementing traits implemented by all subfields.

  • Error types: Implement the Copy trait whenever possible. Copy types are simpler to manage and propagate because they can be passed by value without moving ownership. An example of a case where this is not possible is if the error must contain non-Copy fields like a String.

  • Copy: Implement whenever it is reasonably clear that the type will continue to be Copy in the future.

  • Clone: Most types should implement Clone unless they represent some external resource.

  • Default: Implement if the type implements new() or otherwise has a default state. For context, see Clippy lint new_without_default.

  • PartialEq / Eq: If you want users to use the == operator, prefer implementing PartialEq over Eq. When you do implement Eq, be aware of the additionally required reflexivity (details: <a href="https://doc.rust-lang.org/std/cmp/trait.Eq.html" rel="nofollow">https://doc.rust-lang.org/std/cmp/trait.Eq.html</a>).

  • PartialOrd / Ord: If you want users to use the < or > operators, prefer implementing PartialOrd over Ord. When you do implement Ord, be aware that it requires total ordering (details: <a href="https://doc.rust-lang.org/std/cmp/trait.Ord.html" rel="nofollow">https://doc.rust-lang.org/std/cmp/trait.Ord.html</a>).

  • Map keys

    • If you think it is likely that a type will be used as a map key or otherwise cached, implement Hash and Eq.
    • If, in addition, the type has a clear total ordering, implement Ord (and any traits it needs).

Pair important From impls with explicit constructor methods

If a type has an important From impl that is expected to be one of the primary ways of constructing it, add an inherent ::new(), ::new_from_foo()¸ or ::from_foo() method.

Options structs should implement Default

Structs with all-public fields that are primarily used as a “bag of options” for a function should implement Default.

Rationale

This enables convenient use of struct update syntax (Struct { a: 0, ..Default::default()} ).

Concurrency

Handling poison

Never reach inside a poisoned mutex to get at its insides, no matter how tempting the use case may seem.

Don’t use async / .await [chrome]

Don’t use async / .await in Chromium.

Don’t start new threads through the Rust standard library [chrome]

In Chromium, prefer to use sequences instead of threads.

Use fuchsia_sync::Mutex [Fuchsia]

Use Fuchsia’s mutex type, rather than one from std::sync or parking_lot.

Macros

When considering how to express an API, prefer in order:

  • Regular Rust functions, types, traits, objects, and modules. (Avoid macros, code generation, and metaprogramming where possible).

  • macro_rules!-style macros.

  • Other code generation strategies (e.g. genrule)

  • proc_macro macros.

When you decide to use a macro or code generation, please weigh the benefits against the costs:

  • IDEs, Code Search and other developer tools will work less well on code that uses macros.
  • Macros can create dialects of the language that make it harder for engineers to work across the codebase.
  • Large scale changes are more difficult and expensive to apply.

Rationale

Prefer non-macro facilities: Macros can create dialects of the language that make it harder for engineers to work across the codebase. When a reader sees a usage of a macro, they can make fewer assumptions about that code than if it didn’t use a macro. Macros are harder to write correctly than regular Rust code.

Use macro_rules! for simple code generation: Since Rust macros operate on the AST, they can be less fragile than textual code generation.

Naming

Naming methods

Follow the Rust standard library style for naming methods. See also the guidance on naming conventions in the Comprehensive Rust course.

Specific considerations:

  • Use prefixes such as as_...(), to_...(), or into_...() with the appropriate self convention. See also this section of the Rust API Guidelines and the Clippy lint wrong_self_convention.

Naming lifetimes

Lifetimes should have descriptive names, especially type lifetimes.

Exceptions:

  • Type lifetimes that are extremely generic abstractions like Cow.
  • It is fine to use anonymous lifetimes ('_) where they work.
  • Single-character lifetimes are okay when they are simply expressing the relationship between a method’s input and output, or needed for a local higher-ranked lifetime bounds (for<'a>). Up to two such local lifetimes are fine, if more are needed, we strongly advise you to use descriptive names.

Naming generic parameters

Use a short name for your generic parameters (either single or multi character), related to the kind of thing they represent. Single-letter generics are okay, but the letter chosen should be somewhat indicative of the object type, ideally taken from the most important trait bound.

T or U are fine when the generics are in a relatively unbound context; i.e. it is an API intended for use with most types, like Vec<T>.

Naming extension traits

When naming an extension trait, give the name an ~Ext suffix (for example, ForeignTypeExt).

Naming initialization functions

Follow these guidelines when naming initialization functions:

  • Constructors: Use new(), or new_with_foo() if there is more than one. If the constructor returns a Result, use try_new().

  • Value-to-value conversions: Use from_foo() with an associated From impl. See here for details on when an operation can be considered to be a value-to-value conversion.

Avoid abbreviations

Don’t use abbreviations (especially acronyms and initialisms) that someone outside your project is unlikely to know. Don’t abbreviate by deleting letters within a word.

See also the Google C++ Style Guide.

Exceptions:

  • Common abbreviations found in Wikipedia.
  • Abbreviations that are unlikely to be confusing to people outside of your project, for example IpAddr. You and your reviewer should make this determination. In general, the preference should still be not to abbreviate.

When and how to write documentation

Add documentation comments to public APIs, potentially using the missing_doc lint to help with this.

Best practices for documentation comments:

  • Consider including examples: It is not necessary for every function to have examples, but enough functionality should be covered in examples somewhere to give an idea of how the crate works.

  • Always document panic cases for functions which may panic due to malformed input.

  • Clearly document invariants for unsafe functions and traits.

  • Use capitalization and punctuation.

  • Don’t explain the basics of the programming language. For example, don’t do the following:

    struct MockFileSystem { ... }
    
    /// A mock implementation of the FileSystem trait, intended to be used in unit tests.
    impl FileSystem for MockFileSystem
    

Exceptions:

  • Comments should say something that the code does not already say. If there’s nothing insightful to say, it is better to omit the comment than to repeat the function name in a trivial way. For example, don’t do the following (it’s fine to allow the lint in that case):

    /// Removes an element.
    pub fn remove_element() -> Element {}
    
  • It is generally okay to omit documentation on individual enum variants, especially if their name is self-explanatory.

How to format documentation comments

Follow these guidelines when writing documentation comments:

  • Use /// comments to document items following the comment, such as functions, structs, enums, traits, and type aliases.
  • Use //! comments to document the module or the library itself. These comments should be placed at the top of the file.
  • Follow the rules laid out in the official Rust style guide on doc comments.
  • Use Markdown syntax.
  • Use ordinary comment syntax (//) for non-documentation comments, such as implementation comments, TODOs that are not interesting for users, commented out source code, etc.
  • Prefer to document things in a straightforward manner, using /// and //! comments, rather than generating documentation from macros. This makes the documentation directly readable when viewing or modifying the source code.

It is acceptable, and even encouraged, to mix doc comments (/// and //!) and ordinary comments (//). If you have one thing you wish to document publicly, and another thing you want to note as an implementation comment, then use both! For example:

/// Finds the first `FooItem` with the given `name`, or `None` if not present.
// TODO: crbug.com/12345 - `name` should be &str, not String.
pub fn get_named(items: &[FooItem], name: String) -> Option<&FooItem> {...}

Rationale

Many, though not all, languages have a different commenting convention (such as extra * characters) for doc comments as opposed to implementation comments. Rust and Python are notable for not just having different commenting conventions, but an actual different syntax entirely. This has concrete benefits to the Rust ecosystem:

  • Doc comments can be consumed by tooling, including documentation generators like rustdoc, IDE features, and Kythe.
  • Doc comments have a standard structure and formatting rules (Markdown), allowing for rich text in those contexts.
  • Errors in applying doc comment syntax (e.g., applying it to an expression instead of a documentable item) are linted against, catching documentation errors early. If there are no lint errors, the doc comments are guaranteed to be visible to documentation tooling, removing a degree of flakiness and unpredictability from the documentation process.
  • Doc comments are a part of the AST, and so can be produced and consumed by macros, including both macro_rules! macros and procedural macros.

Other Rust features

Unsafe code

Don’t write unsafe code unless there is no way to implement the same functionality with safe code.

If you do have to write unsafe code:

  • Encapsulate unsafe code in a safe abstraction and isolate it from business logic as much as possible. Ideally, unsafe code should be in a separate crate with few dependencies beyond the standard library.
  • Add a safety comment starting with SAFETY: that explains why the code is correct.
  • Add a technical justification for unsafe code. Depending on context, this can be a justification for an individual unsafe block or a more general justification that covers an entire file or build target. The justification should answer these questions:
    • Why is this code not written in an equivalent safe way?
    • If the reason is performance, are there benchmarks?

No unstable features

Don’t use unstable features.

Exceptions:

  • Android Embedded programming may require the use of certain unstable features. While review will be required to enable them (to help search for alternatives), features absolutely required to build an artifact correctly will generally be approved.
  • Chrome Style arbiters may blanket-approve a small list of features for the entire codebase. Typically, only features that have a clear path to stabilization will be approved.
  • Fuchsia See guidance here.

Getters and setters

Writing and naming getters and setters should follow these guidelines:

  • foo() for a getter that returns a T by value.
  • foo_mut() for a getter that returns a &mut T.
  • take_foo() for a getter that “clears” the value (by setting it to None or some other default state) and returns the old value.
  • set_foo() for a setter that takes a T by value.
  • with_foo() / with_mut_foo() for an accessor that operates via a closure (see details below). Such an accessor is invoked as object.with_foo(|foo| frob(foo)).
  • replace_foo() for a setter that returns the old value.

Examples

impl Stuff {
   fn foo(&self) -> &Foo { ...  }
   fn foo_mut(&mut self) -> &mut Foo { ... }

   /// Sets "baz" property to `None` and returns the old value.
   fn take_baz(&mut self) -> Option<Baz> { ... }

   fn set_foo(&mut self, foo: Foo) { ... }

   /// Calls `f` with a borrow of the "bar" property.
   /// Returns the return value of `f`.
   fn with_bar<R>(&self, f: impl FnOnce(&Bar) -> R) -> R { ... }

   /// Calls `f` with a mutable borrow of the "bar" property.
   /// Returns the return value of `f`.
   fn with_mut_bar<R>(&self, f: impl FnOnce(&mut i32) -> R) -> R { ... }

   /// Sets "foo" property to `foo` and returns the old value.
   fn replace_foo(&mut self, foo: Foo) -> Foo  { ... }
}

More on accessors that accept closures

Accessors that accept a closure (.with_foo()) are typically invoked as object.with_foo(|foo| do_something_with(foo)). They can be a good choice in the following situations:

  • When the property is contained in an interior mutability wrapper (e.g. Mutex or RefCell). These mechanisms need to constrain access to the property to a scope, and the closure provides this scope. This is usually preferable to directly returning the RAII object that implements the scope (e.g. MutexGuard<T>) because the latter would leak an implementation detail.

  • When follow-up work or validation is needed after a property is changed and a set_foo() accessor cannot be used (for example because the property’s type is expensive to clone or cannot be cloned at all).

A with_foo() accessor will often allow the closure to return a value and forward it by returning it from with_foo().

If you need to provide two closure-accepting accessors, one for read-only access and one for mutable access, it is common to name them with_foo() and with_mut_foo(). Otherwise, with_foo() is often also used for mutating accessors.

Implement expected API sets together

If you implement len(), also implement is_empty(). For context, see Clippy lint len_without_is_empty.

Do not access items marked #[doc(hidden)] that you don’t own

Treat items marked #[doc(hidden)] as private. Don’t access them unless you own the code.

Use const functions responsibly

Mark a public function as const only if you expect it to remain const in the future (or can commit to helping clients migrate when it changes).

Rationale

It’s a breaking change to turn a const function into a regular function.

Avoid overloading for non-mathematical purposes

Use operator overloads for their original mathematical/logical purpose, not just to make the code more concise (as “syntax sugar”).

Apply #[non_exhaustive] to publicly-exposed enums

If an enum is part of a public API used by other teams, mark it #[non_exhaustive].

Exceptions:

  • The enum will never change. For example:
    • Enums describing an existing ontology (e.g., enum Color { Red, Green, Blue }).
    • Enums with two states that could be Booleans (e.g., enum Visibility { Visible, Hidden })
  • The enum may change in the future, but you are committed to fixing resulting breakages via an LSC.

For context, see Clippy lint exhaustive_enums.

Rationale

Marking an enum #[non_exhaustive] forces users to include a wildcard arm (_ => ...) in match statements. This ensures code continues to compile when new variants are added to the enum later.

This rule is primarily important for enums used across team boundaries, because the scope of the breakage could be significant. Enums used only a small number of times can’t create a large breakage and thus may ignore this rule.

Apply #[non_exhaustive] to public structs with public fields

Structs with public fields that are part of a public API used by other teams should be marked #[non_exhaustive].

Exceptions:

  • The struct fields are unlikely to change, e.g., because they describe an existing ontology (e.g., a Rectangle type) or the reviewer can otherwise be confident that they will not change.
  • The struct fields may change in the future, but you are committed to fixing resulting breakages via an LSC.
  • You want to enable your users to use functional record update (FRU) struct initialization syntax (Person { name: "foo", ..Default::default() }).

Rationale

Adding a new field (even a private one) to a struct with all public fields is a breaking change, because callers can no longer name all fields in a struct literal.

A #[non_exhaustive] struct can’t be constructed with the struct literal syntax, except for the code in the same crate. This allows new private fields to be added without breaking users.

This rule is primarily important for structs used across team boundaries, because the scope of the breakage could be significant. Structs used only a small number of times can’t create a large breakage and thus may ignore this rule.

#[must_use] on functions

Use #[must_use] on functions in these cases:

  • The caller cannot use the function correctly if they ignore the return value. This includes pure functions (functions without side effects), such as HashMap::get().

  • Any function whose return value represents an error or otherwise critical information about the function’s behavior. If the return type is already annotated #[must_use] (which is true of Result, for example), the function does not need to be annotated #[must_use].

#[must_use] on types

Use #[must_use] on types intended to be returned by functions that would otherwise be annotated #[must_use] according to the rules above.

Examples:

  • The type is an error type (similar to Result or StatusOr).

  • The type represents a lazily evaluated computation. In other words, the function call doesn’t do anything by itself, and the return value needs to be accessed to cause some desired side effect.

Do not run code before main()

Do not cause code to run before main(), for example by using techniques such as the ctor crate.

Implementation concerns

Don’t overuse type inference

Add enough explicit types so that someone familiar with the codebase can figure out what the inferred types are.

Prefer to add the types to variable declarations, rather than as turbofishes. If the compiler forces you to add a turbofish, it is generally fine to add a _ inference marker.

Specific situations in which you should probably add types:

  • When calling methods with a generic return type, such as .into() or .parse().
  • For the result of complex iterator chains.

Prefer pattern matching over check-then-unwrap

Avoid potentially panicking operations when the input data guarantees they cannot panic. For example, instead of using Option::unwrap on a value known to be valid, use pattern matching to prove correctness to the compiler.

// Good: The match arm proves to the compiler that `x` is `Some`.
// A panic is obviously not possible.
if let Some(value) = option {
    use_value(value);
}
// Bad: Correctness of `unwrap` relies on a runtime check that `option` is `Some`.
if option.is_some() {
    use_value(option.unwrap());
}

Prefer expect over unwrap

If a panic is necessary because an invariant cannot be expressed in the type system, prefer expect over unwrap to document the reason.

Don’t omit patterns on non_exhaustive enums when matching

Prefer to list all enum variants in match statements over non_exhaustive enums.

If you don’t care about specific variants, place them in the wildcard arm. If you only care about specific variants, use an if let or a let … else {}.

Examples:

#[non_exhaustive]
pub enum Shape {
    Circle,
    Square,
    Triangle,
}
fn process_shape(shape: Shape) {
  match shape {
    Shape::Circle => println!("It's a circle"),
    Shape::Square => println!("It's a square"),
    // Triangle and any future variants don't require any actions.
    Shape::Triangle | _ => {}
  }
}
fn process_shape(shape: Shape) {
  if let Shape::Circle = shape {
    println!("It's a circle"),
  }
}
fn process_shape(shape: Shape) {
  match shape {
    Shape::Circle => println!("It's a circle"),
    _ => {}
  }
}

Exceptions:

  • You only care about specific variants and match is cleaner than if let.

This is covered by the built-in lint non_exhaustive_omitted_patterns which you can enable on your code.

Use context strings to clarify assertions

While some assertions are self-explanatory, many benefit from additional context that clarifies why an assertion is expected to hold, or what caused it to be violated. Use the optional format string argument for assert! to convey this context.

You should always consider adding context, but particularly in the following situations:

  • If you’re thinking of adding a comment to the assertion. Use a format string instead: It has the benefit that it will appear in logs.
  • If you expect failing assertions to be seen by users. In this case, a human-readable explanation is more helpful than a Rust expression.
  • If logging a value helps in understanding why the assertion failed.
  • To clarify whether a failing assertion means that
    • The client misused an API (i.e., they should change their use of the API), or
    • The implementation contains a bug (i.e., the client should file a bug report).

Examples

Explaining why an assertion should hold

    assert!(
        values.is_sorted(),
        "`values` is populated from a `PriorityQueue`, so we expect it to be sorted"
    );

Logging a value to clarify why the assertion failed

assert!(refund > 0, "Refund must be positive, got {refund:?});

Flagging misuse of an API

impl Database {
    ...

    fn execute_query(&self, query: &str) {
        assert!(self.stream.is_some(), "Call connect() before executing queries");
    }

    ...
}

Don’t use the context string merely to repeat the condition

assert(!values.is_sorted(), "values should always be sorted");

Don’t use iterator combinator chains when direct iteration is clearer

Avoid using combinator chains in ways that hurt readability. Specifically:

  • Avoid long and complex combinator chains.
  • Don’t construct combinator chains in the header of a for loop. Instead, assign the combinator chain to a variable, then consume that variable in the for loop.
  • Avoid Iterator::for_each unless you’re convinced you have a good reason. A for loop is usually easier to read.

Combinators are preferable for shorter chains and for cases where the combinator itself provides a lot of logic that would otherwise have to be written out in the corresponding for loop.

Examples

Complex combinator chains

Here’s an example of a combinator chain that is hard to read because it involves conditionals and mutates external state:

let mut error_count = 0;
let _processed_items: Vec<_> = items
    .into_iter()
    .map(transformation_that_may_fail)
    .map(|result| {
        if result.is_err() {
            error_count += 1;
        }
        result
    })
    .filter_map(Result::ok)
    .collect();

The corresponding for loop is easier to read:

let mut error_count = 0;
let mut processed_items = Vec::with_capacity(items.len());

for item in items {
    match transformation_that_may_fail(item) {
        Ok(val) => processed_items.push(val),
        Err(_) => error_count += 1,
    }
}

Combinator chains in for loops

Avoid constructing a combinator chain in the header of a for loop:

for bug in bug_descriptions
    .iter()
    .filter_map(|description| parse_bug_description(description).ok())
    .filter(|bug| bug.priority <= Priority::P1)
{
    assign_bug(bug);
}

Instead, assign the combinator chain to a variable. This reduces clutter, and the variable name documents what the combinator chain does:

let urgent_bugs = bug_descriptions
    .iter()
    .filter_map(|description| parse_bug_description(description).ok())
    .filter(|bug| bug.priority <= Priority::P1);

for bug in urgent_bugs {
    assign_bug(bug);
}

When to prefer a combinator chain

Here’s an example where a combinator chain is preferable:

let threshold_exceeded = values.iter().any(|&v| v > threshold);

The corresponding for loop is harder to read and reinvents the logic contained in any():

let mut threshold_exceeded = false;
for v in values {
  if v > threshold {
    threshold_exceeded = true;
    break;
  }
}

Don’t call panic::catch_unwind

Use of panic::catch_unwind is prohibited in first-party Rust code.

Exceptions:

  • It is acceptable as a defense in depth mechanism as long as it is never relied upon for any properties. A codebase that is already attempting to avoid panics may choose to wrap critical entry points with a catch_unwind recovery mechanism just in case a panic slips through. Code authors must convince the reviewer that the codebase is doing most of what it can to avoid panics. This exception only applies to code that has the possibility of being run with panic=unwind (so this does not apply to Chrome, Android, Fuchsia).

Rationale

Rust code may be used in many environments, and panic::catch_unwind will only function correctly in environments with panic=unwind. While the final panic disposition of prod binaries has not yet been determined, Chrome, Android, and Fuchsia all require panic=abort.

Prefer format! / write! for concatenation and substitution

Prefer format!() and write!() to other alternatives in the following situations:

  • To concatenate more than two strings. For example, use format!("{a}{b}{c}") instead of a + b + c.
  • To perform substitutions on a literal string (instead of using str::replace).

Rationale

  • format! / write! require fewer allocations than concatenating strings using +.
  • Prefer standard format strings to inventing your own templating syntax.

Use the right per-codebase test framework

Most codebases use their own specific test framework rather than #[test].

  • Chrome Use rust_gtest_interop. This makes it possible to integrate Rust tests into C++ test binaries.

  • Fuchsia Use #[fuchsia::test]. See here for more details.

Read the whole story
emrox
1 day ago
reply
Hamburg, Germany
Share this story
Delete

Joshua Idehen – Mum Does the Washing

1 Comment

The world according to your mum doing the washing

Capitalism
Your mum does the washing, you pay her a dollar
You get her to do your mate's washing, your mate pays you fifty dollars

Communism
Your mum does the washing, you do the washing
Every night, you salute a picture of your dad

Socialism
Your mum does the washing, you do the cooking
Everybody is happy, in theory

Feudalism
Your mum does the washing and pays you tax

Colonialism
You barge into your mum's room
Claim you discovered your mum's room
Dump all your dirty clothes on the floor

Fascism
Your mum does the washing
And you are obsessed with her nose

Liberalism
You watch your mum do the washing
And you feel really, really bad
"Something must be done" you say
Something may or may not get done

Libertarianism
Your mum does the washing
You believe you did the washing

Religion
Your mum does the washing
You thank god

Atheism
Your mum does the washing
You make a twelve-part YouTube video demanding peer-reviewed evidence
That she did, in fact, do the washing

Zionism
You shoved your mum into the washing machine
And the spinning made her dizzy
And that dizziness made her vomit
And you point to that vomit and call it antisemitism

Americanism
Your mum does the washing
It's in the constitution
End of discussion

Mansplaining
Your mum does the washing
You tell her how best to do the washing
You have never done the washing

Same-sex marriage
Your mums do the washing

Patriarchy
Your mum doesn't exist
The washing is mysteriously done

TERFism
Your mum doesn't want your other mum doing the washing
Because of like, genitals and stuff

Polling
There is a seventy percent chance
You will do the washing on Saturday night
Come Sunday morning
It was one-hundred percent done by your mum

Africanism
You go to Benny's house
Benny's mum does the washing
You ask your mum why she doesn't do the washing
You do not survive the night

Feminism
Your mum insists you grow up and do your own washing

White feminism
Your mum hired a woman of color to do the washing

Male feminism
The one time I did the washing
I told everyone I did the washing
I blogged about it
Bragged about it
Took a selfie
Insta story
Went on TV
Won an Oscar
Went on Fortnite
Did a dance
I am an ally
Hashtag metoo

Egalitarianism
The one time you did the washing
Is proof it's all equal and no one needs feminism anymore

Hip-hop
Everyday I'm hustling
Everyday I'm hustling
When I bring the basket
Mama puts the washing in

Narcissism
You look good in the clothes your mum washed

Surrealism
The washing does your mum

Read the whole story
emrox
1 day ago
reply
I heard that one on a mixset yesterday. Really made me laugh (you can read it there, but listening might be more fun)
Hamburg, Germany
Share this story
Delete
Next Page of Stories