Your core components,
moved a part at a time
We build services and tools in Rust, move C and C++ components to it one at a time behind the interfaces they already have, and keep the code the compiler can’t check small and reviewed, so each part can be tested on its own.
Thanks, we’ll be in touch soon.
{{ heroStatus }}
You’re in good company
A few of the companies we’ve worked with.
When the oldest code reads every input
The code that reads files, messages and network input is often among the oldest in a system, and often written in C or C++.
There, a memory error, such as reading past the end of a buffer or using memory after it’s been freed, can be hard to find in review and in tests. Safe Rust rules those out: the compiler won’t build code that uses memory after it’s freed, and a read past the end of a buffer stops the program instead of reading on.
A rewrite of the whole system is rarely the sensible move. Moving the part that reads the input, behind the interface it already has, usually is. How much it helps depends on the system: on what that part does, what calls it and how well it’s tested.
What we build
Each change is built, tested and checked before a person reviews it, and nothing reaches the code you run until it passes.
New services and tools
Back-end services, command-line tools and libraries written in Rust, with tests from the first commit.
Components moved from C and C++
One part at a time, behind the interface it already has, with the same tests run against the old part and the new, while the rest of the system keeps running.
Modules your other code calls
Rust that your Python or Node.js code calls, or WebAssembly for the browser, behind a small boundary with tests of its own.
Unsafe code, kept small
Code marked unsafe, Rust’s keyword for the few operations the compiler can’t check, wrapped in a safe interface. Each block has a note saying what it relies on, and its tests run under Miri, a tool that reports some kinds of undefined behavior, wherever it can run them.
The latest stable release
The toolchain, the compiler and its tools, moved to each new stable release on a branch with the tests passing, and the oldest Rust release each library needs checked first.
Faster code, measured first
Profiles and benchmarks before any change, and each change measured before and after it merges.
APIs and integrations
REST and gRPC services and the systems they talk to, with every request’s data checked where it enters.
Checks on every change
Tests, Clippy (Rust’s linter), rustfmt (its formatter), Miri for the unsafe parts it can run, and the advisory check on the libraries you depend on, in CI. Nothing merges until they pass.
AI where it helps
An AI coding assistant can draft a change and its tests. The same checks run on it, and a person reviews it before it merges.
- For hosting and deploys:Cloud, DevOps & Security
- Older systems, improved a part at a time:Legacy Modernization
- APIs between your systems:Systems & API Integration
- The tests behind each change:QA & Test Automation
- Developers in your own team:Developers for Rust codebases
Where the compiler’s checks stop
In safe Rust, the compiler checks who owns each value, who may borrow it and for how long, and how it’s shared between threads, and it won’t build code that breaks those rules. That rules out classes of memory errors, such as references to memory that’s been freed, and data races between threads, before the code runs.
Some work can’t be checked that way: calling C or C++, working directly with the operating system or the hardware, and some low-level speedups, such as the processor’s vector instructions. Rust marks that code unsafe. That doesn’t switch the compiler’s checks off: it allows a few operations they can’t check, and a person makes sure each one uses memory correctly.
So we keep unsafe code small and wrap it in a safe interface, which the rest of the code calls instead. Each block gets a safety note saying what it relies on, and its tests run under Miri, a tool that reports some kinds of undefined behavior when a test reaches them. Code Miri can’t run, such as a C library, is tested as it is. Below is a crate of our own, Rust’s word for a library or a program, in five modules: open them, and choose a function.
A crate of our own: open a module, choose a function
- calderbrook
- uploads
- read_scan()
- check_size()
- records
- save_record()
- requests
- handle_request()
- imaging
- decode()
- decode_raw()
- resize
- resize()
- resize_rows()
- uploads
calderbrookthe crate
What it does: Calderbrook Archive’s crate: it reads uploaded scans, stores their records, answers the site’s requests and makes each scan’s preview.
Checked by: The compiler’s rules, in every function. Two also use operations the compiler can’t check, and each of those has a safety note, a person’s review and tests of its own.
What we add: Tests at the crate’s edges, and a safe interface around each unsafe part, so the rest of the crate never calls one directly.
uploadsmodule
What it does: Reads each uploaded scan, and checks its size before anything else uses it.
Checked by: The compiler’s rules on owning, borrowing and sharing, in both its functions. Then their tests.
What we add: Tests with files that are empty, cut short or over the size limit.
read_scan()
What it does: Opens an uploaded scan and reads it into memory.
Checked by: The compiler’s rules: who owns the scan’s data, who may borrow it and for how long. Then its tests.
What we add: Tests at its edges: an empty file, a file cut short, and one over the size limit.
check_size()
What it does: Checks a scan’s size against the limit, before it’s read any further.
Checked by: The compiler’s rules, then its tests.
What we add: Tests at the limit: one byte under it, and one byte over.
recordsmodule
What it does: Stores each scan’s record: its name, where it came from and when it arrived.
Checked by: The compiler’s rules, then the tests of save_record().
What we add: Tests that each record is stored once, and never half written.
save_record()
What it does: Saves one scan’s record to the archive’s database.
Checked by: The compiler’s rules on owning and borrowing the record, then its tests.
What we add: Tests that a record saved twice is still stored once, and that a failed save leaves nothing half written.
requestsmodule
What it does: Answers the site’s requests for scans and previews.
Checked by: The compiler’s rules, including how data is shared between the threads that answer requests at once. Then the tests of handle_request().
What we add: Tests with requests that are malformed, too large or cut off.
handle_request()
What it does: Answers one request from the site, after checking the request’s data where it enters.
Checked by: The compiler’s rules, including how data is shared between threads: it won’t build code with data races. Then its tests.
What we add: Tests with requests that are malformed, too large or cut off, and a time limit on each.
imagingmodule
What it does: Turns each scan into pixels, through an older image library written in C.
Checked by: For decode(), the compiler’s rules and its tests. For decode_raw(), its safety note, a person’s review, and tests run against the C library.
What we add: decode(), a safe interface, so the rest of the crate never calls decode_raw() directly.
decode()
What it does: The safe way in: checks the scan’s data and the space for its pixels, then calls decode_raw().
Checked by: The compiler’s rules, then its tests. It’s the only function that calls decode_raw().
What we add: This interface: the rest of the crate calls decode(), never decode_raw(), so the unsafe part stays in one place.
decode_raw()unsafe
What it does: Calls the older C library that reads a scan’s pixels into memory.
Checked by: Its safety note: what the C library expects, space of the stated size, and which side frees it. Then a person’s review, and tests run against the C library itself, since Miri can’t run C code.
What we add: A safe interface, decode(), so the rest of the crate never calls it directly.
resizemodule
What it does: Makes each scan’s preview, several pixels at a time where the processor allows.
Checked by: For resize(), the compiler’s rules and its tests. For resize_rows(), its safety note, a person’s review, and its tests run under Miri.
What we add: resize(), a safe interface, and a plain version for processors without those instructions.
resize()
What it does: The safe way in: checks that the processor has the vector instructions, then calls resize_rows(), or a plain version if it hasn’t.
Checked by: The compiler’s rules, then its tests, run on both paths.
What we add: The plain version, and tests that both paths give the same preview.
resize_rows()unsafe
What it does: Shrinks rows of pixels with the processor’s vector instructions, several pixels at a time.
Checked by: Its safety note: it relies on the processor having those instructions, and on each row being the stated length. Then a person’s review, and its tests run under Miri.
What we add: A safe interface, resize(), so the rest of the crate never calls it directly.
We keep the unsafe parts small, and review each one.
{{ utStatus }}
Six-week releases, editions you choose
Rust ships a new stable release about every six weeks. The project’s security policy gives support and security updates to the most recent one, and to no earlier release. Once a feature is stable, the project keeps supporting it in every later release, so moving to each new one is usually a small step.
Changes that aren’t backward compatible wait for an edition, which each crate chooses for itself: Rust 2015, 2018, 2021 and 2024 so far, the last arriving with Rust 1.85.0 on February 20, 2025. Crates on different editions build together, and Cargo’s migration tool makes most of the changes; the rest, often in macros, are made by hand.
A crate can also state the oldest Rust release it supports, and Cargo can take that into account when it picks versions of the crate’s dependencies.
So we move the toolchain with the releases, on a branch, with the tests and Clippy passing, and move a crate to a new edition when it helps, one crate at a time.
Start with the unsafe code and the lock file
The review covers the toolchain each crate builds with, in development, in CI and in its images, and any file that pins it, against the latest stable release; each crate’s edition, and the oldest Rust release it says it supports; the lock file, which records the exact version of every crate you depend on: what the advisory database reports, the crates no longer maintained, and their licenses; code that runs at build time, build scripts and procedural macros, in your crates and in your dependencies, since Rust’s security policy treats your dependencies as fully trusted; unsafe code: where it is, what each block relies on, whether it has a safety note, and whether its tests run under Miri; the boundary with C or C++: the interfaces, who owns each piece of memory that crosses them, and their tests; tests, Clippy and rustfmt in CI; async code: blocking calls inside async tasks, and time limits on outgoing calls; performance: profiles and benchmarks, measured as a baseline for the review; builds: the targets you build for, the release settings, and how binaries are shipped; and logs, metrics and traces.
The first step is one of these, each tried on a branch: the toolchain moved to the latest stable release, with the tests and Clippy passing; the advisory check, Clippy and rustfmt added to CI; one unsafe block documented, its tests run under Miri, and a safe interface put around it; one C or C++ component’s interface mapped and its tests written, ready for the move; or one hot path profiled, with one change measured before and after.
What you receive is a written review and a proposal for that first step.
Questions worth asking
Which Rust releases get security fixes?
The most recent stable release, and no earlier one, as the project’s security policy says. A new one arrives about every six weeks, so we keep the toolchain on the latest stable release, and move it with each new one.
What’s an edition, and do we have to move?
An edition is a set of changes that aren’t backward compatible, which each crate opts into for itself: Rust 2015, 2018, 2021 or 2024. Crates on different editions build together, as the Edition Guide says, so a crate doesn’t have to move to keep building. We move one crate at a time, when the new edition helps.
Is Rust a good fit for us?
It depends on the work. Rust fits best where speed, memory use or memory errors matter most, and where your team can take on the time it takes to learn. Where another language already serves you well, we’d usually keep it, and add Rust just where it helps most.
Should we rewrite our C or C++ code in Rust?
Usually not all of it. We move one component at a time, behind the interface it already has, with the same tests run against the old and the new, where it helps most: often the code that reads input.
Can Rust work with the code we already have?
Yes, in three ways: through C interfaces, which C and C++ code can call and be called through; as modules your Python or Node.js code calls; or as WebAssembly, for the browser. Each keeps the boundary between the two small and tested.
Does Rust make our code secure?
No language does on its own. Safe Rust rules out classes of memory errors, while logic errors, unsafe code and the crates you depend on still need review. Rust’s own security policy treats your dependencies as fully trusted, so checking them is part of the work.
How do you handle unsafe code?
We keep it small and behind a safe interface. Each block gets a safety note saying what it relies on, a person reviews it, and its tests run under Miri wherever Miri can run them.
How do you check the crates we depend on?
On every change, the advisory check compares your lock file with the RustSec advisory database. We also look for crates no longer maintained, read the code that runs at build time, and check each crate’s license.
Will Rust make our services faster?
It depends on where the time goes. We profile first, change what the profile shows, and measure before and after. Sometimes the answer is a small Rust module on the hot path; sometimes it’s a change to the code you already have.
Can AI assistants help with our Rust code?
Yes, within the same checks as everyone’s code. An assistant can draft a change and its tests; the compiler, Clippy and the tests run on it, and a person reviews it before it merges.
Can you take over our existing codebase?
Yes. First we read it: the crates, the unsafe parts, the tests, and how it’s built and shipped. Then we agree on a first piece of work and a named owner on our side, and work alongside any developers you already have.
What does it cost?
It depends on your codebase, its size and the first step you choose. Before any work starts, you get a written proposal with the scope, the price or rate, and how you pay.
Can we hire developers for our own team?
Yes. Within 48 hours of your request we introduce developers who know Rust codebases and work in your time zone. You meet them first, and decide who joins.
Rust and Cargo are trademarks of the Rust Foundation. Webair is independent of the Rust Project and the Rust Foundation, and isn’t sponsored by, endorsed by or affiliated with either.
Let’s build it better
Tell us what the code does, which languages it’s written in, and where it runs. We’ll agree on a first step in a short call.
Thanks, we’ll be in touch soon.
{{ ctaStatus }}