We’re going to construct a Svelte retailer, written in Rust, and run it in an Electron app.
This spun out of a video editor which I initially in-built Swift. It seems individuals on Home windows wish to edit movies too, so now I’m rewriting it with Rust, Electron, and Svelte with a watch for efficiency.
Rust is a techniques programming language, akin to C or C++, however safer. It’s recognized for being tremendous quick.
Electron is a framework for constructing cross-platform desktop apps with HTML, CSS, and JavaScript. It’s recognized for being kinda sluggish and bloaty. However there’s an ace up its sleeve: Electron apps will be prolonged with compiled native code, and should you do the heavy stuff in native code you’ll be able to a pleasant pace increase.
Svelte is a JavaScript UI framework – a substitute for React, Vue, Angular, or one of many different 7500 frameworks. Svelte makes use of a compiler to generate small, quick, reactive code.
I figured by combining them, and doing many of the heavy lifting in Rust, I’d find yourself with an app that feels snappy.
The full, accomplished mission is on GitHub and it has directions on tips on how to run it, together with my rollercoaster of a commit historical past whereas I used to be making an attempt to get it working.
Right here’s what it seems to be like:
How Svelte Shops Work
One of many issues I really like about Svelte is its reactivity mannequin, and specifically, its idea of shops. A retailer is a reactive variable that holds a single worth.
Any a part of the app can subscribe to the shop, and each subscriber shall be (synchronously!) notified when the shop’s worth is modified.
Right here’s a easy instance (dwell model right here):
<script>
import { onDestroy } from 'svelte';
import { writable } from 'svelte/retailer';
// Make a retailer
const depend = writable(0);
// Subscribe to it, and replace the displayed worth
let visibleCount = 0;
const unsubscribe = depend.subscribe(worth => {
visibleCount = worth;
});
perform increment() {
// Change the shop's worth with (worth + 1)
depend.replace(n => n + 1);
}
// Tidy up when this part is unmounted
onDestroy(unsubscribe);
</script>
<button on:click on={increment}>Increment</button>
<p>Present worth: {visibleCount}</p>
You click on the button, it updates. Nothing too mindblowing. However that is simply the “low-level” API.
It seems to be quite a bit nicer whenever you introduce Svelte’s particular reactive retailer syntax with the $
(strive the dwell instance):
<script>
import { onDestroy } from 'svelte';
import { writable } from 'svelte/retailer';
// Make a retailer
const depend = writable(0);
perform increment() {
$depend += 1;
}
</script>
<button on:click on={increment}>Increment</button>
<p>Present worth: {$depend}</p>
It does the very same factor, simply with much less code.
The particular $depend
syntax contained in the <p>
is establishing a subscription behind the scenes, and updating that particular DOM aspect when the worth adjustments. And it handles the unsubscribe
cleanup routinely.
There’s additionally the $depend += 1
(which can be written $depend = $depend + 1
). It reads like plain outdated JavaScript, however after the worth is modified, this retailer will notify all of its subscribers – on this case that’s simply the $depend
within the HTML under.
The Svelte docs have a fantastic interactive tutorial on shops if you wish to be taught extra.
The Essential Factor is the Contract
It’s straightforward to have a look at code like this and assume it’s all magic, particularly when there’s fancy syntax like $retailer
.
There’s a bit of data-intensive code that I wrote in JS as a substitute of Rust as a result of I had this mindset of, “I would like the reactivity so it needs to be in JavaScript”.
However should you take a step again and have a look at the underpinnings of how the magic really works, typically yow will discover new and attention-grabbing methods to increase it!
Svelte shops had been designed properly to permit this: they comply with a contract.
The brief model is that in an effort to be a “Svelte retailer,” an object wants:
- A
subscribe
technique that returns anunsubscribe
perform - A
set
technique if you wish to make it writable - It should name the subscribers synchronously (a) at subscribe time and (b) any time the worth adjustments.
If any JS object follows these guidelines, it’s a Svelte retailer. And if it’s a Svelte retailer, it may be used with the flamboyant $retailer
syntax and every thing!
Calling Rust From JavaScript
The subsequent piece of this puzzle is to put in writing some Rust code that may be uncovered as an object in JavaScript.
For this, we’re utilizing napi-rs, an superior framework for connecting Rust and JavaScript collectively. The creator, LongYinan aka Broooooklyn is doing superb work on it, and the most recent updates (in v2) have made the Rust code very good to put in writing. Right here’s a style, the “hey world” of Rust capabilities:
#[macro_use]
extern crate napi;
/// import the preludes
use napi::bindgen_prelude::*;
/// annotating a perform with #[napi] makes it out there to JS,
/// kinda like `export { sum };`
#[napi]
pub fn sum(a: u32, b: u32) -> u32 {
a + b
}
Then in JavaScript, we will do that:
// Hand-wavy pseudocode for now...
// The native module has its personal folder and
// construct setup, which we'll have a look at under.
import { sum } from './bindings';
console.log(sum(2, 2)) // offers right reply
A Boilerplate Venture with Electron, Rust, and Svelte
We’ve received the massive items in thoughts: Electron, Svelte shops, Rust that may be known as from JS.
Now we simply have to… really wire up a mission with 3 totally different construct techniques. Hoorayyyyy. I hope you’ll be able to hear the thrill in my voice.
So, for this prototype, I took the lazy method out.
It’s a barebones Electron app with the Svelte template cloned into one subfolder, and the native Rust module in one other (generated by the NAPI-RS CLI).
The dev expertise (DX) is old-school: give up the entire app, rebuild, and restart. Certain, some kind of auto-building, auto-reloading Rube Goldberg-esque tangle of scripts and configuration would’ve been neat, however I didn’t wanna.
So it has this mile-long begin
script that simply cd
’s into every subfolder and builds it. It’s not fairly, however it will get the job accomplished!
"scripts": {
"begin": "cd bindings && npm run construct && cd .. && cd ui && npm run construct && cd .. && electron .",
"begin:debug": "cd bindings && npm run construct:debug && cd .. && cd ui && npm run construct && cd .. && electron .",
"begin:clear": "npm run clear && npm run begin:debug",
"clear": "cd bindings && rm -rf goal"
},
We’re not going for superior DX right here. It is a prototype. Superior DX is Future Work™.
Starting To Finish: How It Works
Personally I actually prefer to hint the execution from the very first entry level. I believe it helps me perceive how all of the items match collectively. So right here’s the chain of occasions that results in this factor working, with the related bits of code:
1. You run npm begin
. It builds every thing, after which runs electron .
bundle.json
"begin": "cd bindings && npm run construct && cd .. && cd ui && npm run construct && cd .. && electron .",
2. Electron finds and executes principal.js
as a result of bundle.json
tells it to (by way of the principal
key)
bundle.json
{
"title": "electron-quick-start",
"model": "1.0.0",
"description": "A minimal Electron software",
"principal": "principal.js",
...
}
3. principal.js
spawns a BrowserWindow, and masses up index.html
principal.js
perform createWindow() {
// Create the browser window.
const mainWindow = new BrowserWindow({
width: 800,
top: 600,
webPreferences: {
contextIsolation: true,
// Preload will make the native module out there
preload: path.be a part of(__dirname, 'preload.js')
}
})
// Load the index.html of the app.
mainWindow.loadFile('index.html')
}
4. principal.js
additionally specifies a preload.js
, the place you’re allowed to show native modules. That is the place the Rust module is imported and uncovered as window.Napi
. (See Safety under)
preload.js
// Make native bindings out there to the renderer course of
window.Napi = require('./bindings');
5. index.html
masses the Svelte app’s JavaScript that was in-built Step 1
index.html
<html>
...
<physique>
<!-- You can even require different recordsdata to run on this course of -->
<script src="./ui/public/construct/bundle.js"></script>
</physique>
</html>
6. Svelte has its personal ui/principal.js
, which imports and creates the App
part, and mounts it at doc.physique
.
ui/principal.js
import App from './App.svelte';
const app = new App({
goal: doc.physique,
});
export default app;
7. App.svelte
instantiates our Rust retailer with an preliminary worth, which calls the constructor in Rust.
ui/App.svelte
<script>
import Counter from "./Counter.svelte";
let showCounter = true;
let counter = new Napi.Counter(42);
</script>
8. Since Svelte must render the counter, it instantly calls .subscribe
with a callback, which calls subscribe
in Rust.
ui/public/construct/bundle.js [compiled by Svelte]
perform occasion($$self, $$props, $$invalidate) {
let $counter;
let showCounter = true;
let counter = new Napi.Counter(42);
component_subscribe($$self, counter, worth => $$invalidate(1, $counter = worth));
const click_handler = () => $$invalidate(0, showCounter = !showCounter);
const click_handler_1 = () => set_store_value(counter, $counter = Math.flooring(Math.random() * 1234), $counter);
return [showCounter, $counter, counter, click_handler, click_handler_1];
}
9. The subscribe
perform, in response to the contract, wants to instantly name the offered callback with the present worth, so it does that, after which saves the callback for later use. It additionally returns an unsubscribe
perform that Svelte will name when the part is unmounted.
#[napi]
impl Counter {
// ...
#[napi]
pub fn subscribe(
&mut self, env: Env, callback: JsFunction
) -> Outcome<JsFunction> {
// Create a threadsafe wrapper.
// (to make sure the callback would not
// instantly get rubbish collected)
let tsfn: ThreadsafeFunction<u32, ErrorStrategy::Deadly> = callback
.create_threadsafe_function(0, |ctx| v)?;
// Name as soon as with the preliminary worth
tsfn.name(self.worth, ThreadsafeFunctionCallMode::Blocking);
// Save the callback in order that we will name it later
let key = self.next_subscriber;
self.next_subscriber += 1;
self.subscribers.borrow_mut().insert(key, tsfn);
// Cross again an unsubscribe callback that
// will take away the subscription when known as
let subscribers = self.subscribers.clone();
let unsubscribe = transfer |ctx: CallContext| -> Outcome<JsUndefined> {
subscribers.borrow_mut().take away(&key);
ctx.env.get_undefined()
};
env.create_function_from_closure("unsubscribe", unsubscribe)
}
}
Safety: Electron and contextIsolation
Electron is cut up into 2 processes: the “principal” one (which runs Node, and is working principal.js
in our case) and the “renderer”, which is the place your UI code runs. Between the 2 sits the preload.js
. Electron’s official docs clarify the course of mannequin in additional element.
There are a couple of layers of safety in place to stop random scripts from gaining unfettered entry to your total laptop (as a result of that might be dangerous).
The primary is the nodeIntegration
flag, defaulted to false
. This makes it to be able to’t use Node’s require()
contained in the renderer course of. It’s a bit annoying, however the upside is that in case your Electron app occurs to open (or is coerced into opening) a sketchy script from someplace, that script gained’t be capable of import Node modules and wreak havoc.
The second is the contextIsolation
flag, which defaults to true
. This makes the preload
script, which runs contained in the renderer, can’t entry window
and due to this fact can’t expose any delicate APIs instantly. You must use the contextBridge to show an API that the renderer can use.
Why am I telling you all this? Properly, should you have a look at the preload.js
instance above, you’ll see that it units window.Napi
instantly. It’s not utilizing contextBridge
, and contextIsolation
is disabled on this mission. I attempted turning it on, however evidently constructors can’t be handed by the bridge. There is perhaps an alternate solution to clear up this – if you realize of 1 please let me know!
In case your app doesn’t load exterior assets, and solely masses recordsdata from disk, my understanding is that leaving contextIsolation
disabled is okay.
I’m penning this as a proof of idea WITH THE CAVEAT that that is much less safe than it could possibly be (you probably have concepts for enchancment, let me know on Twitter).
How the Rust Works
The brief reply is: it follows the Svelte retailer contract 🙂 Let’s see how.
All of it occurs in a single file, bindings/src/lib.rs
.
First, there’s a struct
to carry the present worth of the counter, together with its subscribers.
I don’t assume the ThreadsafeFunction
s will be in contrast for equality, so I put them in a map as a substitute of a vector, and used the next_subscriber
to carry an incrementing key to retailer the subscribers.
#[napi]
pub struct Counter {
worth: u32,
subscribers: Rc<RefCell<HashMap<u64, ThreadsafeFunction<u32, ErrorStrategy::Deadly>>>>,
next_subscriber: u64,
}
Then there are a couple of capabilities applied on this struct. There’s the constructor, which initializes a Counter
with no subscribers:
#[napi]
impl Counter {
#[napi(constructor)]
pub fn new(worth: Choice<u32>) -> Counter {
Counter {
worth: worth.unwrap_or(0),
subscribers: Rc::new(RefCell::new(HashMap::new())),
next_subscriber: 0,
}
}
And there are increment
and set
capabilities that do almost the identical factor. Of the 2, set
is particular in that it’s the one which makes this retailer “writable” within the eyes of Svelte. After we write $depend = 7
in JS, that can in the end name into set
right here.
#[napi]
pub fn increment(&mut self) -> Outcome<()> {
self.worth += 1;
self.notify_subscribers()
}
#[napi]
pub fn set(&mut self, worth: u32) -> Outcome<()> {
self.worth = worth;
self.notify_subscribers()
}
After modifying the worth, these capabilities name notify_subscribers
. This one doesn’t have the #[napi]
annotation, which implies it gained’t be callable from JS. This iterates over the subscribers and calls each with the present worth.
As a result of self.subscribers
is an Rc<RefCell<...>>
we have to explicitly borrow()
it earlier than iterating. This borrow occurs at runtime, versus the standard compile-time borrow-checking accomplished by Rust. If one thing else has this borrowed once we attempt to borrow it right here, this system will panic (aka crash).
I’m reasoning that is panic-free as a result of each the notify_subscribers
and the subscribe
(the opposite place that borrows this variable) are working within the single JS principal thread, so it shouldn’t be doable for them to step on every others’ entry.
fn notify_subscribers(&mut self) -> Outcome<()> {
for (_, cbref) in self.subscribers.borrow().iter() {
cbref.name(self.worth, ThreadsafeFunctionCallMode::Blocking);
}
Okay(())
}
Many of the actual work occurs inside subscribe
. There are some feedback, but additionally some subtleties that took me a while to determine.
First, it wraps the callback with a ThreadsafeFunction
. I believe the rationale this works is that ThreadsafeFunction
internally units up a reference counter across the callback. I attempted with out this at first, and it turned out that the callback was getting garbage-collected instantly after subscribing. Regardless of storing the callback
(and making Rust comfortable about its possession), making an attempt to really name it was failing.
The ErrorStrategy::Deadly
would possibly look alarming, however the different, ErrorStrategy::CalleeHandled
, doesn’t work in any respect right here. The CalleeHandled
model makes use of Node’s callback calling conference, the place it passes the error as the primary argument (or null). That doesn’t match Svelte’s retailer contract, which solely expects a single argument. The Deadly
technique passes the argument straight by.
The create_threadsafe_function
name itself has quite a bit occurring. The closure being handed in |ctx| { ... }
will get known as every time we run .name()
on the threadsafe perform. The closure’s job is to take the worth you cross in and remodel it into an array of JavaScript values. So this closure takes the u32
worth, wraps it in a JsNumber with create_uint32
, after which places that in a vector. That vector, in flip, will get unfold into the arguments to the JS callback.
Saving the callback is necessary so we will name it later, so self.subscribers.borrow_mut().insert(key, tsfn);
does that. We’d like the borrow_mut
as a result of we’re doing runtime borrow checking right here.
I initially went with borrow checking at compile time, however the unsubscribe
closure threw a wrench within the works. See, we have to add one thing to the hashmap at subscribe time, and we have to take away one thing from the identical hashmap at unsubscribe time. In JS it is a piece of cake. In Rust, due to the way in which possession works, just one factor can “personal” self.subscribers
at a time. If we moved it out of self and into the unsubscribe
closure, then we couldn’t add any extra subscribers, or notify them.
The answer I discovered was to wrap the HashMap
with Rc<RefCell<...>>
. The Rc
half signifies that the innards will be shared between a number of homeowners by calling .clone()
. The RefCell
half means we will mutate the internals with out having to cross the borrow checker’s strict guidelines about mutation. The tradeoff is that it’s as much as us to ensure we by no means have overlapping calls to .borrow()
and .borrow_mut()
, or this system will panic.
#[napi]
impl Counter {
// ...
#[napi]
pub fn subscribe(
&mut self, env: Env, callback: JsFunction
) -> Outcome<JsFunction> {
// Create a threadsafe wrapper.
// (to make sure the callback would not
// instantly get rubbish collected)
let tsfn: ThreadsafeFunction<u32, ErrorStrategy::Deadly> = callback
.create_threadsafe_function(0, |ctx| v)?;
// Name as soon as with the preliminary worth
tsfn.name(self.worth, ThreadsafeFunctionCallMode::Blocking);
// Save the callback in order that we will name it later
let key = self.next_subscriber;
self.next_subscriber += 1;
self.subscribers.borrow_mut().insert(key, tsfn);
// Cross again an unsubscribe callback that
// will take away the subscription when known as
let subscribers = self.subscribers.clone();
let unsubscribe = transfer |ctx: CallContext| -> Outcome<JsUndefined> {
subscribers.borrow_mut().take away(&key);
ctx.env.get_undefined()
};
env.create_function_from_closure("unsubscribe", unsubscribe)
}
}
That about wraps this up!
I hope I conveyed that this took chunk of time fiddling round and working into useless ends, and I’m undecided if I did this the “proper” method or if I simply occurred to stumble into one thing that works. So please let me know you probably have any concepts for enhancements. Pull requests welcome 🙂
Success! Now test your electronic mail.
Studying React could be a wrestle — so many libraries and instruments!
My recommendation? Ignore all of them 🙂
For a step-by-step strategy, try my Pure React workshop.

Be taught to assume in React
- 90+ screencast classes
- Full transcripts and closed captions
- All of the code from the teachings
- Developer interviews
Dave Ceddia’s Pure React is a piece of monumental readability and depth. Hats off. I am a React coach in London and would completely suggest this to all entrance finish devs eager to upskill or consolidate.