Transcript
This transcript was autogenerated. To make changes, submit a PR.
Hi there, my name is Dmitry Kudryavtsev. I'm a senior software
engineer and I'm passionate about two things, JavaScript and
Rust. And so today I want to talk two you how you can supercharge
your nodejs and Javascript experience using Rust.
Wait, what? Javascript? Yes,
I know this is a rust conference, and don't worry, we will get
into rust code in just a minute. But before that, let's talk a little
bit about JavaScript. We can't ignore the fact JavaScript has taken
the world by storm. It's the de facto standard language for
anything HTML related. If you want to make your web pages
dynamic. JavaScript is the only language you can use AWS of today,
and thanks to frameworks like nodeJs, it's becoming increasingly
popular in the backend as well. And there are even tools like Electron
and Tauri, which is written in rust by the way, that allows
you to make so called native desktop applications.
So yes, JavaScript is very popular, and whether you like
it or not, it's probably here to stay.
So aws, you know, as they say, if you can win
them, join them. So JavaScript and Node Js are great,
but they are sometimes slow, for example if you
want to do like cpu bound tasks, maybe image processing or
3d rendering. And luckily there are ways
to speed up JavaScript, especially node JS, with the
use of native modules, which are written mainly in C or C plus
plus or rust. Now you
probably want to ask, why do
we want to use rust? So let's do a little comparison between Rust
and C. Well, C and C,
they are very mature, they're very old languages and they're
showing their edge, so we can't ignore this fact.
Also they lack any modern tooling, so you don't have decent dependency
manager has a relatively poor standard library.
From my memory you almost always
need to include third party libraries like boost if you want to get
fancy iterators or collections, maybe changing the new
C plus plus versions. I don't follow it anymore.
And the biggest problem with those languages
is that they are not memory safe. So you get into Sec votes a
lot. You probably saw this one, but why rust then?
Well it's strongly typed and compiled language,
so it has the same performance
as a native C or C has a rich standard library,
smart pointers, containers, iterators,
mutexes, everything you want for it's there
has modern tooling, so you have cargo for dependency management
and task execution. And the most important thing
is that it's memory safe, so you don't get any SeC votes.
I know youve can write unsafe rust as well, but let's stay in
the safe world of rust. Youve say great, but how
do we do that? So we will look at two different
approaches and we will compare them. And the first approach that I want to
start with is writing native models, especially for
nodejs. So meet Neon. Neon is a
library in the toolchain for embedded rust into JavaScript.
And so we will take a look now at a Fibonacci function
that we can write in pure rust and see how we can use it from
JavaScript. Below is the code. Don't get overwhelmed,
we will go over the code just in a minute. So first
we have our requires like we always do. This is all
the code that we need from neon. You need to understand the
row of neon in this neon is more like a glue layer between
the JavaScript world and the rust world. So we have all
the different types like js numbers and js strings maybe.
So neon provides all this as well.
Aws the context for function execution and model execution.
Then we have our Fibonacci logic, simple recursive fibonacci function,
nothing special in here. And then we have the glue layer. The glue
layer is responsible for converting JavaScript and
rust and vice versa. So if you have like
JavaScript number, which is just a number, and you
want to convert it maybe to an Int 32 bit
or a float. So this is the conversion layer. When you get
a result from rust and you want to pass it back into JavaScript,
this is the glue layer. This is what Neon is doing. And this is the
most important part between connecting the JavaScript and
the rust worlds together. And then we have the main function.
Like all executables we have a main
function. The main function is responsible into exporting all the
functions that we want to access from the JavaScript
world. So in this case we are executing the Fibonacci API function
and we are naming it as Fibonacci Rs in
our JavaScript world. And in just in a minute we will see how
we actually use it in JavaScript. Before importing into JavaScript,
we obviously need to compile rust code into the
native node modules. The neon team maintains
a package called Cargo CP Artifact, which essentially
once you run this command it will produce a dynamic library.
So if you're familiar with dlls from the Windows world or
so files from Unix world.
So this is essentially what it produces,
but it wraps it into the node API
so that it will be accessible from nodejs.
And so in order to access this code from node js.
Don't worry, the code is pretty readable. If you don't know JavaScript,
we just require the Fibonacci Rs from the previous file that we've compiled,
and we simply execute it as a regular function.
So this is one way we can incorporate
rust as a native extension into the
Nodejs world. Now some of you who probably worked with
maybe JavaScript or high performance JavaScript,
you probably heard about WaSm. For those of
you who didn't heard about WaSm, let's talk a little bit about
what is Wasm, which is an abbreviation for webassembly.
So webassembly is a portable binary format, and it's a
corresponding text format. You can think of it as the assembly
language. We have the text format which is the assembly
that you write, and the binary format which is converted
into the specific architecture of your machine,
be it ex 80, 86 or arm
whatever youve running on, it's executing by a virtual machine.
So there is a VM that's doing the translation between the wasm binary
into the actual architecture. It's supported in all major browsers
and nodejs, so the VM is implemented in all the
major browsers as of today, except for Internet Explorer.
And in node js it can be written in a special language
called assembly script. If you're familiar with typescript,
it's a bit similar to the assembly script is a bit similar to
JavaScript, but there are some differences
because webassembly is actually typed. But the
biggest pro, in my opinion, is that
Webassembly is actually a compilation target for other languages.
So we can take other languages and rust among them and actually
compile them into webassembly. In order to do that,
we have another crate called Wassenbindgan.
And let's now look at an example how we can compile a Fibonacci
function using webassembly. It's way simpler than the neon example,
so we only have one macro that is responsible for
converting the code into the webassembly.
It's then compiled with the special output target as
webAssembly. So you get a native webassembly that you can
then load into browser or nodejs.
Now when we have two or more different
tools, the obvious question that you probably ask is that okay,
but what about performance? So let's take a look at
performant. Don't be overwhelmed by the
table and we will go over the numbers here.
As you can see, I've run benchmark with the two code
hyperfine and I try to compute different
Fibonacci numbers. The thing with recursive Fibonacci is
that the higher you go with the numbers, the more intensive
the computation becomes, because it's a recursive function
and it relies on the previous computations.
So you can see that the 30th Fibonacci number is relatively
fast in all the tree. Actually, you can see that JavaScript is managing
pretty good, although native rust is the fastest
and wasn't being the second. The changes
are not that important. By the way, the green numbers you
can see is the performance increase you get from the base nodejs.
But then as we look at higher numbers such as the 44th or
the 35th Fibonacci numbers, we can see that JavaScript
is becoming two struggle really much.
While native rust is the performance is increasing,
the latency, the time that it takes to compute is increasing as well.
But you can see that also it's way, way faster,
in some instances around 60% faster
than the JavaScript solution.
And same can be said about the webassembly version which
was compiled from Rust. We can see that it's also faster,
it's slower than the native rust, but it's still way faster than
the JavaScript. And so if we analyze the data,
we can come to two solutions. Number one is that
rust increases the performance roughly by 60% compared to node
js, while webassembly increases the performance for around
45% compared to the JavaScript.
The second conclusion is that rust is around 45% faster
than webAssembly, which should not be a
surprise because webassembly is executed by
a virtual machine in the end. And there
is also a third conclusion is that benchmarks like this are
useless. They are made to
demonstrate a point, but you should always run youve
own benchmarks against the real case scenarios.
So it's good as a reference point, but don't rely
on it when you are making a decision, because as you saw in the example,
if you don't go up too much in the Fibonacci numbers,
the performance increase you gain might not be worth the hassle
in introducing native models or even webAssembly,
because as you can see, the forter Fibonacci number is computed
relatively fast, even in JavaScript itself. So always
run your own benchmarks. Let's do a little comparison.
So when to choose native models, when to choose webassembly
there are some nuances and let's cover them.
The first point I want to touch is if youve
talking about maximizing every output, getting the best performance
you can get. Youve should absolutely choose native models.
It's no surprise native will always be faster than the VM you
can see. Look at Java versus
C of C Plus Plus. C and C Plus plus will always be faster because
they compile natively even though the Java VM is
very optimized. Very good. If you want to squeeze
every possible performance, then you should
go to the native solution as well. Having said that,
I want to point out that webassembly is relatively fast,
so if you don't need the absolute best performance,
consider webassembly. It's a good middle ground
between going fully native versus rewriting some
of the application parts into webassembly. Let's talk about
reusability when we talk about reusability when
I talk about reusability, I mean taking your code and using it in a
different environment. It's a but complicated with both of
these. Let's try to unwrap it and see where the complication comes.
Now, native libraries can be reused in other languages through
foreign function interfaces. So a classic example,
let's say you have a back end that's written in Rust, you have some business
logic that is written in rust, and you want to port it into your web
application as well as your mobile applications like Android and iOS.
The same binary can be shared between
the web application using WebAssembly, for example,
or the native model, and it can be compiled and
reused inside Java or Swift, for example, so you can
share the same code using pure
rust. It's not true for WASM because once you compile it
to a WaSM, it can only be executed by webassembly
Vm. WebAssembly vms are available in all the browsers and node js,
as I've said, but they are not available in mobile. For example, if you need
to share your logic with your mobile application, and your mobile
application is native, meaning it's written Java or Swift,
then you can share the webassembly. Because as far as I
know, maybe there are implementations for the Webassembly
VM in Java or Swift, but as
far as I know, it's not that easy. So if you're
talking about reusability, the native models can
be reused in other languages that support foreign function interfaces.
Let's talk about ergonomics. Ergonomics is roughly
the amount of code you need to write in order to produce a working example.
And if you paid attention that youve saw that the amount of code we
need with the neon wrapper is relatively
big. You need to import all the neon types,
you need to write the glue layer, you need to write the export function.
And the glue layer can become very messy because you have error handling,
because remember, the Javascript isn't typed,
there are no types. So you need to be able to do
casts between maybe it's a string, maybe it's can odd string,
maybe it's a number, maybe it's not a number. So you need to handle all
the edge cases. Webassembly on the other hand is
typed. So webassembly have types,
they have basic types, but nevertheless the
waslin binding and package is able to seamlessly convert
your rust types into webassembly types,
which with neon requires a glue layer, as I've said.
So in my opinion, the ergonomics with webassembly,
especially if youve doing like small optimizations,
you want to rewrite maybe two to three functions.
Then the ergonomics with the webassembly compilation
are a lot nicer in my opinion, especially with the bus and binding
and function, because all you need to do is just macro the function
and you got an executable webassembly,
it's a perfectly valid rasp code. Add the macro
and you get a webassembly output. Let's talk about
standard library for those of you who don't know,
standard library or Stdlib is talking about
the access to the file system, networking and anything OS
related. And it's a funny one, because when
I worked on this presentation and
the blog post that inspired this presentation, I learned
that you actually can't access files from webassembly because
Webassembly does not have access. Two, the standard library and
if you look at the WaSm Biengan package, you can see that
anything that is related into the OS and
file system and the standard library, it's actually not implemented.
Inside the code there is a proposal.
It's called wazi, which stands for Webassembly
system interface I things which
does give you access to,
which is a proposal to give you access to the native operation
system, things like file system and network.
But for now it's only a proposal. And if youve need to have
access to the standard library, your only solution
today is to use native modules. Unless you want to experiment
with the webassembly system interface, which is still in development.
I also found out that there is
an option that you can mount like a virtual file system, so your
files will be actually compiled into the webassembly itself
and then you have a virtual file system like
in RAM file system that you can read the files from, but it's not the
same as reading dynamic files, you can't use it
for if you want to read a dynamic file from the disk for example.
So if that's your use case youve absolutely have to
go to the native model solution. If we are talking about portability,
it's the famous phrase that it works in my machine.
You need to remember that native models are host machine dependent.
This means that when I compile my rust code on MacBook
M one it will be compiled into the ARM
architecture. I can just take this binary and give it to my
friend who runs a Windows machine because his windows machine is running a different
architecture, the X 86 64.
And this is probably most likely it's
not true for WaSM because WaSM is executing by a vm.
So once I have the WASM executable I
can give it to the other person and as long as he have the vm
he can executing the code. And for native models it's
always important to recompile to the target architecture.
So always remember that if you are using native models
and you are running let's say on an Alpine Linux docker
in your production environment, you should compile
the native models on the same environment. So dockerized
containers are good solutions. Don't just upload your binary
files, always compile them in the required environment
as you need. Let's talk about node JS and browser.
And up until now I refer to NodeJs and JavaScript
interchangeably. But they are two different things.
JavaScript is a language, node JS is a framework, and the
big takeaway that you can take from this is that native modules can't be
used in the browser because JavaScript has no support for foreign
function interface. The reason we can use native models
inside Node Js is that NodeJs provides the so called can API,
which is node API. It's an API to build
native extensions with a stable API so
you can write extensions in rust or in c
or C and to extend your node JS
ecosystem. So remember that native modules cannot
be used in the browser. So your only solution for the browser
is to actually use Wasm because
all browsers have a VM for that except for Internet explorer
as well as Node Js. But if
youve don't target the browser and you target only node JS, so for example only
backend code, then it's perfectly safe to write native
models. Let's look at a recap.
When I think. But whether to choose native models or
webassembly, I like to have two mental models
in my head. The first mental model says that native models
are meant to extend your node JS code. So if
you have a Nodejs code that you want to optimize,
then you can use native modules to extend it beyond
what Javascript and nodejs can provide you.
While webassembly is meant to replace non performant Javascript
code. So let's say you have some image processing in the browser,
you want the user to be able to manipulate images.
So webassembly will be a great place for this.
Let's say youve developing a visualization application in the browser
itself. It's very common now that many so
called professional desktop tools are moving into the web,
and so webassembly is a great place to squeeze all
the performance that you need to squeeze from them.
So those are the mental models I hold in my head.
They work pretty well. And thank you very much.
I hope you learned something. You can
find me in LinkedIn I'm mostly active in
LinkedIn. You can follow me on Twitter, you can find me in GitHub.
You can scan this QR code which will lead you to my blog where
you can find the articles that this talk is based
on. There is more technical information inside
the articles AWS, well as link to GitHub repos that you can execute
the Fibonacci examples. And thank
you very much and I hope you enjoyed my talk and enjoyed
the rest of the conference.