This section deep dives into the architecture of the Marine runtime component.
Marine uses a generic Wasm backend interface defined in this crate under the hood. There is only implementation for Wasmtime at the moment. And uses this crate for interface types V1 implementation. By V1 we mean the interface-types proposal before November 2020:
Each export and import function in a Marine Wasm module is wrapped by a corresponding import or export adapter. The purpose of an import adapter is to lift Wasm types to corresponding IT types, while the purpose of an export adapter is to lower IT types to corresponding Wasm types.
Adapters consist of IT instructions that are executed by a special interpreter. The full list of all supported instructions could be found here.
Let's consider a passing scheme between three modules in the curl service:
gen_n_save function from the
facade module calls the
download function from the
curl module and
put from the
local_storage module (more info about how these modules are structured and used could be found here).
For each module
Marine creates corresponding Wasmtime instance and for each import and export function, it creates a special interface-types adapter with an interpreter. In the picture below you could see the scheme with Wasmtime instances for each module and adapters for each import and export linked together.
How do multi-module calls work
In this section, we're going to discuss what it costs to call a function from another module. Let's consider how it looks step-by-step when the
facade module calls
download(url: String) -> String exported from the
This interop scheme starts with calling an adapter of the imported function
This part aims to lift a string (remember that
download function receives one
String as an argument) meaning that it constructs it from a pointer and a size provided by the Wasm part.
In the picture above the first two instructions push a pointer and a size to the operand stack, then
string.lift_memory consumes them creating a resulted string. Then
call-core 1 calls
release_objects that frees the memory occupied by the lifted string in a Wasm module. And, finally, the
curl.download export adapter from the curl module is called.
Also note that a list of instructions each adapter consists on could be obtained with the Marine CLI, you could find the guide here.
Then a prolog of the export adapter is being called. Its goal is vise-versa to the prolog of the import adapter - it should lower the passed string to make it possible to call the raw curl Wasm module with a pointer and a size.
In this picture, the prolog starts with obtaining a string size by pushing it to the operand stack and calling
string.size. Then it calls
allocate exported from the
curl module. Finally, it calls
string.lower_memory that takes a string with a pointer to the allocated memory region and writes a string to this region.
In the next step the "raw"
curl.download is called with the pointer and the size produced on the previous step.
The goal of 4th step is to lift the string produced by the
The code snippet above starts with retrieving a pointer and a size of the
download function. It's done via two functions: get_result_ptr and get_result_size. They are used because at the moment our interface-type implementation doesn't support multi-value. The last instruction is used to clean up this string from the
curl module memory.
The last step aims to lower string back to the
The lowering is done the similar to step 2 way: obtaining string size, calling
allocate, and then
string.lower_memory. The last two instructions call set_result_size and set_result_ptr, which are needed for the current multi-value passing limitation.
And, finally, the control flow returns back to the
facade Wasm module with the resulted string from the
Despite the complex passing scheme between modules, Marine is a fast runtime with a lot of optimizations on different levels. The Marine REPL measures each operation and playing with Redis compiled to Wasm one could see the following values: