SURMA: Are you ready, Jake, to
get your acting face on, and-- [LAUGHS] JAKE: I'm going to pretend to
be surprised by all the content, and it's totally
not the third time we've tried recording this. SURMA: Technology. [MUSIC PLAYING] So AssemblyScript. We've actually talked
about it before. We mentioned it and kind
of introduced a little bit. JAKE: That was in our
episode about loops. SURMA: Loop tiling. Yeah, I think that's
where we talked about it. JAKE: Loop tiling
and rotating, yes. SURMA: This is a
WebAssembly topic, as AssemblyScript is a
language for WebAssembly. And we've talked about
WebAssembly in general as well before, but I
think most of the times, I showed WebAssembly a bit like
this, which is the WebAssembly human-readable text format. And I think you pointed out
that many humans might not enjoy reading this at all, even
though it's, you know, letters. JAKE: The common file extension
for this file format is what? SURMA: And it's
very appropriate. [LAUGHS] I mean, to be fair, as
far as assembly languages go, it is a good one. I actually-- I think
it's more readable, and-- JAKE: That's like saying,
as far as the Kray twins go, Ronnie is the best. You know, that's a low bar. SURMA: It's a low bar, sure. But if you can get away with it. It's that. It can be-- I agree. This is not how you
want to write code. It's not how you
want to read code. This is not usually where
you're going to put everything in production, like this. And I think the folks behind
AssemblyScript kind of agree, because the alternatives
currently out are to use C and C++ or Rust-- I mean, there's more languages
now compiling to WebAssembly, but I would argue C and Rust
are the most predominant ones, the most well-known ones. And I think part of that
is that as a web developer, we live our lives. We live and breathe
JavaScript and TypeScript, and none of these languages,
which are like C and Rust, are necessarily
in our daily work. I mean, maybe you know them, but
if you don't and you're a web developer and you only use
JavaScript and CSS and HTML, WebAssembly seems
a bit inaccessible. And that's exactly, I think,
what the AssemblyScript folks wanted to change, because
it's a language designed for WebAssembly. So in contrast to
like C and Rust, which are languages that
already existed and that then added
support for WebAssembly, AssemblyScript was designed
for WebAssembly specifically. It is still fairly low level. That's why they called
it AssemblyScript. And as you can see here on the
landing page of the website, they're saying they're using
the familiar TypeScript syntax. Which is really nice,
because I think most of us are familiar with the
TypeScript syntax, because it is pretty much the
JavaScript syntax with a couple of additions here and there. JAKE: Yeah, and I would say,
even if you're watching this and you're not familiar
with TypeScript, yeah, the important bit there is
it's additions to JavaScript. So you will look at TypeScript
and understand most of it, and you'll be able to see-- there's just a
couple of extra bits. Yeah. SURMA: Exactly. Although one thing we
should be clear about-- and they are very clear about
this on their repository-- it is definitely
not a TypeScript to WebAssembly compiler. It just reuses the
syntax and the language and, as far as they
can, the semantics. The code that you read
in a AssemblyScript looks like TypeScript. You'd think you
know what it does. It is very, very likely that
that's also what it actually does once you compile it. But it is not the case you can
take just existing TypeScript, throw it into AssemblyScript,
and get working WebAssembly out the other side. JAKE: And when you say
that, it sounds like you're going to have this huge uncanny
valley problem, because ooh, it looks like TypeScript,
but it's not TypeScript. It's not going to work like
you expect TypeScript to work. It's going to work
like something else. But in practice, it's fine. It actually works really well. SURMA: Yeah, and I
think that's the thing. They tried to mimic the
semantics of TypeScript as far as they can, so that
if it looks like TypeScript, it will behave like TypeScript,
as far as WebAssembly allows. It's just TypeScript,
in general, has things that just
don't work in WebAssembly. Like the same variable cannot
hold a string, a number, an object, a class. You don't know until
runtime those things don't work in WebAssembly. So they had to add
some restrictions to the language and also
a different type system. And that's kind of what I wanted
to talk about a little bit. But before, I think it's kind
of important to talk about why would you even use WebAssembly. And I think there's four
major reasons why you would be looking into WebAssembly. And the main reason we have
seen so far in the WebAssembly ecosystem is making
use of existing code. So we have Emscripten, the
compiler that brings C and C++ to WebAssembly. Rust supports WebAssembly. And so that allows
you to make use of the entire ecosystem
of these two languages and bring them to the web, even
though those libraries might not have been written
with the web in mind. And that's really cool,
because not everything is available in a
web-compatible language. And I think that's something
that we kind of exploited, almost, with Squoosh. Because yeah, there
might be a JPEG encoder written in JavaScript. There might be a PNG
encoder in JavaScript. But I don't think anyone has
written down and sat down yet and wrote an AVIF encoder
in JavaScript or a JPEG XL encoder. And so we were at a loss
until either the browser shipped those or we sat
down and wrote them. But instead, with WebAssembly,
we can make use of these libraries written in C and C++
and just bring them to the web. And that's really cool. However, as I just said,
with AssemblyScript, that's kind of not the
case, because there's now some AssemblyScript-specific
libraries out there, but it wasn't targeting
an existing language. So the whole
bring-legacy-code-to-the-web thing wasn't really an argument
for AssemblyScript to begin with. But it is one of
the major use cases that we see for people
to look into WebAssembly. The next point, which
many people bring up, is performance, even though
I've been very, very vocal about the fact that WebAssembly
and JavaScript, for the longest time, had the same
peak performance. They were optimized
by the same engine. They both generated machine
code under the hood. And so really, they have
the same peak performance they could reach. The difference is that for
JavaScript to get optimized, it needs to run. It needs to be observed,
because just from the code, you cannot tell what kind
of type a variable has, which you need to
generate machine code. While with WebAssembly,
that is already encoded in the WebAssembly file. So the Engine for JavaScript
needs to run the JavaScript, observe it, and then starts
optimizing as it runs the code. And many JavaScript
optimization experts have experienced this warm-up
phase, as they often call it, when your code runs slower at
the start and then gets faster, which is often also
seen and benchmarked. Then they'll run a couple
of loops, which they don't measure, because warm up. In WebAssembly, the optimizing
compiler kicks in immediately. So we have a fast
compiler, which is called Liftoff, which
generates code really, really fast at the cost of
not generating optimal code. And then the
WebAssembly can start running as fast as possible,
but the second that compiler is done, the optimizing
compiler called TurboFan kicks in
and starts optimizing one function at a time and
switches them out bit by bit, even if you haven't even
run your WebAssembly. So just with a bit
of waiting, you will just get optimized
WebAssembly code under the hood. JAKE: Yeah, I think
it was actually Williams who said the
difference between JavaScript and WebAssembly is
that the performance is reliable with
WebAssembly, as in you're in control of when things like
garbage collection happen, that sort of thing. And yeah, a lot of the
optimizations happen up front. So it's easier to measure the
performance of WebAssembly code, and it's not going
to change in between runs. SURMA: Exactly. I think that's really well put. WebAssembly is just
much more predictable in how it will behave. Because in JavaScript, you
also have this phenomenon that you can call the same
function with a string and with an object
and with an array. As I said, the engine observes-- if this function gets called
with a string all the time, then it generates machine
code for handling a string. But the first time you call
it with a different type, it then has to fall
back to interpreting, because the machine code is
wrong now, if you call it with an array all of a sudden. And that's called
de-optimization, a de-opt, which will slow you back down. And so while you can reach peak
performance with JavaScript, it can also kind of fall
back to becoming slower. With WebAssembly,
that's not possible, because everything
is strongly typed. The third point-- and
there's actually something that you already kind
of mentioned just now, which is there's
more performance. On the one hand, WebAssembly
is getting increasing access to things that JavaScript
doesn't have access to, like actual shared memory
concurrency with threads, SIMD, which there were proposals
back in the day for JavaScript, but they expanded them all
in favor of WebAssembly. And what you just said. Because the language decides
what part of the runtime to ship, things
like garbage control can be under your control. So in JavaScript, there
is a garbage collector. And it will run, and sometimes
you don't want it to run. But there's not much you
can do about it, except bend your code backwards
to prevent garbage collection in the first place
by keeping references around. But in WebAssembly,
you can actually change the garbage
collector for something that works more in how you like it. And that is something I think
game engines, for example, would really, really like. Because they often
like to decide when there is a good time for
garbage collection and when there is not a good time
for garbage collection. And lastly, and this is going to
be interesting, is binary size. Because the spec says that
one of the design goals is that the WebAssembly
binary format, the bytecode file is a very
small binary representation of the code that it contains. JAKE: But then the codex we have
in Squoosh are not small, but-- SURMA: Yeah. JAKE: --is that just
because they're a big code, or is that down to more of the
standard library sort of stuff that C and Rust need? SURMA: It's all of
the above, really. On the one hand,
an image encoder will have a lot of logic. And I think if you
wrote it in JavaScript, it would also be quite big. But at the same time, it is
what you said, that WebAssembly is a pure abstract VM. And in JavaScript,
we already have-- people often say that
JavaScript doesn't have a standard library,
but the web platform does provide a
huge amount of code that is just in the browser. And you can handle strings,
you can handle arrays, you can iterate over
them, you can map them, you can filter them,
you can split strings, join strings, filter strings. All these things are just there. If you do use that logic in
WebAssembly or in a language compiling to
WebAssembly, that code needs to get compiled and
put into the binary as well. And then lastly,
there's also the aspect that WebAssembly has a
strictly numerical interface to JavaScript. So whenever you want to
pass anything that is not a number-- an object, an array,
a string-- from JavaScript to WebAssembly and
back, there needs to be a bit of JavaScript,
which is called the "glue code" often, that converts
between what the WebAssembly file understands and what
JavaScript understands. That is also code
that we actually ship over and over in Squoosh. So we have actually not
made great experience with binary size. Although I will say,
A, that WebAssembly compresses really, really
well with Brotli and gzip. It is very compressible. Often 80% of the
file just goes away, and you're left with 20%
of the original size. But also that you can make
these modules very, very small at times. But with tools like
Emscripten and Rust, it is quite hard to remove. It just assumes that you need
a lot of standard libraries, even if you don't, and it
sometimes takes a lot of work to get rid of all these bits
that you could sometimes do in a different way. But we've seen-- and
I think we talked about this in our loop
tiling episode, where we made the WebAssembly
actually be smaller than the comparable
JavaScript code. JAKE: Yes, yes, we did. SURMA: All right, I think
that was enough waffling. It is time for some
code-y bits, at least something that is close to code,
which is the install command. It's closer. It will lead us
to the code, Jake. You don't worry. You have seen the slide
deck before three times. You know you don't
have to worry. So with this command
you install assembly. But this is actually kind of
nice that's it's just on NPM. It uses WebAssembly itself
under the hood, which I thought is kind
of interesting, but you don't really
notice when you use it. If this is too much of
a commitment for any of our viewers, you can also
just go to webassembly.studio, where you can try out some
Emscripten with C and C++, Rust, and AssemblyScript in
a little IDE in the browser. It compiles in the
browser, and you can just play around without having
to install anything. So both of these paths are
completely valid to play around with WebAssembly a little bit. All right, time for our
first AssemblyScript code. This is a TypeScript file. In this case, it's an
AssemblyScript file. It exports a function called
main, and it returns 42. Anyone who knows
TypeScript should be fairly comfortable with reading this. We can compile it. So the AssemblyScript
compiler is called asc, because it's the AssemblyScript
compiler, very intuitive. And we pass it our main.ts
file, and with dash b, we say the binary output
file should be main.wasm. And now the only
thing left to do is really load this file
somehow in the browser, probably with an HTML page,
and that would look like this. We just use
WebAssembly.instantiateStreaming and throw the fetch in
there, of the wasm file. It will give us
back an instance, and every exported function
will be on instance.exports. So you really get a
WebAssembly module instance, and that module has exports,
just like the modules we have been writing in
that AssemblyScript file. And so that's a really
nice, clear mapping between WebAssembly
modules and code modules, if you want to talk
about them that way. And this would pretty much
exactly log the number 42 into your console. JAKE: Success. SURMA: If you're
into it, the compiler can also generate this
human-readable text format I showed at the start
that can be helpful sometimes to see if certain
exports are present, or suddenly you have
a different name, or maybe if you want to see
what the function looks like. It can sometimes be helpful,
but it's definitely not required to use this at all. What is important is that
the AssemblyScript compiler by default uses no
optimizations to make it A, fast, but also to make source
maps work really clearly. There's a very clear mapping
from WebAssembly code blocks to individual commands in the
AssemblyScript file, which means you can step through
your AssemblyScript code in DevTools, which is really,
really nice for a debugging experience. Even though, one
thing I should note, something that doesn't work
is like inspecting variables. You can't hover over a variable
to see its current value, because source maps cannot
contain that kind of mapping. But there is something in the
works there, as far as I know. But if you end up shipping
your AssemblyScript WebAssembly to production, make sure you
use the dash O flag, which is an optimization pass
which will not only make your AssemblyScript code
even faster, but probably also significantly smaller. So this is quite important. But this is pretty much it. This is how you
use AssemblyScript. And as you already pointed out,
the return type is a bit weird. Because if you know
TypeScript, you also know that this is not
a TypeScript type. And I think this is-- so VSCode, if you
use it, will probably put a nice, red
squiggly line under it. But this is one
of the differences between AssemblyScript
and TypeScript, in that they changed
the types from-- JavaScript has
numbers and strings, while WebAssembly only has
unsigned 32-bit integers, which is what this is. It has a couple more. I won't get into
that, but these types are a direct mapping from
what WebAssembly supports to the language
that you're writing, which is actually quite nice. JAKE: So this is where it's
different to TypeScript, because in TypeScript, the types
are used as like validation as part of the compilation step. But in AssemblyScript here,
these are runtime types. SURMA: Exactly. Well, they're
both-- they're also validation at compile time,
but yeah, that's exactly right, that these carry over to
the runtime and actually impact what kind of
code is being generated. As I said, VSCode, by
default, will probably put a red squiggly
line under it. To fix that, they do provide
a default tsconfig.json that you can just extend, and
then the red squiggly lines will go away. And VSCode will think that this
is kind of valid TypeScript. So things like refactoring,
jump to definition, all those things will work,
and that's actually really, really neat. All right, let's make
our AssemblyScript code a bit more interesting. We are now going
to use an import. We're going to import a function
from a different module. You can split your code into
multiple modules and import and export stuff, like you're
used to from JavaScript and TypeScript, and
the compiler will take care of like
bundling it all together and linking and recognizing
which function you're actually calling. So that shouldn't
be very surprising. So in this case, I also
create a utils.ts file. And now we want to look at
the implementation of alert, obviously, but there is none. And this is really interesting,
because the "declare" keyword also exists in TypeScript,
and it tells the TypeScript validator basically
that, I assure you, this function exists
once you run this code. I just haven't
implemented it myself. JAKE: Yeah, so it's
common to use this if there's an API that the
browser supports but TypeScript doesn't know about it yet. You use "declare" for that,
to say, look, it's here. It looks like this. SURMA: Exactly. And so you were saying, we're
declaring this function exists and that this module will
export it, which kind of doesn't make sense. But we're just saying,
this is what reality will look like in the end. And so what this means is that
now the AssemblyScript compiler will say, all right,
I'm just going to note down this
function takes a u32, and I will be provided a
function for this later. And so if we now used our
instantiateStreaming call, this would actually
throw, because it says, hey, the utils module
expected an alert function, but you didn't give me one. I can't run this. And so what you need
to do is you actually need to provide a
so-called imports object for the instantiate function. And it looks like this. We're saying, OK, it's
an object of modules, and we can just
provide functions. And what you see here-- I'm literally just passing
through window.alert, which we all know and love from
our good old debugging days. So this way, it is really
cool that we can not only call from JavaScript
into WebAssembly or our main function,
but our main function can now call back
into JavaScript through this imports object. JAKE: That's amazing. So here, we're passing
a JavaScript function into AssemblyScript,
which we can call. But presumably, you could
compile some WebAssembly from C++ or Rust and take its
exports and make them imports into AssemblyScript. So you're using
AssemblyScript to call Rust, which is calling C++, which
is calling JavaScript, and that just all works. SURMA: Exactly. You could do that, and that's
actually kind of fascinating. I think it's something that
even we in Squoosh don't do yet, and we could look into that. But yeah, you can-- just anything that is available
in the host environment-- which, in the browser, is
JavaScript-- you can just use that as an import, and then
that will just kind of work, as long as you
stick to the types that WebAssembly understands
or that WebAssembly knows how to convert to
JavaScript and vice versa. That's a little bit
more interesting. Jake, I've asked you
this quiz before, but I will ask it to you again-- what do you think is
different about this example than all our other ones? JAKE: I don't think
array.push will work, because this is not TypeScript,
this is not JavaScript, this is AssemblyScript. So arrays work, but
maybe not array methods work, things like
the prototype chain. That sort of stuff is
not going to be there. SURMA: Right-- no,
that is not right. Because this is
actually what people mean when they say a language
is shipping its own runtime. AssemblyScript actually
has an implementation for these growable arrays. So all these methods
do exist, and they will cause code to be added
to your WebAssembly binary. And this is what I mean-- it starts to ship
more and more runtime the more you start using it. But this works. They actually have
memory management. And I think this, for me, is
the big difference with this. We are now in memory
management territory, because we can push into
arrays as much as we want, which mean our memory
usage can grow. Which also means, at some
point, we can run out of memory. And so the second you
use this kind of code, your instantiateStreaming
will start to fail again,
because it will say, hey, I'm expecting you to
provide me an abort function. Because we need
something that gets called when things go wrong--
or AssemblyScript needs something that it can
call when things go wrong. And so it expects
for this u32 array to have an abort method in
case the push doesn't succeed, because it might try
to grow the memory, and then it's not available. The abort function
actually takes parameters, so you can figure out which
actual function from which actual file calls this abort. So the way I'm handling the
error here is probably not very elegant, but I
just wanted to explain why this is suddenly there. Overall, I would
recommend to make use of the AssemblyScript
loader, which takes care of all of this for you. Not only does it provide
an abort function, but it also generates the
appropriate error messages to tell you, in this file, this
function call, on this line, calls the abort, and
here is the actual error description, fix your code. So the AssemblyScript
loader takes care of all these little
details under the hood for you. It gives you a proper
abort function. It gives you in this
file, on this line, something went wrong, here's the
error message, fix your code. So most of the time,
I would recommend using the AssemblyScript loader,
because it is actually not that big and just takes care
of the basic fundamentals of good error messages for you. JAKE: Right. What's not that big? Well, what are we talking here? Because the glue code
for Emscripten and Rust can be quite big. SURMA: Right, so the
good news about this code is that the loader is
not module specific. With Emscripten, you
get glue code generated for each WebAssembly file. And so even though
they share some code, you usually will end
up double loading it. The AssemblyScript's loader,
you only need once for-- it doesn't matter how
many AssemblyScript modules you have. And I think even then, if you
use everything it provides-- I think we're talking
about 1KG zipped, so it's definitely not
something that should hurt you that much to load into
your bundle, which I think is really, really nice. All right, let's make it
a bit more interesting. Because as I said, the interface
that WebAssembly supports is pretty strictly numerical. But as developers,
we know that we work with more than just numbers. Most notably, strings
are usually a primitive that we often use. And so I turned our
main function now into a function that
takes a name as a string. And our alert function
now also takes a string, which we know in
JavaScript land is true, and so far that conversion
just happened to work. But now we want to pass
an actual string value. One thing that we
will need to do is now to export the
runtime, because strings are runtime values. They are not something
that can just be passed along as a
parameter but that needs to be a pointer to a string. And so by exporting
the runtime, we can create those
strings through the API that the module exposes. And this is something, again,
that the AssemblyScript loader can take care of for you. So it exposes two functions,
among many others-- a newString function
that creates a new string in AssemblyScript land that
AssemblyScript can then understand, and a
getString function that turns a number, a
message pointer, back into a string in
JavaScript land. This is not very nice. I think I kind of like the
approach that they give you, a non-automatic interface, so
that you are in control when the conversion happens,
and you exactly know-- no cost here is hidden from you. You can see that there's
extra function calls happening to make this interface work. But I definitely
feel that you have to know your
interface very well, and it is a lot of extra
work and makes it very noisy. We can't just pass
through alert anymore, we have to do a
wrapper alert function. There is something that
does it better, which is called as-bind, which
is a library from one of the core maintainers
of AssemblyScript that tries to do all of
this automatically for you. And that can be
really, really nice. So if you want to
use that, I would recommend looking at a README. And even this
library, I actually don't know how big it
is, but it is still way below the glue
code that we usually see in some of the more
established languages. All right, one last thing
I want to talk about, which I mentioned earlier,
is the runtime itself. By default, the runtime that is
used is called "incremental." So I have it
explicitly as a flag here, but you don't
need to specify it, because that's the default. That
is a full, automatic garbage collector that every now and
then will just run and collect all the memory and free it up
of things you don't use anymore. That will add about
3 to 4 kilobytes of gzipped WebAssembly
to your WebAssembly file, depending on how many
runtime functions you use. JAKE: And that's
not too bad, either. We start to get concerned
about that kind of file size when it's blocking
your initial render or blocking fetching some
data or something like that. But we tend to use WebAssembly
in a way that it's lazy loaded. You get an initial render
down, and then the WebAssembly comes to do something else. So in terms of that,
a few k is nothing. SURMA: Exactly. I think it's an absolutely
acceptable payload, but it is a garbage collector
that will just run every now and then, I think,
to just inject garbage collect calls at the
end of functions or something? I don't know. But as I said,
sometimes when you have a really high
performance use case, like a game that wants to
ship 60 frames a second, that might be unacceptable. And for that, they
provide a second runtime, which is called "minimal." It is still a full
garbage collector with memory management
and everything, but it doesn't garbage
collect until you explicitly call the function to tell
it to garbage collect. And that can be really nice. So sometimes it might be
worth your while to build up some unused memory that
is still allocated. And then once you know
the level is over, you're showing your high score
at the end, where the frame rate isn't as important
anymore, or you know there's not much input
happening, that's where you run the garbage collector. And I think that's actually
really interesting. There even is a "stub" runtime,
which we used in the past for our image rotation example. Because we exactly knew
what kind of memory we need, how much we need. We didn't want to
free up any memory, and that basically reduces
the 3k almost completely from the file. I think it's about 500, 600
bytes gzipped that are left. Because you can still
allocate memory, but you can't free it anymore. But this is completely
adequate for these modules that you instantiate,
you run them, and you throw them away,
which is exactly what we did with the image rotation. We just create a new instance,
put in the image, waited for it to rotate the
image, got it back out, and then the module
gets destroyed. And if we want to rotate
again, we create a new one. And in that case,
you could actually use AssemblyScript, even
for render-critical code. I probably wouldn't recommend
that, because you still need to load it, instantiate it. But yeah, you can really
squash the file size down. You could even-- this
flag even accepts a file, so if you wanted to, you
could write your own runtime. But we're not going to get into
that, because that obviously needs a bit more detailed
knowledge about WebAssembly and memory management. And that's not easy. JAKE: But the design
here is brilliant, right? And it's something that
a lot of JavaScript projects, JavaScript libraries,
JavaScript frameworks could really learn from,
this layered approach. Because it's great to
know that, well, OK, so we've given you the easiest
thing, but here's a flag you can swap it out
for something simpler. You can even write your own. You can control the
whole stack if you want. I really like this design. SURMA: I also think it's
a very nice approach, and it's very
extensible in general. And they have a Discord linked
on their main website, which is linked here, with a
super helpful community. So if you are trying to
get started but you're getting stuck, there's always
someone in that Discord channel willing to help. But this was basically my
AssemblyScript speedrun introduction. JAKE: Yes, and I
would say, if you're unfamiliar with lower level
programming languages, like me-- so I don't have a lot of
background in C or C++-- I found AssemblyScript really
easy to get started with. Because it was in that
familiar JavaScript-esque, TypeScript-esque
environment, but it was just introducing the more
manual memory management stuff and the lower level types. I find it really easy
to get going with and write some really
optimized WebAssembly code. SURMA: Well, then
there's no better way to end than with that. [MUSIC PLAYING] JAKE: If the light
falls over, it will change the shot
quite dramatically. It'll knock all kinds
of [BLEEP] over. So all right, OK. It'll be funny if it
falls over, won't it? SURMA: Breaks your
nose, blood everywhere. JAKE: Yeah. SURMA: Humor. JAKE: [LAUGHS]