K3 Wasm Internal Specifications
If you’re new to the K3 framework and are just trying to make applications with it this document is not meant for you. This document outlines the way our Rust SDK works internally and what the output format for compiled K3 apps looks like. It can be useful if you intend to port the SDK to another language or are trying to build and compile apps for our network from scratch.
Output format & handlers
Let’s start at the beginning, when you look at a basic Rust K3 app all you see is something like this:
How exactly is this compiled into a final .wasm
binary? All the user has to do is call the Rust compiler’s native command to output Wasm: cargo build --target wasm32-wasi
and everything just works. How do K3 executors know which export’s in your module tree correspond to which routes?
There are some constraints in Wasm that lead to us even needing such a specification:
Wasm binaries are flat (i.e. only one module, no nested module structure)
Wasm types are only primitives (just integers, floats, and pointers)
So the K3 SDK here needs to take this nested module tree you provide and flatten it out into a list of exports, each of which can take a number as an input and return one as output. For example the export in the first code snippet would be translated to something like:
If you added an API directory to your source code and had a /api/users/mod.rs
file with another get
export like previously we would translate that to:
The expected naming format is __k3_handler_{PREFIX}_{METHOD}
where the prefix is the expected HTTP route with _
replacing the /
s and the method is the HTTP method (GET, POST, PUT, etc.). Executors will translate the HTTP URL they get in their request into this format and look for the appropriate handler, if one is not found a 404 responses is returned with more details about the route. As you can see the executor is also expected to provide the request they have as a pointer to this export and translate back another pointer into a response, this brings us nicely to serialization.
Serialization & deserialization
The base shared translation unit used across the layers of abstraction is the buffer. Specifically a length prefixed buffer. This means given a pointer into a memory to get a buffer in your target language, you read the first 4 bytes to get the length (in little endian) and then allocate and copy in memory to a buffer based on that. For example in Rust this is what we do:
It does require some low level memory access to do this, so it can be difficult for higher level languages that compile to Wasm through a nested runtime.
Executors will have to allocate buffers into the Wasm module’s memory to create these buffers which is why the module should also export a couple special functions that give the outside access into the native allocator your language uses. Like in Rust we give access to the std::alloc
interface:
Different parts of the SDK have different serialization conventions/formats they use, but it’s mostly just Strings which are converted into UTF-8 buffers in transit. One special case is the http
module in our SDK which is used to serialize and deserialize the HTTP request/response objects. The executor running on our operators currently is written in Rust and uses the httpcodec
crate to serialize the request it receives into a buffer and it is expected that the response follow the same format. Which means if you are implementing this in another language you will have to do some reverse engineering to replicate this format. We are still open to change on this and would be willing to change to a more standardized codec if the community has better suggestions!
Environment variables
Environment variables are created at deployment time, encrypted and stored in our backend. These environment variables are called during runtime by the Executor and installed into the WASM instance before execution. The environment string is in the same format as local .env
files, lines of KEY=VALUE
pairs. The Env
attribute tells the executor to install these Env vars to the WASM runtime:
Inputs
The current SDK can accept inputs using the define_k3_write_inputs!()
macro. This macro adds the necessary functions to the WASM during compilation to accept inputs from the outside:
Current SDK reference
Here is the current SDK functions that executors provide (types are in Rust but are primitives so can be inferred for other languages):
All _ptr: u32
types are the previously explained buffer types. Any u64
types are usually executor-generated handles. The k3_wasm_sdk
crate wraps these functions to provide a nicer API for all of them, you can consider that as a reference if you are doing something similar in a different language.
Some specific things (since the types might not be fully explanatory):
sc_call
returns the transaction hash as a stringsc_query
returns some fetched data serialized as a bufferexecute_raw
returns a JSON string specifying changes made to the DBexecute_create_table
exists because Space & Time requires tables to have their public/private key pair and biscuits which the executors can handle for you
Flow of operations
<DIAGRAM OF THIS WHOLE FLOW>
Last updated