How and why Daily is using Rust for our WebRTC API platform

Introduction

Daily provides a user-friendly WebRTC API platform to help customers build video and audio call functionality into their applications. As part of this goal, we strive to support a multitude of platforms for our customers. Up until now, however, Daily has not offered true native development support. Our offering has included a REST API, a JavaScript library, and a React Native library.

That's not to say one couldn't build iOS and Android apps with Daily before. For example, our Party Line audio-only demo uses a headless WebView to load daily-js on iOS and Android devices. While this worked for our purpose, it was still not a truly native experience.

We're using Rust to change that!

In this post, we'll tell you a little bit about how we utilize Rust at Daily and why we chose Rust to build daily-core: the Daily library to rule them all.

What are we doing with Rust?

We've used Rust to create a central, cross-platform library called daily-core. daily-core is intended to sit behind lightweight, platform-specific API wrappers. This allows us to keep our core business logic consistent across platforms while taking full advantage of the various native platforms’ capabilities.

The platform-specific wrappers communicate with this core layer using a Foreign Function Interface (FFI) layer (WebAssembly for web APIs, C for native APIs).

The main challenge of interfacing our Rust code with WebRTC has to do with WebRTC’s C++ API, which is typically not a problem for browser vendors, as most modern browsers are also written in C++. However, Rust, as most other languages, cannot interface directly with C++ due to lack of Application Binary Interface (ABI) stability. This is why we wrote and maintain a thin C layer around the WebRTC APIs we need to use. Rust also helps us here as we can write thin, “smart” types around some necessary C++ types like smart pointers to ensure we are handling different kinds of pointer and memory lifetimes across all levels of the stack.

Smart pointers are a feature of modern C++. A smart pointer lives on the stack and wraps a "bare" pointer. The smart pointer then handles destruction of the underlying resource when it goes out of scope, allowing developers to do less manual memory management by hand.
A markdown graphic showing Daily's native stack with daily-core. From top to bottom layers: libwebrtc + libmediasoupclient, FFI, daily-core, FFI, platform-specific wrappers.
Native/mobile stack with daily-core

Why did we choose Rust?

As previously mentioned, one of the main goals of this initiative was to provide consistent core business logic across all platforms. Additionally, we also wanted to provide APIs that feel native to each platform, while minimizing the differences between the different platforms. This should allow our customers to more easily reason about how they are interacting with our APIs when developing applications in different platforms, and help them feel confident that all of their applications will behave exactly the same no matter the platform of their choosing.

Of course, this includes the Web as many of our customers also wish to provide browser-native versions of their applications.

The two main options we considered for building our core library were C++ and Rust.

Some of our favorite features of Rust include:

  • Multi platform cross-compilation, especially WebAssembly
  • Memory management advantages with Rust’s borrow checker
  • A robust, extensible type system
  • A concurrency model which gives us the control we want without introducing complexity
  • Performance on-par with C++
  • A rich package (crate) ecosystem

Now, let's look at these features and why they suit our vision for daily-core so well in a little more detail.

WebAssembly as a first class citizen

Rust supports WebAssembly as a first class citizen. It is possible to compile C++ to WASM, but not as nicely supported as it is with Rust. Rust has a whole host of active projects aimed toward enabling convenient interaction with WebAssembly. Take gloo for example, which provides a collection of Rust wrappers for browser APIs. For C++, the options for compiling to WASM runtimes are limited to Emscripten.

Safety advantages

We also liked the safety and memory management advantages Rust offered over C++. Rust's borrow checker lowers the cognitive load compared to C++ when dealing with memory and ownership. Our developers don't have to worry about preventing dangling pointers or data races at runtime. Rust's compiler, rustc, will help us catch these issues much earlier than we might with C++. We decided that it would be easier to write code safely and debug it over time with Rust.

Robust type system

Rust’s robust type system brings several key advantages that help us feel confident in the quality of the product we are developing.

By wrapping WebRTC’s C++ API with Rust, we have access to richer types which can eliminate entire classes of problems which otherwise would require significant changes to WebRTC itself. These richer types can carry information that is used during type checking and erased afterwards, ensuring that there is no runtime cost for the benefit of having safer code.

One such use case is related to WebRTC’s threading model. WebRTC makes use of three threads with different purposes: a network thread, a signaling thread, and a worker thread. These threads are not interchangeable and different interactions with WebRTC require the use of different threads.

An example is the creation of media devices and the media tracks from those devices. All of these should occur in the same thread (the worker thread), but the WebRTC C++ APIs for interacting with media devices and creation of media tracks only require that a thread is passed as a parameter, and it is up to the developer to ensure that it is the same worker thread in both cases.

In Rust, we can write a thin wrapper around WebRTC threads as follows:

pub trait ThreadPurpose {};

pub struct Network;

Impl ThreadPurpose for Network;

Pub struct Signaling;

Impl ThreadPurpose for Signaling;

Pub struct Worker;

Impl ThreadPurpose for Worker;

pub struct Thread<T: ThreadPurpose> {
  thread_ptr: AtomicPtr<webrtc::Thread>,
  Phantom_data: PhantomData<T>
}

Impl Thread<Network> {
   // This will always create a `Network` thread
  Pub fn new() -> Self { … }
}

Impl Thread<Signaling> {
   // This will always create a `Signaling` thread
  Pub fn new() -> Self { … }
}

Impl Thread<Worker> {
   // This will always create a `Worker` thread
  Pub fn new() -> Self { … }
}

Now, whenever we have a Rust method or function calling into WebRTC which requires one of these threads, it can have a signature like the following artificial example:

fn call_webrtc_function(worker_thread: Thread<Worker>, …) { … }

This Rust function only accepts a thread of type Thread<Worker>, which can be created via Thread::<Worker>::new(). If we would attempt to pass a non-worker thread to this function (maybe as a result of a refactoring gone wrong), the compiler immediately lets us know that the thread we are passing is not of the right type, and we avoid getting hit with a nasty runtime exception when WebRTC detects that the wrong thread was used. To make things even better, this kind of approach to type-safety also brings no performance impact as the type markers are erased at compile-time, so we get all the same performance, but with the benefit of having the Rust type-checker looking over our shoulder.

Rust’s excellent error messages, combined with Clippy, a linter that catches common mistakes and anti-patterns, makes sure we stay productive while following the language’s best-practices.

By compiling our Rust code to WebAssembly, we can bring all of these type-safety advantages to the Web, where normally our developers would be subject to the well-documented warts and gotchas of JavaScript.

Concurrency independent of thread count

In addition, Rust's async/.await allows us to express concurrent logic regardless of whether we're running on a single thread or not. This is particularly important for the WebAssembly use case.

Most major browsers support WebAssembly threads as of the time of writing, but we also wanted to ensure we supported the largest set of devices possible, particularly in mobile platforms. Therefore, it was important to ensure that our code can run even in browser versions with more basic WebAssembly support.

This means that whenever we decide to throw the switch to multi-threaded WebAssembly, essentially no code needs to change as all of our logic is already written in a concurrent-friendly manner. We simply reap the benefits of a multi-threaded runtime on the Web.

Performance

Performance-wise, Rust is on par with C++ and in some cases even beats it, as we can see by comparing the performance and resource usage of both Rust and C++ in Benchmarks Game.

The lack of garbage collection in Rust ensures the performance of our libraries is predictable, and that our code is as lightweight as we can write it.

Rich crate ecosystem

Aside from the language itself, the Rust crate ecosystem is extremely rich. We stand on the shoulders of giants with high quality, well maintained, and well documented libraries that reduce the amount of work a project of this nature would entail. This goes for everything from robust, battle-tested asynchronous runtimes such as Tokio, to versatile JSON serialization via serde, to automatic binding generation with wasm-bindgen and bindgen.

We also make use of the excellent Rust tooling ecosystem to ensure software supply chain quality via cargo-deny, which allows us to automatically audit our codebase for known security vulnerabilities.

What are our long term plans for Rust?

Keep improving! With a single core library at the root of all of our APIs, the performance, stability, and other improvements we make can immediately benefit all the platform-specific wrappers that consume it.

From here, we can build on a solid foundation to create lightweight, easy to use platform libraries for Daily's customers to use when building their applications.

We hope this brief primer about how — and why — we use Rust at Daily has been interesting. Please reach out if you have any questions or thoughts about Daily or our journey into building a single core to rule them all with Rust.

If you’re curious to learn more about how we’re developing a cross-platform WebRTC using Rust and WebAssembly at Daily, check out the recording of our talk at Kranky Geek!

Rust logo by Mozilla is licensed under CC BY 4.0.

Never miss a story

Get the latest direct to your inbox.