Beyond the Obvious: Our Journey into Rust
When Fermyon set out to implement Spin, the open-source framework for building serverless WebAssembly applications, the decision to use Rust wasn’t just logical - it felt inevitable. Sure, we could cite the obvious characteristics of Rust, like its memory safety guarantees without garbage collection, its blazing-fast performance, or its growing ecosystem. But for us at Fermyon, these reasons were only the beginning of the story. We appreciated the depth of Rust’s philosophy and tooling, and their alignment with our goals for Spin. Here’s why we chose Rust, and how it has helped us shape Spin into the powerful tool we envisioned.
It All Began With wasmtime
Spin assists you in building and running WebAssembly (Wasm) workloads. We use wasmtime as the WebAssembly runtime in Spin. wasmtime
itself is written in Rust, and this gave us a significant head start. By choosing Rust for Spin, we gained immediate interoperability with wasmtime
and access to its rich ecosystem of libraries and tools. This synergy allowed us to focus on crafting Spin’s remarkable developer experience and features while leveraging the stability and performance of wasmtime
. In other words, Rust didn’t just complement our goals—it supercharged them.
The Developer Experience: A Core Objective
At Fermyon, we set ourselves an ambitious goal: to provide the best possible experience for developers working with Spin. This objective guided every decision we made, from the architecture to the implementation. Specifically, we envisioned Spin as a framework that developers could extend and personalize with ease. To achieve this, we introduced:
Spin Plugins
Rust’s modularity and ergonomic design, combined with the powerful clap crate, allowed us to build Spin’s highly extensible Command Line Interface (CLI).
clap
provided the foundation for designing and implementing the Spin CLI and its plugin architecture, enabling a seamless and intuitive developer experience. With Spin Plugins, developers can extend Spin’s capabilities by leveraging community-contributed plugins or even creating their own. Notably, plugins themselves can be implemented using a wide range of programming languages, with Rust and Go being the most popular choices – as of today. Looking at the source code, we can see how sub-commands provided through plugins are added to the Spin CLI using APIs provided by clap
:
#[derive(Parser)]
#[clap(name = "spin", version = version())]
enum SpinApp {
// snip
#[clap(alias = "n")]
New(NewCommand),
#[clap(alias = "b")]
Build(BuildCommand),
#[clap(alias = "u")]
Up(UpCommand),
#[clap(external_subcommand)]
External(Vec<String>),
}
async fn _main() -> anyhow::Result<()> {
// snip
let mut cmd = SpinApp::command();
for plugin in &plugin_help_entries {
let subcmd = clap::Command::new(plugin.display_text())
.about(plugin.about.as_str())
.allow_hyphen_values(true)
.disable_help_flag(true)
.arg(clap::Arg::new("command")
.allow_hyphen_values(true)
.multiple_values(true));
cmd = cmd.subcommand(subcmd);
}
if !plugin_help_entries.is_empty() {
cmd = cmd.after_help("* implemented via plugin");
}
// snip
}
Custom Spin Plugins empowers developers to tailor Spin to their unique workflows and project requirements, fostering innovation and adaptability.
Spin Templates
Rust’s expressive type system and tooling enabled us to build Spin Templates, which are pre-defined scaffolds for creating Spin applications. These templates allow users to bootstrap new applications quickly and effectively. Template customization is powered by the Liquid template language and its Rust implementation provided by the liquid crate, offering flexibility and ease of use.
Additionally, we leverage the dialoguer crate to render a robust and user-friendly terminal user interface (TUI), simplifying the process of collecting template parameters and enhancing the overall developer experience.
The following snippet shows how spin new
asks users for template parameters - using either Input
or Select
provided by dialoguer
- when creating a new Spin application:
pub(crate) fn prompt_parameter(parameter: &TemplateParameter) -> Option<String> {
let prompt = parameter.prompt();
let default_value = parameter.default_value();
loop {
let input = match parameter.data_type() {
TemplateParameterDataType::String(constraints) => match &constraints.allowed_values {
Some(allowed_values) => ask_choice(prompt, default_value, allowed_values),
None => ask_free_text(prompt, default_value),
},
};
match input {
Ok(text) => match parameter.validate_value(text) {
Ok(text) => return Some(text),
Err(e) => {
println!("Invalid value: {}", e);
}
},
Err(e) => {
println!("Invalid value: {}", e);
}
}
}
}
fn ask_free_text(prompt: &str, default_value: &Option<String>) -> anyhow::Result<String> {
let mut input = Input::<String>::new();
input = input.with_prompt(prompt);
if let Some(s) = default_value {
input = input.default(s.to_owned());
}
let result = input.interact_text()?;
Ok(result)
}
fn ask_choice(
prompt: &str,
default_value: &Option<String>,
allowed_values: &[String],
) -> anyhow::Result<String> {
let mut select = Select::new().with_prompt(prompt).items(allowed_values);
if let Some(s) = default_value {
if let Some(default_index) = allowed_values.iter().position(|item| item == s) {
select = select.default(default_index);
}
}
let selected_index = select.interact()?;
Ok(allowed_values[selected_index].clone())
}
Furthermore, users could roll their own templates to enhance their productivity or to ensure alignment with specific regulatory compliance requirements, tailoring the development process to their unique needs.
Spin Factors
Spin Factors are a core design principle and pattern within the Spin codebase. They allow users and organizations to add or customize the behavior of the Spin Runtime (Host). This flexibility is pivotal in adapting Spin to specific needs and use cases, making it not only a tool but a platform that evolves with its users. By leveraging Spin Factors, developers gain the ability to shape the runtime itself, empowering them to innovate at a higher level.
For the sake of this article, let’s take a look at one of the unit tests that the team wrote for Spin Factors. The test implementation should give you a sense of Spin’s flexibility when it comes to creating a tailored runtime for running Spin Applications in restricted environments and only providing a subset of the default Spin Factors (key-value store in this instance).
#[derive(RuntimeFactors)]
struct TestFactors {
key_value: KeyValueFactor,
}
impl From<RuntimeConfig> for TestFactorsRuntimeConfig {
fn from(value: RuntimeConfig) -> Self {
Self {
key_value: Some(value),
}
}
}
#[tokio::test]
async fn works_when_allowed_store_is_defined() -> anyhow::Result<()> {
let mut runtime_config = RuntimeConfig::default();
runtime_config.add_store_manager("default".into(), mock_store_manager());
let factors = TestFactors {
key_value: KeyValueFactor::new(),
};
let env = TestEnvironment::new(factors).extend_manifest(toml! {
[component.test-component]
source = "does-not-exist.wasm"
key_value_stores = ["default"]
});
let mut state = env
.runtime_config(runtime_config)?
.build_instance_state()
.await?;
assert_eq!(
state.key_value.allowed_stores(),
&["default".into()].into_iter().collect::<HashSet<_>>()
);
assert!(state.key_value.open("default".to_owned()).await?.is_ok());
Ok(())
}
By incorporating these features and patterns, we’ve been able to bake extensibility directly into Spin, empowering users to shape the framework rather than be constrained by it.
Managing Code at Scale: How We Use Rust in the Spin Codebase
Spin is a large-scale project, and maintaining its complexity without losing simplicity requires discipline. Thankfully, Rust’s ecosystem is not just about writing code—it’s about writing manageable, maintainable, and scalable code. Below are three examples, outlining why Rust is invaluable for building and managing codebases at scale:
1. Cargo Workspaces
Cargo Workspaces are a game-changer for managing large projects. Spin is composed of multiple interdependent crates, and using workspaces allows us to organize these crates into logical units without sacrificing efficiency. Workspaces lay the foundation for increasing the end-to-end build performance, managing shared dependencies, centralized configuration management, simplifying development and reducing overhead. For example, the bespoke Spin plugin architecture, Spin Templates, and Spin Factors are managed as separate crates within a shared workspace, ensuring clean separation of concerns while maintaining cohesion.
2. Rust’s Strong Type System
Rust’s strong type system, coupled with its support for traits and generics, provides a powerful foundation for building scalable and maintainable codebases like Spin. Traits enable the definition of shared behaviors across diverse components, ensuring consistency and reducing duplication, while generics allow developers to write highly reusable abstractions that are both type-safe and performant. These features significantly enhance the robustness and adaptability of the Spin codebase, simplifying the management of its complexity as it grows. By leveraging Rust’s expressive type system, Spin achieves a higher level of reliability and clarity, making it easier to scale and maintain without compromising on speed or safety.
3. Powerful, Robust & Efficient Toolchain
cargo, rustfmt, clippy, rust-analyzer, and Rust’s robust unit testing capabilities together form a powerful ecosystem for managing large-scale projects like Spin.
cargo
streamlines dependency management and builds, while rustfmt
ensures consistent code formatting across the project.
clippy
offers insightful linting capabilities, guiding developers toward best practices and preventing common programming pitfalls.
Meanwhile, rust-analyzer
enhances development workflows through intelligent code navigation and suggestions, significantly accelerating productivity.
Combined with Rust’s native support for unit testing, these tools collectively empower developers to maintain clarity, reliability, and scalability in even the most complex codebases.
Why Rust Matters
Rust isn’t just a programming language; it’s a mindset. Its emphasis on safety, performance, and maintainability resonates deeply with our goals for Spin. By choosing Rust, we’ve been able to deliver a toolchain that is fast, reliable, and extensible—all without compromising on developer experience.
Rust allows us release new features at an incredible pace. As Spin continues to grow, we remain confident that Rust will scale with us, supporting our mission to redefine serverless development.
Conclusion
For those exploring Rust or questioning why others choose it, our experience with Spin offers a compelling case. Beyond its technical merits, Rust provides a foundation for creativity and scalability that few other languages can match. At Fermyon, we didn’t just choose Rust for what it is —we chose it for what it empowers us to build.
If you’re a developer intrigued by Rust, consider diving into the Spin codebase. You might discover that Rust isn’t just a tool for building software — it’s a framework for building possibilities.