All right, hi everyone, so yeah, my name is Eduardo Vacki, all right, and besides a bunch
of other things, you can find me online with that nickname in different places, and so I've
worked on was zero for the best of a year, a year and a half, 23, 20, 24, and now at a company
called TechRit, and I'm going to tell you a little bit of a was zero, and then I joined
this company called Delipso, and there I'm working a little bit on was zero, and especially
recently on Cheekery. So I'm going to tell you how these two web assembly runtime work,
then gave you a great introduction on web assembly in case you were that familiar with
it, so I'll obviously skip the details there, and I want you to tell you a little bit
bit about Delipso, so we built open source framework called ex-dism. Ex-dism is a frame
to build web assembly plugins, so this is a different perspective, you sell the perspective
of virtualizing and containerizing your workload with then, and here instead we are proposing
web assembly as a host, as a way to create code that can be hosted in different platforms,
which is the thing that was born for after all. So instead of the browser, you have a
bunch of different run times, and these run times work better when they are embedded in some
languages, so I don't know, wasm time was great for rust, V8 works great for a C in JavaScript,
but what about web assembly that you want to run in your go software, or on your job
of EM, all right? So we have different implementation, there are tailored for different run times
depending on the application where you want to host code, and then we have the opposite side.
We have frameworks so that you can compile your code into web assembly and host it on all
these platforms. The key thing is, this ABI is tender diced, I mean it is the same across
platform, so you can run the same plugin across these different platforms. Yeah, I think I made
this pretty clear. One thing that we just launched is go mcp.run, I don't know if you've heard
all the buzz in the AI space, or if you care. So this modal context protocol, it's something
that was recently released by unfrop, it can allow you essentially to do agents, plugin functions,
inside your LLM, whatever. All right, anyway, it's kind of fun. We built this using our open source
tooling, it's called ncp.run, and you can play with it, there's a bunch of different services
that you can plug into your LLM, and it's pretty fun. You can write your own using web assembly
and our tooling, and you can see web assembly in the wild for practical use cases with this,
and yeah, I'm going to show you a LLM example at the end. All right, so point is,
wasn't lets your users write software extensions, and then use their favorite language to run them
in a sandbox environment. So we made pretty clear that the peak here is sandboxing, right?
All right, in wasom IO 2024, my good friend Andrea, who's working on security, and then
who's our CTO, he produced a concept of language native run times. What is a language native run time?
Language, native wasom run times are untimed, it is written in the host language, and language is
going to host the web assembly, the web assembly run time. Isn't that right for all run times?
I mean, if it's the host, it's going to be rust. So yeah, we make this distinction,
especially for managed languages, where it's a bit harder and more complicated and inconvenient
to interact with native so-called native code. So in the go run time, we have a wasom run time that
it's actually written in go, and for the JVM, we have a run time written in Java. So what
out of the benefits of this approach? Well, why are we doing this? And that's the whole point
of the talk, in a way. All right, so wasom run time written in go, it comes with an interpreter,
and the interpreter is portable everywhere, go runs, who wasn't the happen to be in the go room yesterday?
All right, quite a few people. So you can leave the room, pick a coffee, and come back for the rest.
Because there's a bit of a little overlap here. We're going to see how with zero translates
web assembly into native code, for the second part of the web part. So the first part of the
interpreter, it's code that's been interpreted obviously, and runs everywhere, go runs, and compiles.
But we also have on the head of time optimizing compiler for Andy 64, and I'm 64. I'm going to
show you that in depth if we have enough time, and then we're going to do a comparison with
chicory. Now, chicory, it's kind of different, well, conceptually, the same, like we have a run time
that's written in Java for the Java run time, implements an interpreter, and that's not supposed
to be fast by this portable everywhere, and then we have on a head of time bytecode translator. Now,
it's a translator to the Java bytecode, so that means that is also portable.
But not quite. I mean, that's this guy here, and we're going to have a section about that
too, which is the most exciting part of this to me, at least. All right. So we're going to first
do a comparison between Go and Java, and hopefully, surprising for a few of you. I'm going to talk
a little bit more about language native run times, and then we're going to go into function
compilation and evaluation was zero, and then the same for chicory. So Go versus Java,
couldn't be more different, right? So one has a different, they have a different form of an
inheritance, like Java is the OOP, object-oriented language, like for forever, like the
distender nowadays in a way, because of its history, and because it was designed like 30 years
ago now, it has deep time hierarchies in their standard library. While Go tends to prefer shallow
type hierarchies, and the form of an inheritance is different, it's nesting for strots, and that's
going to different. In Go, you tend to use a lot of libraries while in Java, you tend to use frameworks.
That's kind of, yeah, kind of through, but it depends also on the language that you use on the
JVM. So JVM now hosts many languages, and they might prefer libraries too, but in general,
that's a, that's a, I would say, good distinction. Go Compost in native executables, and
in the Chiefs Cross, as it shifts portability by having a great cross compilation story. So you
don't need to bring in a sequimpiler, have a CIS route, you just use the GoTool chain, and it will
just support cross compilation everywhere, and it will just work, and you can compile for arm
on Windows, X86 machine, and target Linux, and that's awesome. Right, and then the portability
is also through static linking, and libraries are shared as source code, but it's super fast as
a compiler, because it's as some aggressive compile time cache. On the other hand, Java works
in a different way. It's a byte-cut target, portability is achieved by porting the runtime,
much like WebAssembly, duh. And it's really based a lot around dynamic linking.
I'm going to tell you a little bit about that in a minute, and libraries are shared as binary
artifacts, where binary means here, byte-cut. Okay, a little bit about dynamic linking.
So this is the diagram, this is a diagram of startup time for Java. It's in a talk by one of the
Java, GVM contributors and architect, then I think, starting fast. So this shows an
typical Java framework, how long it takes to actually boot user code, and it takes a while.
Why? Because before you actually reach to user code, there's a bunch of plus loaders and
plus as initializers that have to run. And this increases startup time. This was it's all about
moving stuff at build time to make things faster, and one go into that detail, but
keep the capabilities. Someone was asking about dynamic linking. This is a way to deal with that
in a byte-cut environment. And it brings some issues to, right? But yeah, the way the JVM works
is loading a lot of these modules, these classes, and every class, every non-treegel class,
as an initializer, just like WebAssembly modules, might have an initializer, and loading each of
them, obviously, takes some time. All right. Okay, they also are very similar. In many ways,
they have somewhat large runtime, and they have got garbage collection. Gill has good routines.
Well, Java makes some obstruction over threads. You can instantiate system threads, but
very recently, especially they have a so-called virtual threads, which are really similar to
go routines. Nowadays, it's not news, but it's not new, but now recently we have a
better system to build native executables. So the difference between the two really are
getting smaller. All right. Now, when you want to interact with foreign code, and usually that
means native code, you have FFI, right? Foreign function interfaces. In Java, those are called
the daddy's called Java native interface, J&I, and you go that's usually called, that's let's see
go. That's what we know as see go. Now, surprisingly enough, maybe. When it comes to limitations,
they are actually kind of similar. So you have portability issues, because now you rely on a
second piler. You have two link issues, because native code is opaque to the tooling of your platform.
You cannot debug step-wise debugging using the go-ty bugger, the native code that you are hosting,
and the same goes for the J&M. A runtime issues, because your native code is not aware of your virtual
threads or your guru-teens or your garbage collector. So essentially, the native code runs on
its own thread, that it's stolen from the rest of the VM, and there's not much we can do about it,
because it's a completely opaque to the rest of the runtime. And you also have potential performance
issues, because crossing the FFI boundary usually as a cost and safety issues. Let's talk about that in a minute.
So let's suppose you want to do software extensions in go. You could use go-plug-in or
bright and native extensions, which are kind of related. Go-plug-in, there's two different flavors
of go-plug-in, but the one that comes with a standard library essentially only works with unique
systems, and it's super easy to break, because it depends on the specific version of it's compiler.
And it's kind of like works everywhere, form-tating, and dynamic libraries, and that's painful.
The other version that you have, it's something that comes from an article, exactly, which essentially
spawns a new process and calls a JRPC instead. So yeah, the other thing that you can do,
use a scripting language, we can go. There are so many to pick from, like, two, three, I don't know.
And you have then to make a choice for your end users. And if you have native code
within any foreign language, what happens is that your memory kind of looks like this,
it's all shared. So you may be able to corrupt the space from Zig, let's say you write some code,
some buggy code, some unsafe code, that corrupts the state of the go-vm, the guru and time.
So there's no isolation there. Well, wasm gives you that kind of isolation.
What about Java? You could use class loading, because that's, as we said at the beginning,
class loading, it's one of the core mechanisms for which you achieve dynamicity. You achieve
achieve, it needs to shape code in Java. But then you'll code, your code will run into
the same levels or privileges of the rest of the code. You could use a scripting language
implemented in Java. There are many, actually, but you are then, again, making a choice for
your end users. All you could write native extensions, and you have all of the issues.
Then you said earlier, including the problem with memory. So this is where we like language
native run times. We talk about this earlier and it's the same slide, in fact. They allow you
to use this code without having an FFI because they implement the wasm run time in the language.
Thereby actually maximum portability, safely interacting with the platform. The performance may
not be a state of the art. There are better run times that there are more optimized, but the cost
of crossing FFI boundaries sometimes is so high that even having slightly worse performances
is good enough. Especially if you're doing a lot of IO. And I have an example on that. So yeah,
I'm going to try to give you a very fast overview of how was zero does stuff because I want to
move on to Gickory as well, so we have FFI for each. All right. So let's say you're loading a
web assembly module. First, you parse it, you validate it, and then what we do is compiling it.
What that means is that depending whether you're running an interpreted mode or compiling mode,
a completion mode, two different things happen. In the interpreter mode, it's not really a
compilation. We just translated to an internal representation. The internal representation is
essentially has fewer or fewer op codes and uses unstructured control flow. Instead of the structure
control flow that's appropriate to wasm. So instead we have number of different instructions
in wasm for the specialized for each type. Instead, we use just one operation and we switch
over the type and it's how we do it, but it's just an internal implementation detail. And then
it's just like any interpreter you might have seen, like it's switch over the op code and loop
and it's all right. What about the optimizing compiler? You're optimizing compiler,
I think some hints, a lot of hints from state-of-the-art compiler architecture, like VA,
sportsjitting, or LVM, or goes on compiler. And it works pretty much how most of these optimizing
compiler work. It has a front end and a backend. The front end translates into an essay. It
does optimizations. There are independent from the platform, from the target platform, and then
on the backend you generate the actual code for that. But that's pretty interesting because this
is essentially a sort of adjusting time compiler, even though usually I don't call it just the
time of call it low time I end up time, or reasons. You can ask me later. But it does. And then the
interesting part is how this gets executed. I'll go super fast here because we don't have a lot
of time. Essentially, you start from the wasm bytecode that looks like that and it's a stack-based
machine. You translate it into representation that uses values. It's a standard kind of transformation.
You split the code into basic blocks, like you will do usually for compilers, and then this
basic blocks are linked together by edges that are represents the branch instructions in the code.
And this allows you to do relatively simple control flow. Now is this? It's a standard procedure.
Yeah, let's get forward this one. Now once you have this SSA representation, you can do optimizations
over this code. And a few classic optimizations are debt code elimination. Let's say you have this
C code here, and you have a debug flag that evaluates to 1 or 0. If it's 0, then you know statically
at built time that this block of code will never execute, so you can drop it. And let's say you have
this expression here, A times B divided by 2, A and B in this expression are actually
effectively constant, so you can propagate those constants, evaluate the expression of
build time, and that's called constant folding, and essentially your function co-lapses to a single return
instruction. And well, not a lot of deviations delete code. Some optimization might blow up the
size of your code depending on strategies, but it's a good strategy strategies. Anyway, once you've
done that, you have your new code in your intermediate representation. And what you want to do,
now is generate the code on your back and forward, your target platform. Let's say it's arm.
You use a bunch of virtual registers because the register and your machine are limited in numbers.
And first you generate this code, and then this is the instruction selection process.
And then you fit those virtual registers onto the limited number of registers that you have.
Arm as a limited number of register, but MD64 it's even more limited, and these are actually
plenty. It's 32 for SIMD and floating point and 34 general purpose, which is quite a lot compared to
X86. And finally, you allocate registers. And finally, we encode all the code that we have with
the proper epilogue and product into actual machine code. This happens all in the gore and time,
which is kind of funny because it goes not a jitter language. So, so we generate bytes like
right, and then we jump into it, that's how the function invocation works. So, we compile at
load time, and then at run time, this still happens at run time. So, in your go code, you will
easily shape, compile your wasn't binary, then you easily shape it. And then you can
use to shape many times, it will be only compile once. And then you jump into this code, using a
trampoline, which basically stashes away the registers that are used by the gore and time.
And then restores them when you have to leave the compile code and get back into the gore
compile code. And these happens, for instance, when there's an error, in that case, we return with an
error code, let it be handled properly by traditional go code. And finally enough, that's also
what happens when you have host functions, that is, functions that are written in the go language,
in this case. We return a code that represents a function as to be called, and then we resume
the wasn't execution. Or what about chicory? So, I've been going around and telling me, yeah,
wasn't means now the same as the jvm, but now I have a wasn't runtime that runs into jvm.
The wasn't byte code is kind of different, but in many ways it's similar. So, for our
written, I think it's really similar, just like there's a one-to-one correspondence. But for
control flow, I mentioned this earlier, the jvm uses unstructured control flows, conditional branches,
and unconditional branches, and labels, and you can jump anywhere, even backwards. This is only
jumping forwards, but you can jump also backwards, and nobody will care. Right. And instead,
wasm, you have structure control flow, and then allows the, I mean, loves the byte code to be
validated more safely than what you can do on the, and quickly faster. Okay, the interpreted is
relatively simple. It's even simpler than was zero. Was zero does this transformation on
control flow, while the interpreter in what in a chicory is actually following the spec. So,
it's easy to hack, it's written in pure job, it's really easy to follow.
And it's obviously portable to any jvm, right? It's, of course. We also have a nailty compiler
that translates WebAssembly byte code to, to Java byte code. And it works in two modes, and essentially
the mechanism is the same. It can generate byte code at one time and add through class loading,
which is a very common strategy in the jvm work. It loads that dynamically, but you can also dump
that byte code on disk and then load it whenever you want, so you don't have to recompile it
every single time. There's, well, there's benefits and downsides to both things. And then this is
the interface. You have this machine factory. When you instantiate your module, you can pass this
machine factory. It's a factory in Java tradition. We love factories. And you can have an
interpreter factory, or you can have a compiler factory. Let's keep over to the ALT machine factory.
The ALT machine factory what it does is, in this space generating byte code. So, of course,
we validate the module, we parse it, but then we recompile. All right. The interesting thing here is
that because both of them are stack-based machines, the analysis and the steps to generate code
are really much simpler than with the was zero that has to generate in any case native code. So,
we don't have to target R or MD or whatever. We just target the jvm byte code. So, and we generate
a bunch of classes, specifically implementing some interfaces, and then essentially all we do is
calling those functions and those methods. This is, for instance, a few lines of code that we
used to do control flow analysis. This is where most of the code goes for the analysis because
that's where the differences are. But then there's a huge, analyze, there's a huge default case,
well, it's small, but this, analyze simple, it's kind of huge. And the reason is,
this is which is over all of the up codes that don't need any special analysis.
And there are many because, in fact, we can reuse a lot of the code across the interpreter
and the compiler. In fact, a lot of this code, it's in our methods of the interpreter. And we can
rely on the just-intentioned compiler to align that code. So, they're not even aligned. I 50 or like
two minutes to talk about how Android fits in the picture, and I'll try to be quick. These are
performance figure. I'll have to go quick and skip those. But thing is, even compared to a
state-of-the-art implementation on the wasm runtime, it's pretty good. And we didn't make a good
huge effort to make it good. So, that's cool. So, speaking of Android, Rubber Cheekery, say,
call it, this is ncp.run, the thing I showed you earlier. And this is an ncp library based on
chicory running an interpreted mode on Android, interacting with Gemini, and calling WebAssembly
functions to do, in this case, calling Colling Google Maps. And because this is essentially
calling a service, an external service, and external service, it's mostly viewing IO. So,
it's pretty fast, even though it's interpreted mode. But what about ALT? So, if you are generating
code on this, go flying, then it works, because that's how the Android will change work, one second.
So, the missing thing is runtime AOT, and that requires some changes, because the Dolby
VM is a registered based machine. Most of the code is still shared, and that's pretty cool,
and the end goal is to actually be able to do this at the end. And that was it. Thank you.
What question is, was the arrow is it runtime compilation?
It's runtime compilation. Yes.