This is a prerelease version of this book. Feel free to check if out! I would greatly appreciate it if you left me some feedback. If something is wrong, feel free to leave a merge request on the repository
Rust Project Primer
A Practical Guide on how to Structure and Maintain your Rust Projects
by
Patrick M. Elsen
CC BY-NC-SA 4.0 Licensed
Preface
I have always been a bit of a programming language nerd. Growing up, I realized that we live in some of the most exciting time to be alive, the digital revolution is in full force. In the span of a century, we have fundamentally changed the way we operate and communicate, by bringing computers into our daily lives.
Every programming language is a product of its time. Early programming languages existed in much more resource-constrained environments. They had to be designed so that they could be compiled without needing to use too much computing power or memory.
But now, well into the 21st century, we live in an overabundance of computing resources. Yet, we still continue using the same languages that we came up with 50 years ago. And we can feel the pain: the applications we use are often either insecure or slow.
In my mind, Rust is a bit of fresh air in the programming language world. It is unique in being one of the few languages that manages to pack revolutionary ideas (memory safety in a systems language, borrow checker) into a language that is usable in the real world. Previous attempts at adding safety have typically ended up as language that are neat from an academic viewpoint but not usable in practise. But now, even Microsoft and Google are adopting it.
To me, Rust makes programming very joyful. It is like LEGO, you have all these little pieces and you can put them together any way you want. You can write multithreaded code with confidence. You can write async code with confidence. You can mix and match. And unlike C and C++, you can have confidence that they actually work, and that your application doesn’t collapse like a Jenga tower once it gets too big.
Rust certainly isn’t perfect, but in my opinion it is fun. And I would like more people to be able to enjoy it. In this book, I try to compress all of the things I have learned from using Rust in the past 8 years, to make sure that you can build cool things, too.
License
I’m licensing this book under the CC BY-NC-SA 4.0 license. Licensing it this way gives you a lot of freedom to adapt this book and update it, as long as you do not do so for commercial gain. I hope that it will be useful to some.
If you want to give something back to the Rust community, I suggest you get involved in the community, for example:
- Helping with the Rust compiler development, RFC process or joining a workgroup,
- Helping the Rust crate ecosystem, by participating in building features or fixing bugs,
- Sharing your knowledge through blog posts, guides or tutorials.
If you are new to the Rust programming language, I recommend you to spend some time writing documentation for Rust crates that need it. It is a good way to be exposed to some Rust code and make an impact. Adding good documentation is usually appreciated and uncontroversial.
Introduction
Rust as a langauage is magnificent in many ways. While it has a steep learning curve that can make it difficult to get started, it will give you superpowers once you are familiar with it. You can suddenly write fast, heavily multithreaded code that would previously require a team of very senior developers writing thorough documentation on which locks are needed to access what, and in which order they need to be acquired to make sure it doesn’t crash. You can write bespoke data structures, knowing there aren’t any odd edge-cases that would make it unsafe to use. You can safely work with untrusted data, knowing well that you can’t accidentally forget a length check, which leads to a stack overflow and remote-code execution in your production environment. All of these properties mean that Rust is very scalable: your code bases are not a house of cards, waiting to collapse. For the most part, if your code compiles, you know that it works.
Rust has other properties that make it quite interesting. It is somewhat unique amongst systems programming languages in that you can deploy on a vast breadth of environments: from lower-power microcontrollers with kilobytes worth of RAM, to large servers, and even write frontend applications that run in the browser. The applications are endless, and the ecosystem is ever evolving to make this easy.
At the same time, this power can be frightning. Once your have learned the basics, where do you go from here? What parts of the ecosystem do you use for what? What are some common issues that your project might run into, and how do you solve them? How do you structure your project, what are some common pitfalls that you need to avoid?
The idea of this book is to aggregate information and advice that you can use on your Rust journey. In some ways, it is the book I wish I had read when I got started with Rust. Knowing the language is one thing, but knowing the ecosystem is what lets you be productive. Understanding the tools that exist, and when you should use them. Knowing how you can deploy them. Structuring projects in a way that supports long-term growth and sustainability.
This book will not teach you Rust, nor will it explain Rust syntax in any way. For that, there are already plenty of other books, some of which are linked in the Prerequisites section. Instead, the focus is on practises, high-level advice with examples. Ideally, this book should help you, no matter if you are a project manager evaluating Rust and trying to understand best practises, if you have recently learned Rust and want to embark on your first real-world project, or if you already have some experience but want to lookup specific tooling or solve specific problems.
The book largely follows a recipe format. Each chapter is fairly self-contained, so you can focus on specific topics as needed. It is not intended as a guide for you to implement every single piece of advice, more to give an overview of what exists, how it helps you and when you should use it. To solve specific problems, once they occur. Use it as a source of inspiration to find the approaches that work best for your project.
Target Audience
This book is aimed at anyone who wants to start, maintain or collaborate on Rust software projects.
You can read this book at several levels. If you are a very technical person with a lot of project experience, you can use this as a recipe book showing you examples of how to implement various practises in real-world Rust projects. If you are less technical, but want to understand what is possible in terms of automation that can lead to higher quality code and save development time, you can use this book as an overview various strategies and what they accomplish.
Although the focus is specifically on Rust software projects, some of the information contained in this book is also useful for software projects in general. It covers various good practises of software development, containing insights from various companies and successful projects.
How to read this book
This book is structured like a recipe book: you can read it cover-to-cover, if you like. But you can also use it as a tool to look up recipes for how to solve issues you might run across.
Why Rust?
In his seminal paper, Go To Statement Considered
Harmful the
Dutch computer scientist Edgar Dijkstra postulated something novel: restricting
computer scientists in what they can do can lead to better software projects.
The paper explains that, while it is possible to write programs that use goto
statements to jump around, better code quality and maintainability can be
achieved through the use of structured programming, concept such as for and
while loops, and functions. This abstraction lets programmers write code that
is easy to follow, expand and maintain.
In some ways, the Rust programming language is a manifestation of an extension of this idea: just like the flow through a program needs structure, the ownership of memory needs ownership.
Software Complexity is Growing
- software complexity
- difficulty to write multithreaded applications
- difficulty to scale software
- increase in vulnerabilities
Rust as a Programming Language
As a programming language nerd, I have had the privilege to be able to explore a number of programming languages before, many of which are niche. Every programming language I have encountered has some amount of wisdom embedded in it; I feel that I learn something new from every language I encounter and familiarize myself with.
The three most significant wisdoms that the Rust language has taught me are:
- Abstractions are not always a trade-off. You can design useful abstractions that have no cost (zero-cost abstractions).
- Safety, especially memory safety, is non-negotiable. It is fundamental to building robust software and it cannot be an afterthought.
- Having good tooling makes working with a language delightful.
To me, having a language that both has useful abstractions, prioritizes and has tooling that is a joy to use sounds like a really good time. Combine that with an excellent software ecosystem, a package manager that works well and reliably, and a user base that is helpful makes it my favorite language.
Why robust software?
One of the things I have learned from working at various companies is that bugs are very expensive, and they grow quadratically. That means that when you have a small codebase, it is quite easy to make sure the code you write is correct. But as codebases grow, it becomes harder and harder to ensure that. Systems become large and interact in complex ways, which makes it easy to introduce unintended bugs and difficult to track them down.
I think the reason why this happens is that every programmer has a constant rate of bugs that they produce. As software grows in complexity, it accumulates systems, and it accumulates system interactions. As systems interact with more other systems, directly or transitively, the chance of introducing bugs and the difficulty of tracking them down gets higher.
As a programming language, Rust allows you to write code that is free from a lot of classes of bugs: it makes it impossible, or at the least very difficult to write code that is memory-unsafe or multithreading-unsafe. However, there can still be logic bugs in the application. The majority of this guide is focussed on giving you the tools you need to make sure you structure projects in a way that minimizes the number of bugs.
Articles
https://medium.com/@penberg/why-i-am-not-yet-ready-to-switch-to-zig-from-rust-3660e02f0060
https://matklad.github.io/2024/10/06/ousterhouts-dichotomy.html
https://www.infoq.com/presentations/Null-References-The-Billion-Dollar-Mistake-Tony-Hoare/
https://en.wikipedia.org/wiki/Tony_Hoare
rust is not about memory safety (archived) by Leonardo Santiago
You Can’t Spell Trust without Rust (archived) by Alexis Beingessner
Resources
This guide is aimed at developers and project managers already comfortable with the Rust programming language. It does not cover any fundamentals of the language itself, only how to structure projects.
You don’t necessarily need to be good at Rust for this book to be useful to you, for example if you are reading it from the perspective of an engineering manager or software architect who just wants to understand what Rust is all about or what tools it comes with. But if you do want to write effective Rust, then these resources should be helpful to you to get started.
I have categorized these resouces into two sections: foundational contains resources that explain concepts and strategies, whereas the practical resources contain hands-on projects for you to follow. None of the links here earn me any commission. I am recommending them because I think they are useful, and not because I earn any money from doing so.
Foundational
Below is a list of books that I’ve personally found useful resources for understanding the Rust programming language, and some of the more complex features it has (for example, how async works under the hood, or how atomics work). You should have read at least one of these before you embark on your Rust project.
Rust Programming Language, 2nd Edition by Steve Klabnik and Carol Nichols is the official book of the Rust programming language. It covers the language and toolchain, giving you a thorough starting point for writing real-world Rust code and understanding other people’s code. It also includes some example projects for you to follow to see how to use it in practise. Available online and print.
Rust for Rustaceans by Jon Gjengset is a deep dive into the Rust programming language. It gives you a structured understanding how to apply Rust, covering many parts of Rust projects, from designing interfaces to writing effective tests. In my opinion it is one of the best explanations of how async works. Available in print.
Rust Atomics and Locks by Mara Bos is a book that gives you a deep understanding of atomics. Some of the core assumptions that you have as a programmer (such as, if your code writes to variables in a specific order, that the CPU writes to them in that order) break down the moment you use multi-threading. Rust makes it easy for you to write heavy multithreaded applications, and typically you will use safe abstractions to do so. But there are times, for example when you want to implement custom data-structures, that you need to know how to do so safely. This book gives you that background information. Available in print.
Rust Design Patterns is a catalogue of Rust design patterns, anti-patterns and idioms. Going through these will help you understand common patterns, and avoid anti-patterns. It also gives rationale for why to avoid certain patterns. Available online and archived.
Software Engineering at Google is not a Rust-specific book. Rather it is a generic book about software engineering. The reason I am linking it here is that Google is undoubtedly a company that has originated many of the philosophies of modern software engineering, and many of those philosophies have ended up being codified in the Rust programming language and developer tooling. Understanding this book gives you some of the whys behind why the Rust developer tooling is the way it is, and why it is so effective. Available online and print.
Rust Under the Hood by Sandeep Ahluwalia and Deepa Ahluwalia is a deep-dive into Rust internals and generated assembly. It shows you how Rust concepts map to machine code, how Rust represents various types in-memory, how it uses compiler optimizations (such as loop optimizations and SIMD auto-vectorization). This book is useful if you care about low-level details, even if you know little about x86 assembly. Available online.
There may be more useful foundational Rust resources that I have not listed here, because I might not be aware of them. There are some sites that maintain collections of useful Rust books, for example The Little Book of Rust Books, The Rust Bookshelf.
Practical
Some people, including myself, enjoy learning new things through interactive exploration. These resources teach Rust concepts primarily in such a way.
Effective Rust by David Drysdale is a book that lists hands-on recommendations for writing effective Rust code. It focusses on idioms, giving practical advice on implementing types, traits, Rust concepts, dependencies, and tooling. I would consider it a must-read for anyone new to Rust. Available online, in print, archived.
Zero to Production by Luca Palmieri is a practical guide for building production-ready Rust web applications. This is a great book to get started on understanding how to build real-world Rust application, including handling migrations, logging, error reporting, metrics. Available online.
Comprehensive Rust is a Rust training course developed by Google, aimed at getting people new to Rust up to speed on development quickly. Available online.
CodeCrafters is a learning platform with support for Rust. While not specific to Rust, CodeCrafters has a growing number of courses that are all built around the idea of reimplementing popular software yourself. Some of the courses they have are Build your own Git, Build your own Redis, and Build your own SQLite, to name but a few. What makes the courses fun is that they are broken down into small steps and come with unit tests that allow you to test your implementation as your progress. Available online.
Rust Adventure by Chris Biscardi is a collection of interactive courses that teaches you how to build things in Rust through a set of workshops. Available online.
Articles
Some people in the Rust community have written articles and guides with a similar scope as this book. While some of the takes may be different from those presented in this book, it can be valuable to review these to see which conclusions others in the Rust community have arrived at.
One Hundred Thousand Lines of Rust by Alex Kladov is a series of articles that summarize what Alex has learned in maintaining several mid-sized Rust projects. He has some advice on documentation, writing effective tests and improving build times. Alex Kladov is the driving force behind several high-profile projects in the Rust community, such as rust-analyzer.
Writing Software that’s reliable enough for production by Sciagraph: Sciagraph is a profiler for Python data processing pipelines. In this blog post, they explain how they approach writing software that is reliable, with some very similar approaches as this guide recommends.
Basic Things by Alex Kladov
Alex argues for some basic properties of software projects. He discusses how getting these right can be a force-multiplier as projects grow in scope, developers and users.
My Ideal Rust Workflow by fasterthanlime
Chapter 5: Continuous Deployment for Rust Applications in Zero to Production
Good Practises for Writing Rust Libraries by pascalhertleif (published in 2015)
Videos
Setting up CI and property testing for a Rust crate by Jon Gjengset
In this video, Jon shows how to set up a CI pipeline and property testing for a crate he has authored. This primer explains a lot of the things he does here and why he does them. This stream is worth watching if you are interested in watching the process of getting useful testing setup for a project.
Development Environment
This chapter explains what you need to get started writing a Rust project. It outlines how your can install a Rust toolchain, and what editors or IDEs you can use to write Rust code. If you already have a Rust toolchain installed and you have an editor or an IDE that you are comfortable using, you can safely skip this chapter.
Fundamentally, you need two pieces of software to get started with your Rust project:
- Rust toolchain: with the components needed for formatting, linting Rust code, in the correct version, and with the right targets.
- Code editor: with support for Rust through syntax highlighting and ideally integration with
rust-analyzer
.
This section outlines how you can setup your environment to be able to write Rust productively, by showing you ways to get a Rust toolchain installed and by examining some popular code editors used by the Rust community.
A lot of this book is very command-line centric and as such you may find the experience of using these tools slightly easier on UNIX-like operating systems such as Linux or macOS. This should not come as a surprise, as the majority of Rust developers work on and target Linux according to the 2023 survery. However, Rust loves Windows too, and most of the tools explained here should work on any platform. I try to point out any commands that either don’t work on natively on Windows or require special setup. You can always try WSL2 to to run things if you run into any issues.
Rust Toolchain
The bare minimum you need to get started with to write and build Rust code is a
text editor and rustc
. However, to do meaningful work, you will likely also
need Cargo and some way to manage it, for example to update your Rust
toolchains or install support for other targets like WebAssembly.
Rust toolchain consists of:
Item | Description |
---|---|
rustc | Rust compiler |
cargo | Rust package manager and build system |
rustfmt | Rust code formatter |
clippy | Rust linter, and automatically fix code issues |
rust-std | Rust standard library source code, used when requesting rustc to build it from source |
rust-docs | Documentation for Rust’s standard library |
There are different release channels. The stable
channel tracks
stable Rust releases, such as 1.80
, while the nightly
channel tracks
nightly releases that come with more features, but which might be unstable.
Generally, you want to stick to the stable
release channels, unless you have
a specific reason to use the nightly
ones (for example, you need to use a
feature that is unstable).
Depending on what you are writing software for, you may also want to install
toolchains for different targets. For example, you may need the targets
x86_64-unknown-linux-gnu
to build software for Linux,
wasm32-unknown-unknown
to build software for WebAssembly targets, or
thumbv6m-none-eabi
to target Cortex-M0 ARM microcontrollers.
Your operating system might have Rust available in its package manager, however you should be careful about using it. The version available might be outdated, or there might not be a way to use Rust nightly or install a different target. For some tasks, such as writing WebAssembly web frontends in Rust or doing embedded development, you will need to install additional targets so that Rust knows how to compile your code.
You will likely want some way to not only install Rust, but also manage the components and targets, update the toolchain and have the ability to install different versions of the toolchain side-by-side to work on your project.
Rustup
The recommended approach to install and manage Rust toolchains, components and targets is Rustup. It lets you install different versions of the toolchain side-by-side, switch between them either explicitly or with some configuration inside your project.
To install rustup
on Linux, you can run the following command. If you are
using Windows, you can find installation instructions on the website.
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
With Rustup installed, you should now have access to Cargo and you can use it to manage your Rust installation. Here are some useful commands for reference:
# install a different version of the toolchain (can also give a specific version)
rustup install nightly
rustup install 1.80.0
# install a target
rustup target add wasm32-unknown-unknown
# update your Rust toolchain
rustup update
When you use Cargo, Rustup will use your default toolchain. For most of your
development, this should be sufficient. However, you can always override this
to use a specific toolchain, for example to use nightly
for a specific command
by adding +<version>
to any command:
# build and run tests using the nightly toolchain
cargo +nightly test
If you have Rustup installed and Cargo works, then you are set up for using Rust.
Nix
While Rustup is the most popular and preferred way to manage Rust toolchains, it is not the only way you can use to install and manage Rust toolchains. Another popular tool used by Rustaceans to manage their toolchains is Nix, which is a declarative package manager and build system.
Todo
Editors and IDEs
Preferences for development environments amongst developers varies widely. Some developers prefer light-weight editors such as vim, neovim, or helix. These have the advantage of being fast and portable, tend to be easy to extend and rely on keyboard shortcuts to avoid being slowed down by using a mouse. Especially terminal-native developers tend to prefer enjoy these editors, because it means they can do all of their development in the terminal and can even use these editors remotely over SSH.
The other camp likes using IDEs, which are graphical tools for writing code. They tend to integrate very well into the programming languages and have compelling features such as jump-to-definition, show type information or have debugging support built-in. IDEs used to have a bad reputation for being rigid, but modern ones are just as extensible as command-line editors.
This survey shows that the two most popular editors for Rust are VS Code, and Vi-family editors (which I group together as Vim). The Zed editor is also popular, but did not appear in this survey, likely because it was not stable at the time the survey was run.
We can cluster the editors into two groups:
- Graphical IDEs: Includes VS Code, Rust Rover, Sublime Text, Visual Studio, Xcode, Atom.
- Terminal-based editors: Vim, Helix, Emacs
In general, Graphical IDEs are more friendly to beginners. For this reason, the editors discussed in this chapter focusses mainly on these. The Terminal-based editors have their own advantages, but they require more learning and unless you are already familiar with them, it likely does not make sense to pick them up.
In the subsections of this chapter, we take a look at three editors that yield a good developer experience:
- VS Code: Partially open-source editor developed by Microsoft, has extensive plugin functionality, basically a clone of the once-popular Atom editor.
- Zed: Open-source editor written in Rust, comes with Rust support out of the box. Not available for Windows currently.
- Rust Rover: Commercial, but free-to-use for noncommerical applications, developed by JetBrains.
Rust Analyzer
Language servers are tools that parse understand programming languages, and expose this data to IDEs. Unline compilers, which run once and produce a binary, language servers are designed to run continuously, generated metadata such as inferred types of values, and implement high-level operations such as refactoring code.
The original language server for Rust was called Rust Language Server, and it used rustc to parse projects. This approached worked initially, but there were issues with latency. Additionally, rustc is not great at handling incomplete or broken code, which is important for language servers as they run while you write code. As a result, RLS was deprecated in 2022.
- graph of rls architecture
As a result, a new approach was taken that used a custom parser to be more
error-resiliant than rustc
, called
rust-analyzer.
- graph of rust-analyzer architecture
The core piece that makes Rust IDEs possible is thus rust-analyzer, which is a project that understands Rust projects and implements the Language Server Protocol, which is a way for IDEs to understand them too and display type annotations, warnings, errors, suggestions.
In general, any IDE that supports the LSP protocol can be used for Rust development using rust-analyzer. The only exception is Rust Rover, which implements it’s own parser for Rust projects.
In general, you don’t need to know much about Rust Analyzer to use it. In fact,
many Rust IDEs even bundle it, and will manage and update it for you. You will not
even be aware that it is running in the background. But there are some situations where
you might need to be aware of its existence. If you use build systems other than Cargo to
build your Rust project, for example, then Rust Analyzer might not be able to analyze
your project. There might also be cases where it has bugs, because it uses a different
parser for Rust than rustc
has.
Reading
Book for the Rustup tool used by the Rust community to install and manage Rust toolchains. It explains core concepts such as channels, toolchains, components and profiles, how to configure Rustup to use specific versions of the toolchain on a per-project basis.
Explains what rust-analyzer
is, and how to use it. It has instructions for the
best way to install it for every editor it supports, and outlines ways you can
configure it for your project.
Why LSP? by Alex Kladov
Alex explains what problem LSPs solve.
LSP could have been better by Alex Kladov
This article discusses architectural aspects of LSPs, that Alex does not find as brilliant.
LSP: The good, the bad and the ugly
Improving “Extract Function” in Rust Analyzer
https://rust-lang.github.io/rustup/index.html
Zed
Zed is a code editor that comes with support for Rust out-of-the-box. It deserves a special mention because it itself is written in Rust. It is fairly minimalist, offering limited support for extensions (only themes, grammars and language servers can be extended). But the advantage is that it requires no setup, it understands and can work on Rust projects with no configuration.
If you just want an editor that you can use to write Rust code, and you only
need features that rust-analyzer
comes with out of the box, then it is a good
choice. It is also open-source.
- screenshots of all features zed has for Rust projects
Features
Notes
Notably, the team behind Zed runs a blog documenting their experience building a cross-platform code editor in Rust, with deep dives into challenges they have faced in doing so and how they managed to tackle them. A lot of the articles there are good reading for anyone who is interested in Rust, cross-platform development, real-world asynchronous applications and the like.
Visual Studio Code
- screenshot of vscode (light/dark mode)
Visual Studio Code is a clone of the previously popular Atom editor that is sponsored by Microsoft. Compared to Visual Studio, it is lightweight and relatively fast, and has the advantage of being easily extensible. It has a vast ecosystem of plugins for various programming languages, including Rust.
Plugins
rust-analyzer
https://code.visualstudio.com/docs/languages/rust
RustRover
RustRover is a commercial IDE offered by JetBrains. It has a deeper integration and more intelligent features than the other IDEs listed here, but is only free for personal use.
It is being actively developed, and new features that make writing Rust code and managing Rust projects are constantly added. The advantage is that it is all integrated and works out-of-the-box, unlike Visual Studio Code which needs some custom plugins that achieve what it can do.
The only downside of it is that it is commercial, meaning that it is not open-source.
Continuous Integration
Modern software development tries to be very automated. The days where developers push code to a server using FTP are gone, modern practises use automated testing (often called Continuous Integration) and automated deployment of code (often called Continuous Deployment).
The idea behind these systems is twofold:
- Having automated tests (CI) and enforcing them to succeed dramatically reduces incidents in production. Code projects should not rely on correctness because of knowledge hidden inside senior developer brains, but rather their proprties should be encoded, measured and tested automatically.
- Having frequent and automated deployments (CD) allows teams to react faster, making them automated forces teams to write good tests to prevent production incidents.
There are other resources that go much further into depth of why these systems are useful. This book doesn’t focus too much on the deployment aspects of Rust projects. But this book does focus on the various bits of tooling that Rust has which you can use to build useful CI pipelines that ensure that your Rust project stays in good shape over time.
The CI/CD systems that we have today are all built around a simple idea: the ability to run code in reaction to various events. For example, when a developer creates a merge request, you might have some code that runs unit tests, determines test coverage, and runs other checks against the codebase. Doing this means that the developer can get quick feedback if he or she has made an error, and can rectify it easy. When a code change request is accepted, another job might run which triggers the deployment. Some CI/CD systems also have the ability to run jobs on a schedule, for example to run mode extensive tests on a daily basis, rather than for every single change request.
- Diagram
All of these functionalities are enabled by having a good CI/CD solution. Generally, your code hosting solution should have some functionality built-in, for example GitLab and GitHub both have good CI/CD situations. It is also possible to use or deploy an external CI/CD solution, there is a whole list of options. But generally, unless you have specific requirements, just use whatever your development platform uses or whatever is the easiest to operate.
Reading
Continuous Integration by Martin Fowler
In this article, Martin summarizes continuous integration practises. In his own words: “Continuous Integration is a software development practice where each member of a team merges their changes into a codebase together with their colleagues changes at least daily. Each of these integrations is verified by an automated build (including test) to detect integration errors as quickly as possible. Teams find that this approach reduces the risk of delivery delays, reduces the effort of integration, and enables practices that foster a healthy codebase for rapid enhancement with new features.”
Continuous Integration in Software Engineering at Google
TODO
Issue #5656: Expand “CI Best Practises” section in the guide in rustlang/cargo
GitLab CI
GitLab is an open-source software development platform. It is similar to GitHub, but offers some more advanced features.
GitLab CI works by defining pipelines. These are triggered based on various kinds of events, such as pushes to the repository, merges of code. Pipelines can also be triggered manually or by a pipeline in another repository.
Pipelines consist of jobs, which run in sequence or in parallel. Jobs can have outputs called artefacts, which can be downloaded from the web interface or be ingested as inputs by jobs that follow.
Job Definition
- what is docker?
It is built around Docker containers, every job runs in a Docker container and executes some commands that are configurable. Background services (such as databases) can also be launched in the background by providing Docker images.
test:cargo:
image: "rust:latest"
script:
- rustc --version && cargo --version
- cargo test --workspace --verbose
If you have ever used Docker, then you should easily be able to
graph LR env[Environment Variables] artifacts_in[Artifacts] artifacts_out[Artifacts] status[Status] conditions[Conditions] job[Job] env-->job artifacts_in-->job conditions-->job job-->status job-->artifacts_out
- docker containers
- inputs: environment variables, artifacts
- outputs: success, artifacts
Environment Variables
Artifacts
Conditionals
Services
If your job requires some services running, then you can define those. This is often useful for running integration tests, where your projects requires a database or a similar service running.
Runners
The GitLab CI runner is configurable and can also uses other, non-Docker backends, such as running jobs in virtual machines using QEMU (this is useful for running tests on platforms such as FreeBSD or Windows).
Pipeline Definition
Stages
Graph
Features
GitLab Pages
- publish anything statically
- useful for publishing documentation
- see documentation chapters for examples of this
Examples
test:cargo:
image: "rust:latest"
script:
- rustc --version && cargo --version
- cargo test --workspace --verbose
Reading
Shows you how to get started with GitLab CI.
Deploying Rust with Docker and Kubernetes
In this article, FP complete shows you how to deploy a Rust application with Docker and Kubernetes using GitLab CI.
In this blog post, Emmanuele Bassi shows you how the GNOME project uses GitLab CI to generate coverage reports for every commit.
GitHub Actions
GitHub was the first user-interface for the Git version-control system. It was launched in 2008, and famously acquired by Microsoft in 2018. GitHub has GitHub Actions, which allows you to build Continuous Integration and Continuous Deployment pipelines using TypeScript.
Example
GitHub actions work with YAML configuration files placed in the
.github/workflows
folder in your Git repository.
name: Hello World
on: [push]
jobs:
hello-world:
runs-on: ubuntu-latest
steps:
- run: echo "Hello world"
You can define multiple jobs, define when they should run (on pushing to a branch, on merging a branch), define dependencies (jobs configured in other repositories), what machine the jobs should run on (usually Ubuntu Linux), and what the jobs should do (usually some bash commands).
Reading
Shows you how to get started with GitHub Actions.
GitHub Actions Feels Bad by Amos Wenger
The history and design of GitHub actions, and why they are perhaps not designed in an ideal way.
Ecosystem
Before you start your project, you may need to put some throughts towards what kind of project you want to build, and choose the right ecosystem.
Rust has a vibrant community of all kinds of projects, usually over time certain crates become more popular and establish themselves as the go-to. You should certainly make use of the ecosystem and the ease with which Cargo lets you add and manage dependencies.
Rust can also target a wide variety of platforms: whether you are writing code to run on GPUs, in the browser, on servers, in the terminal, inside your bootloder, on embedded devices or on unusual platforms, Rust typically has you covered.
Most of the time, it is relatively easy to switch between different crates. However, in some cases the crates you decide to use have an influence over the architecture of your project. For example, it is not always so easy to convert a blocking, threaded application into an async one, or to switch from one web framework to another.
It is usually better to put some thought into this before you start developing, because it might be difficult to switch once you’ve already invested in building your project with one ecosystem. This sections aims at showing you the Rust ecosystem for some common tasks, wherever the choices you make have a large impact on the architecture of your project.
Reading
On Dependency Usage in Rust (archived) by Lander Brandt
The C programming language is often critizied for not bringing a lot of foundational data structures out-of-the-box, leading many developers to reinvent the wheel. Adding and managing dependencies in C/C++ is difficult, because there is no standardized build system. On the other hand, in JavaScript it is so easy to add dependencies, that many small projects end up with gigabytes worth of trivial (transitive dependencies), which is criticized as a security risk. This article explains how dependencies work in Rust, and why it’s okay to use them.
Statistics on the Rust ecosystem
Lib.rs publishes some interesting graphics of the Rust ecosystem.
Concurrency
One of Rust’s themes is fearless concurrency, and due to the focus on this, Rust has many safeguards built-in to the language that enable you to easily write correct concurrent (and parallel) code. Because of these safeguards, Rust is one of the most pleasant languages to write heavily concurrent (and parallel) code in. In this section, we will discuss some high-level concepts, strategies and libraries that you can use in your code to make use of this capability. Some of these involve choices that you have to make which affect how you should structure your project.
Before we launch into this section, we should clarify what concurrency and parallelism actually mean.
- Concurrency is your program’s ability to track and execute multiple things at the same time, but not neccessarily in parallel. One example is a single-threaded asynchronous runtime, which can execute multiple futures by switching between them.
- Parallelism is when your program executes multiple tasks at the same time, for example using a multi-threaded model. It implies concurrency.
There are different methods in Rust to write concurrent or parallel programs, depending on the kind of workload you have. Your choice of these impacts the shape of the Rust code you write, so it is important to figure out which model suits your particular project. However, it is possible, to some extent, to mix the two models.
The building blocks that Rust give you to write concurrent applications are:
- Multi-threading with synchronous code
- Asynchronous concurrency or parallelism
Primer on Multithreading
The main difference between async and blocking programming paradigms is the introduction of futures, which represent a computation. While in blocking code, when you run some code, your thread will do only that:
#![allow(unused)] fn main() { std::time::sleep(Duration::from_secs(1)); }
In async code, you split the definition of a computation from its execution. Every async function returns a future that you need to await.
#![allow(unused)] fn main() { tokio::time::sleep(Duration::from_secs(1)).await; }
The advantage of this is that it lets you perform high-level operations on computations. It lets you compose them. For example, you can execute multiple futures at once:
#![allow(unused)] fn main() { let future_1 = tokio::time::sleep(Duration::from_secs(1)); let future_1 = tokio::time::sleep(Duration::from_secs(1)); futures::join(future_1, future_2).await; }
You can also wrap your futures into something else, for example adding a timeout to some computation that will cancel it when the time runs out:
#![allow(unused)] fn main() { tokio::time::timeout(Duration::from_secs(1), handle_request()).await; }
Async
When should you use async?
When should you consider async code:
- You’re writing something that is heavily I/O bound, such as a web server, and you want it to be able to scale to a lot of requests and still stay efficient.
- You’re writing firmware for a microcontroller, and you want it to perform multiple things simultaneously.
- You want to be able to compose computation in a high-level way, for example wrapping some computation in a timeout.
When should you stick so synchronous code (see also When not to use Tokio):
- You’re writing a command-line application that only does one thing.
- You’re writing an application that mainly performes computation and not I/O, such as cryptographic libraries, data structures.
- Most of the I/O your applications performs is file I/O.
If your crate performs mainly computations, then Rayon is most likely what you want to use. The Rust standard library also comes with code to let you easily and safely create and manage threads.
The caveate around file I/O comes from the fact that in many operating systems
there are no asynchronous interfaces for reading from and writing to files.
While there is a crate that lets Tokio use
io_uring
, this only works on Linux
and is experimental. For that reason, Tokio spawns a dedicated thread for file
I/O and uses blocking calls.
What even is async?
In short, async programming is a paradigm that lets you write scalable applications that have to do a lot of waiting.
If you have some code that is computation-heavy, it will generally not do a lot waiting but rather utilise the CPU efficiently. It might look something like this:
- graphic of compute-bound thread
However, if you think of a typical request handler, it involves a lot of waiting. It has to accept requests, parse headers, wait for the full request, make some queries to the database, and finally send a response. In terms of CPU utilisation, it means that it will spend the majority of it’s time waiting for things from the network (from client or responses from the database).
- graphic of network request thread
In traditional applications, you would spawn a thread per connection. Waiting for responses would be handled by the kernel, which would schedule other threads to run while it is waiting. However, the issue with this approach is that switching between threads is a relatively expensive operation, so this approach does not scale well. This means you can run into the C10k problem.
A better approach here is an event-driven one, where you handle multiple connections in a single thread, asking the operating system to notify you if any of them can progress. This lets you use a thread-per-core model, where you don’t spawn one thread per request, but you spawn as many threads as you have CPU cores, and distribute the requests amongs them.
- graphic of thread-per-core
If you were to implement this in C, you would be using an event-loop library like libuv, which lets you register callbacks when certain operations complete. In Rust, the async runtimes handle all of this for you, letting you write your code “as if” it were running on a thread by itself.
The async runtimes have wrappers for any operations that are “blocking”, meaning that they cause your thread to stall until some event happens. Examples of these are:
- Waiting for new network connection to come in
- Waiting for data on a network connection (receiving or sending)
- Waiting for a write or a read from disk
- Waiting for a timer to expire
What runtimes are recommended?
Although support for async-await style programming was only added in Rust 1.39, it has caught on and the Rust community has seen a large number of frameworks being built for async, and a lot of crates that support it.
In general, there are three runtimes that are recommended:
- Tokio is the go-to runtime for all things async. With over 200 million downloads, it is by far the most popular. At the time of writing, 20,000 crates depend on it (as of August 2024), meaning that it enjoys a very broad support by other libraries.
- Smol is a small and fast async runtime. It does not have the same amount of features as Tokio does, but due to it’s simplicity it is good for resource-constrained environments or if you want to be able to understand all of the code.
- async-std is the main competitor to Tokio. It was a project that aimed at making writing async code as simple as using the standard library. It is not as actively developed as Tokio, and in general is not recommended to be used for new projects. A lot of the ideas it came with have been incorporated into Tokio.
Thread-per-Core vs Shared-Nothing
Epoll vs io_uring
How does aysnc work in Rust?
There is the Async Book that goes into much greater depth. But in general, async in Rust
What are some common pitfalls with async in Rust?
Function coloring: design “sync core, async shell”
https://www.thecodedmessage.com/posts/async-colors/
Reading
Why Async Rust by David Lee Aronson
In this article, David explains the history of the development of async Rust.
This article expalains an approach to architecting asynchronous applications that stricly separate IO code from business logic. This concept helps you design applications that can be easily tested, but can run with an asynchronous executor. While this article is written with Python in mind, the lessons are equally valid for Rust: good software design keeps a synchronous core (without I/O) and wraps it in a thin, asynchronous shell. That way, your business logic is decoupled from your runtime strategy.
\Device\Afd, Or the Deal With the Devil that makes async Rust work on Windows
That Windows has some odd design choices and cruft is has accumulated over the years is not news to any developers that have had to interact with it. This article explains the dark magic that needs to be performed to make async work on Windows for Rust.
Thread-per-core by David Lee Aronson
Todo
Async Rust Complexity by Chris Krycho
Chris argues that one of the reasons why doing async is difficult in Rust is because of the sheer amount of choice. Various async runtimes and libraries exist, and for a beginner it is difficult to pick one without investigating all of the options. This is less true today, as most of the Rust community has centered around the Tokio ecosystem for async.
Rust Stream Visualized by Alex Pushinsky
Visually explains how the Rust asynx stream API works, using diagrams to illustrate the behaviour.
Stats on blocking vs async:
Async Rust can be a pleasure to work with without Send, Sync and ’static
How to deadlock a Tokio application in Rust with just a single Mutex
Asynchronous I/O: The next billion dollar mistake?
Measuring Context switching and memory overheads for Linux threads by Eli Bendersky
Eli measures the overhead of using threads in Linux. While Linux threads have a relatively low overhead, the requirement to do a context switch to switch between threads has a mimimum overhead of about 1.2 to 1.5 µs when using CPU core pinning, and 2.2 µs without. This limits how many requests can be served when using a thread-per-request architecture.
Confusing or misunderstood topics in systems programming: Part 0 by Preston Thorpe
Preston explains processes, threads, context switches and communication between threads. This article provides a good background explainer to be able to understand how asynchronouns programming works behind the scenes.
Rust Tokio task cancellations patterns by Milos Gajdos
In this article, Milos explains different patterns used in asynchronous, Tokio-powered Rust software to cancel tasks.
Async-Task explained John Nunley
John explains the internals of the async-task
crate from the grounds up in
this article. It gives a good background on how async works behind the scenes.
Async Rust in Three Parts by Jack O’Connor
Async Rust is not safe with io_uring
Web Backend
A common use-case of Rust is building backends for web applications. Rust is particularily suited for this, because it offers great performance and a strong async ecosystem that allows you to scale to many concurrent requests easily.
While you can build a web backend manually by using crates such as hyper for HTTP and h3 for HTTP/3, generally you will want to use a framework to implement the backend. Web backend frameworks handle things such as request routing, route authentication, parameter deserialization and building responses for you to make sure your application stays maintainable.
But the important question is then: which framework do you use? The rust crate ecosystem has come up with a large amount of web framework crates with varying levels of popularity.
In general, the two most popular frameworks are Axum and Actix-Web, and they should be your go-to frameworks of choice if you have no specific requirements. Axum is nice because it integrates into the Tower ecosystem of middleware, meaning that you will easily find some existing middleware implementations for whatever you are trying to do, such as adaptive rate limiting. Actix-Web is known for being easy to get started with, and for being very fast.
On a reasonably powerful system, either one of these can handle up to one million requests per second, meaning that most likely your database will be the bottleneck in scaling Rust web backends.
Template engines in Rust
TODO
https://blog.logrocket.com/top-3-templating-libraries-for-rust/
https://lib.rs/template-engine
Routing
- macro-based vs dynamic
Query Parsing
Middleware
- tower ecosystem
WebSockets
- websocket support
Tracing
Metrics
State
Testing
Axum
Axum is currently the most popular web framework in the Rust ecosystem. It is developed by the same people that wrote Tokio, and uses hyper as the underlying HTTP implementation. It supports WebSockets, has built-in routing and parameter decoding. It also integrates with the tracing ecosystem and uses tower to build middleware.
use axum::{ routing::get, Router, }; #[tokio::main] async fn main() { // build our application with a single route let app = Router::new().route("/", get(|| async { "Hello, World!" })); // run our app with hyper, listening globally on port 3000 let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap(); axum::serve(listener, app).await.unwrap(); }
One thing that is nice about Axum is that it does not use custom proc-macros to implement routing or request handling, which makes it easier to use it with IDEs that might not understand the syntax. The downside is that it’s generics approach sometimes leads to difficult-to-understand error messages.
Actix-Web
Actix started out as a framework implementing the actor model for message-passing concurrency. Actix-Web, a framework for building web application on top of it gained quite a lot of popularity. It remains the second-most popular framework for building web backend application.
use actix_web::{get, web, App, HttpServer, Responder}; #[get("/hello/{name}")] async fn greet(name: web::Path<String>) -> impl Responder { format!("Hello {}!", name) } #[actix_web::main] // or #[tokio::main] async fn main() -> std::io::Result<()> { HttpServer::new(|| { App::new().service(greet) }) .bind(("127.0.0.1", 8080))? .run() .await }
Actix-Web is quite fast
Rocket
Rocket was an early framework for building web backends. Initially, it only supported blocking code and used threads, but since version 0.5.0 it supports async as well.
#![allow(unused)] fn main() { extern crate rocket; #[get("/")] fn hello() -> &'static str { "Hello, world!" } #[launch] fn rocket() -> _ { rocket::build().mount("/", routes![hello]) } }
Warp
https://github.com/seanmonstar/warp
Tide
https://github.com/http-rs/tide
Poem
https://github.com/poem-web/poem
Deploying
Shuttle
https://www.shuttle.rs/
AWS Lambda
Reading
Are We Web Yet: Web Frameworks maintains a list of web frameworks along with some stats on them.
https://www.arewewebyet.org/
Web Frameworks Benchmark: Rust
Compares the performance (as measured by requests-per-second) of various web frameworks.
Rusts Axum style magic function params example by Alex Puschinsky
In this article, Alex explains how Axum’s magic function parameter handling is implemented in Rust.
Web Frontend
This section discusses the frameworks you can use in Rust to build web frontends that run in the browser. If you are already familiar with the architecture of single-page web applications, you can skip down to the frameworks for a discussion of how they work.
Background
Websites use HTML for both content and structure. CSS is used to style how the website looks. The browser reads the HTML to get the content and layout, then applies CSS to style it, and finally renders it to the screen. When writing web applications, the first question is where and when this HTML is generated.
In traditional web applications, the HTML is created on the server. When the backend gets a request, it processes it and generates an HTML response. This response is then sent to the browser. On any interaction, such as a click on a link, press of a button or submission of a form, a new request is made to the browser, and a new HTML response is sent.
The Rust web backend frameworks have good support for writing web applications this way, often combined with templating crates such as handlebars or tera. The downside to this traditional approach is higher latencies. Since the entire page needs to be regenerated, transmitted and rendered on every interaction, there is a noticeable delay. When structing a web application this way, it is difficult to implement interactive widgets on pages, or update information in real-time.
Modern web frontend applications are often single-page applications (SPAs), written in languages like JavaScript or TypeScript and run in the browser. They are called “single-page” because the entire app is loaded in the initial request. After that, the frontend reacts to interactions and dynamically updates the content, without needing to realod the page. Communication with the backend typically happens through an API. This keeps the app responsive while waiting for server responses and allows for real-time events from the server using technologies like WebSockets.
Since the standardization of WebAssembly and the broad browser support it has gained, it has been possible to write frontend web applications languages other than JavaScript. This section explores Rust frameworks that allow you to write single-page web applications for full-stack Rust projects.
Using Rust for web frontends has some benefits. It allows you to write performant frontends, make use of the Rust crate ecosystem, and share type definitions between your backend and frontend easily. However, the availability of this is a relatively new and JavaScript-based frameworks tend to be more mature. Finding frontend engineers that are familiar with JavaScript-based frameworks is also a lot easier. If you want to build a prototype frontend for an existing Rust project, it may be worth exploring these as it allows you use a single language across the project.
The Component Model
All Rust web frontend frameworks discussed here use the component model to implement applications. In web frontend development, the component model is a way to build applications using reusable and self-contained pieces called components. Each component has its own logic and can manage its own state and appearance. Components can be nested within other components to build complex user interfaces. If you are familiar with React or similar JavaScript frontend libraries, then you should already be familiar with the component model.
Typically, web frameworks use a HTML-like domain-specific language to represent the outputs of components. For example, the root component of this example application might look like this:
#![allow(unused)] fn main() { html! { <main> <Header /> <div class="content"> <SideBar /> <Content /> </div> </main> } }
Like functions can have arguments, components can have properties. These are
inputs to the component. In this example, the HTML div
element has the property
class="content"
. In the same way, Rust components can have properties, which
can be any Rust type.
As a convention, HTML native components are usually lowercased (such as main
, div
, p
)
whereas Rust components are uppercased (such as Header
, SideBar
, Content
).
Components can also have state.
Finally, many frameworks also support context. Unlike properties, which a parent explicitly passes down to its child components, context is implicitly passed down to all child components (even children-of-children). This is often useful to pass global state such as whether the user is logged in down to all components in the tree, or utilities such as data caches.
The the web frameworks do is they handle changing of data. If any of the inputs to a component changes, whether that be properties, state or context, the component is re-rendered.
The way frameworks can track these changes depend on the framework itself, but generally they are able to do so because they have hooks that allow them to track what is changed and when.
- animation of changes propagating
In this section, we will not cover all available frontend frameworks, only a few of the post popular. As this is a relatively new development, there is a lot of activity in the various frameworks and you should expect some volatility in which frameworks are the most popular.
Raw Web APIs with web-sys
While most of the Rust web frameworks handle all of the interactions with the
underlying web APIs, sometimes you may find the need to go “deeper” and interact
with the raw APIs. The way you do this is by using the web-sys
crate, which has
safe Rust wrappers for all of the APIs the browser exposes.
One quirk of the web-sys
crate is that it puts every single API behind a
feature flag. In doing so, it has over 1,500 features, and as such needs an
exception to bypass the crates.io crate features
limit.
If you use it, don’t be surprised if you get compiler errors, make sure that
you have enabled the correct set of features. The crate documentation shows you
for every interface, which feature it requires.
Most frontend libraries will allow you to get raw access to the underlying DOM
nodes and perform raw operations on them. One example is when you want to use a
<canvas>
element, you can use this to draw on it. Here is an example of what
this looks like in Yew:
#![allow(unused)] fn main() { // todo }
You must keep in mind how the framework renders, to make sure that your raw access is not broken by components refreshing.
Compiling and Deploying Frontend Applications
Deploying a Rust frontend web application in the browser is a bit more complex
than just running cargo build
, since the resulting WebAssembly blob still
needs to be packaged in a way that a browser can consume, and it needs some
JavaScript glue to make it usable. For this, a lot of frameworks use
Trunk to bundle and ship the raw Rust WebAssembly binaries into
something the browser can understand. The Trunk section below
explains how that works and how you can configure it.
Some Rust web frontend frameworks also support server-side rendering, where it can fallback to a traditional web application style where the HTML is generated server-side. This can also help search engines index the websites better by not needing WebAssembly support to render the website. The frameworks support partial hydration, where parts of the website are rendered server-side, or full hydration where every page can be fully rendered server-side.
If you use this feature, you also need to integrate your frontend application with your backend.
Rendering Methods
Browsers represent a loaded website (with HTML and styling) in their Document Object Model. Web frontend frameworks have to update this DOM whenever components change their outputs. One important difference between frameworks is in how they do this.
Some frameworks have a shadow DOM (sometimes also called virtual DOM), which is a copy of the DOM that is in the browser, that components modify. The framework then synchronizes this copy with the real DOM.
Other frameworks modify the DOM directly, which can have some performance benefits.
WebAssembly Support in the Ecosystem
Thanks to Rust’s use of LLVM, a compiler infrastructure that makes it easy to write new backends for different targets, it gained support for targetting WebAssembly relatively early. This means you can write entire applications that live and run in the browser in Rust, and make use of Rust’s extensive ecosystem.
Not all of Rust crates will work on WebAssembly out-of-the-box, for example because they access native operating system APIs that do not exist in WebAssembly, but many will work out-of-the-box or have feature flags that can be enabled to add support for it.
All of the low-level APIs that are relevant for running in the browser are exposed by the web_sys crate. This is a large crate that is automatically generated, and you need to enable features to enable it’s various APIs. Ergonomic wrappers for a lot of functionality are exposed by the gloo crate, and you should use this if you can.
Async Support
Thanks to the hard work of the community, it is even possible to use Rust async code in a WebAssembly environment through the use of wasm-bindgen-futures. These map the interface of Rust’s Futures to JavaScript Promises.
For example, you can use this to spawn a future in the background to make a network request and get the body of some web resource using the reqwest library:
#![allow(unused)] fn main() { wasm_bindgen_futures::spawn_local(async { let test = reqwest::get("https://www.rust-lang.org") .await? .text() .await?; }); }
Most frameworks have some kind of wrapper around these raw futures to be able to use them in the applications.
Server-Side Rendering
Differences between frameworks
The rest of this section discusses some frameworks for Rust-based frontend programming. Generally, the conceptual model of these frameworks is very similar, because they used the same component model.
Differences between the frameworks exist between:
- The language they use to describe the output of a component. Usually, this is some kind of macro that allows you to specify a tree of components (HTML or native), their properties and children.
- The method in which they render the output of the components into the browser (using direct rendering or a shadow DOM). Either rendering methods can have advantages, it depends on what you are doing. Unless you are rendering a large amount of data or update frequently, it likely does not make a difference.
- The ecosystem of premade components and hooks. Some frameworks are more established and have third-part support for premade hooks and component libraries. These make your life easier.
- The degree to which they allow you to access raw browser APIs. Frameworks that have multiple rendering backends might be more limited in their support for raw browser APIs for compatibility.
- The syntax they use for defining components, properties and create and access hooks.
- The build system they use and support (either Trunk or a custom build system)
- Support for server-side rendering, for example having plugins for popular web backend
crates such as
axum
oractix-web
.
In the next sections, we will showcase some popular frameworks and attempt to give an overview of their features.
Yew
Yew is currently the most popular framework for web frontend
development in Rust. It uses a reactive component model, has a useful
ecosystem of plugins, supports server-side rendering, routing, and has a html!
macro that makes it relatively easy to get started.
To define a component, you can either implement the Component
trait, or use the function_component
derive
macro. In general, the latter leads to more concise code, and is the recommended way.
Functional components return Html
, using the html
macro. This macro can output
raw HTML, or other child components.
#![allow(unused)] fn main() { #[function_component] fn app() -> Html { html! { <h1>{ "Hello World" }</h1> } } }
You can think of this function as always being run whenever your component needs to re-render, for example if any of the inputs (props or state) have changed. To declare state in your component, you use hooks. Here is an example:
#![allow(unused)] fn main() { #[function_component] fn App() -> Html { let state = use_state(|| 0); let increment_counter = { let state = state.clone(); Callback::from(move |_| state.set(*state + 1)) }; let decrement_counter = { let state = state.clone(); Callback::from(move |_| state.set(*state - 1)) }; html! { <> <p> {"current count: "} {*state} </p> <button onclick={increment_counter}> {"+"} </button> <button onclick={decrement_counter}> {"-"} </button> </> } } }
Simple hooks come built-in, but there are also external crates offering more hooks.
The idea is that you can compose these small components into bigger applications. Yew also comes with a plugin for routing.
One thing that is nice about Yew is that the html!
macro it uses very closely
resembles HTML. There is not a steep learning curve if you are familiar with
it. The only downside with it is that values require quoting, you can see that
to have text inside a paragraph element, you need to write <p>{"Text here"}</p>
. Another downside of it is that the state handles it uses require
cloning, which adds some clutter to the code.
Example: Yew Todo App
Here is an example of a todo-list application written in Yew. It showcases
props, child components, raw HTML rendering, the use_state
hook and how to
package it with trunk.
- src/
/target
/dist
stages:
- publish
# build application with trunk, use pinned versions for reproducible build.
pages:
stage: publish
image: rust:1.80
variables:
TRUNK_VERSION: 0.20.3
TRUNK_BUILD_PUBLIC_URL: "/$CI_PROJECT_NAME"
before_script:
- rustup target add wasm32-unknown-unknown
- wget -qO- https://github.com/thedodd/trunk/releases/download/v${TRUNK_VERSION}/trunk-x86_64-unknown-linux-gnu.tar.gz | tar -xzf- -C /usr/local/bin
script:
- trunk build --release
- mv dist public
artifacts:
paths:
- public
only:
- master
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "addr2line"
version = "0.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678"
dependencies = [
"gimli",
]
[[package]]
name = "adler"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
[[package]]
name = "anymap2"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d301b3b94cb4b2f23d7917810addbbaff90738e0ca2be692bd027e70d7e0330c"
[[package]]
name = "autocfg"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0"
[[package]]
name = "backtrace"
version = "0.3.73"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a"
dependencies = [
"addr2line",
"cc",
"cfg-if",
"libc",
"miniz_oxide",
"object",
"rustc-demangle",
]
[[package]]
name = "bincode"
version = "1.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad"
dependencies = [
"serde",
]
[[package]]
name = "boolinator"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cfa8873f51c92e232f9bac4065cddef41b714152812bfc5f7672ba16d6ef8cd9"
[[package]]
name = "bumpalo"
version = "3.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
[[package]]
name = "bytes"
version = "1.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50"
[[package]]
name = "cc"
version = "1.1.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b62ac837cdb5cb22e10a256099b4fc502b1dfe560cb282963a974d7abd80e476"
dependencies = [
"shlex",
]
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "console_error_panic_hook"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc"
dependencies = [
"cfg-if",
"wasm-bindgen",
]
[[package]]
name = "equivalent"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
[[package]]
name = "fnv"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "form_urlencoded"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456"
dependencies = [
"percent-encoding",
]
[[package]]
name = "futures"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0"
dependencies = [
"futures-channel",
"futures-core",
"futures-io",
"futures-sink",
"futures-task",
"futures-util",
]
[[package]]
name = "futures-channel"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78"
dependencies = [
"futures-core",
"futures-sink",
]
[[package]]
name = "futures-core"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d"
[[package]]
name = "futures-io"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1"
[[package]]
name = "futures-macro"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.77",
]
[[package]]
name = "futures-sink"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5"
[[package]]
name = "futures-task"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004"
[[package]]
name = "futures-util"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48"
dependencies = [
"futures-channel",
"futures-core",
"futures-io",
"futures-macro",
"futures-sink",
"futures-task",
"memchr",
"pin-project-lite",
"pin-utils",
"slab",
]
[[package]]
name = "getrandom"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
dependencies = [
"cfg-if",
"js-sys",
"libc",
"wasi",
"wasm-bindgen",
]
[[package]]
name = "gimli"
version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd"
[[package]]
name = "gloo"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28999cda5ef6916ffd33fb4a7b87e1de633c47c0dc6d97905fee1cdaa142b94d"
dependencies = [
"gloo-console 0.2.3",
"gloo-dialogs 0.1.1",
"gloo-events 0.1.2",
"gloo-file 0.2.3",
"gloo-history 0.1.5",
"gloo-net 0.3.1",
"gloo-render 0.1.1",
"gloo-storage 0.2.2",
"gloo-timers 0.2.6",
"gloo-utils 0.1.7",
"gloo-worker 0.2.1",
]
[[package]]
name = "gloo"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd35526c28cc55c1db77aed6296de58677dbab863b118483a27845631d870249"
dependencies = [
"gloo-console 0.3.0",
"gloo-dialogs 0.2.0",
"gloo-events 0.2.0",
"gloo-file 0.3.0",
"gloo-history 0.2.2",
"gloo-net 0.4.0",
"gloo-render 0.2.0",
"gloo-storage 0.3.0",
"gloo-timers 0.3.0",
"gloo-utils 0.2.0",
"gloo-worker 0.4.0",
]
[[package]]
name = "gloo-console"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "82b7ce3c05debe147233596904981848862b068862e9ec3e34be446077190d3f"
dependencies = [
"gloo-utils 0.1.7",
"js-sys",
"serde",
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "gloo-console"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a17868f56b4a24f677b17c8cb69958385102fa879418052d60b50bc1727e261"
dependencies = [
"gloo-utils 0.2.0",
"js-sys",
"serde",
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "gloo-dialogs"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67062364ac72d27f08445a46cab428188e2e224ec9e37efdba48ae8c289002e6"
dependencies = [
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "gloo-dialogs"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf4748e10122b01435750ff530095b1217cf6546173459448b83913ebe7815df"
dependencies = [
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "gloo-events"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68b107f8abed8105e4182de63845afcc7b69c098b7852a813ea7462a320992fc"
dependencies = [
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "gloo-events"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "27c26fb45f7c385ba980f5fa87ac677e363949e065a083722697ef1b2cc91e41"
dependencies = [
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "gloo-file"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8d5564e570a38b43d78bdc063374a0c3098c4f0d64005b12f9bbe87e869b6d7"
dependencies = [
"gloo-events 0.1.2",
"js-sys",
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "gloo-file"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97563d71863fb2824b2e974e754a81d19c4a7ec47b09ced8a0e6656b6d54bd1f"
dependencies = [
"gloo-events 0.2.0",
"js-sys",
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "gloo-history"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85725d90bf0ed47063b3930ef28e863658a7905989e9929a8708aab74a1d5e7f"
dependencies = [
"gloo-events 0.1.2",
"gloo-utils 0.1.7",
"serde",
"serde-wasm-bindgen 0.5.0",
"serde_urlencoded",
"thiserror",
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "gloo-history"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "903f432be5ba34427eac5e16048ef65604a82061fe93789f2212afc73d8617d6"
dependencies = [
"getrandom",
"gloo-events 0.2.0",
"gloo-utils 0.2.0",
"serde",
"serde-wasm-bindgen 0.6.5",
"serde_urlencoded",
"thiserror",
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "gloo-net"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a66b4e3c7d9ed8d315fd6b97c8b1f74a7c6ecbbc2320e65ae7ed38b7068cc620"
dependencies = [
"futures-channel",
"futures-core",
"futures-sink",
"gloo-utils 0.1.7",
"http",
"js-sys",
"pin-project",
"serde",
"serde_json",
"thiserror",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
]
[[package]]
name = "gloo-net"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ac9e8288ae2c632fa9f8657ac70bfe38a1530f345282d7ba66a1f70b72b7dc4"
dependencies = [
"futures-channel",
"futures-core",
"futures-sink",
"gloo-utils 0.2.0",
"http",
"js-sys",
"pin-project",
"serde",
"serde_json",
"thiserror",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
]
[[package]]
name = "gloo-render"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2fd9306aef67cfd4449823aadcd14e3958e0800aa2183955a309112a84ec7764"
dependencies = [
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "gloo-render"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56008b6744713a8e8d98ac3dcb7d06543d5662358c9c805b4ce2167ad4649833"
dependencies = [
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "gloo-storage"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d6ab60bf5dbfd6f0ed1f7843da31b41010515c745735c970e821945ca91e480"
dependencies = [
"gloo-utils 0.1.7",
"js-sys",
"serde",
"serde_json",
"thiserror",
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "gloo-storage"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fbc8031e8c92758af912f9bc08fbbadd3c6f3cfcbf6b64cdf3d6a81f0139277a"
dependencies = [
"gloo-utils 0.2.0",
"js-sys",
"serde",
"serde_json",
"thiserror",
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "gloo-timers"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b995a66bb87bebce9a0f4a95aed01daca4872c050bfcb21653361c03bc35e5c"
dependencies = [
"js-sys",
"wasm-bindgen",
]
[[package]]
name = "gloo-timers"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994"
dependencies = [
"js-sys",
"wasm-bindgen",
]
[[package]]
name = "gloo-utils"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "037fcb07216cb3a30f7292bd0176b050b7b9a052ba830ef7d5d65f6dc64ba58e"
dependencies = [
"js-sys",
"serde",
"serde_json",
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "gloo-utils"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b5555354113b18c547c1d3a98fbf7fb32a9ff4f6fa112ce823a21641a0ba3aa"
dependencies = [
"js-sys",
"serde",
"serde_json",
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "gloo-worker"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13471584da78061a28306d1359dd0178d8d6fc1c7c80e5e35d27260346e0516a"
dependencies = [
"anymap2",
"bincode",
"gloo-console 0.2.3",
"gloo-utils 0.1.7",
"js-sys",
"serde",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
]
[[package]]
name = "gloo-worker"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76495d3dd87de51da268fa3a593da118ab43eb7f8809e17eb38d3319b424e400"
dependencies = [
"bincode",
"futures",
"gloo-utils 0.2.0",
"gloo-worker-macros",
"js-sys",
"pinned",
"serde",
"thiserror",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
]
[[package]]
name = "gloo-worker-macros"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "956caa58d4857bc9941749d55e4bd3000032d8212762586fa5705632967140e7"
dependencies = [
"proc-macro-crate",
"proc-macro2",
"quote",
"syn 2.0.77",
]
[[package]]
name = "hashbrown"
version = "0.14.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
[[package]]
name = "hermit-abi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
[[package]]
name = "http"
version = "0.2.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1"
dependencies = [
"bytes",
"fnv",
"itoa",
]
[[package]]
name = "implicit-clone"
version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8a9aa791c7b5a71b636b7a68207fdebf171ddfc593d9c8506ec4cbc527b6a84"
dependencies = [
"implicit-clone-derive",
"indexmap",
]
[[package]]
name = "implicit-clone-derive"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9311685eb9a34808bbb0608ad2fcab9ae216266beca5848613e95553ac914e3b"
dependencies = [
"quote",
"syn 2.0.77",
]
[[package]]
name = "indexmap"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5"
dependencies = [
"equivalent",
"hashbrown",
]
[[package]]
name = "itoa"
version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
[[package]]
name = "js-sys"
version = "0.3.70"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1868808506b929d7b0cfa8f75951347aa71bb21144b7791bae35d9bccfcfe37a"
dependencies = [
"wasm-bindgen",
]
[[package]]
name = "libc"
version = "0.2.158"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439"
[[package]]
name = "log"
version = "0.4.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
[[package]]
name = "memchr"
version = "2.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
[[package]]
name = "miniz_oxide"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08"
dependencies = [
"adler",
]
[[package]]
name = "num_cpus"
version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43"
dependencies = [
"hermit-abi",
"libc",
]
[[package]]
name = "object"
version = "0.36.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "084f1a5821ac4c651660a94a7153d27ac9d8a53736203f58b31945ded098070a"
dependencies = [
"memchr",
]
[[package]]
name = "once_cell"
version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
[[package]]
name = "percent-encoding"
version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
[[package]]
name = "pin-project"
version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3"
dependencies = [
"pin-project-internal",
]
[[package]]
name = "pin-project-internal"
version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.77",
]
[[package]]
name = "pin-project-lite"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02"
[[package]]
name = "pin-utils"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "pinned"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a829027bd95e54cfe13e3e258a1ae7b645960553fb82b75ff852c29688ee595b"
dependencies = [
"futures",
"rustversion",
"thiserror",
]
[[package]]
name = "prettyplease"
version = "0.2.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "479cf940fbbb3426c32c5d5176f62ad57549a0bb84773423ba8be9d089f5faba"
dependencies = [
"proc-macro2",
"syn 2.0.77",
]
[[package]]
name = "proc-macro-crate"
version = "1.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919"
dependencies = [
"once_cell",
"toml_edit",
]
[[package]]
name = "proc-macro-error"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
dependencies = [
"proc-macro-error-attr",
"proc-macro2",
"quote",
"syn 1.0.109",
"version_check",
]
[[package]]
name = "proc-macro-error-attr"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
dependencies = [
"proc-macro2",
"quote",
"version_check",
]
[[package]]
name = "proc-macro2"
version = "1.0.86"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77"
dependencies = [
"unicode-ident",
]
[[package]]
name = "prokio"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03b55e106e5791fa5a13abd13c85d6127312e8e09098059ca2bc9b03ca4cf488"
dependencies = [
"futures",
"gloo 0.8.1",
"num_cpus",
"once_cell",
"pin-project",
"pinned",
"tokio",
"tokio-stream",
"wasm-bindgen-futures",
]
[[package]]
name = "quote"
version = "1.0.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af"
dependencies = [
"proc-macro2",
]
[[package]]
name = "rustc-demangle"
version = "0.1.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
[[package]]
name = "rustversion"
version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6"
[[package]]
name = "ryu"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
[[package]]
name = "serde"
version = "1.0.210"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde-wasm-bindgen"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3b143e2833c57ab9ad3ea280d21fd34e285a42837aeb0ee301f4f41890fa00e"
dependencies = [
"js-sys",
"serde",
"wasm-bindgen",
]
[[package]]
name = "serde-wasm-bindgen"
version = "0.6.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8302e169f0eddcc139c70f139d19d6467353af16f9fce27e8c30158036a1e16b"
dependencies = [
"js-sys",
"serde",
"wasm-bindgen",
]
[[package]]
name = "serde_derive"
version = "1.0.210"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.77",
]
[[package]]
name = "serde_json"
version = "1.0.128"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8"
dependencies = [
"itoa",
"memchr",
"ryu",
"serde",
]
[[package]]
name = "serde_urlencoded"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
dependencies = [
"form_urlencoded",
"itoa",
"ryu",
"serde",
]
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "slab"
version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67"
dependencies = [
"autocfg",
]
[[package]]
name = "syn"
version = "1.0.109"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
dependencies = [
"proc-macro2",
"unicode-ident",
]
[[package]]
name = "syn"
version = "2.0.77"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "thiserror"
version = "1.0.63"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.63"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.77",
]
[[package]]
name = "todo-yew"
version = "0.1.0"
dependencies = [
"web-sys",
"yew",
]
[[package]]
name = "tokio"
version = "1.40.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2b070231665d27ad9ec9b8df639893f46727666c6767db40317fbe920a5d998"
dependencies = [
"backtrace",
"pin-project-lite",
]
[[package]]
name = "tokio-stream"
version = "0.1.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4f4e6ce100d0eb49a2734f8c0812bcd324cf357d21810932c5df6b96ef2b86f1"
dependencies = [
"futures-core",
"pin-project-lite",
"tokio",
]
[[package]]
name = "toml_datetime"
version = "0.6.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41"
[[package]]
name = "toml_edit"
version = "0.19.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421"
dependencies = [
"indexmap",
"toml_datetime",
"winnow",
]
[[package]]
name = "tracing"
version = "0.1.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef"
dependencies = [
"pin-project-lite",
"tracing-attributes",
"tracing-core",
]
[[package]]
name = "tracing-attributes"
version = "0.1.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.77",
]
[[package]]
name = "tracing-core"
version = "0.1.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54"
dependencies = [
"once_cell",
]
[[package]]
name = "unicode-ident"
version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
[[package]]
name = "version_check"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "wasi"
version = "0.11.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "wasm-bindgen"
version = "0.2.93"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5"
dependencies = [
"cfg-if",
"once_cell",
"wasm-bindgen-macro",
]
[[package]]
name = "wasm-bindgen-backend"
version = "0.2.93"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b"
dependencies = [
"bumpalo",
"log",
"once_cell",
"proc-macro2",
"quote",
"syn 2.0.77",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-futures"
version = "0.4.43"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61e9300f63a621e96ed275155c108eb6f843b6a26d053f122ab69724559dc8ed"
dependencies = [
"cfg-if",
"js-sys",
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.93"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.93"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.77",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.93"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484"
[[package]]
name = "web-sys"
version = "0.3.70"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26fdeaafd9bd129f65e7c031593c24d62186301e0c72c8978fa1678be7d532c0"
dependencies = [
"js-sys",
"wasm-bindgen",
]
[[package]]
name = "winnow"
version = "0.5.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876"
dependencies = [
"memchr",
]
[[package]]
name = "yew"
version = "0.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f1a03f255c70c7aa3e9c62e15292f142ede0564123543c1cc0c7a4f31660cac"
dependencies = [
"console_error_panic_hook",
"futures",
"gloo 0.10.0",
"implicit-clone",
"indexmap",
"js-sys",
"prokio",
"rustversion",
"serde",
"slab",
"thiserror",
"tokio",
"tracing",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
"yew-macro",
]
[[package]]
name = "yew-macro"
version = "0.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02fd8ca5166d69e59f796500a2ce432ff751edecbbb308ca59fd3fe4d0343de2"
dependencies = [
"boolinator",
"once_cell",
"prettyplease",
"proc-macro-error",
"proc-macro2",
"quote",
"syn 2.0.77",
]
[package]
name = "todo-yew"
version = "0.1.0"
edition = "2021"
[dependencies]
web-sys = { version = "0.3.70", features = ["HtmlInputElement"] }
yew = { version = "0.21.0", features = ["csr"] }
# Yew Todo App
A port of a [React Todo
App](https://www.digitalocean.com/community/tutorials/how-to-build-a-react-to-do-app-with-react-hooks)
to use the [Yew](https://yew.rs) framework.
This is an example project for the [Web
Frontend](https://rustprojectprimer.com/ecosystem/web-frontend.html) section of
the [Rust Project Primer](https://rustprojectprimer.com/) book.
## Prerequisites
You need two prerequisites to build this:
- Rust 1.80 with support for `wasm32-unknown-unknown` target
- Trunk build tool
### Setup
You can install Rust using [Rustup](https://rustup.rs):
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
You need to tell Rustup to add the WebAssembly target:
rustup target add wasm32-unknown-unknown
You need to install [Trunk](https://trunkrs.dev) to build and serve it:
cargo install trunk
## Running it
You can run it locally with Trunk:
trunk serve
This will build and serve it, and watch the project for any changes. When you
edit the code, it will recompile and cause your browser to refresh.
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link data-trunk rel="rust" />
<link data-trunk data-inline rel="css" href="src/style.css" />
<title>Todo Yew</title>
</head>
<body>
</body>
</html>
use web_sys::HtmlInputElement;
use yew::prelude::*;
/// Represents a single Todo item.
#[derive(PartialEq, Clone)]
pub struct Todo {
pub text: String,
pub completed: bool,
}
impl Todo {
/// Create a new todo item that is not completed.
fn new<S: Into<String>>(text: S) -> Self {
Self {
text: text.into(),
completed: false,
}
}
fn complete(&mut self) {
self.completed = !self.completed;
}
}
#[function_component]
pub fn App() -> Html {
// list of default todos to show
let items = use_state(|| {
vec![
Todo::new("Buy milk"),
Todo::new("Learn Rust"),
Todo::new("Drink enough water"),
Todo::new("Spend time with family"),
]
});
// submit a new todo item to the list
let submit = {
let items = items.clone();
move |entry: String| {
let mut current = (*items).clone();
current.push(Todo::new(entry));
items.set(current);
}
};
html! {
<div class="app">
<div class="heading">
{"Todo List"}
</div>
<div class="todo-list">
{
items.iter().enumerate().map(|(index, item)| {
// mark current todo entry as completed
let complete = {
let items = items.clone();
move |()| {
let mut current = (*items).clone();
current[index].complete();
items.set(current);
}
};
// remove current todo entry
let remove = {
let items = items.clone();
move |()| {
let mut current = (*items).clone();
current.remove(index);
items.set(current);
}
};
html! {
<TodoRow key={index} item={item.clone()} {complete} {remove} />
}
}).collect::<Html>()
}
</div>
<div class="footer">
<TodoForm {submit} />
</div>
</div>
}
}
/// Props for the todo row. Takes a todo item, and callbacks for what happens when the complete and
/// remove buttons are clicked.
#[derive(PartialEq, Properties, Clone)]
struct TodoRowProps {
item: Todo,
#[prop_or_default]
complete: Callback<()>,
#[prop_or_default]
remove: Callback<()>,
}
/// Represents a single todo line, with buttons to mark it as complete and a button to delete it.
#[function_component]
fn TodoRow(props: &TodoRowProps) -> Html {
let props = props.clone();
html! {
<div class={classes!("todo", props.item.completed.then_some("completed"))}>
{ &props.item.text }
<div>
<button class="complete" onclick={move |_| props.complete.emit(())}>{"✓"}</button>
<button class="remove" onclick={move |_| props.remove.emit(())}>{"⨯"}</button>
</div>
</div>
}
}
#[derive(PartialEq, Properties)]
struct TodoFormProps {
#[prop_or_default]
submit: Callback<String>,
}
/// Represents a form for adding iodos, as a text-input field.
#[function_component]
fn TodoForm(props: &TodoFormProps) -> Html {
let value = use_state(String::default);
let onsubmit = {
let value = value.clone();
let submit = props.submit.clone();
move |event: SubmitEvent| {
event.prevent_default();
if !value.is_empty() {
submit.emit((*value).clone());
}
value.set(String::default());
}
};
let onchange = {
let value = value.clone();
move |event: Event| {
let target: HtmlInputElement = event.target_dyn_into().unwrap();
value.set(target.value());
}
};
html! {
<form {onsubmit}>
<input r#type="text" class="input" value={value.as_str().to_string()} {onchange} />
</form>
}
}
use todo_yew::App;
fn main() {
yew::Renderer::<App>::new().render();
}
body {
background: #209cee;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
}
.app {
height: 100vh;
padding: 10px;
padding-top: 20px;
padding-bottom: 20px;
max-width: 600px;
margin-left: auto;
margin-right: auto;
}
.app .heading {
padding: 5px;
padding-top: 10px;
padding-bottom: 10px;
text-align: center;
font-size: 20px;
font-weight: 600;
border-radius: 7px 7px 0px 0px;
background: #e8e8e8;
border: 1px solid #d8d8d8;
border-bottom: 1px solid #b4b4b4;
background: linear-gradient(to bottom, #f6f6f6 0%,#dadada 100%);
}
.app .todo-list {
padding: 5px;
background: #ffffff;
/*border-top: 1px solid #b4b4b4;*/
border-left: 1px solid #d8d8d8;
border-right: 1px solid #d8d8d8;
}
.app .footer {
padding: 5px;
border-radius: 0px 0px 7px 7px;
background: #ffffff;
padding-bottom: 10px;
border-left: 1px solid #d8d8d8;
border-right: 1px solid #d8d8d8;
border-bottom: 1px solid #d8d8d8;
}
.app .footer form * {
box-sizing: border-box;
width: 100%;
}
.todo-list .todo {
align-items: center;
background: #f0f0f0;
border-radius: 3px;
box-shadow: 1px 1px 1px rgba(0, 0, 0, 0.15);
display: flex;
font-size: 14px;
justify-content: space-between;
margin-bottom: 6px;
padding: 3px 10px;
}
.todo-list .todo button {
width: 20px;
height: 20px;
font-size: 10px;
background: #f9f9f9;
border-radius: 50%;
margin: 0 4px 0 0;
opacity: 20%;
text-align: center;
background: #e9e9e9;
border: 1px solid #e0e0e0;
}
.todo-list .todo button:hover {
opacity: 100%;
transition: 100ms;
}
.todo-list .todo button.complete {
background: #27C93F;
border: 1px solid #1DAD2B;
transition: 100ms;
}
.todo-list .todo button.remove {
background: #FF6057;
border: 1px solid #E14640;
transition: 100ms;
}
.todo.completed {
text-decoration: line-through;
}
You can see this application in action here. In this example, you
can see how properties in Yew are structs that derive the Properties
trait.
You can also see how state is represented with the use_state()
hook, and how
Callback
is used to pass callbacks down to child components. The html!
macro is used to output HTML elements and child components, and the classes!
macro is used to create a list of classes.
Leptos
Leptos is a web frontend framework for Rust that is quite similar to Yew. The primary difference is in how it renders: Yew renders to a shadow DOM, and then synchronizes it to the real DOM, while Leptos directly updates the DOM. This has some implications in terms of speed.
#![allow(unused)] fn main() { #[component] pub fn SimpleCounter(initial_value: i32) -> impl IntoView { // create a reactive signal with the initial value let (value, set_value) = create_signal(initial_value); // create event handlers for our buttons // note that `value` and `set_value` are `Copy`, so it's super easy to move them into closures let clear = move |_| set_value(0); let decrement = move |_| set_value.update(|value| *value -= 1); let increment = move |_| set_value.update(|value| *value += 1); // create user interfaces with the declarative `view!` macro view! { <div> <button on:click=clear>Clear</button> <button on:click=decrement>-1</button> // text nodes can be quoted or unquoted <span>"Value: " {value} "!"</span> <button on:click=increment>+1</button> </div> } } }
One thing to note is that the view!
macro it uses to create the component
output tree has some slightly different syntax from regular HTML. For example,
it uses on:click=value
instead of onclick=value
. An upside is that values
do not require being put into braces, so <span>"Hello"</span>
is valid. Also,
the state handles it uses do not require cloning as they do in Yew.
Example: Todo App
Here is an example of a todo-list application written using Leptos. It showcases defining components, rendering child components, passing down properties, handling state and passing callbacks to child components.
- src/
/target
/dist
stages:
- publish
# build application with trunk, use pinned versions for reproducible build.
pages:
stage: publish
image: rust:1.80
variables:
TRUNK_VERSION: 0.20.3
TRUNK_BUILD_PUBLIC_URL: "/$CI_PROJECT_NAME"
before_script:
- rustup target add wasm32-unknown-unknown
- wget -qO- https://github.com/thedodd/trunk/releases/download/v${TRUNK_VERSION}/trunk-x86_64-unknown-linux-gnu.tar.gz | tar -xzf- -C /usr/local/bin
script:
- trunk build --release
- mv dist public
artifacts:
paths:
- public
only:
- master
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "aho-corasick"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
dependencies = [
"memchr",
]
[[package]]
name = "anyhow"
version = "1.0.86"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da"
[[package]]
name = "async-recursion"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "attribute-derive"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f1ee502851995027b06f99f5ffbeffa1406b38d0b318a1ebfa469332c6cbafd"
dependencies = [
"attribute-derive-macro",
"derive-where",
"manyhow",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "attribute-derive-macro"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3601467f634cfe36c4780ca9c75dea9a5b34529c1f2810676a337e7e0997f954"
dependencies = [
"collection_literals",
"interpolator",
"manyhow",
"proc-macro-utils",
"proc-macro2",
"quote",
"quote-use",
"syn",
]
[[package]]
name = "autocfg"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0"
[[package]]
name = "base64"
version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "bitflags"
version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
[[package]]
name = "bumpalo"
version = "3.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
[[package]]
name = "bytes"
version = "1.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50"
[[package]]
name = "camino"
version = "1.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b96ec4966b5813e2c0507c1f86115c8c5abaadc3980879c3424042a02fd1ad3"
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "ciborium"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e"
dependencies = [
"ciborium-io",
"ciborium-ll",
"serde",
]
[[package]]
name = "ciborium-io"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757"
[[package]]
name = "ciborium-ll"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9"
dependencies = [
"ciborium-io",
"half",
]
[[package]]
name = "collection_literals"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "186dce98367766de751c42c4f03970fc60fc012296e706ccbb9d5df9b6c1e271"
[[package]]
name = "config"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7328b20597b53c2454f0b1919720c25c7339051c02b72b7e05409e00b14132be"
dependencies = [
"convert_case",
"lazy_static",
"nom",
"pathdiff",
"serde",
"toml",
]
[[package]]
name = "const_format"
version = "0.2.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3a214c7af3d04997541b18d432afaff4c455e79e2029079647e72fc2bd27673"
dependencies = [
"const_format_proc_macros",
]
[[package]]
name = "const_format_proc_macros"
version = "0.2.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7f6ff08fd20f4f299298a28e2dfa8a8ba1036e6cd2460ac1de7b425d76f2500"
dependencies = [
"proc-macro2",
"quote",
"unicode-xid",
]
[[package]]
name = "convert_case"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca"
dependencies = [
"unicode-segmentation",
]
[[package]]
name = "crunchy"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7"
[[package]]
name = "dashmap"
version = "5.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856"
dependencies = [
"cfg-if",
"hashbrown",
"lock_api",
"once_cell",
"parking_lot_core",
]
[[package]]
name = "derive-where"
version = "1.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62d671cc41a825ebabc75757b62d3d168c577f9149b2d49ece1dad1f72119d25"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "drain_filter_polyfill"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "669a445ee724c5c69b1b06fe0b63e70a1c84bc9bb7d9696cd4f4e3ec45050408"
[[package]]
name = "either"
version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0"
[[package]]
name = "equivalent"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
[[package]]
name = "fnv"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "form_urlencoded"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456"
dependencies = [
"percent-encoding",
]
[[package]]
name = "futures"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0"
dependencies = [
"futures-channel",
"futures-core",
"futures-executor",
"futures-io",
"futures-sink",
"futures-task",
"futures-util",
]
[[package]]
name = "futures-channel"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78"
dependencies = [
"futures-core",
"futures-sink",
]
[[package]]
name = "futures-core"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d"
[[package]]
name = "futures-executor"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d"
dependencies = [
"futures-core",
"futures-task",
"futures-util",
]
[[package]]
name = "futures-io"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1"
[[package]]
name = "futures-macro"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "futures-sink"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5"
[[package]]
name = "futures-task"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004"
[[package]]
name = "futures-util"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48"
dependencies = [
"futures-channel",
"futures-core",
"futures-io",
"futures-macro",
"futures-sink",
"futures-task",
"memchr",
"pin-project-lite",
"pin-utils",
"slab",
]
[[package]]
name = "getrandom"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
dependencies = [
"cfg-if",
"js-sys",
"libc",
"wasi",
"wasm-bindgen",
]
[[package]]
name = "gloo-net"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c06f627b1a58ca3d42b45d6104bf1e1a03799df472df00988b6ba21accc10580"
dependencies = [
"futures-channel",
"futures-core",
"futures-sink",
"gloo-utils",
"http",
"js-sys",
"pin-project",
"serde",
"serde_json",
"thiserror",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
]
[[package]]
name = "gloo-utils"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b5555354113b18c547c1d3a98fbf7fb32a9ff4f6fa112ce823a21641a0ba3aa"
dependencies = [
"js-sys",
"serde",
"serde_json",
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "half"
version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6dd08c532ae367adf81c312a4580bc67f1d0fe8bc9c460520283f4c0ff277888"
dependencies = [
"cfg-if",
"crunchy",
]
[[package]]
name = "hashbrown"
version = "0.14.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
[[package]]
name = "html-escape"
version = "0.2.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d1ad449764d627e22bfd7cd5e8868264fc9236e07c752972b4080cd351cb476"
dependencies = [
"utf8-width",
]
[[package]]
name = "http"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258"
dependencies = [
"bytes",
"fnv",
"itoa",
]
[[package]]
name = "idna"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6"
dependencies = [
"unicode-bidi",
"unicode-normalization",
]
[[package]]
name = "indexmap"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93ead53efc7ea8ed3cfb0c79fc8023fbb782a5432b52830b6518941cebe6505c"
dependencies = [
"equivalent",
"hashbrown",
]
[[package]]
name = "interpolator"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71dd52191aae121e8611f1e8dc3e324dd0dd1dee1e6dd91d10ee07a3cfb4d9d8"
[[package]]
name = "inventory"
version = "0.3.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f958d3d68f4167080a18141e10381e7634563984a537f2a49a30fd8e53ac5767"
[[package]]
name = "itertools"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569"
dependencies = [
"either",
]
[[package]]
name = "itoa"
version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
[[package]]
name = "js-sys"
version = "0.3.70"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1868808506b929d7b0cfa8f75951347aa71bb21144b7791bae35d9bccfcfe37a"
dependencies = [
"wasm-bindgen",
]
[[package]]
name = "lazy_static"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]]
name = "leptos"
version = "0.6.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a15911b4e53bb6e1b033d717eadb39924418a4a288279128122e5a65c70ba3e6"
dependencies = [
"cfg-if",
"leptos_config",
"leptos_dom",
"leptos_macro",
"leptos_reactive",
"leptos_server",
"server_fn",
"tracing",
"typed-builder",
"typed-builder-macro",
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "leptos_config"
version = "0.6.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dbc4d78fba18c1ccab48ffc9f3d35b39821f896b0a28bdd616a846b6241036c9"
dependencies = [
"config",
"regex",
"serde",
"thiserror",
"typed-builder",
]
[[package]]
name = "leptos_dom"
version = "0.6.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ccb04d4763603bb665fa35cb9642d0bd75313117d10efda9b79243c023e69df"
dependencies = [
"async-recursion",
"cfg-if",
"drain_filter_polyfill",
"futures",
"getrandom",
"html-escape",
"indexmap",
"itertools",
"js-sys",
"leptos_reactive",
"once_cell",
"pad-adapter",
"paste",
"rustc-hash",
"serde",
"serde_json",
"server_fn",
"smallvec",
"tracing",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
]
[[package]]
name = "leptos_hot_reload"
version = "0.6.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2cc61e5cce26761562cd3332630b3fbaddb1c4f77744e41474c7212ad279c5d9"
dependencies = [
"anyhow",
"camino",
"indexmap",
"parking_lot",
"proc-macro2",
"quote",
"rstml",
"serde",
"syn",
"walkdir",
]
[[package]]
name = "leptos_macro"
version = "0.6.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90eaea005cabb879c091c84cfec604687ececfd540469e5a30a60c93489a2f23"
dependencies = [
"attribute-derive",
"cfg-if",
"convert_case",
"html-escape",
"itertools",
"leptos_hot_reload",
"prettyplease",
"proc-macro-error",
"proc-macro2",
"quote",
"rstml",
"server_fn_macro",
"syn",
"tracing",
"uuid",
]
[[package]]
name = "leptos_reactive"
version = "0.6.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ef2f99f377472459b0d320b46e9a9516b0e68dee5ed8c9eeb7e8eb9fefec5d2"
dependencies = [
"base64",
"cfg-if",
"futures",
"indexmap",
"js-sys",
"oco_ref",
"paste",
"pin-project",
"rustc-hash",
"self_cell",
"serde",
"serde-wasm-bindgen",
"serde_json",
"slotmap",
"thiserror",
"tracing",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
]
[[package]]
name = "leptos_server"
version = "0.6.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f07be202a433baa8c50050de4f9c116efccffc57208bcda7bd1bb9b8e87dca9"
dependencies = [
"inventory",
"lazy_static",
"leptos_macro",
"leptos_reactive",
"serde",
"server_fn",
"thiserror",
"tracing",
]
[[package]]
name = "libc"
version = "0.2.158"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439"
[[package]]
name = "lock_api"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17"
dependencies = [
"autocfg",
"scopeguard",
]
[[package]]
name = "log"
version = "0.4.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
[[package]]
name = "manyhow"
version = "0.10.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f91ea592d76c0b6471965708ccff7e6a5d277f676b90ab31f4d3f3fc77fade64"
dependencies = [
"manyhow-macros",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "manyhow-macros"
version = "0.10.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c64621e2c08f2576e4194ea8be11daf24ac01249a4f53cd8befcbb7077120ead"
dependencies = [
"proc-macro-utils",
"proc-macro2",
"quote",
]
[[package]]
name = "memchr"
version = "2.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
[[package]]
name = "minimal-lexical"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]]
name = "nom"
version = "7.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
dependencies = [
"memchr",
"minimal-lexical",
]
[[package]]
name = "oco_ref"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c51ebcefb2f0b9a5e0bea115532c8ae4215d1b01eff176d0f4ba4192895c2708"
dependencies = [
"serde",
"thiserror",
]
[[package]]
name = "once_cell"
version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
[[package]]
name = "pad-adapter"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56d80efc4b6721e8be2a10a5df21a30fa0b470f1539e53d8b4e6e75faf938b63"
[[package]]
name = "parking_lot"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27"
dependencies = [
"lock_api",
"parking_lot_core",
]
[[package]]
name = "parking_lot_core"
version = "0.9.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8"
dependencies = [
"cfg-if",
"libc",
"redox_syscall",
"smallvec",
"windows-targets",
]
[[package]]
name = "paste"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
[[package]]
name = "pathdiff"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd"
[[package]]
name = "percent-encoding"
version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
[[package]]
name = "pin-project"
version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3"
dependencies = [
"pin-project-internal",
]
[[package]]
name = "pin-project-internal"
version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "pin-project-lite"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02"
[[package]]
name = "pin-utils"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "prettyplease"
version = "0.2.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f12335488a2f3b0a83b14edad48dca9879ce89b2edd10e80237e4e852dd645e"
dependencies = [
"proc-macro2",
"syn",
]
[[package]]
name = "proc-macro-error"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
dependencies = [
"proc-macro-error-attr",
"proc-macro2",
"quote",
"version_check",
]
[[package]]
name = "proc-macro-error-attr"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
dependencies = [
"proc-macro2",
"quote",
"version_check",
]
[[package]]
name = "proc-macro-utils"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f59e109e2f795a5070e69578c4dc101068139f74616778025ae1011d4cd41a8"
dependencies = [
"proc-macro2",
"quote",
"smallvec",
]
[[package]]
name = "proc-macro2"
version = "1.0.86"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77"
dependencies = [
"unicode-ident",
]
[[package]]
name = "proc-macro2-diagnostics"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8"
dependencies = [
"proc-macro2",
"quote",
"syn",
"version_check",
"yansi",
]
[[package]]
name = "quote"
version = "1.0.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7"
dependencies = [
"proc-macro2",
]
[[package]]
name = "quote-use"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48e96ac59974192a2fa6ee55a41211cf1385c5b2a8636a4c3068b3b3dd599ece"
dependencies = [
"quote",
"quote-use-macros",
]
[[package]]
name = "quote-use-macros"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4c57308e9dde4d7be9af804f6deeaa9951e1de1d5ffce6142eb964750109f7e"
dependencies = [
"derive-where",
"proc-macro-utils",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "redox_syscall"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4"
dependencies = [
"bitflags",
]
[[package]]
name = "regex"
version = "1.10.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata",
"regex-syntax",
]
[[package]]
name = "regex-automata"
version = "0.4.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b"
[[package]]
name = "rstml"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe542870b8f59dd45ad11d382e5339c9a1047cde059be136a7016095bbdefa77"
dependencies = [
"proc-macro2",
"proc-macro2-diagnostics",
"quote",
"syn",
"syn_derive",
"thiserror",
]
[[package]]
name = "rustc-hash"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
[[package]]
name = "ryu"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
[[package]]
name = "same-file"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
dependencies = [
"winapi-util",
]
[[package]]
name = "scopeguard"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "self_cell"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d369a96f978623eb3dc28807c4852d6cc617fed53da5d3c400feff1ef34a714a"
[[package]]
name = "send_wrapper"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73"
dependencies = [
"futures-core",
]
[[package]]
name = "serde"
version = "1.0.208"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cff085d2cb684faa248efb494c39b68e522822ac0de72ccf08109abde717cfb2"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde-wasm-bindgen"
version = "0.6.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8302e169f0eddcc139c70f139d19d6467353af16f9fce27e8c30158036a1e16b"
dependencies = [
"js-sys",
"serde",
"wasm-bindgen",
]
[[package]]
name = "serde_derive"
version = "1.0.208"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24008e81ff7613ed8e5ba0cfaf24e2c2f1e5b8a0495711e44fcd4882fca62bcf"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.125"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83c8e735a073ccf5be70aa8066aa984eaf2fa000db6c8d0100ae605b366d31ed"
dependencies = [
"itoa",
"memchr",
"ryu",
"serde",
]
[[package]]
name = "serde_qs"
version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0431a35568651e363364210c91983c1da5eb29404d9f0928b67d4ebcfa7d330c"
dependencies = [
"percent-encoding",
"serde",
"thiserror",
]
[[package]]
name = "serde_spanned"
version = "0.6.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb5b1b31579f3811bf615c144393417496f152e12ac8b7663bf664f4a815306d"
dependencies = [
"serde",
]
[[package]]
name = "server_fn"
version = "0.6.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "024b400db1aca5bd4188714f7bbbf7a2e1962b9a12a80b2a21e937e509086963"
dependencies = [
"bytes",
"ciborium",
"const_format",
"dashmap",
"futures",
"gloo-net",
"http",
"js-sys",
"once_cell",
"send_wrapper",
"serde",
"serde_json",
"serde_qs",
"server_fn_macro_default",
"thiserror",
"url",
"wasm-bindgen",
"wasm-bindgen-futures",
"wasm-streams",
"web-sys",
"xxhash-rust",
]
[[package]]
name = "server_fn_macro"
version = "0.6.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9cf0e6f71fc924df36e87f27dfbd447f0bedd092d365db3a5396878256d9f00c"
dependencies = [
"const_format",
"convert_case",
"proc-macro2",
"quote",
"syn",
"xxhash-rust",
]
[[package]]
name = "server_fn_macro_default"
version = "0.6.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "556e4fd51eb9ee3e7d9fb0febec6cef486dcbc8f7f427591dfcfebee1abe1ad4"
dependencies = [
"server_fn_macro",
"syn",
]
[[package]]
name = "slab"
version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67"
dependencies = [
"autocfg",
]
[[package]]
name = "slotmap"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dbff4acf519f630b3a3ddcfaea6c06b42174d9a44bc70c620e9ed1649d58b82a"
dependencies = [
"serde",
"version_check",
]
[[package]]
name = "smallvec"
version = "1.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
[[package]]
name = "syn"
version = "2.0.75"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6af063034fc1935ede7be0122941bafa9bacb949334d090b77ca98b5817c7d9"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "syn_derive"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1329189c02ff984e9736652b1631330da25eaa6bc639089ed4915d25446cbe7b"
dependencies = [
"proc-macro-error",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "thiserror"
version = "1.0.63"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.63"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "tinyvec"
version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938"
dependencies = [
"tinyvec_macros",
]
[[package]]
name = "tinyvec_macros"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "todo-leptos"
version = "0.1.0"
dependencies = [
"leptos",
]
[[package]]
name = "toml"
version = "0.8.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e"
dependencies = [
"serde",
"serde_spanned",
"toml_datetime",
"toml_edit",
]
[[package]]
name = "toml_datetime"
version = "0.6.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41"
dependencies = [
"serde",
]
[[package]]
name = "toml_edit"
version = "0.22.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "583c44c02ad26b0c3f3066fe629275e50627026c51ac2e595cca4c230ce1ce1d"
dependencies = [
"indexmap",
"serde",
"serde_spanned",
"toml_datetime",
"winnow",
]
[[package]]
name = "tracing"
version = "0.1.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef"
dependencies = [
"pin-project-lite",
"tracing-attributes",
"tracing-core",
]
[[package]]
name = "tracing-attributes"
version = "0.1.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "tracing-core"
version = "0.1.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54"
dependencies = [
"once_cell",
]
[[package]]
name = "typed-builder"
version = "0.18.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77739c880e00693faef3d65ea3aad725f196da38b22fdc7ea6ded6e1ce4d3add"
dependencies = [
"typed-builder-macro",
]
[[package]]
name = "typed-builder-macro"
version = "0.18.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f718dfaf347dcb5b983bfc87608144b0bad87970aebcbea5ce44d2a30c08e63"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "unicode-bidi"
version = "0.3.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75"
[[package]]
name = "unicode-ident"
version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
[[package]]
name = "unicode-normalization"
version = "0.1.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5"
dependencies = [
"tinyvec",
]
[[package]]
name = "unicode-segmentation"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202"
[[package]]
name = "unicode-xid"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "229730647fbc343e3a80e463c1db7f78f3855d3f3739bee0dda773c9a037c90a"
[[package]]
name = "url"
version = "2.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c"
dependencies = [
"form_urlencoded",
"idna",
"percent-encoding",
]
[[package]]
name = "utf8-width"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86bd8d4e895da8537e5315b8254664e6b769c4ff3db18321b297a1e7004392e3"
[[package]]
name = "uuid"
version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314"
dependencies = [
"getrandom",
]
[[package]]
name = "version_check"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "walkdir"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
dependencies = [
"same-file",
"winapi-util",
]
[[package]]
name = "wasi"
version = "0.11.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "wasm-bindgen"
version = "0.2.93"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5"
dependencies = [
"cfg-if",
"once_cell",
"wasm-bindgen-macro",
]
[[package]]
name = "wasm-bindgen-backend"
version = "0.2.93"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b"
dependencies = [
"bumpalo",
"log",
"once_cell",
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-futures"
version = "0.4.43"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61e9300f63a621e96ed275155c108eb6f843b6a26d053f122ab69724559dc8ed"
dependencies = [
"cfg-if",
"js-sys",
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.93"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.93"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836"
dependencies = [
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.93"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484"
[[package]]
name = "wasm-streams"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b65dc4c90b63b118468cf747d8bf3566c1913ef60be765b5730ead9e0a3ba129"
dependencies = [
"futures-util",
"js-sys",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
]
[[package]]
name = "web-sys"
version = "0.3.70"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26fdeaafd9bd129f65e7c031593c24d62186301e0c72c8978fa1678be7d532c0"
dependencies = [
"js-sys",
"wasm-bindgen",
]
[[package]]
name = "winapi-util"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
dependencies = [
"windows-sys",
]
[[package]]
name = "windows-sys"
version = "0.59.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-targets"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_gnullvm",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "winnow"
version = "0.6.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68a9bda4691f099d435ad181000724da8e5899daa10713c2d432552b9ccd3a6f"
dependencies = [
"memchr",
]
[[package]]
name = "xxhash-rust"
version = "0.8.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a5cbf750400958819fb6178eaa83bee5cd9c29a26a40cc241df8c70fdd46984"
[[package]]
name = "yansi"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049"
[package]
name = "todo-leptos"
version = "0.1.0"
edition = "2021"
[dependencies]
leptos = { version = "0.6.14", features = ["csr"] }
# Todo App Leptos
A port of a [React Todo
App](https://www.digitalocean.com/community/tutorials/how-to-build-a-react-to-do-app-with-react-hooks)
to use the [Leptos](https://leptos.dev) framework.
This is an example project for the [Web
Frontend](https://rustprojectprimer.com/ecosystem/web-frontend.html) section of
the [Rust Project Primer](https://rustprojectprimer.com/) book.
## Prerequisites
You need two prerequisites to build this:
- Rust 1.80 with support for `wasm32-unknown-unknown` target
- Trunk build tool
### Setup
You can install Rust using [Rustup](https://rustup.rs):
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
You need to tell Rustup to add the WebAssembly target:
rustup target add wasm32-unknown-unknown
You need to install [Trunk](https://trunkrs.dev) to build and serve it:
cargo install trunk
## Running it
You can run it locally with Trunk:
trunk serve
This will build and serve it, and watch the project for any changes. When you
edit the code, it will recompile and cause your browser to refresh.
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link data-trunk rel="rust" />
<link data-trunk data-inline rel="css" href="src/style.css" />
<title>Todo Leptos</title>
</head>
<body></body>
</html>
use leptos::*;
/// Represents a single Todo item.
#[derive(PartialEq, Clone)]
pub struct Todo {
pub text: String,
pub completed: bool,
}
impl Todo {
/// Create a new todo item that is not completed.
fn new<S: Into<String>>(text: S) -> Self {
Self {
text: text.into(),
completed: false,
}
}
fn complete(&mut self) {
self.completed = !self.completed;
}
}
/// Main application, consists of title, todo list and entry form.
#[component]
pub fn App() -> impl IntoView {
// signal that holds all of the todo entries.
let (todos, set_todos) = create_signal(vec![
Todo::new("Buy milk"),
Todo::new("Learn Rust"),
Todo::new("Drink enough water"),
Todo::new("Spend time with family"),
]);
let submit = move |string| {
let mut todos = todos.get().clone();
todos.push(Todo::new(string));
set_todos.set(todos);
};
view! {
<div class="app">
<div class="heading">
"Todo List"
</div>
<div class="todo-list">
{
move || {
todos.get().iter().enumerate().map(|(index, todo)| {
let complete = move |()| {
let mut todos = todos.get().clone();
todos[index].complete();
set_todos.set(todos);
};
let remove = move |()| {
let mut todos = todos.get().clone();
todos.remove(index);
set_todos.set(todos);
};
view! {
<TodoRow item={todo.clone()} complete remove />
}
}).collect_view()
}
}
</div>
<div class="footer">
<TodoForm submit />
</div>
</div>
}
}
/// Contains the todo text and buttons to mark as complete and delete.
#[component]
pub fn TodoRow(
item: Todo,
#[prop(into)] complete: Callback<()>,
#[prop(into)] remove: Callback<()>,
) -> impl IntoView {
view! {
<div class:todo=true class:completed=item.completed>
<div class:text=true>
{&item.text}
</div>
<div>
<button class:complete=true on:click={move |_| complete.call(())}>{"✓"}</button>
<button class:remove=true on:click={move |_| remove.call(())}>{"⨯"}</button>
</div>
</div>
}
}
/// Entry form to add new todo item to list.
#[component]
pub fn TodoForm(#[prop(into)] submit: Callback<String>) -> impl IntoView {
let (input, set_input) = create_signal(String::new());
let submit_form = move |event: ev::SubmitEvent| {
submit.call(input.get().clone());
set_input.set(String::new());
event.prevent_default();
};
view! {
<form on:submit=submit_form>
<input
type="text"
class="input"
on:input=move |ev| set_input.set(event_target_value(&ev))
prop:value=input
/>
</form>
}
}
use leptos::*;
use todo_leptos::App;
fn main() {
mount_to_body(|| view! { <App /> })
}
body {
background: #209cee;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
}
.app {
height: 100vh;
padding: 10px;
padding-top: 20px;
padding-bottom: 20px;
max-width: 600px;
margin-left: auto;
margin-right: auto;
}
.app .heading {
padding: 5px;
padding-top: 10px;
padding-bottom: 10px;
text-align: center;
font-size: 20px;
font-weight: 600;
border-radius: 7px 7px 0px 0px;
background: #e8e8e8;
border: 1px solid #d8d8d8;
border-bottom: 1px solid #b4b4b4;
background: linear-gradient(to bottom, #f6f6f6 0%,#dadada 100%);
}
.app .todo-list {
padding: 5px;
background: #ffffff;
/*border-top: 1px solid #b4b4b4;*/
border-left: 1px solid #d8d8d8;
border-right: 1px solid #d8d8d8;
}
.app .footer {
padding: 5px;
border-radius: 0px 0px 7px 7px;
background: #ffffff;
padding-bottom: 10px;
border-left: 1px solid #d8d8d8;
border-right: 1px solid #d8d8d8;
border-bottom: 1px solid #d8d8d8;
}
.app .footer form * {
box-sizing: border-box;
width: 100%;
}
.todo-list .todo {
align-items: center;
background: #f0f0f0;
border-radius: 3px;
box-shadow: 1px 1px 1px rgba(0, 0, 0, 0.15);
display: flex;
font-size: 14px;
justify-content: space-between;
margin-bottom: 6px;
padding: 3px 10px;
}
.todo-list .todo button {
width: 20px;
height: 20px;
font-size: 10px;
background: #f9f9f9;
border-radius: 50%;
margin: 0 4px 0 0;
opacity: 20%;
text-align: center;
background: #e9e9e9;
border: 1px solid #e0e0e0;
}
.todo-list .todo button:hover {
opacity: 100%;
transition: 100ms;
}
.todo-list .todo button.complete {
background: #27C93F;
border: 1px solid #1DAD2B;
transition: 100ms;
}
.todo-list .todo button.remove {
background: #FF6057;
border: 1px solid #E14640;
transition: 100ms;
}
.todo.completed {
text-decoration: line-through;
}
You can see this app in action here. You can see how Leptos
represents properties using function parameters when defining components. You
can also see how Leptos manages state using create_signal()
, which returns a
getter and a setter for the signal value. It further showcases how the view!
macro is used to construct a tree of HTML elements and child components, and
how Callback
can be used to pass callbacks down to child components.
Dioxus
Dioxus is another frontend framework. Like Yew and Leptos, it also uses the component model, hooks and has a domain-specific language for describing the graph of HTML elements and components that a component renders into.
What makes Dioxus interesting is that it is easy to build Desktop and Mobile
applications with it. The Dioxus team is also working on
Blitz, a minimal web renderer for use
with writing Desktop applications with Dioxus but without the need for a full
browser engine. Dioxus also used to support rendering to the Terminal,
but it appears as if the support for this has been dropped since 0.4.3
.
The domain-specific language of Dioxus uses the rsx!
macro and is distinct from
the XML-style that the other frameworks use.
#![allow(unused)] fn main() { fn app() -> Element { rsx! { div { "Hello, world!" } } } }
Dioxus comes with its own CLI to use for initializing, building and serving Dioxus applications. I was not able to get it working with Trunk.
Example: Todo App
This is an example todo application written using Dioxus. It looks and functions similar to the example applications written with Yew and Leptos.
- assets/
- src/
# Generated by Cargo
# will have compiled files and executables
/target/
/dist/
/static/
/.dioxus/
# this file will generate by tailwind:
/assets/tailwind.css
# These are backup files generated by rustfmt
**/*.rs.bk
stages:
- publish
# build application with trunk, use pinned versions for reproducible build.
pages:
stage: publish
image: rust:1.80
before_script:
- rustup target add wasm32-unknown-unknown
- cargo install dioxus-cli
script:
- dx build --release
- mv dist public
artifacts:
paths:
- public
only:
- master
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "ahash"
version = "0.8.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011"
dependencies = [
"cfg-if",
"once_cell",
"version_check",
"zerocopy",
]
[[package]]
name = "allocator-api2"
version = "0.2.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f"
[[package]]
name = "anymap2"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d301b3b94cb4b2f23d7917810addbbaff90738e0ca2be692bd027e70d7e0330c"
[[package]]
name = "async-channel"
version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89b47800b0be77592da0afd425cc03468052844aff33b84e33cc696f64e77b6a"
dependencies = [
"concurrent-queue",
"event-listener-strategy",
"futures-core",
"pin-project-lite",
]
[[package]]
name = "async-task"
version = "4.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de"
[[package]]
name = "async-trait"
version = "0.1.82"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a27b8a3a6e1a44fa4c8baf1f653e4172e81486d4941f2237e20dc2d0cf4ddff1"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "atomic-waker"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
[[package]]
name = "autocfg"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0"
[[package]]
name = "base64"
version = "0.21.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
[[package]]
name = "bincode"
version = "1.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad"
dependencies = [
"serde",
]
[[package]]
name = "bitflags"
version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
dependencies = [
"serde",
]
[[package]]
name = "blocking"
version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "703f41c54fc768e63e091340b424302bb1c29ef4aa0c7f10fe849dfb114d29ea"
dependencies = [
"async-channel",
"async-task",
"futures-io",
"futures-lite",
"piper",
]
[[package]]
name = "bumpalo"
version = "3.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
[[package]]
name = "bytes"
version = "1.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50"
[[package]]
name = "camino"
version = "1.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b96ec4966b5813e2c0507c1f86115c8c5abaadc3980879c3424042a02fd1ad3"
dependencies = [
"serde",
]
[[package]]
name = "cargo-platform"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24b1f0365a6c6bb4020cd05806fd0d33c44d38046b8bd7f0e40814b9763cabfc"
dependencies = [
"serde",
]
[[package]]
name = "cargo_metadata"
version = "0.18.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d886547e41f740c616ae73108f6eb70afe6d940c7bc697cb30f13daec073037"
dependencies = [
"camino",
"cargo-platform",
"semver",
"serde",
"serde_json",
"thiserror",
]
[[package]]
name = "cfg-expr"
version = "0.15.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02"
dependencies = [
"smallvec",
]
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "ciborium"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e"
dependencies = [
"ciborium-io",
"ciborium-ll",
"serde",
]
[[package]]
name = "ciborium-io"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757"
[[package]]
name = "ciborium-ll"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9"
dependencies = [
"ciborium-io",
"half",
]
[[package]]
name = "concurrent-queue"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973"
dependencies = [
"crossbeam-utils",
]
[[package]]
name = "console_error_panic_hook"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc"
dependencies = [
"cfg-if",
"wasm-bindgen",
]
[[package]]
name = "const_format"
version = "0.2.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "50c655d81ff1114fb0dcdea9225ea9f0cc712a6f8d189378e82bdf62a473a64b"
dependencies = [
"const_format_proc_macros",
]
[[package]]
name = "const_format_proc_macros"
version = "0.2.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eff1a44b93f47b1bac19a27932f5c591e43d1ba357ee4f61526c8a25603f0eb1"
dependencies = [
"proc-macro2",
"quote",
"unicode-xid",
]
[[package]]
name = "constcat"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd7e35aee659887cbfb97aaf227ac12cad1a9d7c71e55ff3376839ed4e282d08"
[[package]]
name = "convert_case"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca"
dependencies = [
"unicode-segmentation",
]
[[package]]
name = "crossbeam-utils"
version = "0.8.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80"
[[package]]
name = "crunchy"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7"
[[package]]
name = "darling"
version = "0.20.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989"
dependencies = [
"darling_core",
"darling_macro",
]
[[package]]
name = "darling_core"
version = "0.20.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5"
dependencies = [
"fnv",
"ident_case",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "darling_macro"
version = "0.20.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806"
dependencies = [
"darling_core",
"quote",
"syn",
]
[[package]]
name = "dashmap"
version = "5.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856"
dependencies = [
"cfg-if",
"hashbrown",
"lock_api",
"once_cell",
"parking_lot_core",
]
[[package]]
name = "dioxus"
version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8e7fe217b50d43b27528b0f24c89b411f742a3e7564d1cfbf85253f967954db"
dependencies = [
"dioxus-config-macro",
"dioxus-core",
"dioxus-core-macro",
"dioxus-fullstack",
"dioxus-hooks",
"dioxus-hot-reload",
"dioxus-html",
"dioxus-router",
"dioxus-signals",
"dioxus-web",
]
[[package]]
name = "dioxus-cli-config"
version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7dffc452ed91af6ef772b0d9a5899573f6785314e97c533733ec55413c01df3"
dependencies = [
"once_cell",
"serde",
"serde_json",
"tracing",
]
[[package]]
name = "dioxus-config-macro"
version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb1a1aa34cc04c1f7fcbb7a10791ba773cc02d834fe3ec1fe05647699f3b101f"
dependencies = [
"proc-macro2",
"quote",
]
[[package]]
name = "dioxus-core"
version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3730d2459ab66951cedf10b09eb84141a6eda7f403c28057cbe010495be156b7"
dependencies = [
"futures-channel",
"futures-util",
"generational-box",
"longest-increasing-subsequence",
"rustc-hash",
"serde",
"slab",
"slotmap",
"tracing",
"tracing-subscriber",
]
[[package]]
name = "dioxus-core-macro"
version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d9c0dfe0e6a46626fa716c4aa1d2ccb273441337909cfeacad5bb6fcfb947d2"
dependencies = [
"constcat",
"convert_case",
"dioxus-rsx",
"prettyplease",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "dioxus-debug-cell"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2ea539174bb236e0e7dc9c12b19b88eae3cb574dedbd0252a2d43ea7e6de13e2"
[[package]]
name = "dioxus-fullstack"
version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b80f0ac18166302341164e681322e0385131c08a11c3cc1c51ee8df799ab0d3d"
dependencies = [
"async-trait",
"base64",
"bytes",
"ciborium",
"dioxus-hot-reload",
"dioxus-lib",
"dioxus-web",
"dioxus_server_macro",
"futures-util",
"once_cell",
"serde",
"serde_json",
"server_fn",
"tracing",
"web-sys",
]
[[package]]
name = "dioxus-hooks"
version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa8f9c661eea82295219d25555d5c0b597e74186b029038ceb5e3700ccbd4380"
dependencies = [
"dioxus-core",
"dioxus-debug-cell",
"dioxus-signals",
"futures-channel",
"futures-util",
"generational-box",
"slab",
"thiserror",
"tracing",
]
[[package]]
name = "dioxus-hot-reload"
version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77d01246cb1b93437fb0bbd0dd11cfc66342d86b4311819e76654f2017ce1473"
dependencies = [
"dioxus-core",
"dioxus-html",
"dioxus-rsx",
"interprocess-docfix",
"serde",
"serde_json",
]
[[package]]
name = "dioxus-html"
version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f01a0826f179adad6ea8d6586746e8edde0c602cc86f4eb8e5df7a3b204c4018"
dependencies = [
"async-trait",
"dioxus-core",
"dioxus-html-internal-macro",
"enumset",
"euclid",
"futures-channel",
"generational-box",
"keyboard-types",
"serde",
"serde-value",
"serde_json",
"serde_repr",
"tracing",
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "dioxus-html-internal-macro"
version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b96f35a608d0ab8f4ca6f66ce1828354e4ebd41580b12454f490221a11da93c"
dependencies = [
"convert_case",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "dioxus-interpreter-js"
version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "351fad098c657d14f3ac2900362d2b86e83c22c4c620a404839e1ab628f3395b"
dependencies = [
"js-sys",
"md5",
"sledgehammer_bindgen",
"sledgehammer_utils",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
]
[[package]]
name = "dioxus-lib"
version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8bd39b2c41dd1915dcb91d914ea72d8b646f1f8995aaeff82816b862ec586ecd"
dependencies = [
"dioxus-core",
"dioxus-core-macro",
"dioxus-hooks",
"dioxus-html",
"dioxus-rsx",
"dioxus-signals",
]
[[package]]
name = "dioxus-logger"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81fe09dc9773dc1f1bb0d38529203d6555d08f67aadca5cf955ac3d1a9e69880"
dependencies = [
"console_error_panic_hook",
"tracing",
"tracing-subscriber",
"tracing-wasm",
]
[[package]]
name = "dioxus-router"
version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c235c5dbeb528c0c2b0424763da812e7500df69b82eddac54db6f4975e065c5f"
dependencies = [
"dioxus-cli-config",
"dioxus-lib",
"dioxus-router-macro",
"gloo",
"gloo-utils 0.1.7",
"js-sys",
"tracing",
"url",
"urlencoding",
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "dioxus-router-macro"
version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e7cd1c5137ba361f2150cdea6b3bc9ddda7b1af84b22c9ee6b5499bf43e1381"
dependencies = [
"proc-macro2",
"quote",
"slab",
"syn",
]
[[package]]
name = "dioxus-rsx"
version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "15c400bc8a779107d8f3a67b14375db07dbd2bc31163bf085a8e9097f36f7179"
dependencies = [
"dioxus-core",
"internment",
"krates",
"proc-macro2",
"quote",
"syn",
"tracing",
]
[[package]]
name = "dioxus-signals"
version = "0.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e3e224cd3d3713f159f0199fc088c292a0f4adb94996b48120157f6a8f8342d"
dependencies = [
"dioxus-core",
"futures-channel",
"futures-util",
"generational-box",
"once_cell",
"parking_lot",
"rustc-hash",
"tracing",
]
[[package]]
name = "dioxus-web"
version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0855ac81fcc9252a0863930a7a7cbb2504fc1b6efe893489c8d0e23aaeb2cb9"
dependencies = [
"async-trait",
"console_error_panic_hook",
"dioxus-core",
"dioxus-html",
"dioxus-interpreter-js",
"futures-channel",
"futures-util",
"generational-box",
"js-sys",
"rustc-hash",
"serde",
"serde-wasm-bindgen",
"serde_json",
"tracing",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
]
[[package]]
name = "dioxus_server_macro"
version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5ef2cad17001c1155f019cb69adbacd620644566d78a77d0778807bb106a337"
dependencies = [
"convert_case",
"proc-macro2",
"quote",
"server_fn_macro",
"syn",
]
[[package]]
name = "enumset"
version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d07a4b049558765cef5f0c1a273c3fc57084d768b44d2f98127aef4cceb17293"
dependencies = [
"enumset_derive",
]
[[package]]
name = "enumset_derive"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59c3b24c345d8c314966bdc1832f6c2635bfcce8e7cf363bd115987bba2ee242"
dependencies = [
"darling",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "equivalent"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
[[package]]
name = "euclid"
version = "0.22.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad9cdb4b747e485a12abb0e6566612956c7a1bafa3bdb8d682c5b6d403589e48"
dependencies = [
"num-traits",
"serde",
]
[[package]]
name = "event-listener"
version = "5.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6032be9bd27023a771701cc49f9f053c751055f71efb2e0ae5c15809093675ba"
dependencies = [
"concurrent-queue",
"parking",
"pin-project-lite",
]
[[package]]
name = "event-listener-strategy"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f214dc438f977e6d4e3500aaa277f5ad94ca83fbbd9b1a15713ce2344ccc5a1"
dependencies = [
"event-listener",
"pin-project-lite",
]
[[package]]
name = "fastrand"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6"
[[package]]
name = "fixedbitset"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
[[package]]
name = "fnv"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "form_urlencoded"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456"
dependencies = [
"percent-encoding",
]
[[package]]
name = "futures"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0"
dependencies = [
"futures-channel",
"futures-core",
"futures-executor",
"futures-io",
"futures-sink",
"futures-task",
"futures-util",
]
[[package]]
name = "futures-channel"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78"
dependencies = [
"futures-core",
"futures-sink",
]
[[package]]
name = "futures-core"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d"
[[package]]
name = "futures-executor"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d"
dependencies = [
"futures-core",
"futures-task",
"futures-util",
]
[[package]]
name = "futures-io"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1"
[[package]]
name = "futures-lite"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52527eb5074e35e9339c6b4e8d12600c7128b68fb25dcb9fa9dec18f7c25f3a5"
dependencies = [
"futures-core",
"pin-project-lite",
]
[[package]]
name = "futures-macro"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "futures-sink"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5"
[[package]]
name = "futures-task"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004"
[[package]]
name = "futures-util"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48"
dependencies = [
"futures-channel",
"futures-core",
"futures-io",
"futures-macro",
"futures-sink",
"futures-task",
"memchr",
"pin-project-lite",
"pin-utils",
"slab",
]
[[package]]
name = "generational-box"
version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "557cf2cbacd0504c6bf8c29f52f8071e0de1d9783346713dc6121d7fa1e5d0e0"
dependencies = [
"parking_lot",
]
[[package]]
name = "gloo"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28999cda5ef6916ffd33fb4a7b87e1de633c47c0dc6d97905fee1cdaa142b94d"
dependencies = [
"gloo-console",
"gloo-dialogs",
"gloo-events",
"gloo-file",
"gloo-history",
"gloo-net 0.3.1",
"gloo-render",
"gloo-storage",
"gloo-timers",
"gloo-utils 0.1.7",
"gloo-worker",
]
[[package]]
name = "gloo-console"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "82b7ce3c05debe147233596904981848862b068862e9ec3e34be446077190d3f"
dependencies = [
"gloo-utils 0.1.7",
"js-sys",
"serde",
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "gloo-dialogs"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67062364ac72d27f08445a46cab428188e2e224ec9e37efdba48ae8c289002e6"
dependencies = [
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "gloo-events"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68b107f8abed8105e4182de63845afcc7b69c098b7852a813ea7462a320992fc"
dependencies = [
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "gloo-file"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8d5564e570a38b43d78bdc063374a0c3098c4f0d64005b12f9bbe87e869b6d7"
dependencies = [
"gloo-events",
"js-sys",
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "gloo-history"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85725d90bf0ed47063b3930ef28e863658a7905989e9929a8708aab74a1d5e7f"
dependencies = [
"gloo-events",
"gloo-utils 0.1.7",
"serde",
"serde-wasm-bindgen",
"serde_urlencoded",
"thiserror",
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "gloo-net"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a66b4e3c7d9ed8d315fd6b97c8b1f74a7c6ecbbc2320e65ae7ed38b7068cc620"
dependencies = [
"futures-channel",
"futures-core",
"futures-sink",
"gloo-utils 0.1.7",
"http 0.2.12",
"js-sys",
"pin-project",
"serde",
"serde_json",
"thiserror",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
]
[[package]]
name = "gloo-net"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c06f627b1a58ca3d42b45d6104bf1e1a03799df472df00988b6ba21accc10580"
dependencies = [
"futures-channel",
"futures-core",
"futures-sink",
"gloo-utils 0.2.0",
"http 1.1.0",
"js-sys",
"pin-project",
"serde",
"serde_json",
"thiserror",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
]
[[package]]
name = "gloo-render"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2fd9306aef67cfd4449823aadcd14e3958e0800aa2183955a309112a84ec7764"
dependencies = [
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "gloo-storage"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d6ab60bf5dbfd6f0ed1f7843da31b41010515c745735c970e821945ca91e480"
dependencies = [
"gloo-utils 0.1.7",
"js-sys",
"serde",
"serde_json",
"thiserror",
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "gloo-timers"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b995a66bb87bebce9a0f4a95aed01daca4872c050bfcb21653361c03bc35e5c"
dependencies = [
"js-sys",
"wasm-bindgen",
]
[[package]]
name = "gloo-utils"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "037fcb07216cb3a30f7292bd0176b050b7b9a052ba830ef7d5d65f6dc64ba58e"
dependencies = [
"js-sys",
"serde",
"serde_json",
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "gloo-utils"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b5555354113b18c547c1d3a98fbf7fb32a9ff4f6fa112ce823a21641a0ba3aa"
dependencies = [
"js-sys",
"serde",
"serde_json",
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "gloo-worker"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13471584da78061a28306d1359dd0178d8d6fc1c7c80e5e35d27260346e0516a"
dependencies = [
"anymap2",
"bincode",
"gloo-console",
"gloo-utils 0.1.7",
"js-sys",
"serde",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
]
[[package]]
name = "half"
version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6dd08c532ae367adf81c312a4580bc67f1d0fe8bc9c460520283f4c0ff277888"
dependencies = [
"cfg-if",
"crunchy",
]
[[package]]
name = "hashbrown"
version = "0.14.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
dependencies = [
"ahash",
"allocator-api2",
]
[[package]]
name = "http"
version = "0.2.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1"
dependencies = [
"bytes",
"fnv",
"itoa",
]
[[package]]
name = "http"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258"
dependencies = [
"bytes",
"fnv",
"itoa",
]
[[package]]
name = "ident_case"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
[[package]]
name = "idna"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6"
dependencies = [
"unicode-bidi",
"unicode-normalization",
]
[[package]]
name = "indexmap"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5"
dependencies = [
"equivalent",
"hashbrown",
]
[[package]]
name = "internment"
version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04e8e537b529b8674e97e9fb82c10ff168a290ac3867a0295f112061ffbca1ef"
dependencies = [
"hashbrown",
"parking_lot",
]
[[package]]
name = "interprocess-docfix"
version = "1.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4b84ee245c606aeb0841649a9288e3eae8c61b853a8cd5c0e14450e96d53d28f"
dependencies = [
"blocking",
"cfg-if",
"futures-core",
"futures-io",
"intmap",
"libc",
"once_cell",
"rustc_version",
"spinning",
"thiserror",
"to_method",
"winapi",
]
[[package]]
name = "intmap"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae52f28f45ac2bc96edb7714de995cffc174a395fb0abf5bff453587c980d7b9"
[[package]]
name = "itoa"
version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
[[package]]
name = "js-sys"
version = "0.3.70"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1868808506b929d7b0cfa8f75951347aa71bb21144b7791bae35d9bccfcfe37a"
dependencies = [
"wasm-bindgen",
]
[[package]]
name = "keyboard-types"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a"
dependencies = [
"bitflags",
"serde",
"unicode-segmentation",
]
[[package]]
name = "krates"
version = "0.16.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fcb3baf2360eb25ad31f0ada3add63927ada6db457791979b82ac199f835cb9"
dependencies = [
"cargo-platform",
"cargo_metadata",
"cfg-expr",
"petgraph",
"semver",
]
[[package]]
name = "lazy_static"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]]
name = "libc"
version = "0.2.158"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439"
[[package]]
name = "lock_api"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17"
dependencies = [
"autocfg",
"scopeguard",
]
[[package]]
name = "log"
version = "0.4.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
[[package]]
name = "longest-increasing-subsequence"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3bd0dd2cd90571056fdb71f6275fada10131182f84899f4b2a916e565d81d86"
[[package]]
name = "lru"
version = "0.12.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37ee39891760e7d94734f6f63fedc29a2e4a152f836120753a72503f09fcf904"
dependencies = [
"hashbrown",
]
[[package]]
name = "md5"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771"
[[package]]
name = "memchr"
version = "2.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
[[package]]
name = "nu-ansi-term"
version = "0.46.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84"
dependencies = [
"overload",
"winapi",
]
[[package]]
name = "num-traits"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
]
[[package]]
name = "once_cell"
version = "1.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33ea5043e58958ee56f3e15a90aee535795cd7dfd319846288d93c5b57d85cbe"
[[package]]
name = "ordered-float"
version = "2.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68f19d67e5a2795c94e73e0bb1cc1a7edeb2e28efd39e2e1c9b7a40c1108b11c"
dependencies = [
"num-traits",
]
[[package]]
name = "overload"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
[[package]]
name = "parking"
version = "2.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba"
[[package]]
name = "parking_lot"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27"
dependencies = [
"lock_api",
"parking_lot_core",
]
[[package]]
name = "parking_lot_core"
version = "0.9.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8"
dependencies = [
"cfg-if",
"libc",
"redox_syscall",
"smallvec",
"windows-targets",
]
[[package]]
name = "percent-encoding"
version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
[[package]]
name = "petgraph"
version = "0.6.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db"
dependencies = [
"fixedbitset",
"indexmap",
]
[[package]]
name = "pin-project"
version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3"
dependencies = [
"pin-project-internal",
]
[[package]]
name = "pin-project-internal"
version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "pin-project-lite"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02"
[[package]]
name = "pin-utils"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "piper"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066"
dependencies = [
"atomic-waker",
"fastrand",
"futures-io",
]
[[package]]
name = "prettyplease"
version = "0.2.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "479cf940fbbb3426c32c5d5176f62ad57549a0bb84773423ba8be9d089f5faba"
dependencies = [
"proc-macro2",
"syn",
]
[[package]]
name = "proc-macro2"
version = "1.0.86"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af"
dependencies = [
"proc-macro2",
]
[[package]]
name = "redox_syscall"
version = "0.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0884ad60e090bf1345b93da0a5de8923c93884cd03f40dfcfddd3b4bee661853"
dependencies = [
"bitflags",
]
[[package]]
name = "rustc-hash"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
[[package]]
name = "rustc_version"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
dependencies = [
"semver",
]
[[package]]
name = "ryu"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
[[package]]
name = "scopeguard"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "semver"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b"
dependencies = [
"serde",
]
[[package]]
name = "send_wrapper"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73"
dependencies = [
"futures-core",
]
[[package]]
name = "serde"
version = "1.0.210"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde-value"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c"
dependencies = [
"ordered-float",
"serde",
]
[[package]]
name = "serde-wasm-bindgen"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3b143e2833c57ab9ad3ea280d21fd34e285a42837aeb0ee301f4f41890fa00e"
dependencies = [
"js-sys",
"serde",
"wasm-bindgen",
]
[[package]]
name = "serde_derive"
version = "1.0.210"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.128"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8"
dependencies = [
"itoa",
"memchr",
"ryu",
"serde",
]
[[package]]
name = "serde_qs"
version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0431a35568651e363364210c91983c1da5eb29404d9f0928b67d4ebcfa7d330c"
dependencies = [
"percent-encoding",
"serde",
"thiserror",
]
[[package]]
name = "serde_repr"
version = "0.1.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_urlencoded"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
dependencies = [
"form_urlencoded",
"itoa",
"ryu",
"serde",
]
[[package]]
name = "server_fn"
version = "0.6.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fae7a3038a32e5a34ba32c6c45eb4852f8affaf8b794ebfcd4b1099e2d62ebe"
dependencies = [
"bytes",
"const_format",
"dashmap",
"futures",
"gloo-net 0.6.0",
"http 1.1.0",
"js-sys",
"once_cell",
"send_wrapper",
"serde",
"serde_json",
"serde_qs",
"server_fn_macro_default",
"thiserror",
"url",
"wasm-bindgen",
"wasm-bindgen-futures",
"wasm-streams",
"web-sys",
"xxhash-rust",
]
[[package]]
name = "server_fn_macro"
version = "0.6.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "faaaf648c6967aef78177c0610478abb5a3455811f401f3c62d10ae9bd3901a1"
dependencies = [
"const_format",
"convert_case",
"proc-macro2",
"quote",
"syn",
"xxhash-rust",
]
[[package]]
name = "server_fn_macro_default"
version = "0.6.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f2aa8119b558a17992e0ac1fd07f080099564f24532858811ce04f742542440"
dependencies = [
"server_fn_macro",
"syn",
]
[[package]]
name = "sharded-slab"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
dependencies = [
"lazy_static",
]
[[package]]
name = "slab"
version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67"
dependencies = [
"autocfg",
]
[[package]]
name = "sledgehammer_bindgen"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fcfaf791ff02f48f3518ce825d32cf419c13a43c1d8b1232f74ac89f339c46d2"
dependencies = [
"sledgehammer_bindgen_macro",
"wasm-bindgen",
]
[[package]]
name = "sledgehammer_bindgen_macro"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "edc90d3e8623d29a664cd8dba5078b600dd203444f00b9739f744e4c6e7aeaf2"
dependencies = [
"quote",
"syn",
]
[[package]]
name = "sledgehammer_utils"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f20798defa0e9d4eff9ca451c7f84774c7378a9c3b5a40112cfa2b3eadb97ae2"
dependencies = [
"lru",
"once_cell",
"rustc-hash",
]
[[package]]
name = "slotmap"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dbff4acf519f630b3a3ddcfaea6c06b42174d9a44bc70c620e9ed1649d58b82a"
dependencies = [
"serde",
"version_check",
]
[[package]]
name = "smallvec"
version = "1.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
[[package]]
name = "spinning"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d4f0e86297cad2658d92a707320d87bf4e6ae1050287f51d19b67ef3f153a7b"
dependencies = [
"lock_api",
]
[[package]]
name = "syn"
version = "2.0.77"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "thiserror"
version = "1.0.63"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.63"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "thread_local"
version = "1.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c"
dependencies = [
"cfg-if",
"once_cell",
]
[[package]]
name = "tinyvec"
version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938"
dependencies = [
"tinyvec_macros",
]
[[package]]
name = "tinyvec_macros"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "to_method"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7c4ceeeca15c8384bbc3e011dbd8fccb7f068a440b752b7d9b32ceb0ca0e2e8"
[[package]]
name = "todo-dioxus"
version = "0.1.0"
dependencies = [
"dioxus",
"dioxus-logger",
]
[[package]]
name = "tracing"
version = "0.1.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef"
dependencies = [
"pin-project-lite",
"tracing-attributes",
"tracing-core",
]
[[package]]
name = "tracing-attributes"
version = "0.1.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "tracing-core"
version = "0.1.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54"
dependencies = [
"once_cell",
"valuable",
]
[[package]]
name = "tracing-log"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
dependencies = [
"log",
"once_cell",
"tracing-core",
]
[[package]]
name = "tracing-subscriber"
version = "0.3.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b"
dependencies = [
"nu-ansi-term",
"sharded-slab",
"smallvec",
"thread_local",
"tracing-core",
"tracing-log",
]
[[package]]
name = "tracing-wasm"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4575c663a174420fa2d78f4108ff68f65bf2fbb7dd89f33749b6e826b3626e07"
dependencies = [
"tracing",
"tracing-subscriber",
"wasm-bindgen",
]
[[package]]
name = "unicode-bidi"
version = "0.3.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75"
[[package]]
name = "unicode-ident"
version = "1.0.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe"
[[package]]
name = "unicode-normalization"
version = "0.1.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5"
dependencies = [
"tinyvec",
]
[[package]]
name = "unicode-segmentation"
version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
[[package]]
name = "unicode-xid"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "229730647fbc343e3a80e463c1db7f78f3855d3f3739bee0dda773c9a037c90a"
[[package]]
name = "url"
version = "2.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c"
dependencies = [
"form_urlencoded",
"idna",
"percent-encoding",
]
[[package]]
name = "urlencoding"
version = "2.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
[[package]]
name = "valuable"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
[[package]]
name = "version_check"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "wasm-bindgen"
version = "0.2.93"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5"
dependencies = [
"cfg-if",
"once_cell",
"wasm-bindgen-macro",
]
[[package]]
name = "wasm-bindgen-backend"
version = "0.2.93"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b"
dependencies = [
"bumpalo",
"log",
"once_cell",
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-futures"
version = "0.4.43"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61e9300f63a621e96ed275155c108eb6f843b6a26d053f122ab69724559dc8ed"
dependencies = [
"cfg-if",
"js-sys",
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.93"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.93"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836"
dependencies = [
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.93"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484"
[[package]]
name = "wasm-streams"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b65dc4c90b63b118468cf747d8bf3566c1913ef60be765b5730ead9e0a3ba129"
dependencies = [
"futures-util",
"js-sys",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
]
[[package]]
name = "web-sys"
version = "0.3.70"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26fdeaafd9bd129f65e7c031593c24d62186301e0c72c8978fa1678be7d532c0"
dependencies = [
"js-sys",
"wasm-bindgen",
]
[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows-targets"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_gnullvm",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "xxhash-rust"
version = "0.8.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a5cbf750400958819fb6178eaa83bee5cd9c29a26a40cc241df8c70fdd46984"
[[package]]
name = "zerocopy"
version = "0.7.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.7.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[package]
name = "todo-dioxus"
version = "0.1.0"
authors = ["Patrick Elsen <pelsen@xfbs.net>"]
edition = "2021"
[dependencies]
dioxus = { version = "0.5", features = ["web"] }
dioxus-logger = "0.5.1"
[application]
name = "todo-dioxus"
default_platform = "web"
out_dir = "dist"
asset_dir = "assets"
[web.app]
title = "Todo App"
[web.watcher]
reload_html = true
watch_path = ["src", "assets"]
[web.resource]
style = ["style.css"]
script = []
[web.resource.dev]
script = []
# Development
Run the following command in the root of the project to start the Dioxus dev server:
```bash
dx serve --hot-reload
```
- Open the browser to http://localhost:8080
body {
background: #209cee;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
}
.app {
height: 100vh;
padding: 10px;
padding-top: 20px;
padding-bottom: 20px;
max-width: 600px;
margin-left: auto;
margin-right: auto;
}
.app .heading {
padding: 5px;
padding-top: 10px;
padding-bottom: 10px;
text-align: center;
font-size: 20px;
font-weight: 600;
border-radius: 7px 7px 0px 0px;
background: #e8e8e8;
border: 1px solid #d8d8d8;
border-bottom: 1px solid #b4b4b4;
background: linear-gradient(to bottom, #f6f6f6 0%,#dadada 100%);
}
.app .todo-list {
padding: 5px;
background: #ffffff;
/*border-top: 1px solid #b4b4b4;*/
border-left: 1px solid #d8d8d8;
border-right: 1px solid #d8d8d8;
}
.app .footer {
padding: 5px;
border-radius: 0px 0px 7px 7px;
background: #ffffff;
padding-bottom: 10px;
border-left: 1px solid #d8d8d8;
border-right: 1px solid #d8d8d8;
border-bottom: 1px solid #d8d8d8;
}
.app .footer form * {
box-sizing: border-box;
width: 100%;
}
.todo-list .todo {
align-items: center;
background: #f0f0f0;
border-radius: 3px;
box-shadow: 1px 1px 1px rgba(0, 0, 0, 0.15);
display: flex;
font-size: 14px;
justify-content: space-between;
margin-bottom: 6px;
padding: 3px 10px;
}
.todo-list .todo button {
width: 20px;
height: 20px;
font-size: 10px;
background: #f9f9f9;
border-radius: 50%;
margin: 0 4px 0 0;
opacity: 20%;
text-align: center;
background: #e9e9e9;
border: 1px solid #e0e0e0;
}
.todo-list .todo button:hover {
opacity: 100%;
transition: 100ms;
}
.todo-list .todo button.complete {
background: #27C93F;
border: 1px solid #1DAD2B;
transition: 100ms;
}
.todo-list .todo button.remove {
background: #FF6057;
border: 1px solid #E14640;
transition: 100ms;
}
.todo.completed {
text-decoration: line-through;
}
use dioxus::prelude::*;
/// Represents a single Todo item.
#[derive(PartialEq, Clone)]
pub struct Todo {
pub text: String,
pub completed: bool,
}
impl Todo {
/// Create a new todo item that is not completed.
fn new<S: Into<String>>(text: S) -> Self {
Self {
text: text.into(),
completed: false,
}
}
fn complete(&mut self) {
self.completed = !self.completed;
}
}
/// Main application, contains title, todo list and entry form.
#[component]
pub fn App() -> Element {
// stores the todo list. this signal is handed down to children for modification.
let todos = use_signal(|| {
vec![
Todo::new("Buy milk"),
Todo::new("Learn Rust"),
Todo::new("Drink enough water"),
Todo::new("Spend time with family"),
]
});
rsx! {
div { class: "app",
div { class: "heading",
{"Todo List"}
}
div { class: "todo-list",
for (i, _) in todos().into_iter().enumerate() {
TodoRow {
key: "{i}",
index: i,
todos: todos.clone(),
}
}
}
div { class: "footer",
TodoForm {
todos: todos.clone(),
}
}
}
}
}
/// Single Todo row, includes buttons for marking as complete and deletion.
#[component]
pub fn TodoRow(index: usize, todos: Signal<Vec<Todo>>) -> Element {
// current todo
let todo = todos()[index].clone();
rsx! {
div {
class: "todo",
class: if todo.completed { "completed" },
{ todo.text }
div {
button {
class: "complete",
onclick: move |_| {
let mut cur = todos().clone();
cur[index].complete();
todos.set(cur)
},
},
button {
class: "remove",
onclick: move |_| {
let mut cur = todos().clone();
cur.remove(index);
todos.set(cur);
},
}
}
}
}
}
/// Entry form to add new todo.
#[component]
pub fn TodoForm(todos: Signal<Vec<Todo>>) -> Element {
let mut value = use_signal(String::new);
rsx! {
form {
onsubmit: move |_| {
let mut cur = todos().clone();
cur.push(Todo::new(value()));
todos.set(cur);
value.set(String::new());
},
input {
r#type: "text",
class: "input",
value: "{value}",
oninput: move |event| value.set(event.value())
}
}
}
}
use dioxus::prelude::*;
use dioxus_logger::tracing::{info, Level};
use todo_dioxus::App;
fn main() {
dioxus_logger::init(Level::INFO).expect("failed to init logger");
info!("starting app");
launch(App);
}
You can see this application in action here. Note that this implementation is slightly different from the Yew and Leptos implementations, because here we pass the signal that contains the list of todo items directly down to the child components and have them change it, rather than using callbacks to update it.
Trunk
Trunk is is a build tool for Rust web frontends. It handles some of the nitty-gritty in getting a WebAssembly blog runnable in a browser. You can install it by running:
cargo install trunk --locked
If you have not done so already, you also need to enable compiling to WebAssembly. If you installed Rust using rustup, you can do this easily:
rustup target add wasm32-unknown-unknown
Some interesting points is that it has some integration with external tooling,
such as wasm-opt
to optimize and slim down WebAssembly binaries, and
Tailwind CSS for generating CSS styles.
Setup
To get started with Trunk, you need to create an index.html
file. This is used
by Trunk as a template, and it contains some metadata for Trunk that tells it what
assets you want to include in the build.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<title>Hello World</title>
<link data-trunk rel="rust" data-wasm-opt="z">
</head>
<body>
</body>
</html>
The data-wasm-opt
property here tells Trunk to call wasm-opt
over the resulting
WebAssembly output when doing a release build.
Assets
Most of the content of this does not matter. Trunk only cares about any tags
that have the data-trunk
property. In this example, we have only one entry
that Trunk processes, which is the rel=rust
one. This tells Trunk to link the
current crate into this site, and run wasm-opt
on it to optimize the
WebAssembly.
You can include some CSS in the output of your site like this:
<link data-trunk rel="rust" data-wasm-opt="z">
If you want to use Tailwind CSS, you can use this to tell Trunk to run it and include the generated CSS file in your site:
<link data-trunk rel="tailwind-css" href="src/tailwind.css">
See the Trunk Assets documentation page for a full list of the types of assets that Trunk supports including in your application. It can run the SASS preprocessor, copy static assets such as images, inline content, copy files or directory,
Configuration
Trunk also has an additional configuration file that you can use to configure
how it works, Trunk.toml
. In this file, you can configure some hooks, which
are run before, during or after the build for custom steps, set up proxying for
the Trunk development server, or change where and how your site is built.
Request Forwarding
A common pattern for developing is to use trunk serve
to build and serve your
frontend, and to have it talk to your backend via API requests. To make it easier
to route the API requests to your backend, you can tell Trunk to forward proxy
requests matching a specific route to another service.
[[proxy]]
rewrite = "/api/v1/"
backend = "http://localhost:9000/"
Example: Trunk and Tailwind CSS
Example: Proxying API requests to backend
Reading
Are We Web Yet: Web Frameworks on Are We Web Yet
List of frontend web frameworks for Rust along with some statistics indicating popularity. Good for discovery of new and rising frameworks or to explore all the different ideas.
Rust Web Framework Comparison by Markus Kohlhase
Overview of different Rust frontend and backend frameworks. Unfortunately, it marks some frameworks that are still heavily used as outdated, so take that with a grain of salt.
Full-stack Rust: A complete tutorial with examples by Mario Zupan
Tutorial showing how to build a full-stack Rust web application using Yew, Tokio, Postgres, and Warp. Good tutorial to see how everything fits together, unfortunately it is a bit older and uses an outdated version of Yew that is pre-functional components. But it is still a good article to get a feeling for how a full-stack Rust application fits together.
Book that explains how to use Rust to target WebAssembly. Has some good low-level information, such as how to debug and profile WebAssembly applications, keeping code size small, interoperation with JavaScript.
A Rust web server / frontend setup like it’s 2022 (with axum and yew) by Robert Krahn
Shows how to setup a full-stack Rust web application with Yew and Axum from scratch.
Using Dioxus with Rust to build performant single-page apps by Eze Sunday
Eze shows how to use Dioxus to implement a todo application. Uses an older version of Dioxus, the interface has since changed.
User Interface
While most development these days targets web or mobile, there are situations where a traditional local GUI applications is needed. This section explains some approaches that are popular in Rust.
In general, most Rust development targets places that the end user does not directly interact with: backend applications, servers, firmware. But there are cases where it makes sense to slap together a quick GUI for something, for prototyping or to be able to use the ecosystem of libraries that Rust offers.
Tauri
Tauri is a project that achieves something similar to Electron: it embeds a web view into an application, and allows you to use web technology to write your user interface. This can be combined with a Rust frontend application, or it can be a traditional JavaScript application. In addition, Tauri offers some ways to expose an API to the application.
Tauri is very lightweight and is a good choice for anything from quick prototyping to releasing production applications that work cross-platform.
- example: tauri with yew rs
GTK-rs
GTK is a library that spawned out of the GIMP image editor, and has since become the standard UI framework for the GNOME desktop environment, which is used by many Linux distributions. GTK works on most platforms and is conceptually quite simple.
The GTK-rs project aims to create wrappers around it to expose it’s functionality natively to Rust, making it possible to write portable GUI applications. They have succeeded in making it somewhat idiomatic, working around the quirks of GTK with decent documentation and procedural macros.
- example: gtk rs calculator
egui
Reading
Game Development
Game Development often requires one to write code that performs relatively well, because even small latencies are noticeable to end-users. Game engines have to be able to track and update a relatively complex world, run physics simulations, run game logic, and render the world in 2D or 3D.
Are We Game Yet tracks the progress of the Rust ecosystem around game development. But as of writing, there are two game engines that have received some amonut of popularity.
Bevy
TODO
Fyrox
TODO
Embedded
Embedded development is one of the areas where Rust really shines. The ability to use zero-cost abstractions to write idiomatic code, that still compiles down to tiny executables that run on underpowered microcontrollers makes for a pleasant development experience. The ecosystem’s ability to abstract hardware makes it possible to easily retarget firmware for different microcontrollers, something which is usually not as easy when writing in C.
Embassy
Embassy is one of those projects that makes writing embedded code feel like magic. It is a framework for building firmware for a variety of mostly ARM-based microcontrollers.
What makes embassy special is that it supports Async. The async programming model maps very well to embedded systems: often times, there are many simultaneous pieces of code waiting for various events to happen, for example button presses, timers firing, or data coming in from various ports.
If you were to write firmware manually, you would have the choice of manually programming timers, writing interrupt handlers and building a giant, complicated mess, or you would have the choice of using a real-time operating system which comes with it’s own headaches.
Embassy lets you write readable and portable code and avoid all of the details on how to program the hardware in a way to do what you want. For example, a loop that toggles an LED connected to a pin every 150 milliseconds looks like this:
#![allow(unused)] fn main() { #[embassy_executor::task] async fn blink(pin: AnyPin) { let mut led = Output::new(pin, Level::Low, OutputDrive::Standard); loop { // Timekeeping is globally available, no need to mess with hardware timers. led.set_high(); Timer::after_millis(150).await; led.set_low(); Timer::after_millis(150).await; } } }
You don’t have to be a seasoned firmware developer to understand how this works, it reads like regular, blocking code. But behind the scenes, the executor programs a timer that the microcontroller has, and registers an interrupt handle that when it fires will resume the future.
Embedded HAL
Embedded HAL is the Rust project’s attempt at building useful abstractions over several microcontrollers, such that you can write code (drivers, firmware) that are generic over the underlying hardware.
If Embassy is Tokio, then Embedded HAL is the standard library. It is simple, it works well. It does not support async, so if you want the microcontroller to do multiple things at the same time, you have to handle it yourself. But at the same time, it supports a wider variety of targets.
It is even possible to mix Embedded HAL and Embassy to some extent.
RTIC: Real-Time Interrupt-driven Concurrency
Reading
Deploying Rust in Existing Firmware Codebases by Ivan Lozano and Dominik Maier
Async Rust vs RTOS Showdown by Dion Dokter
*Dion compares a simple firmware for an STM32F446 ARMv7 microcontroller
Logging
Logging is the process of recording significant events, actions, or errors within a software system. Typically, it involves recording them in a textual format as log messages, with the ability to designate each at different levels (such as error, warning, info, debug). This can be used to observe a system (such as flagging error logs) or to debug issues (such as deducing why a system is failing from debug or info logs).
There are some additional ways that logging can be implemented:
- Structured logging involves adding metadata to log messages, often in the form of key-value pairs. This data can be used to filter log messages, for example by user or by resource.
- Tracing means generating log events to be able to trace the propagation of asynchronous tasks. For example, it might mean that the log library issues log events whenever an asynchronous task (request handler) is launched, and when it is completed, or attach metadata to a log event to be able to trace its propagation through several services.
The Rust ecosystem has centred around three crates which are used for logging. These vary in terms of their intended use-case, and to some extend can even be mixed through interop libraries. In the next sections, we will discuss each of them and finally show some ways to mix-and-match them.
For your Rust project, it makes sense to think about what you want our of your logging
system and choose the right kind of logging infrastructure. Part of the consideration
should also be what other libraries or frameworks you are using, because many of them
come with logging support built-in that you can enable. For example, many of the
asynchronous HTTP libraries have built-in support for the tracing
crate.
Log
The log
crate is the most popular logging infrastructure.
Tracing
The tracing
crate implements scoped structured logging. It is maintained by
the Tokio developers and is commonly used in async projects, as it excels at
tracing asynchronous functions.
Slog
slog is a logging crate for Rust with the tagline structured, contextual, extensible, composable logging for Rust.
Interopability
Crate | Description |
---|---|
tracing-slog | slog to tracing |
tracing-log | log to tracing |
slog-stdlog | slog to log , or log to slog |
Reading
Structured logging By Rust telemetry exercises
What is the Difference Between Tracing and Logging? by Amanda Viescinski
Error handling
Error handling is essential to writing robust software. Rust has chosen a model for error handling that emphasizes correctness.
Many programming languages use exceptions to communicate errors. In some way, exceptions are a kind of hidden return value: a function can either return the value it declares it will return, or it can throw an exception.
Rust deliberately chose not to do this, and rather uses return types. This
ensures that it is always clearly communicated which failure modes a function
has, and failure handling does not use a different channel. It also forces
programmers to handle errors, at least to some degree: a fallible function
returns a Result<T, E>
, and you have to either handle the error (with a
match
statement), or propagate it up with a ?
.
Part of the reason that doing this is ergonomic in Rust is because Rust has great syntactical support for pattern matching. This is not the case for many other languages, which is partially why exceptions were created and remain in use.
Communicating Failures in Rust
Rust has three principal methods of communicating failures. In the order of utility, this is what they are:
- Missing data: Rust has the
Option<T>
type, which can communicate if something is missing. Generally, this is not an error. For example, when you look up a value in a map, it will return eitherNone
orSome(T)
. - Recoverable errors: Rust has the
Result<T, E>
type, which can either contain your data asOk(T)
, or contain an error asErr(E)
. - Unrecorverable errors: Panics are the Rust way to express an error that cannot be recovered from. This is perhaps the closest thing Rust has to exceptions. These are generated when invariants are invalid, or when memory cannot be allocated. When they are encountered, a backtrace is printed and your program is aborted, although there are some ways to catch them if need be.
Rust also has ways to convert between these types of errors. For example, if a missing key in a map is to be treated as an error, you can write:
#![allow(unused)] fn main() { // get user name, or else return a user missing error let value = map.get("user").ok_or(Error::UserMissing)?; }
If an error is unrecoverable (or perhaps, you are prototyping some code and
chose not to properly handle errors yet), then you can turn a Err(T)
into
a panic using unwrap()
or expect()
.
#![allow(unused)] fn main() { let file = std::fs::read_to_string("file.txt").unwrap(); }
Panics in Rust
We can’t talk about error handling in Rust without mentioning panicing. They are a way to signify failures that cannot reasonable be recovered from. Panics are not a general way to communicate errors, they are a method of last resort.
There are different ways to trigger panics in Rust. Commonly, using panics is used when writing prototype code, because you want to focus on the code and defer implementing error handling when the code works.
For example, when you write some code which traverses a data structure, you
might defer implementing the functionality for all edge cases. You can do this
by using the todo!()
macro, which will trigger a panic if called.
#![allow(unused)] fn main() { fn test_value(value: &Value) -> bool { match value { Value::String(string) => string.len() > 0, Value::Number(number) => number > 0, Value::Map(map) => todo!(), Value::Array(array) => todo!(), } } }
Using catch_unwind(), you can catch panics. This might be useful if you use libraries that might panic.
#![allow(unused)] fn main() { std::panic::catch_unwind(|| { panic!("oops!"); }); }
The Error
trait
Libraries for custom error types
Rust comes with some libraries, which can help you integrate into the Rust error system. On a high level, these libraries fall into one of two categories:
- Custom error types: these libraries allow you to define custom error types,
by implementing the
Error
trait and any neccessary other traits. A common pattern is to define an error type, which is an enumeration of all possible errors your application (or this particular function) may produce. These libraries often also help you by implementing aFrom<T>
implementation for errors that your error type wraps. - Dealing with arbitrary errors: In some cases, you want to be able to handle
arbitrary errors. If you are writing a crate which is to be used by others,
this is generally a bad idea, because you want to expose the raw errors to
consumers of your library. But if you are writing an application, and all you
want to do is to render the error at some point, it is usually beneficial to
use some library which has the equivalent of
Box<dyn Error>
and lets you not worry about defining custom error types. Often times, these libraries also contain functionality for pretty-printing error chains and stack traces.
In general, if you write a crate that is to be used as a library by other crates, you should be using a library which allows you to define custom error types. You want the users of your crate to be able to handle the different failure modes, and if the failure modes change (your error enums change), you want to force them to adjust their code. This makes the failure modes explicity.
If you write an application (such as a command-line application, a web application, or any other code where you are not exposing the errors in any kind of API), then using the latter kind of error-handling library is appropriate. In this case, all you care about is reporting errors and metadata (where they occured) to an end-user.
Thiserror
The thiserror crate is a popular
crate for defining custom errors. It helps you to implement the Error
trait
for your custom error types. For example:
#![allow(unused)] fn main() { #[derive(thiserror::Error, Debug)] pub enum MyError { #[error(transparent)] Io(#[from] std::io::Error), #[error("user {0:} not found")] UserNotFound(String), } }
Anyhow
The anyhow crate gives you the
ability to work with dynamic and ad-hoc errors. It exports the anyhow::Error
type, which can capture any Rust error.
use anyhow::Error; fn main() -> Result<(), Error> { let data = std::fs::read_to_string("file.txt")?; Ok(()) }
Eyre
Miette
Reading
The definitive guide to Rust error handling
Error Handling in The Rust Programming Language
Error handling in Rust: a comprehensive tutorial by Eze Sunday
Rust Error Handling: thiserror, anyhow, and When to Use Each by Momori Nakano
Error Handling in Rust: A Deep Dive by Luca Palmieri
Error Handling in a Correctness-Critical Rust Project by Tyler Neely
Organization
In Rust, code organization is facilitated through a range of structures: files, modules, crates, and workspaces. This chapter aims to provide guidance on how to best utilize these elements to structure your Rust projects effectively. The emphasis will be on achieving two key objectives: enhancing development speed and promoting loose coupling for better code maintainability.
Before we dive into this chapter, we should define what all of these terms mean.
Name | Description |
---|---|
Module | Modules in Rust are used to hierarchically split code into logical units. Modules have a path, for example std::fs . Modules contain functions, structs, traits, impl blocks, and other modules. |
File | A single source file, typically with a .rs extension. Every file is a module, but files can also contain inline (nested) modules. |
Crate | Compilation unit in Rust. Can be a library crate or a binary crate, the latter require the presence of a main() function. They have an entrypoint, which is typically lib.rs or main.rs but can also be called something else. |
Package | Collection of crates. Every package may contain at most one library crate, and may contain multiple binary crates. |
Workspace | A collection of packages, which can share a build cache, dependencies and metadata. |
In this chapter, we will briefly cover how you can use these to structure your project.
Development Speed
Rust emphasizes a feature known as zero-cost abstractions. These are programming abstractions that are beneficial for developers, offering utility without incurring any runtime cost. This focus sets Rust apart from many other programming languages, which offer similar abstractions but with a runtime penalty. However, these zero-cost abstractions in Rust are not without their own trade-off: they often lead to longer compile times1.
This trade-off means Rust code is typically optimized for fast execution at the expense of compile speed. Yet, faster compile times hold their own importance. They are crucial in maintaining a tight iteration loop, allowing developers to quickly make code changes, compile, and test. This rapid feedback loop is essential for efficient feature development and debugging.
In this chapter, we’ll delve into various choices that can be made while setting up a Rust project to optimize compile times. We’ll explore these options and their implications, aiming to balance efficient development cycles with optimal runtime performance.
Loose Coupling
To ensure a system remains maintainable, testable, and easily adaptable, employing a strategy of loose coupling2 is often useful. Working with a large, monolithic application that’s tightly coupled can be challenging and complex, making changes difficult. The ideal scenario involves creating code composed of smaller, independent units that can be tested on an individual basis. In this chapter, we’ll explore how to achieve this level of modularity and loose coupling in Rust, laying out strategies to build systems that are both robust and flexible.
Reading
Chapter 7: Managing Growing Projects with Packages, Crates, and Modules in The Rust Programming Language
This chapter of The Rust Book shows you what facilities Rust has for structuring projects. It introduces the concepts of packages, crates and modules.
Chapter 2.5: Project Layout in The Cargo Book
This section in The Cargo Book explains the basic layout of a Rust project.
Rust at scale: packages, crates, modules by Roman Kashitsyn
Roman discusses how you can scale Rust projects, and what he has learned from participating in several large Rust projects. He gives some guidance on when to put things into modules versus into crates, and what implication this has on compile times. He also gives some advice on programming patterns, such as preferring run-time polymorphism over compile-time polymorphism. This article is a must-read for anyone dealing with a growing Rust project and it encodes a lot of wisdom that otherwise takes a long time to acquire.
Rust compile times by Matthias Endler
The Dark side of inlining and monomorphization by Nick Babcock
Delete Cargo Integration Tests by Alex Kladov
For example, procedural macros allow for eliminating a lot of repeated code, for example by automatically deriving traits on structures. However, they need to be built and executed and thus add to the compilation time.
See Loose Coupling (Wikipedia).
Packages
When you start your project, the very first thing you will likely do is create
a new package. A package is a unit in which Rust organizes code, it consists of
metadata (such as a Cargo.toml
) and crates. You can think of it it like a
Ruby gem, a Python package, or a Node module. Packages allow you to use the
Cargo build system to compile it, run tests and manage dependencies.
A crate is a compilation unit. Unlike C, C++ or Java, which compile individual files, in Rust an entire crate is always compiled in one go. This means you don’t have to worry about the ordering of includes, and it means that all definitions are always visible. It also makes it easier for the compiler to implement certain optimizations, such as inlining code.
Contents of a Package
At the very minimum, a Rust package contains metadata (in the Cargo.toml
file) and a single library or binary crate, otherwise there is nothing to
compile. Generally, you do not need to configure Cargo to tell it where the
crates are: it automatically detects them based on their standard locations.
You can, however, override this and place your source files in non-standard
locations, but this is not recommended. For example, if you have a
src/lib.rs
file in your package, Cargo recognizes this as your library crate.
Every package needs to have either a library crate or a binary crate. It may
also contain other, supporting crates, such as integration tests, benchmarks,
examples. Having first-class support for these is a big bonus, because it means
you can run cargo test
in any Rust project and Cargo will know where the tests
are and is able to run them.
Generally, the library crate of every package is where you want to keep all of
your logic. This is because this code is what all the other crates link to by
default. So, if you write an integration test, it cannot “see” what is inside
your binary crates. In many projects, the binary crate at src/main.rs
is just
a small shell that parses command-line arguments, sets up logging and calls
into the library crate to do the hard work.
Metadata
Every crate contains some metadata, in the Cargo.toml
file. This contains everything
cargo needs to know to build the crate, such as its name, and a list of all dependencies
it needs to build. It also contains metadata neccessary for publishing it on crates.io,
Rust’s crate registry, such as its version, list of authors, license, and description.
Finally, this file can also contain metadata for other tooling, some of which we will discuss
in this book. An example file might look like this:
[package]
name = "example-package"
version = "0.1.0"
version = "MIT"
description = "My awesome crate"
authors = ["Max Mustermann <max@example.io>"]
[dependencies]
serde = { version = "1.0.12", features = ["derive"] }
anyhow = "1"
Dependencies can have optional features. This ensures a faster compilation, by only compiling them when they are explicitly enabled.
Cargo has built-in support for semantic versioning, so the versions
listed here are constraints. For example, when you specify version 1.0.12
, it really means
that your crate will work with any version >=1.0.12
and <1.1.0
, because semver considers
changes in the patch level (the third number) as non-breaking changes.
This means that when you build your crate, Rust has to resolve the version
numbers. It stores those resolved version numbers in a separate file, Cargo.lock
. This
is to ensure that you get reproducible builds: if two pepople build the project,
they always use exactly the same versions of dependencies. You have to manually tell
Cargo to go look if there are newer versions of dependencies that are within the constraints,
using cargo update
. This and some issues around it will be covered in later chapters.
Here is an example of what this looks like:
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "serde"
version = "1.0.197"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2"
dependencies = [
"serde_derive",
]
[[package]]
name = "anyhow"
version = "1.0.80"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ad32ce52e4161730f7098c077cd2ed6229b5804ccf99e5366be1ab72a98b4e1"
Library and Binaries
Besides these two files, crates also contain Rust source code in various places. We will list the default locations for these here, but the locations can be configured and overridden in the metadata.
Every crate can define (at most) one library. The entrypoint for this is in src/lib.rs
.
When you use a crate as a dependency, this is what other crates can see. Even if your
project is primarily an executable and not a library, you should try to put most of the code
into this library section, because this is what is visible to example code and integration
tests. I call this library-first development.
- articles for library-first development
Besides a single library, crates can also define binaries. These must contain a
main()
function, and are compiled into executables. The default location for
binaries is src/main.rs
, and it will produce an executable with the same name as
the crate. You can create additional ones under src/bin/<name>.rs
, which will
create executables with the same name.
- graphic: executables linking against library
While Rust supports writing unit tests directly in the code, sometimes you want to
write tests from the perspective of an external user using your library (without
visibility into private functions). For this reason, you can write integration tests,
in tests/<name>.rs
. These are compiled as if they were an external crate which links
to your crate, and as such only have access to the public API.
- graphic: tests linking against library
Finally, Rust has a large focus on making it easy to write documentation. In fact,
support for generating documentation is a built-in feature. In some cases, writing code
is the best kind of documentation. For this reason, Cargo has first-class support for keeping
code examples. If you put examples into examples/<name>.rs
, they can be built and run by
cargo using cargo build --examples
and cargo run --example <name>
. There is even a
feature in Rust’s built-in support for documentation, where it will pick up and reference
examples in the code documentation automatically.
- graphic: examples linking against library
See also: Package Layout.
Creating a crate
You can use cargo new
to create an empty crate. You have the choice of creating a library
crate (using the --lib
switch) or a binary crate. Using cargo
is recommended over
creating a new crate manually, because it will usually set useful defaults.
# create a binary-only crate
cargo new example-crate
# create a library crate
cargo new --lib example-crate
This is what an example crate layout looks like, after adding some dependencies. You can see what the metadata and the source code looks like.
- src/
target/
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "anyhow"
version = "1.0.86"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da"
[[package]]
name = "example-crate"
version = "0.1.0"
dependencies = [
"anyhow",
"serde",
]
[[package]]
name = "proc-macro2"
version = "1.0.86"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7"
dependencies = [
"proc-macro2",
]
[[package]]
name = "serde"
version = "1.0.205"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e33aedb1a7135da52b7c21791455563facbbcc43d0f0f66165b42c21b3dfb150"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.205"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "692d6f5ac90220161d6774db30c662202721e64aed9058d2c394f451261420c1"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "syn"
version = "2.0.72"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc4b9b9bf2add8093d3f2c0204471e951b2285580335de42f9d2534f3ae7a8af"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "unicode-ident"
version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
[package]
name = "example-crate"
version = "0.1.0"
edition = "2021"
[dependencies]
anyhow = "1.0.86"
serde = "1.0.205"
fn main() {
println!("Hello, world!");
}
A more full-fledged example makes use of both the library and executables, has some documentation strings, tests and examples in it, along with complete crate metadata.
Cargo has some neat features besides being able to create new crates for you.
It can also manage dependencies for you. For example, if you are inside a crate
and you would like to add serde
to the list of dependencies, you can use
cargo add
to add it:
cargo add serde --features derive
This will edit your Cargo.toml
to add the dependency, without touching
anything else. Comments and formatting is preserved. The Cargo team is quite
good at looking how people use it and extending it with functionality that is
commonly requested.
Crate Features
Rust crates can declare optional dependencies. These are additive, meaning that enabling them should not break anything. The reason for this is that Rust performs feature unification: if you have multiple dependendencies in your dependency that depend on a single crate, it will only be built once with the features unified.
- dependency tree: feature unification
This is a good way to add additional, optional features to your crates while keeping
compilation times short for those who don’t use them. If you have a dependency, you
can enable them by setting the features
key:
[dependencies]
serde = { version = "1.0.182", features = ["derive"] }
For your own crates, you can declare optional features using the features
section
in the metadata. Using features, you can enable optional dependencies, and inside your
code you can disable parts (functions, structs, modules) depending on them.
[features]
default = []
cool-feature = ["serde"]
Once you have declared a feature like this, you can use it to conditionally include code in your project, using the cfg attribute.
#![allow(unused)] fn main() { #[cfg(feature = "cool-feature")] fn only_visible_when_cool_feature_enabled() { // ... } }
Doing this can have some advantages, for example it lets you keep compilation times short for developers because they can build a subset of the project for testing purposes. However, it also requires some care, because you often need to be careful to make sure features don’t conflict with each other, see Chapter 6.10: Crate Features.
Crate Size
As mentioned earlier, in Rust a crate is a compilation unit. When you make a change in one file, the entire crate needs to be rebuilt. While it makes sense initially to start a project out with one crate, as the project grows it may make sense to split it up into multiple, smaller crates. This allows for faster development cycles.
The next section discusses how this can be done, and what mechanisms Rust supports for doing so.
Reading
Chapter 3.2.1: Cargo Targets in The Cargo Book
In this section of the Cargo book, all of the possible targets that Cargo can build for a crate are defined.
Chapter 3.1: Specifying Dependencies in The Cargo Book
In this section of the book, it is explained how dependencies are specified in Cargo.
In this article, Chris argues that it is best to default to large modules, because the cost of designing useful abstractions for the interaction is high, and it is possible to split larger modules into smaller ones later when the code is more stable.
Workspace
As your projects grows, you may feel the need to split it up into multiple crates. Maybe the compilation times are becoming a problem, and having multiple smaller crates means that most of the application does not need to be rebuilt when you make a change in one file. Or maybe you want to enforce more loose coupling between the application, and split the responsibility of various parts to separate teams.
Rust is designed to cope well with projects that contain a lot of crates. It even has a feature catered to exactly this use-case: the workspace. When you use a workspace, you tell Cargo that group of crates are related and should share the same build cache, and optionally some metadata.
- statistics on how many Rust projects use workspaces
Creating a Workspace
You can crate a Cargo workspace by adding a [workspace]
section in you
Cargo.toml
:
[workspace]
resolver = "2"
members = ["crates/crate-a", "crates/crate-b"]
The main reasons why you would want to use workspaces rather than simply putting several crates into a repository is twofold:
- When you use a
workspace
, then your entire project uses a singletarget
folder, meaning that every dependency is built exactly once. This speeds up the build time. - When you run operations, such as tests, then you can tell
cargo
to run them for all crates in the workspace.
Workspaces have some other interesting properties. When you run cargo test
in
a workspace, it defaults to running all tests for all crates. Some of the Rust
tooling has --workspace
or --all
flags which tell the tools to act on the
entire workspace instead of only the crate you are currently located in.
Examples
Here is an example of what a cargo workspace project looks like. You can see
how the root Cargo.toml
only contains the workspace definition, and there
are several crates contained in it.
- crate-a/
- src/
- src/
- crate-b/
- src/
- src/
- crate-c/
- src/
- src/
/target
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "crate-a"
version = "0.1.0"
[[package]]
name = "crate-b"
version = "0.1.0"
[[package]]
name = "crate-c"
version = "0.1.0"
[workspace]
resolver = "2"
members = ["crate-a", "crate-b", "crate-c"]
[package]
name = "crate-a"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
fn main() {
println!("Hello, world!");
}
[package]
name = "crate-b"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
fn main() {
println!("Hello, world!");
}
[package]
name = "crate-c"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
fn main() {
println!("Hello, world!");
}
Dependencies
When you work in a large workspace, often times you have a set of dependencies that all of the crates in the workspace use. In that case, typically you want to ensure that they all use the same version of the dependency.
For that use-case, Cargo Workspaces allows you to declare dependencies on a workspace level, and reference them in the daughter crates. This makes it easier to keep versions of dependencies in sync when they are used by a lot of crates.
To use this feature, you can simply set the workspaces.dependencies
in the same way
that you would set dependencies
in a regular crate.
[workspace.dependencies]
anyhow = "1"
In the child crates, you can then reference them like this:
[dependencies]
anyhow = { workspace = true }
It’s still possible to override it, for example to turn on additional features.
[dependencies]
anyhow = { workspace = true, features = ["abc"] }
Metadata
Another commonly used feature of Cargo Workspaces is the ability to set shared
metadata. For example, you can use it to set a license for all crates, or keep the
version of the crates in sync. To do this, you set metadata in the workspace.package
in the workspace config, like this:
[workspace.package]
license = "MIT"
authors = ["John Doe <john.doe@example.com"]
To use this, you have to then reference it in the child crates.
[package]
name = "crate-a"
license.workspace = true
authors.workspace = true
Doing this makes sense if you want all child crates to share some amount of metadata, as is often the case with licenses or authors.
When to split crates
When is the right time to split crates? This is a question that is not so easy to answer. Splitting crates has a cost: it means you need to define the interface well. But if you do it well, it also has advantages. Maybe the code can be reused for future projects, because it is generic enough. Splitting crates out prematurely is probably not a good idea, but doing it too late risks that your code will depend on and use private interfaces that you don’t want it to use.
Reading
Chapter 7: Managing Growing Projects with Packages, Crates and Modules in The Rust Programming Language
This chapter in the Rust book explains the different organizational structures that Rust has, and how they can be used. It mentions the use of workspaces for managing related crates in a project.
Chapter 14.3: Cargo Workspaces in The Rust Programming Language
This section in the Rust book introduces the concept of the workspace, and gives some examples for how it can be used in a project.
Chapter 3.3: Workspaces in The Cargo Book
This section in the Cargo book explains the workspace feature, and all of the configuration options that are available for it in the Crate manifest.
An Opinionated Guide To Structuring Rust Projects by Ryan James Spencer
TODO
Prefer small crates in Rust Design Patterns
This article argues that Rust makes it easy to add dependencies, so there is no downside to having more of them. Additionally, smaller crates are easier to understand and lead to more modular code, therefore small crate sizes should be encouraged.
Brainstorm request: How to get benefits of small and large crates
In this discussion, the upsides and downsides of having small crates is discussed.
rfc: collapse Tokio sub crates into single tokio crate
The Tokio project did the reverse: they used to be composed of many small crates, and merged them all into one crate. This discussion contains important context for why this decision was made, and has some arguments against having many small crates.
Why is my Rust build so slow: splitting into more crates
https://corrode.dev/blog/tips-for-faster-rust-compile-times/#split-big-crates-into-smaller-ones-using-workspaces
TODO
Repository
In software development, one of the longstanding questions is: should you use a monorepo, or should you split components into separate repositories?
Unless you work in a large company with resources to build custom solutions, monorepos will
likely run into scaling issues. Keeping an entire company in sync on a single repository with
standard technology like git
can run into issues.
At the same time, dealing with multiple repositories is also a headache. How do you easily make a change in a library and test that it doesn’t break any of the repositories that depend on it?
Advantages and disadvantages of monorepos
Pro:
- easy to test changes to libraries upstream
- easy to refactor code
- no need to do proper versioning
Cons:
- all consumers of a library have to be refactored at the same time if interface changes, or backwards compatibility needs to be ensured (slows down development)
- complexity of rebasing as repository grows
Start out with a single repository
For your new Rust project, it probably makes sense to start out with a single repository, set up a single crate (or a Cargo Workspace) and start from there. Only when code is stabilized, you can start to factor out atomic pieces into their own crates. When functionality is useful enough, it can be put into it’s own repository, and versioned properly.
- bubble graph with big bubbge containing crates
Split out libraries only if they are stable
- git dependencies
- private registry (see Releasing Crates).
Examples
- tokio project
Reading
Build system
You are implementing a backend service in Rust which offers an API. At some point you realize that you need a frontend to configure it properly. Helpfully, one of your colleagues implements a frontend for you in React. You notice that it would be convenient if the backend would serve the files of the frontend, and you are looking for some way to tell Cargo to build and embed the frontend files into the backend’s binary. How can you achieve this?
With Cargo, Rust has some fanstastic tooling for building, cross-compiling and testing Rust software. Cargo supports installing plugins that extend it’s functionality, a lot of which are discussed in this book. If your Rust project has a relatively simple setup, where it consists only of Rust crates, then Cargo is the ideal tool to get it to build:
--- config: theme: neutral --- graph LR lib_a_lib-->lib_a lib_b_lib-->lib_b lib_a-->bin lib_b-->bin bin_main-->bin
Things start to get tricky when you involve other languages (such as mixing Rust with C, C++, TypeScript) or when the build involves building code for different targets (for example, that some crates need to be built as WebAssembly and the resulting code is needed by other build.
Example architectures
For example, some projects may need to interface with some legacy C/C++ code. In this case, building might involve compiling the library first:
--- config: theme: neutral --- graph LR clib[C Library] wrapper[Wrapper crate] crate[Rust crate] dep1[Dependency crate] clib-->wrapper wrapper-->crate dep1-->crate
Another common pattern when building full-stack web applications with Rust is that you might write the frontend in Rust and need to compile it to WebAssembly, and the backend in Rust. You want the Rust backend to serve the frontend, so it requires the WebAssembly output as a build input:
--- config: theme: neutral --- graph LR frontend[Frontend] wasm[WebAssembly code] backend[Backend] axum[Axum] frontend-->wasm wasm-->backend axum-->backend
If you build a traditional web application with a TypeScript frontend and a Rust backend, you may need to run a TypeScript compiler for some part of your code and use the output as the input for your backend.
graph LR a-->b
Other configurations are also possible, it depends on your particular need.
Build Systems
Build systems are high-level tools to orchestrate the build process. They track tasks and dependencies, and make sure that the build steps are run in the right order and rerun when any of the inputs have changed.
Good build systems will enforce hygiene by sandboxing build steps to make sure you do not accidentally depend on inputs you have not declared. This helps to avoid the “it works on my machine” syndrome, where your code accidentally depends on some system state that is present on your machine but not on other’s.
However, build systems become interesting to your Rust project when one of three things happen:
- Inside your project, you have multi-language components. For example, a frontend written in TypeScript, a backend component written in Kotlin, a C library, some Python tooling.
- Inside your project, you have cross-target dependencies. For example, you
have a project fully written in Rust, and the backend wants to embed the
frontend compiled to WebAssembly using a tool such as
trunk
for ease of deployment. - You depend on some external dependency which is not written in Rust, and you
want to be sure you can use reproducibly it on all platforms. For example,
you depend on the presence of
sqlite
in a specific version.
Many build systems also offer fully reproducible builds by requiring all build inputs and tools to be pinned down by hash, which enables distributed caching which is a big quality of life improvement for developers as it leads to faster development times.
This chapter discusses some build systems that play nice with Rust. Note that build systems are not necessarily mutually-exclusive: most of the time, even when using a build system that is not Cargo, you will still have the necessary Cargo manifests in the project that allows standard Cargo tooling to work.
Reading
The convergence of compilers, build systems and package managers by Edward Z. Yang
Edward explains how build systems, compilers and package managers seem to
converge. This is certainly the case for Rust, which has Cargo which acts as a
build system (cargo build
) and package manager (cargo install
). He explains
that this is not an isolated phenomenon, but inherent. It appears that we are
heading towards a more integrated approach.
Build Systems and Build Philosophy in Software Engineering at Google
This chapter in the book discusses why build systems are vital in scaling software development, because they ensure that software can be built correctly on a number of different systems and architectures.
Multi-language build system options
TODO
Paper which explain build systems, and how they work. It takes popular build systems apart and explains their properties. A useful paper for anyone trying to achieve a deep understanding of what build systems are and how they work.
Merkle trees and build systems
Amazon’s Build System by Carl Meyers
Carl explains the build system that Amazon uses.
Build System Schism: The Curse of Meta Build Systems by Gavin D. Howard
Gavin gives a summary of the evolution of build systems, into the modern ones he calles meta build systems. He summarizes which features they have, and argues that Turing-completeness is a property that is required for a good build system.
Cargo
Cargo is the default build system for Rust projects. It makes it easy to create
build and test Rust code, manages dependencies from crates.io, and allows
you to publish your own crates there. It uses semantic versioning to resolve
dependency version from constraints you define and uses a lockfile to ensure
you are always building with the same dependency versions. Since rustc
is LLVM-based,
it is also easy to cross-compile your Rust code for other targets, see the
list of supported Rust targets.
Cargo supports installing other tools that integrate into it and extend it
with new subcommands. This guide mentions several of such tools, such as cargo-hack
or cargo-llvm-cov
.
One nice property of having Cargo as the default build system for all Rust
projects is that you can typically clone any repository that contains a Rust
crate and run cargo build
to build it, or cargo test
to run tests.
This is quite different to languages such as C, C++ or JavaScript that have
a more fragmented build ecosystem.
What Cargo Lacks
If you only use built-in commands and only build Rust code, then Cargo is a great build system for Rust projects. However, there are some features it does not have.
If you rely on plugins to build your project, such as trunk
for building
WebAssembly-powered web frontend applications powered by Rust, Cargo will not
install it automatically. Rather, developers need to install it manually by
running cargo install trunk
.
If you rely on native dependencies, such as OpenSSL or other libraries, Cargo
will not handle installing them on your behalf. There are some workarounds for
this, for example some crates like rusqlite
ship the C code and have a
feature flag where Cargo will build the required library from source if you
request it.
If you need to execute build steps, such as compiling C code or your have some parts of your project that use for example JavaScript, there is only rudimentary support for doing so with Cargo.
In short, Cargo is great at all things Rust, but it does not help you much if you mix other languages into your project. And that is by design: Cargo’s goal is not to reinvent the world. It does one thing, and it does it well, which is build Rust code.
The next sections discuss some approaches that you can use to use Cargo in situations that it is not designed for, but that yet seem to work.
Complex build steps
Cargo is great at building Rust code, but has few features for building projects that involve other languages. This makes sense, because such functionality is not needed by it.
Cargo does come with some support for running arbitrary steps at build time, through the use of build scripts. These are little Rust programs that you can write that are executed at build time and let you do anything you like, including building other code. It also supports linking with C/C++ libraries by having these build scripts emit some data that Cargo parses.
The other sections of this chapter are only relevant to you if your project
consists of a mixture of languages, and building it is sufficiently complex
that it cannot trivially be expressed or implemented in a build.rs
file (such
as: it needs external dependencies).
build.rs
to define custom build actions
If you have a few more complex steps that you need to do when building your code, you can always use a build script.
Build scripts in Cargo are little Rust programs defined in a build.rs
in the crate
root which are compiled and run before your crate is compiled. They are able to do
some build steps (such as compile an external, vendored C library) and they can
emit some information to Cargo, for example to tell it to link against a specific
library.
Build scripts receive a number of environment variables as inputs, and output some metadata that controls Cargo’s behaviour.
A simple build script might look like this:
fn main() { }
For common tasks such as building C code, generating bindings for native libraries there are crates that allow you to write build scripts easily, these are presented in the next sections.
Compiling C/C++ Code
If you have some C or C++ code that you want built with your crate, you can use
the cc
crate to do so. It is a helper library that you can call inside
your build script to run the native C/C++ compiler to compile some code, link
it into a static archive and tell Cargo to link it when building your crate. It
also has support for compiling CUDA code.
A basic use of this crate looks by adding something like this to the main
function of your build script:
#![allow(unused)] fn main() { cc::Build::new() .file("foo.c") .file("bar.c") .compile("foo"); }
The crate will take care of the rest of finding a suitable compiler and
communicating to Cargo that you wish to link the foo
library.
Here is an example of how this looks like. In this crate, a build script is used to compile and link some C code, and the unsafe C API is wrapped and exposed as a native Rust function.
- src/
/target
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "anstream"
version = "0.6.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526"
dependencies = [
"anstyle",
"anstyle-parse",
"anstyle-query",
"anstyle-wincon",
"colorchoice",
"is_terminal_polyfill",
"utf8parse",
]
[[package]]
name = "anstyle"
version = "1.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1"
[[package]]
name = "anstyle-parse"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb"
dependencies = [
"utf8parse",
]
[[package]]
name = "anstyle-query"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a"
dependencies = [
"windows-sys",
]
[[package]]
name = "anstyle-wincon"
version = "3.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8"
dependencies = [
"anstyle",
"windows-sys",
]
[[package]]
name = "cc"
version = "1.1.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57b6a275aa2903740dc87da01c62040406b8812552e97129a63ea8850a17c6e6"
dependencies = [
"shlex",
]
[[package]]
name = "clap"
version = "4.5.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed6719fffa43d0d87e5fd8caeab59be1554fb028cd30edc88fc4369b17971019"
dependencies = [
"clap_builder",
"clap_derive",
]
[[package]]
name = "clap_builder"
version = "4.5.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "216aec2b177652e3846684cbfe25c9964d18ec45234f0f5da5157b207ed1aab6"
dependencies = [
"anstream",
"anstyle",
"clap_lex",
"strsim",
]
[[package]]
name = "clap_derive"
version = "4.5.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "501d359d5f3dcaf6ecdeee48833ae73ec6e42723a1e52419c79abf9507eec0a0"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "clap_lex"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97"
[[package]]
name = "colorchoice"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0"
[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "is_terminal_polyfill"
version = "1.70.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
[[package]]
name = "levenshtein"
version = "0.1.0"
dependencies = [
"cc",
"clap",
"libc",
]
[[package]]
name = "libc"
version = "0.2.158"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439"
[[package]]
name = "proc-macro2"
version = "1.0.86"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af"
dependencies = [
"proc-macro2",
]
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "strsim"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "syn"
version = "2.0.77"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "unicode-ident"
version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
[[package]]
name = "utf8parse"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "windows-sys"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-targets"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_gnullvm",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[package]
name = "levenshtein"
version = "0.1.0"
edition = "2021"
[dependencies]
# used to parse command-line arguments
clap = { version = "4.5.16", features = ["derive"] }
# used for FFI interface (defines size_t)
libc = "0.2.158"
[build-dependencies]
# used to build the levenshtein.c library
cc = "1.1.15"
# Levenshtein
Wrapper around [levenshtein.c][], a C library to compute the Levenshtein
distance between two strings. Also contains a command-line tool to compute the
distance for two strings passed as command-line parameters.
## Examples
You can build the library using Cargo. Ensure that you have a C compiler installed,
as this crate relies on the [cc][] crate to build the library.
```
$ cargo run -- "hello" "hello"
0
$ cargo run -- "kitten" "sitting"
3
```
[levenshtein.c]: https://github.com/wooorm/levenshtein.c
[cc]: https://docs.rs/cc/latest/cc/
/// Compiles the `levenshtein.c` library using the C compiler and instructs Cargo to link the
/// resulting archive.
fn main() {
cc::Build::new().file("src/levenshtein.c").compile("levenshtein");
}
// `levenshtein.c` - levenshtein
// MIT licensed.
// Copyright (c) 2015 Titus Wormer <tituswormer@gmail.com>
#include <string.h>
#include <stdlib.h>
#include <stdint.h>
#include "levenshtein.h"
// Returns a size_t, depicting the difference between `a` and `b`.
// See <https://en.wikipedia.org/wiki/Levenshtein_distance> for more information.
size_t
levenshtein_n(const char *a, const size_t length, const char *b, const size_t bLength) {
// Shortcut optimizations / degenerate cases.
if (a == b) {
return 0;
}
if (length == 0) {
return bLength;
}
if (bLength == 0) {
return length;
}
size_t *cache = calloc(length, sizeof(size_t));
size_t index = 0;
size_t bIndex = 0;
size_t distance;
size_t bDistance;
size_t result;
char code;
// initialize the vector.
while (index < length) {
cache[index] = index + 1;
index++;
}
// Loop.
while (bIndex < bLength) {
code = b[bIndex];
result = distance = bIndex++;
index = SIZE_MAX;
while (++index < length) {
bDistance = code == a[index] ? distance : distance + 1;
distance = cache[index];
cache[index] = result = distance > result
? bDistance > result
? result + 1
: bDistance
: bDistance > distance
? distance + 1
: bDistance;
}
}
free(cache);
return result;
}
size_t
levenshtein(const char *a, const char *b) {
const size_t length = strlen(a);
const size_t bLength = strlen(b);
return levenshtein_n(a, length, b, bLength);
}
#ifndef LEVENSHTEIN_H
#define LEVENSHTEIN_H
#include <stddef.h>
// `levenshtein.h` - levenshtein
// MIT licensed.
// Copyright (c) 2015 Titus Wormer <tituswormer@gmail.com>
// Returns a size_t, depicting the difference between `a` and `b`.
// See <https://en.wikipedia.org/wiki/Levenshtein_distance> for more information.
#ifdef __cplusplus
extern "C" {
#endif
size_t
levenshtein(const char *a, const char *b);
size_t
levenshtein_n (const char *a, const size_t length, const char *b, const size_t bLength);
#ifdef __cplusplus
}
#endif
#endif // LEVENSHTEIN_H
//! The Levenshtein distance measures how similar two words are, by how many substitutions are
//! needed to get from one word to the other. This Crate wraps a C library that implements this
//! algorithm in a safe Rust interface.
/// Raw access to the unsafe C API of the levenshtein library.
pub mod raw {
use libc::size_t;
use std::ffi::c_char;
extern "C" {
/// Raw binding to the C `levenshtein_n` function.
///
/// `a` and `b` must be valid pointers to character arrays, and `a_length` and `b_length`
/// their lengths respectively.
pub fn levenshtein_n(
a: *const c_char,
a_length: size_t,
b: *const c_char,
b_length: size_t,
) -> size_t;
}
}
/// Computes the Levenshtein distance between the strings `a` and `b`.
///
/// # Examples
///
/// The Levenshtein distance between two equal words is zero.
///
/// ```
/// # use levenshtein::levenshtein;
/// assert_eq!(levenshtein("hello", "hello"), 0);
/// ```
///
/// The Levenshtein distance between two words that have a single letter substituted is one.
///
/// ```
/// # use levenshtein::levenshtein;
/// assert_eq!(levenshtein("hello", "hallo"), 1);
/// ```
pub fn levenshtein(a: &str, b: &str) -> u64 {
use std::ffi::c_char;
let result = unsafe {
raw::levenshtein_n(
a.as_ptr() as *const c_char,
a.len(),
b.as_ptr() as *const c_char,
b.len(),
)
};
result as u64
}
#[test]
fn test_levenshtein() {
macro_rules! assert_distance {
($a:expr, $b:expr, $d:expr) => {
assert_eq!(levenshtein($a, $b), $d);
};
}
assert_distance!("", "a", 1);
assert_distance!("a", "", 1);
assert_distance!("", "", 0);
assert_distance!("levenshtein", "levenshtein", 0);
assert_distance!("sitting", "kitten", 3);
assert_distance!("gumbo", "gambol", 2);
assert_distance!("saturday", "sunday", 3);
// It should match case sensitive.
assert_distance!("DwAyNE", "DUANE", 2);
assert_distance!("dwayne", "DuAnE", 5);
// It not care about parameter ordering.
assert_distance!("aarrgh", "aargh", 1);
assert_distance!("aargh", "aarrgh", 1);
// Some tests form `hiddentao/fast-levenshtein`.
assert_distance!("a", "b", 1);
assert_distance!("ab", "ac", 1);
assert_distance!("ac", "bc", 1);
assert_distance!("abc", "axc", 1);
assert_distance!("xabxcdxxefxgx", "1ab2cd34ef5g6", 6);
assert_distance!("xabxcdxxefxgx", "abcdefg", 6);
assert_distance!("javawasneat", "scalaisgreat", 7);
assert_distance!("example", "samples", 3);
assert_distance!("sturgeon", "urgently", 6);
assert_distance!("levenshtein", "frankenstein", 6);
assert_distance!("distance", "difference", 5);
}
use clap::Parser;
#[derive(Parser)]
struct Options {
a: String,
b: String,
}
fn main() {
let options = Options::parse();
let distance = levenshtein::levenshtein(&options.a, &options.b);
println!("{distance}");
}
Note that in order to make the C function “visible” from Rust, you need to
declare it in an extern "C"
block. It needs a function definition that
matches the one in the C header. Writing this by hand is error-prone, and can
lead to unsafety issues.
This example also shows how this unsafe C function is wrapped into a safe Rust function. Doing so involves dealing with raw pointers, and it is easy to get something wrong. It is important to write good Unit Tests, and often it can help to use Dynamic Analysis to make sure you did it correctly.
Compiling CMake projects
- use the
cmake
crate
Generating Bindings for C/C++ Libraries
- using rust-bindgen
Caching builds
You may find that Rust takes a long time to compile, which is certainly the
case. You can partially mitigate this by using a build cache, which is a
service that will cache the compiled artifacts and allow you to compile
considerably faster. One tool that lets you do this is sccache
, which is
discussed in a future chapter.
Toolchain Pinning
If you depend on specific Cargo or Rust features, you may find that you can run into issues if people with older toolchain versions try to build your code. For this reason, it is sometimes useful to pin a specific version of the Rust toolchain in a project, to make sure everyone is using the same versions.
There are two mechanisms that you can use here, depending on where you want this pinning to work:
- You can use a
rust-toolchain.toml
file to pin the Rust version for the current project. This file is picked up byrustup
, which most people use to manage and update their Rust toolchain. When running any Cargo command in a project that has such a file,rustup
will ensure that the specified toolchain version is installed on the system and will only use that. - Conversely, if you are building a library and you want users of your library (as in, people that depend on your library as a dependency, but do not directly work on it) to use a specified minimum Rust toolchain version, you can set the MSRV in the Cargo metadata. This means that users of your library that are on older Rust versions will get an error or a warning when they try to add your library as a dependency.
Pinning the toolchain version for projects
The way you can solve this is by putting a rust-toolchain.toml
file into the
repository. This will instruct rustup
to fetch the exact toolchain mentioned
in this file whenever you run any operations in the project.
Typically, such a file simply looks like this:
[toolchain]
channel = "1.75"
components = ["rustfmt", "clippy"]
Keep in mind that this file is only picked up by people who use rustup
to manage
their Rust toolchains.
Putting rust-toolchain.toml
file in your project lets you specify exactly which
version of the Rust compiler is used by the people working on the project.
Specifying the minimum toolchain version for library crates
However, this rust-toolchain.toml
file is only consulted when you are building
the current project. What if your crate is used as a dependency by other crates?
How can you communicate that it needs a certain version of the Rust compiler?
For this, Cargo has the option of specifying a MSRV for each crate. This is the minimum version of the Rust compiler that the crate will build with.
In a later chapter, we will show you how you can determine the MSRV programmatically and how you can test it to make sure that the version you put there actually works.
If you build library crates, you should specify the minimum version of the Rust toolchain that is needed to build your library. This helps other crate authors by telling them which version of Rust they need to use your library. You should always specify this.
Convenience Commands
Cargo has a useful selection of convenience commands built-in to it that make using it to manage Rust projects easy.
https://blog.logrocket.com/demystifying-cargo-in-rust/
Initializing Cargo project
To quickly create a Cargo project, you can use cargo new
. By default, it will
create a binary crate, but you can use the --lib
flag to create a library crate
instead.
cargo new my-crate
Building and running Code and Examples
The main thing you likely use Cargo for is to build and run Rust code.
Cargo has two commands for this, cargo build
and cargo run
.
cargo build
cargo run
If you have multiple binaries and you want to build or run a specific one,
you can specify it using the --bin
flag.
cargo build --bin my_binary
cargo run --bin my_binary
If you instead want to build or run an example, you can specify that using
the --example
flag.
cargo build --example my_example
cargo run --example my_example
Running Tests and Benchmarks
Besides building and running Rust code, you will likely also use Cargo to run unit tests and benchmarks. It has built-in commands for this, too.
cargo test
cargo bench
As explained in the Unit testing section, you can
also use the external tool cargo-nextest
to run tests faster.
Managing Dependencies
Cargo comes with built-in commands for managing dependencies. Originally, these commands were part of cargo-edit, but due to their popularity the Cargo team has decided to adopt them as first-class citizens and integrate them into Cargo.
cargo add serde
cargo remove serde
Recently, they also added support for Workspace dependencies. If you use cargo-add
to add
a dependency to a crate, which already exists in the root workspace as a dependency, it will do the
right thing and add it as a workspace dependency to your Cargo manifst.
You can also use Cargo to query the dependency tree. This lets you see a list of all dependencies,
and their child dependencies. It lets you find out if you have duplicate dependencies (with different
versions), and when that is the case, why they get pulled in. For example, if you have one dependency
that uses uuid v1.0.0
, but you depend on uuid v0.7.0
, then you will end up with two versions of
the uuid
crate that are being pulled in.
cargo tree
This command used to be a separate plugin called cargo-tree, but was incorporated into Cargo by the team due to it being useful.
Building Documentation
cargo doc
Installing Rust Tools
Besides just being a build system for Rust, Cargo also acts as a kind of package manager. Any binary Rust crates that are published on a registry can be compiled and installed using it. This is often used to install Cargo plugins or other supporting tools.
cargo install ripgrep
- mention cargo-binstall
Profiling Builds
If you want to figure out what Cargo is spending most of the time on during builds, you can use the built-in profiling support. This generates a HTML report of the timings of the build and allows you to debug slow builds. However, it only works using nightly Rust.
cargo +nightly build --timings=html
Conclusion
If your project can get away with only using it to define and run all of the steps needed to build your project, then you should prefer it over using a third-party build system. Everyone who writes Rust code uses Cargo, it is very simple to use and comes with features that cover the majority of the use-cases you might run into.
If you do have a multi-language project, or a project with complicated build steps, you might soon find that build scripts are rather limited. Dependency tracking is possible with them, but it feels hacky. They are not hermetic, and there is no built-in caching that you can use. In this case, you may find it useful to take a look at the other popular build systems and determine if they might help you achieve what you want in a way that is more robust or more maintainable.
Do keep in mind that usually, using third-party build systems can be more pain than using Cargo itself, because they need to reimplement some functionality that you get for free when using Cargo. However, sometimes there are advantages that they bring that outweigh the additional complexity.
Reading
Reference guide for Cargo. This book discusses all features that Cargo has and how they can be used.
Build Scripts in The Cargo Book
Section in the Cargo Book that talks about using build scripts. It shows some examples for how they can be used and explains what can be achieved with them.
The Missing Parts in Cargo by Weihang Lo
Weihang discusses Cargo, and what is missing from it.
Foreign Function Interface in The Rustonomicon
This chapter in The Rustonomicon explains how to interact with foreign functions, that is code written in C or C++, in Rust.
Bazel
Bazel is an open-source port of the Blaze build system used internally at Google. It is, in some ways, purpose built to solve the kinds of problems that Google faces: building large amounts of code in a giant monorepo with a very diverse set of client machines.
It excels at mixing and matching multiple programming languages, which makes it a great fit when you’re trying to integrate Rust into an existing C or C++ codebase, or build a web application that uses components written in different languages (such as TypeScript for the frontend, and Rust for the backend) but still want to have a simple build process.
It is also an artifact-centric rather than a task-centric build system.
Why Bazel?
It uses a high-level build language and supports multiple languages and platforms. One of Bazel’s key features is its ability to create reproducible builds, meaning that it ensures the output of a build is the same regardless of the environment it’s run in. This is achieved through strict dependency tracking and sandboxed execution environments. Bazel’s performance is enhanced by its advanced caching and parallel execution strategies, allowing it to only rebuild parts of the project that have changed since the last build, significantly reducing build times. It also scales seamlessly, facilitating its use in both small projects and massive codebases like those at Google. This makes Bazel particularly appealing for large, multi-language projects with complex dependencies, where build speed and consistency are critical.
How does Bazel work?
When you use Bazel, you declare how your project should be built in BUILD
files containing a descriptin in the Starlark language, which is similar to
Python. In this language, you define all of the targets and dependencies. From
this, Bazel builds a graph of all targets and their dependencies.
Bazel will try to perform hygienic builds, meaning that you should not rely on native dependencies being available, but rather you tell Bazel how to build them itself. You can also have platform-specific targets and rules to ensure that your project can be built on any platform (that your developers use or deploy to).
Any external resources you rely on, you specifiy with a hash-sum to ensure that the compilation process is always deterministic.
Getting Started with Bazel
Bazel’s build configuration replaces or coexists with the typical Cargo metadata. This means that if you want to migrate a Rust project to use Bazel, you may need to duplicate some definitions.
Installing Bazel
While you can install Bazel, the recommended way to use it is to install bazelisk. Bazelisk is to Bazel as Rustup is to Rust: it manages multiple versions of Bazel and ensures that you are using the appropriate version in each project.
If you do use bazelisk, then you should add a file into your repository telling
it which version of Bazel your project should use. The simplest way to achieve
this is by creating a .bazelversion
file containing the desitred version of
Bazel:
7.3.1
The advantage of doing this is that you ensure all users will use exactly the same version of Bazel.
Project Setup
To use Bazel, you need to configure a Repository (used to be called a Workspace).
You can do this by creating a MODULE.bazel
or REPO.bazel
file in the
root of your repository.
Typically, if you work with Rust you will want to use rules_rust, which is a module that teaches Bazel how to build and interact with Rust projects. A sample Repository configuration might look like this
bazel_dep(name = "rules_rust", version = "0.48.0")
Examples
This sections shows off some example projects that showcase what using Bazel in
a Rust project looks like. Bazel comes with some Rust
examples and
the rules_rust
comes with a more extensive set of
examples that are
also worth looking into.
Bazel Rust Hello World
- smallest possible bazel + rust project
Bazel Rust Workspace
- smallest possible bazel + rust workspace project
Mixing Rust and C
- smallest possible rust + native C code project
Full-stack Rust web application
- smallest possible backend + frontend project
Mixing Rust and JavaScript
- smallest possible rust + javascript (react) project
Integrating with Nix
It is possible to integrate Bazel with Nix. The idea is that Nix is a little bit better of a package manager, and that Bazel is a bit better as a build system. Nix is used to bootstrap the environment: the compiler, the native libraries. Bazel is then used as a build system.
If you don’t Nix, to get a true hermetic build environment you need to instruct it to build all native dependencies from source. You can avoid that when using Nix. And the fact that Nix has a public binary cache means that you rarely need to actually compile the thing you are using, most of the case Nix will be able to just pull it from the cache.
- https://nix-bazel.build/
- https://www.tweag.io/blog/2022-12-15-bazel-nix-migration-experience/
- https://www.tweag.io/blog/2018-03-15-bazel-nix/
- https://www.tweag.io/blog/2024-02-29-remote-execution-rules-nixpkgs/
- https://github.com/tweag/rules_nixpkgs
Reading
Scaling Rust builds with Bazel by Roman Kashitsyn
Roman explains how and why the Internet Computer project switched to using Bazel as it’s build system. He explains how Bazel is good at setting up builds that involve several languages or build targets, such as building some code for WebAssembly and using the resulting binaries as inputs to other builds. He walks you through the process they used to incrementally switch a large project to using Bazel and the implications it had. He considers the migration a success.
Using Bazel with Rust to Build and Deploy an Application by Enoch Chejieh
Enoch walks you through how to get started with a simple Rust project that uses Bazel to build. In particular, he shows to get get dependencies between several crates working, and unit tests running in Bazel.
Rewriting the Modern Web in Rust by Kevin King
Kevin shows how to set up a full-stack Rust application using Axum for the backend and Yew and the Tailwind CSS framework for the frontend. He shows how to use the Bazel build system to wrap it all together, including getting interactive rebuilds working. This is a good example to show how powerful Bazel is, as it involves building the frontend to WebAssembly and embedding it into the frontend.
Building Rust Workspace with Bazel by Ilya Polyakovskiy
Ilya shows you how you can make existing Rust Workspaces build with Bazel, by
taking the ripgrep
crate, which is a popular search tool written in Rust and
converting it to use Bazel for building and testing.
The rules_rust
project is the official Rust bindings for Bazel. It lets you
tell Bazel about the crates you have, and how they depend on each other. If you
want to use Bazel to build Rust code, you should use this plugin.
Bazel: What It Is, How It Works, and Why Developers Need It
This article is an overview of Bazel, it discusses the basics of hot it operates and what advantages it has for developers.
The rules_rust
module in Bazel is the official module to support building
Rust code using Bazel.
Birth of the Bazel by Han-Wen Nienhuys
Han-Wen explains how Bazel was born as an open source build system out of Google’s internal Blaze build system, and why the decision was made to open-source it.
Buck2
Buck2 (source) is written and maintained by Facebook, and is very similar to Bazel. What makes it interesting is that it is written in Rust, which makes it rather likely that it has good support for building Rust projects.
Interestingly, Buck2 uses the same language to write configuration as Bazel does, which is called Starlark. Both the syntax and the APIs are quite similar, but not close enough to say that they are compatible. Buck2 is quite new, having only been released in 2022.
What makes Buck2 exciting for us Rustaceans is that it itself is written in
Rust, and that it has good support for Rust out-of-the-box, without needing any
external plugins (as Bazel does with rules_rust
).
Why Buck2?
As per their website, Buck2 is an extensible and performant build system written in Rust and designed to make your build experience faster and more efficient.
How does it work?
Examples
There are some examples using reindeer, which is used to translate Cargo dependencies into Buck2 configurations.
Building C/C++ code
Building JavaScript
Building WebAssmebly
Reading
Build faster with Buck2: Our open source build system by Chris Hopman and Neil Mitchell
Introduction article of the Buck2 build system. Explains the features Buck2 has.
Getting started guide of the Buck2 build system.
Using Buck to build Rust Projects by Steve Klabnik
Steve explains how Buck2 can be used to build Rust projects.
Using Crates.io with Buck by Steve Klabnik
Steve shows how crates from crates.io can be used in projects built by Buck2.
Updating Buck by Steve Klabnik
Steve shows how Buck2 can be updated.
Nix
Nix is a declarative package manager and build system. It lets you defined dependencies and configurations in a functional language, and uses build isolation to ensure consistend and reproducible builds across machines.
The declarative nature of Nix makes it great at dealing with complex environment. It handles cross-platform builds correctly. Despite being over 20 years old, it has recently gained a lot of support. It is useful for providing consistent development setups between teams, ensuring that the code has the same environments between developers, CI machines and deployment machines.
Nix is quite versatile. It can be used to configure your system, setup a hygienic development shell containing only the dependencies you explicitly requested, build Docker images with the minimal set of runtime dependencies.
Nix Explainer
What can you use Nix for?
Nix is a bit of an oddball in this section because it is more than just a build system. You can use it, or even combine it with other build systems. Some common setups are:
- Using Nix to define a development environment
- Using Nix to define CI tasks that can be easily run locally
- Using Nix as a build system
- Using Nix to deploy your application
Nix has great support for caching. This is one of the principal reasons why it is useful as a build system.
Nix Development Environment
The Rust project comes with rustup
, which you can use to manage your Rust
toolchains. It allows you to install multiple versions of Rust side-by-side,
update them, and select a toolchain version per-project. You can even put a
rust-toolchain.toml
file in your project root, and have rustup
pick this up
and select the appropriate toolchain for you. This is explained in the
Cargo chapter.
However, this doesn’t quite solve all of your environment needs. What if you need to have a specific C library in your environment? What if you need to have specific tooling in your environment? Rustup is great at managing Rust toolchains, that is the primary purpose it serves. But it will not manage all of your native dependencies.
This is where Nix comes in. With Nix, you can declaratively define an environment,
and you can use nix-shell
to spawn a new shell with everything declared
in that environment accessible. That way, you can declare which native dependencies
you need once, and make sure that no matter what platform your developers happen
to use, Nix can make sure that all requirements are satisfied.
{
inputs = {
flake-utils.url = "github:numtide/flake-utils";
naersk.url = "github:nix-community/naersk";
nixpkgs-mozilla = {
url = "github:mozilla/nixpkgs-mozilla";
flake = false;
};
};
outputs = { self, flake-utils, naersk, nixpkgs, nixpkgs-mozilla }:
flake-utils.lib.eachDefaultSystem (system:
let
pkgs = (import nixpkgs) {
inherit system;
overlays = [
(import nixpkgs-mozilla)
];
};
toolchain = (pkgs.rustChannelOf {
rustToolchain = ./rust-toolchain;
sha256 = "";
# ^ After you run `nix build`, replace this with the actual
# hash from the error message
}).rust;
naersk' = pkgs.callPackage naersk {
cargo = toolchain;
rustc = toolchain;
};
in rec {
# For `nix build` & `nix run`:
defaultPackage = naersk'.buildPackage {
src = ./.;
};
# For `nix develop` (optional, can be skipped):
devShell = pkgs.mkShell {
nativeBuildInputs = [ toolchain ];
};
}
);
}
Example: Cargo and OpenSSL
Nix for Continuous Integration
https://nix-ci.com/
https://serokell.io/blog/continuous-delivery-with-nix
https://github.com/NixOS/hydra
https://hercules-ci.com/
Nix as a build system
Building C/C++ dependencies
Building TypeScript dependencies
Building WebAssembly component
https://jordankaye.dev/posts/rust-wasm-nix/
Nix for deployment
https://garnix.io/blog/hosting-nixos
https://x86.lol/generic/2024/08/28/systemd-sysupdate.html
Reading
Reference manual for the Nix package manager.
Rust in the NixOS Wiki
ipetkov/crane on GitHub
Building a Rust service with Nix by Amos Wenger
Amos shows how to build a Rust service in this article.
Introducing Crane: Composable and Cacheable Builds with Cargo and Nix by Ivan Petkov
Ivan introduces Crane in this article, a Nix library for building Cargo projects. He explains how it works and how to use it to build Rust projects.
Building Nix Flakes from Rust Workspaces by Tor Hovland
Tor explains how to package your Rust code using Nix. He explains the
different options you have for doing so: the Nix built-in buildRustPackage
,
Naersk, Crane and Cargo2Nix. He shows how to build a sample application that
consists of a Rust crate that is compiled into WebAssembly, a Rust library and
a Rust application that depends on both of these. He also discusses some
potential other options for building and packaging Rust code in Nix.
Zero to Nix by Determinate Systems
This is a guide on how to get started using Nix. It teaches you how to install it, how to use it for development, how to package your software with it, and how to manage your system with it.
https://jonathanlorimer.dev/posts/nix-thesis.html
https://jvns.ca/blog/2024/01/01/some-notes-on-nixos/
https://jvns.ca/blog/2023/03/03/how-do-nix-builds-work-/
https://jvns.ca/blog/2023/02/28/some-notes-on-using-nix/
Alternative Nix implementations:
https://tvix.dev/ https://lix.systems/about/
Meson
Meson is a bit of an oddball to include here. It does not offer any of the interesting features that other build systems do, for example the ability to easily cache build artifacts.
The reason I am including it here is because it has some features that make it useful for the niche use-case of building GTK or FlatPak applications, which it has built-in support for. This is why you see a lot of GNOME developers that use Rust use it as their primary build system.
Reading
TODO
Checks
The Rust compiler is very good at finding bugs in the code, thanks to the type system and the borrow checker. However, there is some other things in a Rust that can be checked continuously in a project to avoid writing broken code.
This chapter deals with some other aspects of Rust projects that should be checked, and offers some tooling that can be used to check them. It gives you suggestions that you can adopt in your workflows or CI jobs to give you confidence that your project is correct.
Not all of these checks might be interesting or relevant to you. You can use your own judgement of which checks you find are valuable and which ones are not worth adopting.
For every check, you need to decide what the process is. Is it something that you want your developers to be able to run locally? If so, you need to give them instructions on how to install the necessary tooling locally, and how to ensure that they are all using the same version. Is it something you want to run in the CI, or periodically? At the end of this chapter, I provide an overview of each of the tools discussed, and how I would apply them.
This chapter includes sections that show you how to check properties of your entire project, rather than just your Rust code.
Reading
Formatting
Ensuring consistent formatting across a project helps reduce friction. It allows Code Reviews to focus on the content, and not the formatting of the code.
One thing the Rust community does very well is have a consistent
formatting style, which eliminates the surprises you will encounter when
reading other people’s code. The canonical tool used for formatting Rust code
is rustfmt
.
Rustfmt
Rust code can be formatted using rustfmt, which is a core piece of Rust tooling and used by the whole Rust community.
By incorporating rustfmt
checks into the CI system, you can make sure that
issues with formatting are caught before code review.
Installation
Usually, rustfmt
comes preinstalled when you install Rust. However, if you
do not have it, you can install it using:
rustup component add rustfmt
You can run rustfmt
against a crate like this:
cargo fmt
In a CI system, you can check if the code is properly formatted using the
--check
command-line flag.
cargo fmt --check
If the code is not properly formatted, this will return a nonzero exit code and cause the CI check to fail.
Configuration
Rustfmt can also optionally take some configuration in a
rustfmt.toml
file. This allows you to override specific behaviour, for
example to set how it will group imports.
Some configuration options are unstable at the moment and therefore require an
unstable build of Rustfmt. When using it you have to call rustfmt
like this:
cargo +nightly fmt
Examples
Here is one example of a project which has a rustfmt.toml
to configure
rustfmt, and some CI steps which enforce the formatting in CI.
- src/
stages:
- check
formatting:
stage: check
image: rust
script:
- cargo +nightly fmt --check
[package]
name = "check-formatting"
version = "0.1.0"
edition = "2021"
[dependencies]
imports_granularity = "Crate"
group_imports = "One"
edition = "2021"
fn main() {
println!("Hello, world!");
}
Reading
Overview of all of the configuration options of Rustfmt. In general, you should not need to tweak these: the defaults that it comes with out-of-the-box are sane and used by the majority of Rust projects. However, if you have a good reason, you can look around here and configure Rustfmt. Keep in mind that using a non-standard Rustfmt configuration might alienate some developers.
The Rust Style Guide by The Rust Foundation
Style guide issued by the Rust foundation. This is a concise document that outlines good style recommendations for Rust code. Usually, reading these is not as important because Rustfmt will enforce these automatically, but it can be useful to read.
Lints
You have noticed that a lot of trivial mistakes exist in the project. You would like to mechanically prevent these from passing code reviews. How can you mechanically detect them?
In programming, linting refers to the process of performing static code
analysis on a software project to flag programming errors, bugs, stylistic
errors and suspicious constructs. The term originates from an old UNIX tool
named lint
, which was used to check C programs for common mistakes.
The Rust community has an excellent linting tool named Clippy.
Clippy
The linter that is typically used in Rust is Clippy. It is commonly used in Rust projects to enforce good practises, recognize unsafe or slow code. It is also configurable.
When enforcing coding style, it goes beyond mere aesthetic concerns. Coding style linters can do a lot more:
- Detect patterns that have cleaner alternatives
- Detect code that is correct, but slow
- Disallow writing unsafe code
It usually comes preinstalled when installing Rust through Rustup, or it can be added later by running
rustup component add clippy
What makes Clippy interesting is that it is quite configurable. Lints can be enabled either individually, or in groups. Some default lint groups are enabled by default, but the list[^clippy-lints] can be examined to pick out ones that are relevant for the project.
Example: Overriding Lints in Code
Instead of making sure during code reviews that no unsafe code is written, a Clippy annotation can be added to the crates that should not have unsafe code in them.
#![allow(unused)] #![deny(clippy::unsafe_code)] fn main() { }
Example: Overriding Lints in Cargo.toml
Reading
Static Analysis in Software Engineering at Google
Rust Lints you may not know by Andrew Lilley Brinker
Spelling
You notice that in your Rust project, often times spelling errors make it through code reviews because they are not specifically looking for them. This leads to a lot of small follow-up pull requests only to fix obvious spelling errors. Sometimes, they are not detected before a new version is released, leading to spelling issues in the public documentation. How could spelling errors be detected automatically so they do not make it in?
Using a spell checker for a software projects helps ensure that no silly
mistakes make it into the main
branch, without taking resources from
processes such as code review that better focus on the semantics of the change.
Good communication is very valuable. Nothing is worse in a complex project than having a situation where certain parts are only understood by a select few people due to a lack of documentation. Having good documentation helps keep projects agile by allowing faster onboarding of new developers and by helping existing developers understand unfamiliar parts of a project. See the Documentation section of this book for more information.
There is one crate in the Rust ecosystem that is specifically designed
to detect spelling errors: typos-cli
.
typos-cli
The typos-cli
1 crate helps do just that by providing a spell
checker specifically for Rust projects. It is designed to be fast enough to run
on monorepos and have a low false positive rate so that it can be run for pull
requests.
It can be installed using Cargo:
cargo install typos-cli
Once installed, it can be run against a project:
typos
If it detects a spelling error, it outputs a nonzero exit code and a diagnostic message explaining what the error was and how it could be remedied. This is a good tool to run in a CI job to keep pull requests from containing spelling errors.
GitHub repository: https://github.com/crate-ci/typos
Semantic Versioning
The crate you are working on is used by others, either in your own company or externally. You often release new versions of it with added functionality. However, sometimes you get complaints from downstream users that a newly released version introduces breaking changes. Usually, you try to correctly release a new major version when that happens, but sometimes you don’t notice it. How can you make sure that version numbers are correctly managed?
Rust is built heavily on Semantic Versioning to make it easy to compose software and update dependencies without needing to worry about everything breaking when dependencies are updated.
Semantic versioning, often abbreviated as SemVer, is a versioning scheme for
software that aims to convey meaning about the underlying changes in each
release. It uses a three-part version number format, major.minor.match
(e.g., 2.0.1
), where Major versions introduce breaking changes, Minor
versions add new features without breaking backward compatibility, and Patch
versions include bug fixes that don’t affect the API.
This system helps developers and users understand the impact of updating to a new version, ensuring more predictable and manageable software upgrades.
This also puts some responsibilities on you as a crate author: you have to ensure that when you release new versions of your crates, you do not violate semantic versioning by accidentally publishing versions with breaking changes but not marking them as such by incrementing the major version.
Doing this manually is possible, but difficult and it does not always scale well. The more widely used your crate is, the more frustration it causes when you get it wrong. Thankfully, there exists some tooling that can help here by automating the process of determining if you are correctly versioning your crate.
cargo-semver-checks
cargo-semver-checks
is an amazing tool designed to
detect invalid semantic versioning in crates automatically, by parsing both
your crate and the current latest version and determining if the changes
between them can be considered a patch, a minor change or a major change.
As it relies of pulling the latest version of your crate from a registry, it is only really useful for crates which are published.
You can use it by installing it with cargo
, and running it:
cargo install cargo-semver-checks
cargo semver-checks
It will check if the version you have currently specified in the crate is aligned with what it should be. This is a good check to run in a CI system.
Reading
Semantiv Versioning specification which explains the rules of how to apply it.
Chapter 3.15: SemVer Compatibility in The Cargo Book
Provides details on what is conventionally considered a compatible or breaking SemVer change for new releases of a Cargo package.
Semver violations are common, better tooling is the answer by Predrag Gruevski and Tomasz Nowak
Article which analyzes how common SemVer violations are in the Rust ecosystem, and what can be done to address this. Concludes that violations are fairly common, and that better tooling can improve the state of versioning.
Checking semver in the presence of doc(hidden) items by Predrag Gruevski
Explains how difficult it is to check SemVer compatibility in the presence of
hidden code in Rust, and how this was addressed in cargo-semver-checks
.
Contains a deep dive into the implementation details and how they help to make
these checks reliable and maintainable.
Dependency Minimum Versions
One component of the Rust project you are working on is a library. Everything has been going smooth, until someone complains that your library does not work. You are able to reproduce it locally, it seems that there is an issue with an early version of a dependency. Your project depends on it by a range, but in fact the early versions of the range do not work. However, in your CI setup you are always only testing against the latest possible version. How can you make sure these kinds of issues are caught in CI?
To understand this issue, a bit of an explainer for how dependency versioning is performed in Rust is required.
Crate Dependency Versions
In Rust projects, usually Semantic Versioning is used for versioning crates. The semantic part of that name means that versions are not just arbitrary tuples of numbers, but they have a meaning that comes with some stability guarantees that are necessary for writing software that does not implode when you update dependencies.
Semantic versioning, often abbreviated as SemVer, is a versioning scheme for
software that aims to convey meaning about the underlying changes in each
release. It uses a three-part version number format, major.minor.match
(e.g., 2.0.1
), where Major versions introduce breaking changes, Minor
versions add new features without breaking backward compatibility, and Patch
versions include bug fixes that don’t affect the API.
This system helps developers and users understand the impact of updating to a new version, ensuring more predictable and manageable software upgrades.
This allows us to specify dependencies not by their exact versions, but by their version bounds. For example, when you have a dependency bound such as this in your project:
name = "1.2"
This is in fact syntactic sugar for >=1.2.0,<1.3.0
. You are expressing that
you need at least version 1.2.0
, but lower than 1.3.0
. The current crate
version might be at 1.2.77
and it might compile just fine. Since semantic
versioning guarantees that the API remains stable between patch releases, you
can trust that when the dependency receives an update, that the newly released
version should still work with your code.
However, there is one potential issue here: Cargo always tries to use the maximum
possible version. This means that even though version 1.2.0
is within the range
you have specified, Cargo will only ever test it with whatever is the latest
version within those version bounds.
It is possible that your crate depends on some feature or fix that was not
present in 1.2.0
, but only added in 1.2.44
, but since Cargo always tests
against the latest, you will never know.
To detect this issue automatically, Cargo has a feature that allows you to
override the version resolution strategy to always use the minimum possible
version. You can enable this feature using -Z minimum-version
.
cargo-minimial-versions
The cargo-minimal-versions
tool helps to validate
whether your crate works with the minimal versions it advertises.
However, an easier approach is to install cargo-minimal-version
and running
it to check if your code will compile:
cargo install cargo-minimal-version
cargo minimal-version check
Reading
Semantiv Versioning specification which explains the rules of how to apply it.
Chapter 3.1: Specifying Dependencies in The Cargo Book
Explains how Cargo crate dependencies are specified in terms of syntax and semantics.
Chapter 3.14: Dependency Resolution in The Cargo Book
Explains how Cargo resolves crate dependency versions given the version constraints set by the dependencies section of your crate.
Chapter 3.18: Unstable Features in The Cargo Book
Explains the Cargo features miminal-versions
and direct-minimal-versions
which force Cargo to resolve (direct) dependencies to their minimal versions
instead of the latest versions.
Rust minimum versions: SemVer is a lie! by Daniel Wagner-Hall
Article which argues that a lot of crates are broken, because they do not compile with the versions they specify in their manifests. Note that this article is rather old.
Unused Dependencies
After you have been working on your Rust project for a while, you notice complaints that the compilation times are slow. You look into it, and discover that in multiple crates of the project, several dependencies exist which are unused, driving up compilation times. You remove them manually for now, but you wonder if there is a way to ensure that your crates do not accumulate unused dependencies in the future.
Having unused dependencies in your crates means the compiler needs to do extra work to fetch and build them even though they will not be used. Ensuring that you don’t have any is therefore an important part in making sure your compile times are low.
If you have some some dependencies that you need only conditionally, consider using crate features1 or conditional dependencies2 to prevent them from being depended on unneccessarily. These let you express that dependencies are only needed if a certain feature is activated or on a specific platform.
There are two tools in the Rust ecosystem that you can use to detect
unused dependencies: cargo-udeps
and cargo-machete
. These differ in the way
they attempt to detect unused dependencies, and thereby the time they take
to make the determination.
cargo-machete
The cargo-machete
tool should be your go-to to solve this3.
It aims to be very fast, at the expense of some precision. For that reason, it
is very suitable for running in CI for every merge request.
It can be installed like this:
cargo install cargo-machete
Once installed, it can be invoked in a project like this:
cargo machete
cargo-udeps
The cargo-udeps
tool is more accurate, however it is also
slower as it relies on compiling the crate.
It can be installed like this:
cargo install cargo-udeps
Once installed, it can be invoked in a project like this:
cargo udeps
Examples
Having unused dependencies only impacts compile time but not the correctness of the project. Running these checks for every merge request might be too expensive for your project. For that reason, this check could be performed on a schedule (nightly or weekly, for example) rather than on every merge request, or manually by maintainers.
TODO
Reading
cargo machete, find unused dependencies quickly by Benjamin Bouvier
Benjamin introduces the cargo-machete
tool, which lets you quickly find
unused dependencies. He explains how it works, and how it works differently to
cargo-udeps
to be much faster, at the expense of some accuracy.
Item 25: Manage your dependency graph in Effective Rust
Finding unused dependencies with cargo-udeps
by Amos Wenger
See chapter Optional Features.
A feature of Cargo to let you specify dependencies depending on the circumstance, for example the platform. See https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html#platform-specific-dependencies for more information.
cargo-machete, find unused dependencies quickly: https://blog.benj.me/2022/04/27/cargo-machete/
Dependency Auditing
In a recent software audit that your company performed, it was discovered that your Rust project uses several dependencies with known security issues that need to be replaced with safer alternatives. It was also discovered that your project has some dependencies which are unlicensed, which means that your company is not allowed to use it. As this security audit was very expensive, you now wonder if there is a way to automate some of these checks to catch them sooner.
Supply chain attacks are currently discussed a lot. As the complexity of the average software project grows, so does it’s dependency graph. At the same time, bugs are being found, sometimes in high-profile software, leading to attacks. The worst situation is when bugs are found in popular libraries, but mitigations are not applied due to companies not recognizing they (indirectly) depend on the buggy versions.
Talking about software supply chain attacks in the context of Rust cannot be done without mentioning the RustSec project, which collects security advisories against Rust crates.
Another common issue has to do with licensing: Cargo (and other good tooling) makes it very easy to add dependencies, but sometimes these come with incompatible licenses.
Fortunately, the Rust ecosystem has come up with some exceptional tooling that helps address this with very little friction. These tools can do a lot, more so than I can cover in this chapter, but I will attempt to give an overview of what each does.
cargo-audit
A part of the RustSec project, cargo-audit
is a tool that will
validate your crate’s dependencies against the RustSec advisory database and
let you know if your project has any dependencies, direct or transitive, by
looking at the Cargo Lockfile.
To use cargo-audit
, you can install it using Cargo and run it:
cargo install cargo-audit
cargo audit
If your crate contains any dependencies which it deems are insecure, it will produce a useful warning about it.
cargo-deny
cargo-deny
is very similar to cargo-audit
, however it goes
several steps further. Instead of only checking for security advisories, it acts
as a linter of your dependency graph. It allows you to put constraints on the
licenses of dependencies.
You can use it by installing cargo-deny
:
cargo install cargo-deny
Initializing a new configuration:
cargo deny init
And finally checking if the current project satisfies the constraints set in
the deny.toml
file:
cargo deny check
It will produce a report containing the violations it has found. It has the ability to treat violations as either errors or warnings.
cargo-vet
cargo-vet
is another very interesting tool from the Rust ecosystem.
The way it works is quite different from cargo-audit
and cargo-edit
. Instead of
relying on advisories, it enforces that every dependency is audited so that certain
properties can be asserted. These audits can be published, and audits from other
organizations can be imported.
Google announced that it is Open sourcing Rust crate audits, similar as Mozilla is already doing.
It is a good tool to add to your CI pipeline.
Reading
Comparing Rust Supply Chain Safety Tools by Andre Bogus
Blog post summarizing several Rust supply chain safety tools, including the ones discussed in this chapter.
Item 25: Manage your dependency graph in Effective Rust
Chapter about how to manage your crate’s dependency graph. Mentions some of the tools from this chapter.
Cargo Deny documentation, explains what it is and how it works. Has a very good overview of all of the capabilities it has and how to configure them.
Securing the Software Supply Chain
Document released by the US-American Department of Defense, outlining how software supply chains should be secured.
Official documentation book of cargo-vet
. Explains in detail what it does and how to set it up for your project.
Vetting the Cargo by Jonathan Corbet
Article explaining what cargo-vet
does and how it works.
Outdated Dependencies
Your Rust project is going smoothly, however one of your developers complains that you are using an old version of Tokio, and that a feature he would like to use is not available yet. You tell him to upgrade the version of Tokio, and is happy. However, that makes you wonder: would it be possible to find out automatically if you have any outdated dependencies in any of your crates?
Besides fixing bugs, new versions of dependencies also usually come with new features and sometimes better performance. For that reason, it is usually advisable to not fall behind too far in terms of which version is being used.
There is some tooling in the Rust world which can check for outdated dependencies automatically. This can be used as a maintenance task or a periodic CI job.
If you are working on an open source project, you can also rely on the [deps.rs][] service to tell you if your dependencies are outdated.
cargo-upgrades
cargo-upgrades
is a Cargo subcommand to check if any of the
direct dependencies have newer versions available. It has a simpler implementation
than cargo-outdated
and is typically a bit faster, because it does not rely on
using Cargo’s dependency resolution.
You can install it using cargo
and run it against your project:
cargo install cargo-upgrades
cargo upgrades
cargo-outdated
cargo-outdated
is a Cargo subcommand for displaying when
Rust dependencies are out of date. It works by creating a temporary Cargo workspace
and running cargo-update
, and finally comparing the resolved crate versions
against the ones in the original crate.
You can install it using cargo
, and run it against your project:
cargo install cargo-outdated
cargo outdated
Reading
Cleaning up and upgrading third-party crates by Amos Wenger
In this article, Amos shows how to clean up and upgrade crate dependencies. He
uses cargo-outdated
to do this, but he mentions that it has an issue with
path dependencies in Cargo workspaces.
Cargo Manifest
Crate Features
Following advice from Crate Features, you have
added optional features into your crate to reduce compilation times for when
they are not required by downstream users. This has been working well, however
in a recent release you have received a bug report what a specific combination
of enabled features triggers a compilation error. You have fixed the error,
which was introduced by some refactoring that moved a #[cfg]
block. However,
you are wondering whether it is possible to catch these kinds of issues
automatically by CI rather than by downstream users.
Similar to using #ifdef
statements in C and C++, using #[use]
blocks is
inherently brittle. Using a crate such as cfg_if
instead can help
make it more manageable, but it does not address the root issue: you really
need to test the code you write for all features.
Fortunately, the cargo-hack
tool allows you to do just that.
cargo-hack
What cargo-hack
lets you do is run some check (for example
cargo check
to see if it will compile, or cargo test
to run unit tests) for
every possible feature or even for every possible set of features.
To use cargo-hack
, you need to give it two pieces of information: which
sets of features to test for and which command to run.
Feature Sets
For the set of features,
the two popular options are --each-feature
and --feature-powerset
. To illustrate
the different, consider if you have a crate with the features a
, b
, c
and d
.
Flag | Feature Sets |
---|---|
--each-feature | a ; b ; c |
--feature-powerset | a ; b ; c ; a,b ; a,c ; b,c ; a,b,c |
Commands
You also need to tell cargo-hack
what command to run.
Name | Description |
---|---|
check | Runs cargo check for each of the selected feature sets |
test | Runs cargo test for each of the selected feature sets |
Examples
cargo-features-manager
https://github.com/ToBinio/cargo-features-manager https://corrode.dev/blog/tips-for-faster-rust-compile-times/#disable-unused-features-of-crate-dependencies
Minimum Supported Rust Version
In Build systems: Cargo, we’ve explained that when you build library crates, you can specify a MSRV. This specifies the minimum version of the Rust toolchain you need to use your library. Setting this communicates to the users of your library what version of Rust they should be using at least.
If you set this, you might end up in a situation where this is no longer true: you’ve inadvetendly started using Rust features that are not available in the MSRV version. Specifying a MSRV is that is incorrect is arguably worse than not specifying one at all.
So, how can you use tooling to ensure that the MSRV that you specify matches
the reality of what your crate needs? Here is another Cargo plugin that comes
to the rescue: cargo-msrv
allows us to determine our crate’s true MSRV.
Cargo MSRV to determine MSRV
cargo-msrv
cargo-hack --rust-version
Cargo Hack to test MSRV
Reading
TODO
Conclusion
The Rust ecosystem has a lot of great tooling that can be used to verify properties of software projects. Every check has a certain cost associated with it: if it runs in the CI system, it means the CI system needs more resources. If the checks are to be run locally, it takes resources to ensure everyone is using the right checks and in the right way.
In this section, I will summarize all of the available checks, what I consider their value and costs to be and give a recommendation of whether and where to run them.
Goal | Tool | Value | Cost | Use |
---|---|---|---|---|
Formatting | rustfmt | High | Low | Yes |
Linting | clippy | High | Low | Yes |
Spelling | typos | Medium | Low | Yes |
SemVer | cargo-semver-checks | High1 | Medium | Yes |
Minimum Versions | cargo-minimal-versions | High1 | Medium | Yes |
Unused Dependencies | cargo-machete | High | Low | Yes |
Auditing Dependencies | cargo-deny | High | Medium | Yes |
Auditing Dependencies | cargo-vet | High | High | Maybe |
Outdated Dependencies | cargo-upgrades | Medium | Low | Yes |
Crate Features | cargo-hack | High2 | Medium | Yes2 |
If your crate makes use of Cargo features.
If you are working on a library which you intend for others to consume.
Testing
Testing is the process of ensuring that code is correct. It can be done manually, but typically it is done in an automated way because it is cheaper over the long run. There are even some development paradigms that use tests are the primary artifacts of development, such as Test-Drivent Development.
Why Tests are needed
Having rigid tests ensures three things:
- Features are implemented and work correctly for all inputs, expected or unexpected.
- Features that are correct now don’t break in the future
- In the case of missing documentation, tests are often the only thing close to documenting how code is intended to be used.
Having a thorough tests makes development more pleasant (and speedy), because it allows developers to implement new features or refactor code, without having to worry about accidentally breaking existing functionality and only finding out when the code is deployed. But this is only possible when there is a reasonable test coverage.
If you look at some of the most widely deployed and robust software, it tends to have one thing in common: there is an extensive set of tests for it. It goes so far that some projects charge a fee to get the tests. For example, SQLite, the most widely deployed database, is open-source. But the developers charge for access to the private test suite.
How tests are written
Tests are often divided into different categories:
- Unit tests test small units of code. They often have access to the private internal state of the code. Every unit test tests exactly one feature.
- Integration tests test the code from an outside perspective, they don’t have access to the internal state of the code. They often don’t test individual features, but functionality as a whole.
Typically, the aim is to have many unit tests, to make sure the features work, and have some integration tests that tie the system together as a whole. Ideally, running unit tests does not require external dependencies, but integration tests might.
- testing pyramid graphic
Another advantage of writing tests early is that it influences the system design to be in such a way that it is easy to test.
Rust has another category of tests: documentation tests. In Rust, documentation can include code examples, and Cargo will test these as well. This ensures that documentation does not inadvertently get out of date, for example by changing interfaces.
What this chapter covers
- culture
- testability engineering
This chapter discusses various features of Rust and the ecosystem and strategies for using to to ensure correctness of the code.
Reading
How to organize Rust tests by Andre Bogus
In this article, Andre discusses how tests are best organized in Rust project.
Writing software that’s reliable enough for production by Scigraph
Testing Overview in Software Engineering at Google
Adam discusses the philosophy behind writing software tests. He explains that well-written tests are crucial to allow software to change. For tests to scale, they must be automated. Features that other components or teams rely on should have tests to ensure they work correctly. Testing is as much a cultural problem as it is a technical one, and changing the culture in an organization takes time.
Chapter 11: Writing automated tests in The Rust Book
https://doc.rust-lang.org/book/ch11-03-test-organization.html
How to Test by Alex Kladov
Everything you need to know about testing in Rust by Joshua Mo
This article gives an overview of Cargo features for testing and libraries in the Rust ecosystem that can help in writing useful tests for software.
Unit Tests
Unit tests are intended to test one small unit at a time. It might be a feature, it might be a specific input to an algorithm. Rust has native support for them with the built-in testing harness support.
In Rust, you can annotate any function with #[test]
and it will be a (unit or
integration) test. Here is how a simple test case might look like:
#![allow(unused)] fn main() { #[test] fn can_add() { assert_eq!(1 + 1, 2); } }
Running cargo test
will run all of the tests present in a project.
Where to put unit tests
Usually, when you write unit tests in Rust you put them at the end of every
module, and you declare a tests
module inline.
Here’s an example of what this might look like:
#![allow(unused)] fn main() { fn function_one() -> &'static str { "hello" } mod tests { use super::*; #[test] fn test_function_one() { assert_eq!(function_one(), "hello"); } } }
This is, however, a question of style. It’s also perfectly okay to just intersperse tests with the code. Keeping the tests close to the code is important, because it means that they will have visibility into non-public methods and fields.
Testing async code
If you chose to use async code in your project, you migth run into a situation where you need to write unit tests for asynchronous code. Usually, most of the unit tests don’t require it, because you will follow the blocking core, async shell paradigm.
If you do need to write async unit tests, then the Tokio library has some
functionality you can use for that. They have a #[tokio::test]
macro that
you can use to annotate any unit test to turn it into an asynchronous unit test.
#![allow(unused)] fn main() { #[tokio::test] async fn async_unit_test() { assert_eq!(test_something().await, 42); } }
Reading
Unit testing in Rust By Example
Unit Testing in Software Engineering at Google
Integration Tests
Reading
https://xxchan.me/cs/2023/02/17/optimize-rust-comptime-en.html#step-4-single-binary-integration-test
Larger Tests in Software Engineering at Google
Test runners
Rust comes with support for unit tests built-in. You can annotate a function
with #[test]
anywhere and it will be treated as a unit test.
#![allow(unused)] fn main() { #[test] fn example_test() { assert_eq!(42, 40 + 2); } }
Often times, tests will be put into a separate module called tests
, so that
you can import things you need in the tests and not get warnings because those
are not used in normal compilation.
You can also write integration tests by putting them into a tests/
folder in
the crate root. Finally, code you put into crate documentation is also built,
these are called doctests.
Cargo has a subcommnd called cargo test
, which will build and run all of the
tests in a crate. When you use a workspace, then using cargo test --all
will
run all tests from all crates in the workspace.
This subcommand works well, and you should use it. However, there are a few limitations with it that are especially noticeable when you are working in a large Rust workspace with a lot of crates (and thus, a lot of tests and integration tests): it runs a bit slow.
Cargo Nextest
Cargo Nextest is a tool that you can use as a drop-in replacement
for cargo test
. It has some useful features for running tests in CI, but the
main advantage is that it is up to 3x faster according to their
own benchmarks.
In my testing, I’ve observed a 10% speedup, but it depends on how many tests
you have and what they are bottlenecked by. In my case, the tests were
bottlenecked by external services, so there is not too much cargo nextest
can
do. But if you have a lot of tests and/or a large Cargo workspace, be sure to
give cargo nextest
a try, you may see a marked improvement in testing time
and the output is also more terse, which I like.
Reading
How (and why) nextest uses Tokio by Siddharth Agarwal
Siddharth explains in this article how (and why) nextests uses Tokio. Generally, using Tokio often comes with the assumption that some software uses networking, however here it turns out that the async model maps very well to scheduling tests as well. It is a fascinating peek into how nextest works.
External Services
You notice that in a lot of pull requests, authors need to push several fix commits to get the CI pipelines to run correctly. A lot of the time, some simple unit tests need to be fixed. When asked, developers note that they cannot run the test suite locally because it depends on services that run in the cloud. This makes you wonder, if there is a way to increase iteration speed by making sure that tests can run locally.
Having a fast iteration loop is key to fast software development. To make that possible, it is generally advantageous if test suites have no external dependencies, making it easy for developers to launch them locally and test projects end-to-end.
Whenever possible, try to make it such that you can run all tests locally, and that you can do so relatively easily.
When interfacing with external systems, you need to make sure that every test is isolated. Tests in Rust are designed to be able to be run in parallel. This means that every test needs, ideally, a fresh, empty environment to run against.
In general, there are three strategies that I have used, and I will outline them here. If you can make use of one of these strategies, then it might be a worthwhile investment. In some cases, however, it is not possible.
Use Service as Dependency
If you are writing tests for a component which talks to some API, and the API is also written in Rust, then you might be able to simply add a development dependency to the API and launch it for the unit tests.
For example, if you have a project which consists of two crates: api
and
client
, then in the client
crate you could add the api
crate as a test
dependency in the Cargo manifest:
[dev-dependencies]
api = { path = "../api" }
And then you could write your unit tests in such a way that you launch a fresh instance of the API for every test. You may have to pick a random free port or use some feature to bypass the network and inject requests directly.
#![allow(unused)] fn main() { #[test] fn test_some_call() { let server = api::Server::launch(); // make request assert_eq!(make_request(), Response {}); } }
Docker Compose
In many cases, you do not need to run a separate copy of your dependencies for every unit test. Many services, such as databases, allow you to create a fresh, empty database for every unit test. In that case, using docker compose is a good strategy. A docker-compose file can be written which defines all the prerequisite services, which can be launched manually before running the tests.
Testcontainers
Testcontainers is a project that aims to make it simple to use Docker containers in unit tests. They maintain the testcontainers crate, which is the Rust implementation of this project.
This makes it easy to run a fresh copy of whichever service your unit tests need when you execute them.
Mock Service
If you can easily mock the service, that is a good approach as well.
For example, the mockall crate lets you easily mock external services.
Some external systems might have a built-in ability to create an environment. For example, when talking to a storage system, every test might get it’s own bucket with a randomized name. When talking to Postgres, every test might get it’s own database.
Some systems do not have that built-in, in this case one can use something like the Testcontainers crate, which is designed to launch a fresh container for every invocation of a test.
Reading
Google Testing Blog: Increase Test Fidelity By Avoiding Mocks
In this post from Google’s Testing on the Toilet series, the topic of how to interact with external services is discussed. The preference to use real instances is mentioned.
Rust Mock Shootout! by Alan Somers
In this post, Alan discusses various mocking crates in Rust.
Rust Development with Testcontainers
In this blog post, Engin discussed how testcontainers can be used to make sure external dependencies are spawned in Docker containers for each unit test.
Snapshot Testing
Snapshot testing is a strategy to ensure that the output of some code doesn’t change over time. It make writing tests very simple, it lets you record an output of some code once, save it as a unit test, and check that it stays the same. If it does change, it shows you the difference.
Insta
Insta is a crate that lets you to snapshot testing easily in Rust. It comes with a tool that lets you record the output, and shows you the diff.
Reading
Using Insta for Rust snapshot testing by Agustinus Theodorus
In this article, Agustinus explains how to use insta-rs to do snapshot testing in Rust.
Property Testing
One common issue you will run into when writing unit tests is that there is a lot of repetition and artificial test cases. For example, if you test a simple piece of code which appends something to a vector, you might end up with test cases like this:
#![allow(unused)] fn main() { #[test] fn test_append() { assert_eq!(vec![].append(1), vec![1]); assert_eq!(vec![1, 2].append(3), vec![1, 2, 3]); assert_eq!(vec![0, 0, 0].append(0), vec![0, 0, 0, 0]); } }
In this example, you are making sure that your append function works correctly,
but you have actually only tested that it works for these particular cases. What
if you have missed an edge case in your tests? Ideally, you want to be able to
write a test case like given any array, when you call .append()
on it with
a value, make sure that the resulting array is the original array with the
value appended to it.
Property-based testing allows you to express exactly that. What property-based testing lets you do is consume randomized inputs, such that properties of the code you are testing hold true under all possible inputs, rather than just the couple you can come up with and hard-code.
For example, this is how you might write a reasonable test for your code:
#![allow(unused)] fn main() { fn test_append(list: Vec<u8>, value: u8) { let appended = list.append(value); // length of appended is one more than original list. assert_eq!(appended.len(), list.len() + 1); // appended consists of the original list, plus `value` at the end. assert_eq!(appended[0..list.len()], &list[..]); assert_eq!(appended[list.len()], value); } }
In some sense, using property testing is a lot like using fuzzing, but it usually works at the function level rather than the whole program. Because you are testing smaller pieces of code, you can test it more thoroughly and in less time.
To use property testing, you need a framework. Two popular ones in Rust are quickcheck and proptest. While they are both good, I recommend you use the latter.
Proptest
Proptest is a framework that makes it easy to set up property-based testing in Rust. It lets you generate randomized inputs for your property-based tests. When it hits a failure, it attempts to reduce the input to a minimal example. It records failing test inputs such that they will be retried.
If you use proptest
, I recommend you to use it with the test-strategy
crate,
which just contains some macros that make it simpler to set it up and use it
to test async code, for example.
An example proptest, using the test-strategy
crate looks like this:
#![allow(unused)] fn main() { #[proptest] fn test_parser(input: &str) { let ast = parse(input); } }
Example
Reading
The official book of the proptest crate. This is a valuable read if you want to understand how it works and how you can customize it, for example by implementing custom strategies for generating test inputs.
Complete Guide to Testing Code in Rust: Property testing
Property-testing async code in Rust to build reliable distributed systems by Antonio Scandurra
In this presentation, Antonio explains how he used property testing to test the Zed editor for correctness. Being a concurrent, futures-based application, it is important that the code is correct. By testing random permutations of the futures execution ordering, he was able to find bugs in edge cases that would otherwise have been very difficult to discover or reproduce.
Fuzzing
Fuzzing is an approach to testing code that generates random inputs for your code and uses instrumentation to monitor which branches are being triggered, with the goal of triggering all branches inside the code. In doing so, it can test your code very thoroughly and often times discover edge cases.
Fuzzing is a very good strategy when your code parses untrusted data. It allows you to have confidence that for any possible input, your program does not misbehave. The downside of fuzzing is that usually, it can only detect crashes. When possible, it is better to test individual pieces of code using property testing.
Examples
TODO
Reading
Introduction in Rust Fuzz Book
This book explains what fuzz testing is, and how it can be implemented in Rust
using afl.rs
and cargo-fuzz
.
How to fuzz Rust code continuously by Yevgeny Pats
Yevgeny explains why you should fuzz your Rust code, and shows you how to do it in GitLab. GitLab has some features that make running fuzzing inside GitLab CI quite convenient.
Fuzzing Solana by Addison Crump
Addison shows how Rust can be used to fuzz the Solana eBPF JIT compiler, and outlines the security vulnerabilities found within uses this approach.
Mutation Testing
Mutation testing is an approach to randomized testing code that uses a different approach to property testing and fuzzing. Instead of randomly generating inputs to the program or functions, it works by randomly mutating the code and running the existing tests. The goal is to find mutations that do not break the tests: this usually means that that section of code is not covered by tests, or that the tests are not sufficient to explore all possible paths through the code. On a high level, mutation testing frameworks try to inject bugs into your code and see if your existing tests will catch them.
cargo-mutants
Reading
This book explains how cargo-mutants works, and how it can be deployed in Rust projects to find areas where bugs might be lurking.
Dynamic Analysis
The Rust programming language does not prevent you from writing invalid code,
it just makes it a lot harder. The default state is that code is subject to the
borrow checker, which ensures that it is memory-correct. However, sometimes you
do want to write some code that opens up a little hatch and lets you take on
the burden of validating that it is correct yourself: unsafe
code.
A typical Rust program does not use a lot of unsafe
code. It is more the
exception that crates use it, and if they do it tends to be in small,
contained spaces. Rust does not eliminate the ability to shoot yourself
into the foot, it just forces you to be intentional about it. In languages
like C or C++, any piece of code is an unsafe
block, it’s like the wild
west.
Sometimes, you would like to check if the unsafe
code you have written
is in fact valid. This can be a bit tricky, because the thing you are trying
to catch is undefined behaviour. For example, reading one byte past an array
would not necessarily cause your program to crash, instead you would just read
garbage.
One solution here is to use dynamic analysis, where your program is run in a special way (instrumented or emulated) and a higher-level tool validates every action your program takes. If your program triggers any of these undefined behaviours, then you get an error and a description of what it did wrong:
- Read uninitialized memory
- Read past memory allocation/stack
- Write past memory allocation/stack
- Free memory that is already freed (double free)
- Forget to free memory (memory leak)
The idea with these tools is that you can enable them when running unit tests, and they will monitor what your code does and give you a diagnostic error when it does anything invalid. Triggering undefined behavior is quite dangerous, it means that your program can break when you switch compilers or it might work because your CPU happens to support certain things (for example, x86 CPUs will let you perform unaligned reads, but other platforms might not, so if your code performs those it will break on other platforms).
For Rust, due to the protections the language offers, we usually don’t have large amounts of undefined behaviour in the first place, so these tools are not usually needed.
There is one tool that is particularily suited to helping detect invalid operations in Rust code, and that is Miri.
Miri
Miri is a tool that lets you find undefined behaviour in Rust programs. It works by acting as an interpreter for Rust’s mid-level intermediate representation, which is used by the compiler internally. In some ways, it is similar to Valgrind, because it works by interpreting this representation. The advantage of using Miri over Valgrind is that this representation retains a lot of semantic information, which means you get much better diagnostic messages. It has the same downside as Valgrind, in that it makes your program’s execution very slow.
Cargo Careful
Valgrind
Valgrind lets you run your program in a kind of virtual machine, where all memory access is monitored. It is quite powerful, it even incorporates features such as a model of how CPU caches work so you can check how good the memory locality of your program is. Due to the virtualisation, there is some overhead. It can also report how many instructions your program took to run, which is more useful for microbenchmarks than time, because it is stable between machines (but not architectures).
LLVM Sanitizers
LLVM sanitizers (AddressSanitizer, ThreadSanitizer, UndefinedBehaviorSanitizer, LeakSanitizer): these need to be enabled at compile time and instrument your binary with extra code on every memory access or operation (depending on the kind of sanitizer). The added code adds an overhead, depending on the kind of sanitizer this can be a lot. There are some things these can detect that go beyond what Valgrind can detect.
Address Sanitizer
Memory Sanitizer
Undefined Behaviour Sanitizer
Reading
Data-driven performance optimization with Rust and Miri by Keaton Brandt
Keaton shows you how you can use Miri to get detailed profiling information from Rust programs, visualize them in Chrome developer tools and use this information to optimize your program’s execution time.
Unsafe Rust and Miri by Ralf Jung
In this talk, Ralf explains key concepts around writing unsafe code, such as what “undefined behaviour” and “unsoundness” mean, and explains how to write unsafe code in a systematic way that reduces the chance of getting it wrong.
C++ Safety, in context by Herb Sutter
In this article, Herb Sutter discusses the safety issues C++ has. While this is not directly relevant to Rust, he does make a good point about the fact that there is good tooling to catch a lot of issues (sanitiziers, for example) and that they should be more widely used, even by projects that use languages that are safer by design, such as Rust. While some consider C++ to be defective, with the right tooling a majority of issues can be caught.
The Soundness Pledge by Ralph Levien
Ralph talks about the use of unsafe
in Rust. Many developers consider using
it to be bad style, but he argues that it is not unsafe
that is a problem, it
is unsound code that is a problem. As a community, we should strive to
eliminate unsound code. This includes using tools like Miri to ensure
soundness.
Conclusion
In this chapter, we have discussed many of the options that Rust has for testing code. We have discussed some strategies of how to split tests into different tiers, so that local development can be snappy while testing runs in CI can be extensive. We have looked at some randomized testing methods like property testing, fuzzing and mutation testing and showed how they can be used to gain confidence that code behaves correctly in the face of untrusted inputs. We have explored how to handle external services when writing well-tested code. We have also covered how to measure how much of the code is covered during testing.
In my opinion, testing is very valuable for writing robust code. Especially approaches that take little effort but produce great test coverage (such as randomized testing) can help make sure that code really does behave well.
Matrix
Measure
Everybody talks about being data-driven, but few software projects actually are. There might be a set of properties of your software that you care about. For example, some of these properties might be correctness (measured by the ability of the test suite to test all edge cases your software may run into), performance (measured by the execution time of a set of operations that is representative for what your software might do in the real world). Any properties that are critical to the project should be continuously measured, and these (aggregated) measurements made available to engineers to help them shape the direction and implementation of the project.
If you have a Rust software project, you should ask yourself the question: are there any important properties that this software must uphold? Based on your responses to this question, you should think about how you can measure these properties, and ensure that they are continuously monitored. Some of the properties might be implicit and difficult to identify. For example, if you are running a web application, you want users to have a good experience. Part of that could be that your application should be snappy, but it is difficult to quantify that. If there are less users on your side, is it because it is slower? Or is the design worse than it was? One part of being data-driven is identifying what data is critical.
Software constantly changes, and just because you have come up with a data structure that performed well on the workload today, does not mean that it will still be the best data structure for the job tomorrow. Coming up with metrics and continuously monitoring them allows you to notice regressions before they hit production.
Here are some examples for properties that you might want to measure over time, and why they might be critical to a project. Every project is different, and not all proprties might be of equal importance. Setting up and maintaining measurement pipelines takes time, so you should choose the properties you optimize for wisely.
Properties | Use-case |
---|---|
Binary size | You are deploying a WebAssembly-powered website, which needs to be fetched by the browser on every first request. You want to ensure that the website loads quickly, so you want to measure the binary size. |
Memory usage | You are writing a firmware for a microcontroller which has limited amount of memory. You want to measure the dynamic memory usage to ensure that it stays within the allowed limit. |
Correctness | Your project includes a bespoke lock-free data structure to handle data for many concurrent requests, and you want to make sure that it is correct for all possible use-cases. |
Performance | Your application includes some custom data processing code that is mission-critical. You want to measure the performance of them over time to ensure that there are no regressions as it is being developed. |
But measuring them is only one half of the equation. The other half is: how do you collect, aggregate this information and make it available to your engineers to shape the decision process? There are some tools that can help with this, for example:
Tool | Purpose |
---|---|
Bencher | Aggregates benchmark results, allowing you to see how performance changes over time. |
GitLab | GitLab has the ability to visualize code coverage and test results measured in CI jobs in merge requests, allowing developers to assess how well new code is covered by tests. |
This chapter focusses on showing you how you can measure properties of your codebase continuously, and what options you have for aggregating this information and use it in decision-making processes. Naturally, this chapter can’t cover every single metric you might care about, but it can give you an appreciation for how you can approach this.
Reading
Performance Culture by Joe Duffy
Joe argues that performant software is not an accident, but rather a product of a performance culture. He explains what this culture looks like: that the properties that the project wants to uphold (eg performance) have management buy-in and are not afterthoughts, they are constantly measured so that engineers can make data-driven decisions when implementing new features in code reviews.
Systems Performance: Enterprise and the Cloud, 2nd Edition by Brendan Gregg
In this book, Brendan goes into depths of how to analyze the performance of systems, specifically in the context of cloud-deployed software. Linux has powerful capabilities of hooking into application execution at runtime, instrumenting it with eBPF code to measure not only how the application is performing, but also giving the ability to understand why it is performing the way it is. This book is a must-read for anyone who deeply cares about performance, wants to measure and debug it.
Be good-argument-driven, not data-driven by Richard Marmorstein
Richard talks about using data to influence development of software. He explains that while data is useful, at the end it should be used to back up arguments, not an end in itself. There are cases where data could be interpreted wrong, and you should be sceptical of poor arguments made by incorrectly-interpreted data.
Test Coverage
The only way to ensure that you are doing a good job of testing all the paths in the code is by measuring them.
In general, I believe that library crates should aim to get as close to 100% of test coverage as possible. Binary crates may not be able to achieve that, also because they might use libraries that make it difficult to test them at all. This is another good reason for splitting up project into small crates: it allows you to have and enforce a good test coverage for all of the library crates that can be tested, while allowing certain binary crates to not be well-tested.
Cargo LLVM-Cov
In Rust, you can use cargo-llvm-cov
to determine code coverage. It can output
in different formats, including HTML, JSON and text.
$ cargo llvm-cov
Cargo Tarpaulin
Another option is to use cargo-tarpaulin
.
GRCOV
Finally, there is also grcov.
Reading
Instrumentation-based Code Coverage in The rustc Book
This chapter in the rustc book explains low-level features of rustc that enable adding instrumentation to binaries for measuring execution coverage, and how to use the raw output to generate coverage reports.
How to do code coverage in Rust by Dotan J. Nahum
Dotan explains how to measure test coverage in Rust using both Tarpaulin and grcov. He shows how to set it up for a project, with working GitHub Actions workflows.
Performance
Rust often attracts people that care about performance. Often times, performance is not the end goal: instead, higher performance means higher efficiency. In an era of cloud computing, this translates to lower costs per request.
Performance optimizations are a large subject, and this book will not go into depth when it comes to it. There are other books that do a better job of summarizing what can be done to optimize applications, such as the Rust Performance Book. But this book does make a point that performance is something that should be tested and tracked over time, that is the only way to ensure that a project is heading in the right direction and not regressing.
The way you can do that in Rust is by writing benchmarks. In fact, Cargo comes with built-in support for doing so. While the Cargo build-in benchmarking harness is still unstable, there are some crates that allow you to easily build benchmarks for both blocking and async code, and track their performance over time.
Writing benchmarks makes it easy to experiement with different options of implementing a feature, because it makes it easy to compare the performance differences between various approaches. Another application is tracking the performance of your code over time, by running benchmarks on every commit or periodically by a platform such as Bencher or the Continuous Benchmark GitHub Action.
Often times, performance is a tradeoff. While Rust has some zero-cost abstrations that allow you to write simple code that is still fast, there are many situations where you have to make a choice between a simpler implementation or some tech debt, and doing it properly, resulting in more development time or more complex code. The only way to make these decisions properly is to have data for them. How much runtime performance are you trading by keeping your simple implementation? How much performance are you gaining by having a more complex implementation? Projects should make these decisions based on measurements, and not guesses.
Criterion
Typically, the way that you write these is using the criterion crate[^1]. This lets you test both synchronous and asynchronous code, and it provides some support for statistical analysis of the benchmark results. The Rust standard library also has some benchmarking support, but this is currently a nightly-only feature.
Examples
TODO:
- simple benchmarking with criterion
- async benchmarking with criterion
- benchmarking published to bencher
Valgrind
- idea: repeatable measurements (on same architecture).
Flamegraph
Debugging Performance
So, what do you do if you notice that your Rust code is not performing well? There are some common issues you might run into:
- Build mode: Are you building your code in release mode (eg.
cargo build --release
)? It makes a large difference for Rust projects. - Optimization level: Have you changed the optimization level, for example to optimize for size rather than speed? This can also make a large difference.
- Link-time optimization: Have you tried enabling
lto
in your compilation profile? - Build target: Are you building for musl libc instead of glibc (eg.
--target x86_64-unknown-linux-musl
)? Musl tends to produce slower code. - Allocator: Is your application allocation-heavy? Then try using
jemallocator
, it might give you a performance boost. - Data structures: Have you tried using different data structures? For
example, the hashbrown crate
has a
HashMap
implementation that is significantly faster than the standard library.
If these didn’t fix your performance issues, the next step to do is to find out why your performance isn’t good. When it comes to improving performance, the best thing to do is to be guided by data rather than intuition. There are many microoptimizations you can do in your code that lead to negligible benefits. Letting yourself be guided by data allows you to focus on the most important optimizations, this is known as Amdahl’s law.
Visualizing Performance
To get an understanding of where you are losing performance, you want to get some insight into which code in your program is responsible for the majority of the runtime. Doing this guides you to where you should focus your attention towards when trying optimization approaches.
cargo-flamegraph is a Cargo subcommand that lets you visualize what code in your project is taking up the majority of the runtime.
Reading
This book summarizes various approaches of benchmarking and profiling code, and offers some suggestions to use to improve performance.
The Criterion Book explains how to get started using Criterion, and what features it has.
Benchmark It! by Ryan James Spencer
Ryan argues in this blog post that you should benchmark code. He said that users can feel performance and you should care about it. He explains how to get started doing performance benchmarkis in Rust using criterion.
This blog post from Bencher explains the concept of continuous benchmarking. It also talks about some myths surrounding benchmarking, for example benchmarking in CI.
Continuous benchmarking for rustls by Adolfo Ochagavía
Adolfo explains in this blog post how he was able to implement continuous
benchmarking for the rustls library, and how he was able to leverage this
to find performance regressions easily. He explains that using cachegrind
was
instrumental, because it is able to count CPU instructions and easily diff them
per function for different benchmark runs, which allows for tracking down which
function introduced a regression.
Making slow Rust code fast by Patrick Freed
Guidelines on Benchmarking and Rust
Benchmarking and analyzing Rust performance with Criterion and iai
Benchmarking Rust code using Criterion-rs
Windtunnel CI
https://lib.rs/crates/iai-callgrind
https://github.com/bheisler/iai
https://www.magiroux.com/rust-jemalloc-profiling/
https://kobzol.github.io/rust/rustc/2023/08/18/rustc-benchmark-suite.html
https://blog.anp.lol/rust/2016/07/24/profiling-rust-perf-flamegraph/
Benchmarking in The Rust Performance Book
Memory Usage
Generally, Rust programs have three kinds of memory:
- Static memory: Allocated at program startup, fixed size. Used for global statics.
- Stack: Allocated at program startup, fixed size. Used to store local variables in function calls.
- Heap: Allocated dynamically during program execution.
It is not too difficult to estimate static memory and stack memory, because you
can measure the sizes of the types stored in them, for example using
std::mem::size_of()
. However, how do you measure memory that is
allocated dynamically? You might want to do this because you want to evaluate
different data structures, or you want to evaluate the impact on memory
usage a code change has.
To do this, you can use some tools that measure externally. For example, Valgrind and its Dynamic Heap Analysis Tool let you capture all allocations, and later examine them to see where they came from, and which code accessed the memory.
Another strategy is to measure internally. This relies on the fact that Rust
allows you to override the global memory allocator that is used by implementing
std::alloc::GlobalAlloc
. By implementing this trait and
setting it as the #[global_allocator]
, you can intercept allocation and
deallocation requests.
There are some libraries that have helpers to let you do this. This section discusses how they work and how they can help you.
DHAT
dhat-rs tries to achieve the same functionality as Valgrind’s DHAT.
Examples
Tracing Allocator
tracking-allocator is a replacement allocator that allows you to implement tracing hooks to count memory usage. It does not perform the actual allocation, this is deferred to the system allocator. But it does allow you to measure the peak memory usage of code sections.
Examples
Reading
Heap Allocations in The Rust Performance Books
In this chapter, strategies for profiling and optimizing heap memory usage is discussed.
Allocator Designs by Philipp Oppermann
Philipp explains different designs of allocators, and shows you how you can implement them in Rust. This is good background knowledge to have if you want to learn more about how allocators work and how they track and manage allocations. It can also be useful if you want fine-grained control over how memory is allocated, for example if you want to use an arena-style allocator for a specific data structure.
Building
Usually, building a Rust project is as simple as running the appropriate Cargo command, and everything just works:
cargo build --release
However, doing builds on a larger scale can present with some more challenges. For example, always building the same dependencies in CI can present some challenges. Some projects want to provide builds for multiple architectures.
This chapter discusses some issues you might run into when building Rust code in your project, and strategies for how you might solve that.
Reading
Tips For Faster Rust Compile Times by Matthias Endler
Matthias goes through and extensive list of tips for getting faster Rust compile times. These include making sure your toolchain is up-to-date, enabling the parallel compiler frontend, removing unused dependencies, debugging dependency compile times, splitting large crates into smaller ones, optimizing workspaces, compilation caching, and many more.
Fast Rust Builds by Alex Kladov
Alex explains some strategies to speed up Rust compilation. He explains that the Rust programming language has prioritized execution speed and programmer productivity over compilation speed. He gives recommendation for how to setup your CI pipeline, pruning dependencies, what code styles lead to faster compilation times.
Stupidly effective ways to optimize Rust compile time by Tianxiao Shen
What part of Rust compilation is the bottleneck?
Optimizations
When you compile Rust code, you have some control over the compiler as to what it prioritizes when building your executables. Everything is a tradeoff, so when you prioritize one aspect, you might see a regression in another aspect. Common priorities are:
- Speed: You want your executables to run as fast as possible. This might lead to an increase in code size, because the compiler will use techniques like inlining or loop unrolling to achieve this.
- Binary size: You want your executables to be as small as possible, for example because you are targetting a resource-constrained platform like embedded microcontrollers with limited flash memory sizes, or you want to be able to easily distribute your binary. This might lead to a negative impact on performance.
Compilation Profiles
In general, the way you exercise control over this is by creating profiles. Every profile comes with a set of parameters that let you tweak how the compiler performs. Typically, when you make debug builds, your main priority is fast compilation times, so you are happy to sacrifice some runtime speed.
A profile definition looks like this:
[profile.release]
strip = true
opt-level = 3
Runtime speed
Binary Size
One of your customers deploys the Rust-based tool that you are developing on very small embedded systems which run in an industrial environment. They are severely resource-constrained, making Rust an ideal language to target them. However, the customer has started complaining that the binaries you are shipping him are getting quite large. You are wondering if there are some strategies you can use to reduce the size of the binaries.
There are some low-hanging fruits that can be configured to drastically reduce binary sized in Rust projects. Note that some of these have a cost, in that they lead to longer compile times (for release builds). There are also some structural decisions that can lead to smaller binary sizes.
Configuration
The simplest way to reduce code size is to set some configuration in the Cargo manifest.
[profile.release]
# Automatically strip symbols from the binary.
strip = true
opt-level = "z" # Optimize for size.
# Enable link-time optimization
lto = true
Dependencies
Sometimes, the binary size is caused by some dependencies that you are using.
To analyze this, cargo-bloat
can be used, which measures the
resulting binary and lists the amount that each dependency contributes to the
final binary size. In some cases, this can allow you to investigate if the
dependency could be replaced with a lighter one, or if there are any features
that could be disabled.
Structural
Rust’s use of generics means there is a lot of monomorphization.
TODO: Explain monomorphization and boxed trait objects
Reading
Min Sized Rust by John T. Hagen
This is a comprehensive guide to producing minimally sized binaries in Rust. It starts with some low-hanging fruits and ends at building the standard library from source to be able to do link-time optimization on it.
Thoughts on Rust bloat by Raph Levien
Article discussing binary bloat in Rust and strategies that might help.
Build Configuration in The Rust Performance Book
Type Sizes in The Rust Performance Book
Performance
Link-Time Optimizations
Performance-Guided Optimization
Post-Link Optimization
https://kobzol.github.io/rust/cargo/2023/07/28/rust-cargo-pgo.html https://doc.rust-lang.org/rustc/profile-guided-optimization.html https://blog.rust-lang.org/inside-rust/2020/11/11/exploring-pgo-for-the-rust-compiler.html https://github.com/Kobzol/cargo-pgo
Codegen
Rust is built on top of the LLVM compiler, which means it can make use of the optimizations and the wide range of backends. This is a good thing, Rust would not be able to support as many targets as it does without the LLVM underpinnings of its code generation.
However, LLVM is a rather heavy dependency. It is designed to produce fast binaries, not to produce binaries fast. This is a good property when building release binaries, however when you are actively developing on a project, you tend to care about a fast iteration loop more than getting the best performance out of your binaries (which, in this case, will mostly be unit tests).
Therefore, there is an advantage in switching to a different codegen backend during development (non-release builds), if that leads to faster iterations.
Cranelift
Cranelift started out as a library to help in implementing the JIT-based WebAssembly runtime wasmtime. However, due to the way in which it was built, it can be used in more than just this application. The Rust compiler team has adopted it as an alternative codegen backend, in addition to LLVM. Since it is relatively young, it is not as mature as LLVM. It does have some properties that make it very appealing in certain scenarios: because it focusses on generating binaries quickly, it tends to be faster than LLVM. This makes it useful for building in development code, where a fast iteration time is more important than good runtime performance.
In my testing, I was able to get around a 30% speedup by using the cranelift backend instead of the default LLVM backend, but your mileage may vary.
Example
Reading
Cranelift code generation comes to Rust by Daroc Alden
Daroc announces the availability of the Cranelift compiler backend for Rust. He explains the history behind the Cranelift project, having been developed for the Wasmtime runtime, and that it can generate code faster than the LLVM project, at the expense of doing less optimizations.
Caching
While Rust does have a local build cache in the target
folder, you notice
that this is not always useful. Especially in the CI system, the entire project
is always rebuilt. It makes you wonder if there is some way to cache the build
artifacts in a global cache that your team and the CI server can make use of.
Caching build artifacts is a large quality of life improvement: typically, the dependencies do not change too much, and not all of the crates in your project change all the time either. With a good build cache, compiling the project can become very fast.
If you are using a Build System, you may get this for free: Bazel, Buck2 and Nix all support caching compilations.
A simple tool to deploy from the Rust ecosystem is [sccache
][], which allows
you to cache compilation outputs in various storage options, such as a
memcached instance or a cloud storage bucket.
sccache
sccache
is a tool that allows you to cache built artifacts in the cloud.
It allows you to get faster builds, however it also comes with the cost
of having to setup and maintain a storage bucket and access tokens for it.
Reading
Linking
When examining the compile times, you notice that the majority of the build time during development is spent on the linking stage.
Rust link times can be large, especially for bigger projects. They can easily get to and exceed the minute mark. Typically, there are two reasons why this happens: the first one is that there is a lot to link, a big Rust backend project can easily have ~700 dependencies that all need to be linked together to create a single executable.
In debug mode, Rust emits a lot of debug information, a Rust backend service can easily have binaries which are 200 MB large, the majority of which is debug information. All of this data needs to pass through the linker, and linkers are not necessarily fast, the majority of them are essentially single-threaded.
Stripping Symbols
Rust emits a lot of debug information, which can bloat binaries and put a lot of strain on the linkers being used. Turning off or reducing the amount of debug symbols produced leads to faster linking times.
To enable this, you can add a section such as this to the crate:
[profile.dev]
split-debuginfo = "packed"
Using Faster Linker
If reducing the amount of debug information does not help enough, another option is to use a different linker.
Using Parallel Linker
Finally, it is possible to use a linker that works in parallel and provides
much faster build times, at the cost of needing to install it manually and not
it not supporting all platforms.
The mold
linker does just that and works on Linux. It has a cousin,
sold
which works on macOS. Currently, it is the fastest linker on the
market.
Reading
5.1 Faster Linking by Luca Palmieri
Luca explains that when compiling Rust code, a large amount of time is spent
in the linking phase. The default linker that Rust uses, which depends on the
platform you are using, does a good job but is not the fastest. Luca explains
that LLVM’s lld
(on Windows and Linux) and Michael Eisel’s zld
(on macOS)
can get better link times and thereby cut down your compilation times.
Since writing this article, Apple has improved the performance of lld
, and
the author of zld
recommends using that
instead. That is why zld
is not mentioned in this
seciond.
Tips For Faster Rust Compile Times by Matthias Endler
Matthias goes through and extensive list of tips for getting faster Rust
compile times. These include making sure your toolchain is up-to-date,
enabling the parallel compiler frontend, removing unused dependencies,
debugging dependency compile times, splitting large crates into smaller ones,
optimizing workspaces, compilation caching, and many more. He recommends trying
mold
, lld
or zld
linkers to speed up your linking times.
Slightly faster linking for Rust by R. Tyler Croy
Resolving Rust Symbols by Shriram Balaji
Shriram explains how Rust compilation, and specifically the linking phase works. He uses diagrams to break down what the Rust compiler and LLVM do to allow you to compile your Rust code, what object files are and what they store, how name mangling works, and many other nuggets of information. This article is incredibly useful if you want to understand on a deep level how linking works.
In this blog article, the Rust team announces enabling the lld
linker by
default for nightly Rust on Linux targets.
Cross-Compiling
When you compile something, you usually create an executable that is able to run on whichever platform you are compiling it on. This is called native compilation. However, there are some situation where you want to be able to generate binaries for a different platform than the one that you are doing the compilation on. Doing this is called cross compilation.
Rust has a concept of target triples. This is how Rust identifies the
platform that you want the compiler to generate binaries for. For example, if
you are using Linux on an AMD64 system, your target triple might be
x86_64-unknown-linux-gnu
. From Rust’s point of view, cross compilation is
whenever you ask the compiler to generate executables for a different target
triple than your native one. The Rust compiler maintains a list of supported
targets.
In general, doing cross-compilation can be a bit of a hassle, but there are some good reasons to do so. For example:
- If you want to build code for a variant of your native triple, for example
using the
x86_64-unknown-linux-musl
target triple to generate an executable which uses MUSL libc rather than the default glibc. - If the target triple you are building for does not have a Rust toolchain.
For example, you cannot do a native compilation for
wasm32-unknown-unknown
. - If the target you are building for is severaly underpowered, such that you
cannot compile on it natively. This tends to be the case for embedded systems
such as
thumbv6m-none-eabi
. - If you want to create builds for multiple platforms, but you don’t want to purchase and maintain builder machines for every platform. Usually, having a fleet of Linux machines is cheaper and easier to maintain.
Rust uses LLVM to implement its compilation backend. LLVM is written in a modular way that makes it easy to add support for code generation for new targets. This means that Rust comes with good support for cross-compilation out-of-the-box.
Simple Cross-Compilation
If you want to use this, you first need to add support for the target you want to build for to your installed Rust toolchain. What this does is download a pre-built version of the Rust standard library for the target you specify. If you are using Rustup to manage your Rust toolchains, then doing so looks like this:
# add support for WebAssembly
rustup target add wasm32-unknown-unknown
# add support for building binaries with musl libc
rustup target add x86_64-unknown-linux-musl
When you then want to build your code, all you need to do is tell Cargo to
build for the different target. You can do this by passing it the --target
command-line option, or you can define default target in your
.cargo/config.toml
file.
cargo build --target wasm32-unknown-unknown
When you specify the target like this, Cargo will output the resulting binaries
in target/wasm32-unknown-unknown/debug/
rather than the default
target/debug
folder in your project.
Issues with cross-compilation
In some cases, this is all you need to do. However, there are three issues you may run into with this approach:
- Linking errors: You might run into linking errors, because while Rust can compile your crate for the target that you have requested, your system linker might not be able to deal with non-native object files.
- Native dependencies: If your applications links with any native libraries, then you need to have these native libraries compiled for the target that you are compiling for.
- Inability to run tests: You might not be able to execute the code you have just compiled, meaning that you cannot run your unit tests.
Linking issues usually manifest by an error like this, along with some output from the linker complaining about file in wrong format.
error: linking with `cc` failed: exit status: 1
In the rest of this section, I’ll show you how to deal with this. There is no real perfect solution, but several tools and approaches exist that should help you get this working. The main challenge is getting a linker that can handle the target executables, and getting the dependencies for the right target.
Debian
If you use Debian, or some derivative distribution, you can typically get cross-compilation (including native dependencies) working relatively easy by installing a few packages. Linux even lets you install a userspace emulator (for example QEMU) to allow you to run your binaries “as if” they were native, allowing you to run unit tests, for example.
Generally, this can be done in four steps:
- Install a compiler toolchain appropriate for your target. This is often
gcc-<triple>
, for examplegcc-aarch64-linux-gnu
. - Add the target as a dpkg architecture and install whatever native
dependencies your code needs in that architecture. For example, if you
require
libssl-dev
for ARM64, then you must installlibssl-dev:arm64
. - Set some environment variables to tell Cargo to use the correct linker, and
to allow
pkg-config
to find your native dependencies. - If you want to be able to run the executables, install the
qemu-user-binfmt
package. This will install binfmt handlers which will use QEMU to emulate any non-native executables, allowing you to run unit tests.
Example
Docker
You can use Docker to build and image which contains the right packages and environment variables to allow Rust to easily cross-compile your project for another target. This is often useful in CI, where you can build a Docker container from this and use it for the CI job where you cross-compile your code.
To allow Docker to run native dependencies, you can use the
multiarch/qemu-user-static
image, which you can set up like this. After you ran this, you have configured
Docker to be able to run non-native binaries, which stays enabled until a
reboot.
docker run --rm --privileged multiarch/qemu-user-static --reset -p yes
Example: Dockerfile for cross-compiling for ARM64
FROM rust
# install rustfmt and clippy
RUN rustup component add rustfmt
RUN rustup component add clippy
# install build-essential, pkg-config, cmake
RUN apt update && \
apt install -y build-essential pkg-config cmake && \
rm -rf /var/lib/apt/lists/*
# install arm64 cross-compiler
RUN dpkg --add-architecture arm64 && \
apt update && \
apt install -y \
gcc-aarch64-linux-gnu \
g++-aarch64-linux-gnu \
libssl-dev:arm64 && \
rm -rf /var/lib/apt/lists/*
# add arm32 target for rust
RUN rustup target add aarch64-unknown-linux-gnu
# tell rust to use this linker
ENV CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=/usr/bin/aarch64-linux-gnu-gcc
# set pkg-config libdir to allow it to link aarch libraries
ENV PKG_CONFIG_LIBDIR=/usr/lib/aarch64-linux-gnu/pkgconfig
ENV PKG_CONFIG_ALLOW_CROSS=true
Example: Dockerfile for cross-compiling for ARM32
FROM rust
# install rustfmt and clippy
RUN rustup component add rustfmt
RUN rustup component add clippy
# install build-essential, pkg-config, cmake
RUN apt update && \
apt install -y build-essential pkg-config cmake && \
rm -rf /var/lib/apt/lists/*
# install arm32 cross-compiler
RUN dpkg --add-architecture armhf && \
apt update && \
apt install -y \
gcc-arm-linux-gnueabihf \
g++-arm-linux-gnueabihf \
libssl-dev:armhf && \
rm -rf /var/lib/apt/lists/*
# add arm32 target for rust
RUN rustup target add arm-unknown-linux-gnueabihf
# tell rust to use this linker
ENV CARGO_TARGET_ARM_UNKNOWN_LINUX_GNUEABIHF_LINKER=/usr/bin/arm-linux-gnueabihf-gcc
# set pkg-config libdir to allow it to link aarch libraries
ENV PKG_CONFIG_LIBDIR=/usr/lib/arm-linux-gnueabihf/pkgconfig
ENV PKG_CONFIG_ALLOW_CROSS=true
Example: Dockerfile for cross-compiling for RISC-V
FROM rust
# install rustfmt and clippy
RUN rustup component add rustfmt
RUN rustup component add clippy
# install build-essential, pkg-config, cmake
RUN apt update && \
apt install -y build-essential pkg-config cmake && \
rm -rf /var/lib/apt/lists/*
# install arm32 cross-compiler
RUN apt update && \
apt install -y debian-ports-archive-keyring && \
dpkg --add-architecture riscv64 && \
echo "deb [arch=riscv64] http://deb.debian.org/debian-ports sid main" >> /etc/apt/sources.list && \
apt update && \
apt install -y \
gcc-riscv64-linux-gnu \
g++-riscv64-linux-gnu && \
rm -rf /var/lib/apt/lists/*
# add arm32 target for rust
RUN rustup target add riscv64gc-unknown-linux-gnu
# tell rust to use this linker
ENV CARGO_TARGET_RISCV64GC_UNKNOWN_LINUX_GNU_LINKER=/usr/bin/riscv64-linux-gnu-gcc
# set pkg-config libdir to allow it to link aarch libraries
ENV PKG_CONFIG_LIBDIR=/usr/lib/riscv64-linux-gnu/pkgconfig
ENV PKG_CONFIG_ALLOW_CROSS=true
Nix
You can use Nix to implement cross-compilation.
Todo
Cargo Zigbuild
cargo-zigbuild is a Cargo subcommand that lets you build Rust applications while using Zig as the linker.
Cross
Cross advertises itself as a “zero-setup” tool for cross-compilation and cross-testing of Rust crates. Under the hood, it uses Docker containers to run the compilation steps with the right toolchains and libraries preinstalled.
The idea with it is that it acts as a replacement for Cargo.
Reading
Platform Support in The rustc book
This chapter lists all targets which are supported by the Rust toolchain, along with notes explaining what the targets are, and which tools are required for build for them. It also gives information on the level of support for each target.
Guide to cross-compilation in Rust by Greg Stoll
In this article, Greg explains how to cross-compile Rust crates using the cross project.
Zig makes Rust cross-compilation just work by Max Hollmann
Max explains how you can use Zig to simplify cross-compilation for Rust. Zig comes with built-in support for compiling and linking for various targets out-of-the-box, which means you don’t need to install separate toolchains for each target.
LLVM in The Architecture of Open Source Applications (Volume 1)
Chris explains the architecture of LLVM, and how its design choices make it easy to use it as a library to build compilers, and to target a variety of different targets. LLVM decouples the various stages of the compiler and uses a serialisation format to communicate between them.
Cross-compilation in The rustup book
This chapter explains the basics for how to do cross-compilation with rustup.
Configuration (target section) in The Cargo Book
This chapter in the book explains how you can configure Cargo to do cross-compilation, by telling it which linker, rustflags and runner to use.
Documentation
Writing software is as much communicating to other humans as it is communicating with the machine we expect it to run in.
In part, documentation solves the \( O(n^2) \) communication complexity issue: if you have three developers which each own some part of the project, then you can afford to have them communicate with each other to understand how things work and skip the work of documenting it propertly. However, this does not scale to large teams: if you have 100 developers that each own some components, and they all need to talk to each other to understand each other’s work (and no documentation), then your developers will spend more time asking how things work than getting things done (or, worse, reimplement things because it is easier than figuring out how the original thing was supposed to work).
In other words, in a commercial project, having great documentation saves you a lot of cost in the long run. It makes the difference whether you need a year-long onboarding programme for new hires until they hit their productivity peak, because they don’t know how things work and there is not central place to find out, or whether they can hit the ground running and achieve baseline productivity within weeks or a month.
In the context of an open-source project, documentation saves you cost as well, but in a different way. Projects that have good documentation tend to be more discoverable, and the developers need to spend less time giving users support or explaining how to do things. That is the power of words: you write them just once, but they can be read many (millions, thousands) of times.
In some way, we can look at the Rust project for an example of exceptional documentation. The Rust community has put a lot of effort into making sure that there is ample documentation, which helps people get started, get things done and it even makes it easier for people to contribute to the project. The Rust project has many kinds of documentation:
- The Rust Book documents the language itself, helping people get up to speed.
- Standard library documentation documents the standard library
- docs.rs hosts documentation for all crates which are published on crates.io
- Books for various parts of the Rust toolchain (rustc, cargo, clippy)
- Books for various use-cases (embedded, webassembly, command-line applications)
- Books from some popular framework crates (Criterion, Tokio, Serde)
With this breadth of documentation, people new to the Rust language can quickly get to high-quality explanations for whatever it is they are trying to do. Having a service publish documentation for crates also has another effect: it forces crate authors to put good documentation into their crates, because a lack of such is immediately visible. This alone has a strong positive effect on the crate ecosystem.
When you write documentation, the most important question you have to ask is: who are you writing documentation for? What are they trying to do? If you know who you are writing documentation for, it tells you what style you have to write it in, what knowledge you can expect, and into what depth you can go.
Generally, you will have two target audiences:
- End-users: they want to evaluate if your project is fit to solve the problem they are trying to solve, and find out how they can use it.
- Developers: they are trying to understand how your project works, because they want to contribute, or maybe they want to fix an issue with it.
The definition of what your end users are depends on what kind of project you are working on. If you are writing a library, then your end-user will be other developers who consume this library. If you are writing an application, then your end-users will be people who install and use the application.
End-user Documentation
End-users are less interested in the internals (how things work) and more interested in how they can use your project to solve a particular problem. They want to be able to quickly find out if your project is useful to them, and how they can use it. Once they have decided to use your project, they will want an easy way to find out what changed between releases (in terms of features or APIs).
End-user documentation should contain:
- Explanation of what problems your project solves, and what limitations it might have
- Instruction of how to install your application (or compile your library)
- Instruction of how to configure your application (or library)
- Examples or guides on how to use it for specific use-cases
- Changelog of additions, deprecations or removals of features or APIs between releases
- Code-level documentation (if it is a library)
Often times, this documentation exists in a Readme file and/or a web book that is hosted by the project.
Developer documentation
Developers are programmers that want to understand how your project works. Typically, this is because they are working on it, they want to implement a feature, they want to improve it, or they want to fix a bug with it. They need to be able to easily clone and compile it locally, run unit tests to see if they changes broke anything, run benchmarks to check if they changes introduced a regression. They need to be able to submit a patch (merge request) with their changes. Some developers (maintainers) also need to be able to release new versions of the code.
Developer documentation should contain:
- Instructions on how to fetch the code (git clone)
- Architecture of the project (diagram)
- Explanation of why the archiecture is the way it is
- High-level explanation of how the code works
- Instructions on how to compile the library
- Instructions on how to run tests: unit tests, integration tests, benchmarks, fuzzing tests
- Style guide for code, commits, documentation
- Documentation of processes (how to submit a patch, how to cut a release)
- Code-level documentation (APIs, data structures, invariants)
Tools to write and publish documentation
In the sections of this chapter, I will go though some of the functionality Rust has built-in for generating documentation for software projects, and some tools that are useful for writing the kinds of end-user and developer documentation outlined here. I will focus on:
- Types of documentation documents (readme files, code comments, standalone documentation)
- Tools to write effective documentation (diagramming tools, documentation generators)
- Patterns for documentation (changelogs, patterns for documenting architecture)
Reading
Documentation in Software Engineering at Google
Tom explains why documentation is needed for software projects to scale, because they communicate important information about how things work and why they work the way they do. They save valuable engineering time by giving engineers access to the information they need quickly, without needing to look into the code. He explains what good documentation looks like, and what Google does to keep it accurate and of high quality.
Trees, maps, and theorems explains how to get messages across optimally in written documents, oral presentations, graphical displays, and more.
Readme
The purpose of a README is for people to get a very brief introduction to what you project does. For open-source projects it is essential, when people decide if your crate solves the issue they are trying to solve. It does not need to be a comprehensive documentation document, rather a very dense summary that contains some vital pieces of information of what your crate does, how it compares to other crates that achieve similar goals, and what limitations it has.
There are some common patterns that make for useful README files, and this chapter will attempt to illustrate them.
Badges
Badges are little images that you can embed into your README that show up-to-date information on your Rust project. These are useful because they do not need need to be updated manually.
Generally, you can put them on your README like this:
# Project Name
[![crates.io version](https://img.shields.io/crates/v/imstr.svg)](https://crates.io/crates/imstr)
[![docs.rs documentation](https://img.shields.io/docsrs/imstr)](https://docs.rs/imstr)
Common badges for Rust crates
These badges pull information on crates published on crates.io. By definition, these will not pull data from source control, but rather from whatever is published. They render information such as the most recent version, status of automatically built documentation, download counts, and health checks for dependencies.
Crate:
Badge | Markdown |
---|---|
| |
| |
| |
| |
| |
| |
|
Generating a readme file from crate-level documentation
The Readme section shows some tools that you can use to generate a README file from crate-level documentation.
Diagrams
There are some useful tools that can be used to draw such diagrams:
- TODO: show how to include in rustdoc/mdbook
draw.io
draw.io is a web-application that lets you draw diagrams. All of the diagrams in this book are made with it.
Examples
Excalidraw
PlantUML
Mermaid
-
https://brycemecum.com/2023/03/31/til-mermaid-tracing/
Reading
Code Documentation
Code-level documentation in Rust is almost always done using Rustdoc, which is an incredible piece of software that makes writing usable documentation several orders of magnitude easier than other documentation tools that I have worked with.
Rustdoc
Rustdoc works by parsing documentation comments that are left in the code and turning them into a pretty, searchable HTML output. It understands Markdown for simple formatting and is able to link things. There is also a service that builds and renders documentation for all published Rust crates, which is docs.rs.
#![allow(unused)] fn main() { /// This is a documentation comment. /// /// In here, it is also possible to link to other types, such as [`Vec`]. pub fn my_function() { todo!() } }
What is important about the Rustdoc documentation is that it is only useful if
it is published somewhere. For that reason, I suggest publishing it in the CI
on every merge to master
(or whatever the unstable branch name is that is
used) to some location where it can be viewed by the team.
Example
TODO
Usage
TODO
Enforcing
You can enforce that all public API memebers have rustdoc annotations
using the missing_docs
lint. For example this annotation will turn all
places where documentation is missing into compile-time warnings:
#![allow(unused)] #![warn(missing_docs)] fn main() { }
This is recommended for libraries, as documentation is quite important for downstream users.
Reading
The rustdoc book explains what rustdoc is, how the documentation is structured and how you can write documentation for your Rust code using the built-in documentation annotations.
Project Documentation
While having code-level documentation is useful for some cases, another important aspect is having high-level documentation which explains:
- System architecture
- Crate architecture
- How to launch and use things
Not explicitly documenting these somewhere leads to having projects where this important context lives in a few people’s brains. It can block others in the team from making changes by not knowing how things fit together.
mdBook
In the Rust community, the mdBook tool has become the standard way to write this kind of documentation. It consumes the documentation in the form of Markdown and renders it nicely into a HTML book.
Ideally, inside every project you will want to have some kind of book/
folder
containing this high-level documentation. You can even have multiple books or
sections, targeted at different audiences.
You can install mdbook
like this:
cargo install mdbook
You can then initialize a new project like this:
mdbook init
Finally, you can build or serve your project locally like this:
mdbook build
mdbook serve
Examples
Usage
Reading
This is the official book of the mdBook project. It explains all the various features that mdBook has, and how to use them.
A list of third-party plugins for mdBook, contains various preprocessors and backends.
Architecture
Perhaps the most important property of software is the architecture. While the implementation of functions can easily be changed or optimized, rearchitecting software, especially collections of systems, is typically a slow and expensive endeavour.
Software architecture is important for developers to understand. When joining a new team or project, the very first thing to figure out is how the system works on a high level. For developers familiar with the software, it is easy to note down the high-level architecture, but for people unfamiliar with the code base it is a slow and error-prone process to wade through the code and try to understand how everything fits together, how components communicate and how data travels through each component.
If you do not have time to properly document software, the least you should do is document the high-level architecture.
Publishing
- Markdown
- mdBook
Diagrams
It tends to be easier to show architecture rather than to explain it.
There are some useful tools that can be used to draw such diagrams:
draw.io
Excalidraw
PlantUML
Mermaid
Documenting Changes
Another important aspect to software architecture is documenting design decisions. This helps answer why the architecture is chosen the way it is. Having a process around this also helps collaboration, by giving team members the opportunity to give feedback on proposed design decisions, to find the best (or sometimes the least worst) way to achieve an intended outcome.
Reading
https://github.com/joelparkerhenderson/architecture-decision-record?ref=blog.pragmaticengineer.com
ARCHITECTURE.md by Alex Kladov
Alex argues in this article for adding a file named ARCHITECTURE.md
into
software repositories to document the architecture of the code base. He argues
that writing good documentation is hard, and it is not often done. But some
someone starting to work in an unfamiliar codebase, such a document with a
bird’s-eye view of the layout of the project is invaluable.
Architectural Decision Records
ADRs are a tool to record the reasoning behind architecture changes.
More Software Projects need Defenses of Design by Hillel Wayne
Hillel argues that many software projects have some design decisions that might look strange to an outsider. Many of these design decisions are for backwards compatibility, performance, inspiration by similar projects or other reasons that are not immediately obvious. For that reason, projects should have a document defending their design, giving important context and rationale as to why the decisions were made.
Software Architecture is Overrated, Clear and Simple Design is Underrated by Gergely Orosz
Gergely explains how software is architected in modern tech companies. He explains the effectiveness of diagrams in communicating architecture choices, without the need for formal processes such as UML diagrams. He argues having an informal, collaborative process to come up with architecture is better than having decisions be made by a software architect, because it makes it easier to challenge ideas, and that the most important aspect of good architecture is simplicity.
Architecture diagrams should be code by Brian McKenna
Brian explains that different people have different views of the architecture of a complex system, often influenced by which part of the system they work on. He argues that architecture diagrams can also quickly go out of sync with reality, as the system evolves. He argues for writing architecture diagrams as code, using the C4 model and PlantUML, or in his case a Haskell program which produces PlantUML output. That way, these diagrams can be kept in version control and updated as part of development.
Effective Design Docs by Roman Kashitsyn
TODO
Design
Reading
https://ntietz.com/blog/reasons-to-write-design-docs/
https://dzone.com/articles/how-to-write-rfcs-for-open-source-projects
https://opensource.com/article/17/9/6-lessons-rfcs
https://rust-lang.github.io/rfcs/
https://philcalcado.com/2018/11/19/a_structured_rfc_process.html
https://adr.github.io/
https://cloud.google.com/architecture/architecture-decision-records
https://github.com/joelparkerhenderson/architecture-decision-record
https://docs.aws.amazon.com/prescriptive-guidance/latest/architectural-decision-records/adr-process.html
https://learn.microsoft.com/en-us/azure/well-architected/architect-role/architecture-decision-record
Examples
There are some things that I consider to be part of documentation even though technically, they are not documentation. Those two are unit tests and examples.
Reading
Add examples to your Rust libraries by Karol Kuczmarski
Karol explains the need work working examples when using an unfamiliar
library, and how Rust supports this out-of-the-box with its support for
examples. She explains that Rust treats examples as documentation, and builds
them when you run cargo test
. She argues that all Rust projects should come
with good examples, because they make using the code easier and help people get
started.
Releasing
Releasing is the process of publishing new versions of software. Typically, there is some process around it, which includes throrough testing to ensure that it is bug-free, and the publishing of artefacts (binaries).
The release process for every project is different. Some projects combine releases with deployments of the updated software, which is called Continuous Deployment. Some projects create releases when there are enough new features to warrant a new version, others use a fixed calendar-based release cycle where a new version is released at fixed schedules.
Some of the challenges when it comes to releases are:
- How do you communicate breaking changes? Often times, breaking changes are encoded in the release version using Semantic versioning, and a changelog is published that documents all changes for downstream users.
- How do you make the software usable? This includes publishing binaries (for applications), publishing packages (Debian packages, Flatpak files, RPM packages), publishing it to Crate registries (for libraries), publishing Docker containers.
- How do you automate the release process so that it runs smoothly with little human intervention?
This section addresses some of these questions.
Changelog
When it comes to creating releases, an important aspect of doing so is communicating the changes that have occured in the new version. This is true regardless of whether your Rust project is a library or whether it is a product.
In the open-source world, this is typically done using a changelog, which is typically simply a file checked into the repository that keeps a list of the important changes for downstream users of the software for every version that is released.
Reading
Keep A Changelog by Olivier Lacan
Keep A Changelog is a specification for how to structure changelogs. It attempts to standardize their structure and make them useful, and explains why they are useful.
Versioning
Rust comes with built-in support for Semantic Versioning, and you should use it unless you have a strong reason not to.
Semantic versioning encodes information into the version string of the application.
A version looks like 1.2.3
. These three numbers are called major, minor and patch.
When you make a change that only fixes a bug, but does not change the interface, you increment the last number, the patch number. These releases are always safe to apply.
When you add a new interface that does not break existing users, you increment the second number, the minor number.
When you change an interface in a backwards-incompatible way, you increment the first number, the major number.
Cargo understands semantic versioning and lets you express dependency versions as bounds.
If you want to make a prerelease of an upcoming version, for example to let
users test it (but not let Cargo choose it unless explicitly requested), you
can add a suffix. For example 1.3.0-rc.0
would be a prerelease called
rc.0
. The numbering there exists so that you can make multiple prereleases to
fix bugs, before you release version 1.3.0
properly.
There is some tooling you can use to enforce proper versioning, discussed in Semantic Versioning.
Crate Registry
The default way to release Rust crates (libraries and binaries) is to a Crate registry. The default crate registry for Rust is crates.io, with 150k crates published, almost a million crate versions and (at the time of writing) 77 million crate downloads, it is the largest repository of Rust code.
Publishing your crates on there is free, and it allows others to use the
libraries you have written as simple as running cargo add your-library
.
If you publish your code there, the documentation for it is also generated
automatically and published on docs.rs, Rust’s public crate documentation
browser.
Any binary crates you have published there are easily installable by other
Rust users with cargo install
. For example, if you want to install ripgrep,
a useful tool for searching through local git repositories, you can do so simply
by running:
cargo install ripgrep
But in a commerical setting, you may have some internal crates that you want to share, but not publically. Using a crate repository makes for a more pleasant experience than having direct git dependencies, because Cargo supports semantic versioning (but this does not work with git dependencies). RFC 2141 specifies how this works, and today there are some commercial private crate registries that you can use, or you can even host your own registry.
Rust Crate Registry
Crates.io is the public Rust package index. It is free and used by the Rust community to share libraries and tooling. It integrates with docs.rs to automatically build and host documentation for any crates that are published to it.
To use it, all you need is a GitHub account. You can log in on their website and generate an API token. With that, you can log in using Cargo:
cargo login <api-token>
Once you are ready to publish your crate, you can do so using Cargo:
cargo publish
If you accidentally published a version that you did not intend to publish, you can yank it. Yanking does not delete it, to avoid situations like the left-pad disaster, where a developer deleted a package from NPM that a lot of JavaScript libraries relied upon, temporarily breaking the internet.
cargo yank
In order to be able to publish your package, it must contain some required metadata. See Publishing on crates.io for more details on what is required and how you can manage your packages.
Shipyard
Shipyard is a private cargo registry service. It replicates
JFrog
Kellnr
Reading
Chapter 14.2: Publishing to Crates.io in The Rust Book
In this section of the Rust book, it shows you how you can write a Rust crate and publish it on Rust’s crate index, crates.io.
Using the Shipyard private crate registry with Docker by Amos Wenger
Amos explains how you can publish your crates to a private crate registry hosted by Shipyard. He shows how you can configure Cargo to authenticate with Shipyard, and how to push packages to it both locally and from CI.
Containerize
To deploy your Rust services, you have some Dockerfile
s in the repository.
Sometimes, developers like to build these locally for testing purposes.
However, you have received complaints that these are very slow, as there is no
caching going on. This makes you wonder: what is a good way to build Rust
projects with Docker while making use of caching?
Deploying code using Docker is quite popular, as there is a lot of great tooling around deploying, monitoring and scaling containers. The downside is that usually Docker builds are hermetic, meaning that they do not have access to stateful things such as a target folder containing a build cache.
There are some solutions to getting Docker to play nice with Rust.
Docker Cache
https://gist.github.com/noelbundick/6922d26667616e2ba5c3aff59f0824cd
cargo-chef
cargo-chef
is a Cargo subcommand which is designed to help with
making use of Docker’s caching capabilities. It works by splitting your
Dockerfile build process into two stages: one where only the dependencies are
fetched and built, and the second one where the project is built. The advantage
of doing this split is that the dependency layer can be cached and reused as it
rarely changes.
Packaging
When you release software, packaging it often a good way to make it easy to deploy, either by end-users or by other systems. Packaging your software allows you to bundle the executables and libraries with assets (such as man pages, configuration files, documentation or runtime data) into a single file that contains metadata such as the version of your software and runtime dependencies it has. Doing so allows not only for easy installation, but it also makes upgrades easy, as package managers will take care of removing the old version and installing the new file.
Popular package formats are:
- Debian packages
- RPM packages
- FlatPak
- AppImage
The Rust ecosystem has some useful tools to package software. Typically, this is useful for packaging it for Linux systems, as many other operating systems do not have a useful package manager.
Debian Package
If you want to create releases for Linux users, specifically ones that use the APT package manager (which includes Debian and it’s derivatives, such as Ubuntu and Linux Mint), then cargo-deb can help you do just that.
The advantage of releasing builds as Debian packages over tarballs is that it contains metadata, and makes it easy to install and remove packages for end-users. When running software from tarballs, often the installation involves manually copying files to specific folders, without an easy way to later remove the software.
If you want to support automatic updates, you can even host your own APT repository.
Configuration
The cargo-deb tool reads and understands your Cargo.toml
metadata and can
automatically figure out which binaries your project produces, and will add
them to the package. However, Debian packages typically have more metadata than
is captured in there by default, for example paths to assets that need to be
installed or dependencies to other packages.
To give this data to the tool, you can capture it under the
package.metadata.deb
key in the project definition. Here is an example for
what that looks like:
[package.metadata.deb]
maintainer = "Michael Aaron Murphy <mmstickman@gmail.com>"
copyright = "2017, Michael Aaron Murphy <mmstickman@gmail.com>"
license-file = ["LICENSE", "4"]
extended-description = """\
A simple subcommand for the Cargo package manager for \
building Debian packages from Rust projects."""
depends = "$auto"
section = "utility"
priority = "optional"
assets = [
["target/release/cargo-deb", "usr/bin/", "755"],
["README.md", "usr/share/doc/cargo-deb/README", "644"],
]
Once you have set up the metadata, creating your Debian package is as easy as
running cargo deb
in the repository.
In addition to this, the tool also allows you to define variants, has an integration with systemd to allow you to install systemd units, and even supports cross-compilation for other architectures.
Examples
TODO
RPM Package
https://github.com/cat-in-136/cargo-generate-rpm
Flatpak
AppImage
Tools
This chapter showcases some tools the Rust community has come up with that can help you in maintaining, navigating or developing Rust software projects.
Readme
Rust Tooling: 8 tools that will increase your productivity by Joshua Mo
Joshua showcases and explains some tools for Rust developers that can increase your productivity, and gives examples for how they can be used.
This is a list of awesome tools written in Rust. It showcases tools in various categories, from general-purpose command-line tools to tools specifically for Rust development, maintenance or navigation.
This is a list of useful plugins for Cargo, sorted by their popularity (as measured by the download count from the Rust crates registry).
Code Search
When navigating large or unfamiliar code bases, it can often be useful to search over the entire code base to find some patterns. This could be finding any places where a specific crate is used, or finding some code patterns.
Ripgrep
ripgrep is a command-line tool for searching code bases using regular expressions. It is very fast, making use of Rust’s powerful regex crate.
It understands git repositories and will respect .gitignore
files, making it
particularly suitable for search software projects. Visual Studio Code’s search
functionality uses it behind the
scenes.
You can install it with Cargo:
cargo install ripgrep
Running this will install the rg
binary, which you can use to search code
projects. You can then use it to search for patterns.
$ rg uuid::
database/src/main.rs
8:use uuid::Uuid;
protocol/src/types.rs
10:use uuid::Uuid;
common/src/entities.rs
12:use uuid::Uuid;
Reading
ripgrep is faster than {grep, ag, git grep, ucg, pt, sift} by Andrew Gallant
Andrew, the author of ripgrep, introduces the tool in this article, explains how it works and compares it to some common similar tools used by developers, showing how it performs better and how it excels at dealing with Unicode, something other tools struggle with.
Task Runners
Often times, it can be useful to automate tasks in code projects. This could involve:
- Automating processes, such as creating releases of the project,
- Launching services required for running or testing the software, such as a database
- Generating documentation
- Maintenance commands, such as checking for unused dependencies
These tasks can often be captured as command-line scripts of tool invocations. To make developer’s lives easier, it can be useful to use some tool to make it easy to run these tasks. This is what task runners do, they allow you to define a list of preset tasks along with the commands that need to be run, and give developers an easy way to run them.
Some IDEs are even able to parse these definitions and offer some graphical interface for invoking them.
A common pattern that can be found in open-source software is the use of
Makefiles to automate tasks. However, Makefiles are
often not ideal, requiring some workarounds such as making the tasks as
.PHONY
to work.
High-level build system such as Bazel typically have some built-in support for creating custom tasks that can be run, and don’t benefit as much from task runners described in this section.
Just
Just is a simple task runner with a syntax similar to Makefiles, but simpler and with some extensions to allow passing arguments to tasks and to use comments for self-documenting tasks.
To get started, you can install it using Cargo:
cargo install just
To use it, all you need to do is create a Justfile
in your project, which
contains all of the tasks. A sample justfile might look like this:
# release this version
release:
just test
cargo publish
# run unit and integration tests, starts database before tests
test:
docker start database
cargo test
docker stop database
With this definition, you can run the tasks like this:
just release
just test
You can also list all available tasks:
just --list
Just has support for tasks taking arguments, integrations with various IDEs, some built-in functions, support for variables and much more. The Just Programmer’s Manual describes all of the features it has to offer.
Cargo Make
cargo-make
is a Rust task runner and build tool. It lets you define tasks in
a Makefile.toml
. It supports task dependencies and has some built-in features
that are useful in Rust projects, such as the ability to install crates.
You can install it using Cargo:
cargo install cargo-make
Once installed, you can create a Makefile.toml
in your repository to define
the tasks you want it to do.
# generate coverage, will install cargo-llvm-cov if it doesn't exist
[tasks.coverage]
install_crate = "cargo-llvm-cov"
command = "cargo"
args = ["llvm-cov", "--html"]
With this definition, running the coverage
task will ensure that
cargo-llvm-cov
is installed, and run it to produce a HTML coverage report.
cargo make coverage
Tasks can also have dependencies on other tasks, and these dependencies can be set conditionally, such as per-platform, allowing you to write platform-specific or environment-specific implementations for tasks.
Cargo XTask
Cargo XTask
is less of a tool and more a pattern for defining bespoke tasks
for Rust projects. The advantage of it is that you write the tasks themselves
in Rust, and cargo-xtask
is only used to run them.
Reading
Just use just by Tonio Gela
Tonio explains what Just is, and how you can use it. He demonstrates the features it has with some examples.
Automating your Rust workflows with cargo-make by Sagie Gur-Ari
Sagie, the author of cargo-make, explains how you can use it to automate your Rust workflows and gives some examples.
Make your own make by Alex Kladov
Alex explains the idea of using Rust itself for the automation of steps in
this article. This idea is what cargo-xtask
implements.
Readme
Open-source Rust projects have several places for documentation. Often times
they have a README file that contains some general overview of wwhat the crate
does, as well as some crate-level documentation in the main.rs
or lib.rs
file. In many cases the content for these two is similar, or even the same.
For ease of maintenance, it can be beneficial to keep the two in sync.
Cargo Readme
cargo-readme
is a tool that allows you to generate a README file from the
crate-level documentation strings of your Rust crate.
You can install it using Cargo:
cargo install cargo-readme
Cargo Rdme
Watch Files
While you are developing, having a quick feedback loop can be invaluable. What this means is that the time between you writing some code, and getting feedback if it is syntactically incorrect or if it breaks unit tests should be as short as possible.
Often times, the development environment you use can give you fast feedback on syntaxtical issues. Some even let you define shortcuts for quickly running unit tests or other actions.
Another approach is to use a tool that watches your code for changes, and runs some command whenever you make a change. There are some situations where this is useful:
- You want to run some custom tests on the code
- You want to rebuild and relaunch your application, so that you can test it interactively.
If you build Web Frontends in Rust and use the Trunk tool to build them, you will get this for free. When you run Trunk in serve mode, it will automatically rebuild your frontend and reload your browser whenever it detects a change, to minimze your development feedback loop.
Cargo Watch
Cargo Watch is a generic tool you can use to watch your Rust projects and execute commands whenever a file changes.
You can install it using Cargo:
cargo install cargo-watch
By default, it will run cargo check
when a change is detected:
# run `cargo check` whenever files change
cargo watch
You can customize it to run any command you like. Using the -x
flag, you can tell
it to run any other Cargo subcommand. You can also directly give it a command to run.
cargo watch -x test
cargo watch -- just test
It also supports command chaining, where you specify multiple Cargo subcommands to run. When doing so, it will run each of them in the order you specify them, when they are successful.
cargo watch -x check -x test -x run
The repository and help text explain more commands that you can use, such as specifying which files to watch.
Reading
Chapter 1: Setup - Toolchains, IDEs, CI by Luca Palmieri
In this chapter of his book, Luca explains how to setup a real-life Rust
project. He explains that using cargo watch
can reduce the perceived
compilation time, because it triggers immediately after you change and
save a file.
Cargo Issue #9339: Add Cargo watch
In this issue on the Cargo repository, there is some discussion going on to incorporate file watching functionality natively into Cargo.
Expand Macros
On a high level, a macro is some code that generates code. In languages such as C or C++, they are expanded by the preprocessor in a step just before compilation happens. They are commonly used to reduce code repetition, avoid boilerplate.
Instead of relying on a preprocessor, the Rust compiler has built-in support for macros. It supports two kinds of macros: declarative macros and procedural macros. Declarative macros work as a kind of pattern-match-and-replace on tokens. They are fast and functional, but are limited in terms of what they can do. Procedural macros work by compiling a separate Rust program, which is fed the arguments of the macro and outputs Rust code that it is replaced with. They are more powerful, can do potentially non-deterministic things, but have higher overhead.
Declarative macros can be used to implement Domain-Specific Languages within
Rust. For example, the
json!
macro
allows you to write JSON within Rust, or the
html!
macro allows you to write HTML
within Rust. Procedural derive macros are often used to allow you to derive
traits for your types automatically. Commonly used examples are the Serialize
and Deserialize
derive macros from the
serde crate. Procedural
attribute macros such as
rocket::get
are used to
provide metadata for routing requests in the Rocket web backend framework.
Using macros, where appropriate, is good style because it allows you to reduce boilerplate code. At times, they can feel quite magic. However, there are downsides to relying on them heavily as well:
- When you use procedural macros, a separate Rust application needs to be built and run for the compilation, slowing down your compilations.
- Formatting often does not work within macro invocations. Some projects work around this by providing their own formatting tools that are able to do this, for example leptosfmt.
- Macros can be difficult to understand. Because macros are expanded at compile-time, it can be difficult to inspect or debug them, because you cannot see what code the macro expands to.
This section looks at how you can work around (3), by showing you how you can inspect what your code looks like after macro expansion.
Cargo Expand
cargo expand is a Cargo plugin that
allows you to view your code after macro expansion. In addition to performing
macro expansion, it will also run rustfmt
over the result (because the code
that macros expands to is often machine-generated and therefore unformatted)
and syntax-highlights the result.
You can install it simply using Cargo:
cargo install cargo-expand
To run it, simply run it as a Cargo subcommand within a Rust crate:
cargo expand
It has some command-line options that you can use to control the output options, for example turning off the syntax highlighting or selecting a different theme that plays nicer with your terminal color scheme.
Example: Inspecting your own macro
If you want to create a Vec<T>
, Rust has a built-in macro for doing so:
vec![]
. However, the same is not true for creating maps, such as
BTreeMap<T>
. You can work around this by creating your own macro:
#![allow(unused)] fn main() { macro_rules! btreemap { ( $($x:expr => $y:expr),* $(,)? ) => ({ let mut temp_map = std::collections::BTreeMap::new(); $( temp_map.insert($x, $y); )* temp_map }); } }
But how do you verify that this macro works correctly? Besides writing unit tests for it, you can write a small test program that uses this macro, for example:
fn main() { let mapping = btreemap!{ "joesmith" => "joe.smith@example.com", "djb" => "djb@example.com", "elon" => "musk@example.com" }; }
Finally, you can run cargo expand
on this test program to verify that it is expanding
to the right thing.
#![feature(prelude_import)] #[prelude_import] use std::prelude::rust_2021::*; #[macro_use] extern crate std; fn main() { let values = { let mut temp_map = ::std::collections::BTreeMap::new(); temp_map.insert("joesmith", "joe.smith@example.com"); temp_map.insert("djb", "djb@example.com"); temp_map.insert("elon", "musk@example.com"); temp_map }; }
Example: Inspecting the json!
macro
The json!
macro from serde_json
allows you to write JSON inline in Rust,
and get a JSON Value
back. It supports all of JSON syntax, and allows you to
interpolate Rust values inside it as well.
use serde_json::json; use uuid::Uuid; fn main() { let id = Uuid::new_v4(); let person = json!({ "name": "Jeff", "age": 24, "interests": ["guns", "trucks", "bbq"], "nationality": "us", "state": "tx", "id": id.to_string() }); }
To see what this code actually does, calling cargo expand
on it yields the
following:
#![feature(prelude_import)] #[prelude_import] use std::prelude::rust_2021::*; #[macro_use] extern crate std; use serde_json::json; use uuid::Uuid; fn main() { let id = Uuid::new_v4(); let person = ::serde_json::Value::Object({ let mut object = ::serde_json::Map::new(); let _ = object.insert(("name").into(), ::serde_json::to_value(&"Jeff").unwrap()); let _ = object.insert(("age").into(), ::serde_json::to_value(&24).unwrap()); let _ = object .insert( ("interests").into(), ::serde_json::Value::Array( <[_]>::into_vec( #[rustc_box] ::alloc::boxed::Box::new([ ::serde_json::to_value(&"guns").unwrap(), ::serde_json::to_value(&"trucks").unwrap(), ::serde_json::to_value(&"bbq").unwrap(), ]), ), ), ); let _ = object .insert(("nationality").into(), ::serde_json::to_value(&"us").unwrap()); let _ = object.insert(("state").into(), ::serde_json::to_value(&"tx").unwrap()); let _ = object .insert(("id").into(), ::serde_json::to_value(&id.to_string()).unwrap()); object }); }
This shows that under the hood, the macro expands to manual creations of a map, filling it with values.
Example: Inspecting the Serialize
procedural macro
The Serialize
procedural macro auto-generates an implementation for the
Serialize
trait that the serde
crate uses to be able to serialize your
struct to arbitrary data formats. If you have some struct which uses this
derive macro:
#![allow(unused)] fn main() { use serde::Serialize; use uuid::Uuid; #[derive(Serialize)] pub struct Person { name: String, id: Uuid, age: u16, } }
You may want to know what the expanded code looks like. Again, running cargo expand
can show you this.
#![allow(unused)] fn main() { #![feature(prelude_import)] #[prelude_import] use std::prelude::rust_2021::*; #[macro_use] extern crate std; use serde::Serialize; use uuid::Uuid; pub struct Person { name: String, id: Uuid, age: u16, } #[doc(hidden)] #[allow(non_upper_case_globals, unused_attributes, unused_qualifications)] const _: () = { #[allow(unused_extern_crates, clippy::useless_attribute)] extern crate serde as _serde; #[automatically_derived] impl _serde::Serialize for Person { fn serialize<__S>( &self, __serializer: __S, ) -> _serde::__private::Result<__S::Ok, __S::Error> where __S: _serde::Serializer, { let mut __serde_state = _serde::Serializer::serialize_struct( __serializer, "Person", false as usize + 1 + 1 + 1, )?; _serde::ser::SerializeStruct::serialize_field( &mut __serde_state, "name", &self.name, )?; _serde::ser::SerializeStruct::serialize_field( &mut __serde_state, "id", &self.id, )?; _serde::ser::SerializeStruct::serialize_field( &mut __serde_state, "age", &self.age, )?; _serde::ser::SerializeStruct::end(__serde_state) } } }; }
Reading
Chapter 19.5: Macros in The Rust Book
Section in The Rust Book introducing and explaining macros. It explains the difference declarative and procedural macros, and the different types of procedural macros (attribute macros, derive macros, function-like macros) and how they are implemented.
Rust Macros and inspection with cargo expand by Adam Szpilewicz
Adam explains Rust macros and how they can be inspected with cargo expand
.
Examples
Conclusion
Rust is an exciting programming language. The language is unique in that it shifts responsibility for certain correctness principles, such as memory safety, from the developers and maintainers to the compiler. In the long term, it is cheaper and more scalable to have this correctness validated by a machine than by a programmer.
The same principle applies to the tooling which the Rust ecosystem has come up with. The tools discussed in this book allow one to shift responsibility of certain project-level correctness principles from the developers and maintainers of Rust projects to machines. These principles include correct versioning, correct code, comprehensively tested code, correct use of features, and many more.
In my opinion, software development can only be sustainable and scale if we can automate the boring parts. I hope that this book does a good job of teaching you just how to do that, in the context of working on Rust software projects.
Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International
Creative Commons Corporation (“Creative Commons”) is not a law firm and does not provide legal services or legal advice. Distribution of Creative Commons public licenses does not create a lawyer-client or other relationship. Creative Commons makes its licenses and related information available on an “as-is” basis. Creative Commons gives no warranties regarding its licenses, any material licensed under their terms and conditions, or any related information. Creative Commons disclaims all liability for damages resulting from their use to the fullest extent possible.
Using Creative Commons Public Licenses
Creative Commons public licenses provide a standard set of terms and conditions that creators and other rights holders may use to share original works of authorship and other material subject to copyright and certain other rights specified in the public license below. The following considerations are for informational purposes only, are not exhaustive, and do not form part of our licenses.
-
Considerations for licensors: Our public licenses are intended for use by those authorized to give the public permission to use material in ways otherwise restricted by copyright and certain other rights. Our licenses are irrevocable. Licensors should read and understand the terms and conditions of the license they choose before applying it. Licensors should also secure all rights necessary before applying our licenses so that the public can reuse the material as expected. Licensors should clearly mark any material not subject to the license. This includes other CC-licensed material, or material used under an exception or limitation to copyright. More considerations for licensors.
-
Considerations for the public: By using one of our public licenses, a licensor grants the public permission to use the licensed material under specified terms and conditions. If the licensor’s permission is not necessary for any reason–for example, because of any applicable exception or limitation to copyright–then that use is not regulated by the license. Our licenses grant only permissions under copyright and certain other rights that a licensor has authority to grant. Use of the licensed material may still be restricted for other reasons, including because others have copyright or other rights in the material. A licensor may make special requests, such as asking that all changes be marked or described. Although not required by our licenses, you are encouraged to respect those requests where reasonable. More considerations for the public.
Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International Public License
By exercising the Licensed Rights (defined below), You accept and agree to be bound by the terms and conditions of this Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International Public License (“Public License”). To the extent this Public License may be interpreted as a contract, You are granted the Licensed Rights in consideration of Your acceptance of these terms and conditions, and the Licensor grants You such rights in consideration of benefits the Licensor receives from making the Licensed Material available under these terms and conditions.
Section 1 – Definitions.
a. Adapted Material means material subject to Copyright and Similar Rights that is derived from or based upon the Licensed Material and in which the Licensed Material is translated, altered, arranged, transformed, or otherwise modified in a manner requiring permission under the Copyright and Similar Rights held by the Licensor. For purposes of this Public License, where the Licensed Material is a musical work, performance, or sound recording, Adapted Material is always produced where the Licensed Material is synched in timed relation with a moving image.
b. Adapter’s License means the license You apply to Your Copyright and Similar Rights in Your contributions to Adapted Material in accordance with the terms and conditions of this Public License.
c. BY-NC-SA Compatible License means a license listed at creativecommons.org/compatiblelicenses, approved by Creative Commons as essentially the equivalent of this Public License.
d. Copyright and Similar Rights means copyright and/or similar rights closely related to copyright including, without limitation, performance, broadcast, sound recording, and Sui Generis Database Rights, without regard to how the rights are labeled or categorized. For purposes of this Public License, the rights specified in Section 2(b)(1)-(2) are not Copyright and Similar Rights.
e. Effective Technological Measures means those measures that, in the absence of proper authority, may not be circumvented under laws fulfilling obligations under Article 11 of the WIPO Copyright Treaty adopted on December 20, 1996, and/or similar international agreements.
f. Exceptions and Limitations means fair use, fair dealing, and/or any other exception or limitation to Copyright and Similar Rights that applies to Your use of the Licensed Material.
g. License Elements means the license attributes listed in the name of a Creative Commons Public License. The License Elements of this Public License are Attribution, NonCommercial, and ShareAlike.
h. Licensed Material means the artistic or literary work, database, or other material to which the Licensor applied this Public License.
i. Licensed Rights means the rights granted to You subject to the terms and conditions of this Public License, which are limited to all Copyright and Similar Rights that apply to Your use of the Licensed Material and that the Licensor has authority to license.
j. Licensor means the individual(s) or entity(ies) granting rights under this Public License.
k. NonCommercial means not primarily intended for or directed towards commercial advantage or monetary compensation. For purposes of this Public License, the exchange of the Licensed Material for other material subject to Copyright and Similar Rights by digital file-sharing or similar means is NonCommercial provided there is no payment of monetary compensation in connection with the exchange.
l. Share means to provide material to the public by any means or process that requires permission under the Licensed Rights, such as reproduction, public display, public performance, distribution, dissemination, communication, or importation, and to make material available to the public including in ways that members of the public may access the material from a place and at a time individually chosen by them.
m. Sui Generis Database Rights means rights other than copyright resulting from Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, as amended and/or succeeded, as well as other essentially equivalent rights anywhere in the world.
n. You means the individual or entity exercising the Licensed Rights under this Public License. Your has a corresponding meaning.
Section 2 – Scope.
a. License grant.
-
Subject to the terms and conditions of this Public License, the Licensor hereby grants You a worldwide, royalty-free, non-sublicensable, non-exclusive, irrevocable license to exercise the Licensed Rights in the Licensed Material to:
A. reproduce and Share the Licensed Material, in whole or in part, for NonCommercial purposes only; and
B. produce, reproduce, and Share Adapted Material for NonCommercial purposes only.
-
Exceptions and Limitations. For the avoidance of doubt, where Exceptions and Limitations apply to Your use, this Public License does not apply, and You do not need to comply with its terms and conditions.
-
Term. The term of this Public License is specified in Section 6(a).
-
Media and formats; technical modifications allowed. The Licensor authorizes You to exercise the Licensed Rights in all media and formats whether now known or hereafter created, and to make technical modifications necessary to do so. The Licensor waives and/or agrees not to assert any right or authority to forbid You from making technical modifications necessary to exercise the Licensed Rights, including technical modifications necessary to circumvent Effective Technological Measures. For purposes of this Public License, simply making modifications authorized by this Section 2(a)(4) never produces Adapted Material.
-
Downstream recipients.
A. Offer from the Licensor – Licensed Material. Every recipient of the Licensed Material automatically receives an offer from the Licensor to exercise the Licensed Rights under the terms and conditions of this Public License.
B. Additional offer from the Licensor – Adapted Material. Every recipient of Adapted Material from You automatically receives an offer from the Licensor to exercise the Licensed Rights in the Adapted Material under the conditions of the Adapter’s License You apply.
C. No downstream restrictions. You may not offer or impose any additional or different terms or conditions on, or apply any Effective Technological Measures to, the Licensed Material if doing so restricts exercise of the Licensed Rights by any recipient of the Licensed Material.
-
No endorsement. Nothing in this Public License constitutes or may be construed as permission to assert or imply that You are, or that Your use of the Licensed Material is, connected with, or sponsored, endorsed, or granted official status by, the Licensor or others designated to receive attribution as provided in Section 3(a)(1)(A)(i).
b. Other rights.
-
Moral rights, such as the right of integrity, are not licensed under this Public License, nor are publicity, privacy, and/or other similar personality rights; however, to the extent possible, the Licensor waives and/or agrees not to assert any such rights held by the Licensor to the limited extent necessary to allow You to exercise the Licensed Rights, but not otherwise.
-
Patent and trademark rights are not licensed under this Public License.
-
To the extent possible, the Licensor waives any right to collect royalties from You for the exercise of the Licensed Rights, whether directly or through a collecting society under any voluntary or waivable statutory or compulsory licensing scheme. In all other cases the Licensor expressly reserves any right to collect such royalties, including when the Licensed Material is used other than for NonCommercial purposes.
Section 3 – License Conditions.
Your exercise of the Licensed Rights is expressly made subject to the following conditions.
a. Attribution.
-
If You Share the Licensed Material (including in modified form), You must:
A. retain the following if it is supplied by the Licensor with the Licensed Material:
i. identification of the creator(s) of the Licensed Material and any others designated to receive attribution, in any reasonable manner requested by the Licensor (including by pseudonym if designated);
ii. a copyright notice;
iii. a notice that refers to this Public License;
iv. a notice that refers to the disclaimer of warranties;
v. a URI or hyperlink to the Licensed Material to the extent reasonably practicable;
B. indicate if You modified the Licensed Material and retain an indication of any previous modifications; and
C. indicate the Licensed Material is licensed under this Public License, and include the text of, or the URI or hyperlink to, this Public License.
-
You may satisfy the conditions in Section 3(a)(1) in any reasonable manner based on the medium, means, and context in which You Share the Licensed Material. For example, it may be reasonable to satisfy the conditions by providing a URI or hyperlink to a resource that includes the required information.
-
If requested by the Licensor, You must remove any of the information required by Section 3(a)(1)(A) to the extent reasonably practicable.
b. ShareAlike.
In addition to the conditions in Section 3(a), if You Share Adapted Material You produce, the following conditions also apply.
-
The Adapter’s License You apply must be a Creative Commons license with the same License Elements, this version or later, or a BY-NC-SA Compatible License.
-
You must include the text of, or the URI or hyperlink to, the Adapter’s License You apply. You may satisfy this condition in any reasonable manner based on the medium, means, and context in which You Share Adapted Material.
-
You may not offer or impose any additional or different terms or conditions on, or apply any Effective Technological Measures to, Adapted Material that restrict exercise of the rights granted under the Adapter’s License You apply.
Section 4 – Sui Generis Database Rights.
Where the Licensed Rights include Sui Generis Database Rights that apply to Your use of the Licensed Material:
a. for the avoidance of doubt, Section 2(a)(1) grants You the right to extract, reuse, reproduce, and Share all or a substantial portion of the contents of the database for NonCommercial purposes only;
b. if You include all or a substantial portion of the database contents in a database in which You have Sui Generis Database Rights, then the database in which You have Sui Generis Database Rights (but not its individual contents) is Adapted Material, including for purposes of Section 3(b); and
c. You must comply with the conditions in Section 3(a) if You Share all or a substantial portion of the contents of the database.
For the avoidance of doubt, this Section 4 supplements and does not replace Your obligations under this Public License where the Licensed Rights include other Copyright and Similar Rights.
Section 5 – Disclaimer of Warranties and Limitation of Liability.
a. Unless otherwise separately undertaken by the Licensor, to the extent possible, the Licensor offers the Licensed Material as-is and as-available, and makes no representations or warranties of any kind concerning the Licensed Material, whether express, implied, statutory, or other. This includes, without limitation, warranties of title, merchantability, fitness for a particular purpose, non-infringement, absence of latent or other defects, accuracy, or the presence or absence of errors, whether or not known or discoverable. Where disclaimers of warranties are not allowed in full or in part, this disclaimer may not apply to You.
b. To the extent possible, in no event will the Licensor be liable to You on any legal theory (including, without limitation, negligence) or otherwise for any direct, special, indirect, incidental, consequential, punitive, exemplary, or other losses, costs, expenses, or damages arising out of this Public License or use of the Licensed Material, even if the Licensor has been advised of the possibility of such losses, costs, expenses, or damages. Where a limitation of liability is not allowed in full or in part, this limitation may not apply to You.
c. The disclaimer of warranties and limitation of liability provided above shall be interpreted in a manner that, to the extent possible, most closely approximates an absolute disclaimer and waiver of all liability.
Section 6 – Term and Termination.
a. This Public License applies for the term of the Copyright and Similar Rights licensed here. However, if You fail to comply with this Public License, then Your rights under this Public License terminate automatically.
b. Where Your right to use the Licensed Material has terminated under Section 6(a), it reinstates:
-
automatically as of the date the violation is cured, provided it is cured within 30 days of Your discovery of the violation; or
-
upon express reinstatement by the Licensor.
For the avoidance of doubt, this Section 6(b) does not affect any right the Licensor may have to seek remedies for Your violations of this Public License.
c. For the avoidance of doubt, the Licensor may also offer the Licensed Material under separate terms or conditions or stop distributing the Licensed Material at any time; however, doing so will not terminate this Public License.
d. Sections 1, 5, 6, 7, and 8 survive termination of this Public License.
Section 7 – Other Terms and Conditions.
a. The Licensor shall not be bound by any additional or different terms or conditions communicated by You unless expressly agreed.
b. Any arrangements, understandings, or agreements regarding the Licensed Material not stated herein are separate from and independent of the terms and conditions of this Public License.
Section 8 – Interpretation.
a. For the avoidance of doubt, this Public License does not, and shall not be interpreted to, reduce, limit, restrict, or impose conditions on any use of the Licensed Material that could lawfully be made without permission under this Public License.
b. To the extent possible, if any provision of this Public License is deemed unenforceable, it shall be automatically reformed to the minimum extent necessary to make it enforceable. If the provision cannot be reformed, it shall be severed from this Public License without affecting the enforceability of the remaining terms and conditions.
c. No term or condition of this Public License will be waived and no failure to comply consented to unless expressly agreed to by the Licensor.
d. Nothing in this Public License constitutes or may be interpreted as a limitation upon, or waiver of, any privileges and immunities that apply to the Licensor or You, including from the legal processes of any jurisdiction or authority.
Creative Commons is not a party to its public licenses. Notwithstanding, Creative Commons may elect to apply one of its public licenses to material it publishes and in those instances will be considered the “Licensor.” Except for the limited purpose of indicating that material is shared under a Creative Commons public license or as otherwise permitted by the Creative Commons policies published at creativecommons.org/policies, Creative Commons does not authorize the use of the trademark “Creative Commons” or any other trademark or logo of Creative Commons without its prior written consent including, without limitation, in connection with any unauthorized modifications to any of its public licenses or any other arrangements, understandings, or agreements concerning use of licensed material. For the avoidance of doubt, this paragraph does not form part of the public licenses.
Creative Commons may be contacted at creativecommons.org
Privacy
This book is statically hosted by GitLab Pages, therefore their privacy policy applies.
To get some insight into how many people use the book, and which pages they visit, this book uses privacy-perserving analytics provided by Plausible. They use servers located in the EU, are GRPD-compliant and collects only anonymized information (no persistent tracking, no cookies). Because I believe in data transparency, I am making this data available here.
By using this website, you agree to these data policies. If you do not like them, feel free to use an adblocker (such as uBlock Origin, which will block Plausible. You may also print and use a PDF version of this book, or clone the repository and build and view the book locally.