TL;DR
The wasmtime execution environment used to run all canisters should expose functionality accessible through ic_cdk to let us run wasm code from inside our wasm canisters.
I have the need to run untrusted user code in my canister, using sort of a plugin system. Currently I am running a JS runtime in the canister which allows me to run that untrusted code in a fully sandboxed secure environment. One drawback with that is that JS is quite inefficient, hitting the instruction limit is a real risk.
In another thread we discussed the possibility of running wasm compiled code in the canister, effectively running wasm in wasm. This is some orders of magnitude (6) more efficient than interpreting JS. But it is still like 15 times less efficient than original canister code since you have to run a wasm runtime inside your canister that is run in another wasm runtime.
Wouldn’t it be nice/awesome if the wasmtime runtime that runs the canister exposed some functions accessible through ic_cdk that would allow us to run wasm modules from within our canisters in the same runtime environment as the canister? Then those modules would run with the same efficiency in terms of instruction consumption as the canister itself.
Would this even be technically feasible?
Why run wasm in wasm? Running untrusted code easily from within a canister opens up a new universe of functionality!
DAO governance tools with user submitted voting modules
Programmable NFTs
Installable micro applications running in an onchain cloud based linux like environment
Games where users can contribute with interactive building blocks
…
Composability on the IC would be taken to the next level! Any use case where you can imagine a plugin or installable module could be created using the same efficient execution as the canister is using.
Imagine being able to do something like:
let wasm = include_bytes!("awesome_thing.wasm");
let engine = ic_cdk::wasm::Engine::default();
let module = ic_cdk::wasm::Module::new(&engine, &wasm[..]).unwrap();
let mut store = ic_cdk::wasm::Store::new(module.engine(), ());
let mut linker = ic_cdk::wasm::Linker::new(&engine);
let run = linket.get_typed_func::<(), i64>(&store, "run").unwrap();
Ok(run.call(&mut store, ()).unwrap())
This idea is totally lit and in frontend development it is already known as micro-frontend architecture. This architecture allows you to fetch prebuilt modules so like separately hosted components and combine them on the fly into a complete layout. Since this concept is still part of web 2.0 it’s not possible to share state (database data) between third-party services/apps so micro-frontends are generally only possible in-house. This is because data is managed via companies’ self-built authentication systems (which is obviously natural), but sharing identity and state between third-party apps is impossible. The vision of a 3.0 web where we can share state and identity open up millions of possibilities, and that is why I’m fully into ICP. For me, this is a new era of the internet!
The amount of work that the replica would need to do in order to support running arbitrary Wasm would be similar to having a separate canister:
Since the plugin code is untrusted, it needs to run in a separate sandbox process. Otherwise, the plugin code could exploit a bug in Wasmtime to take control over the main canister.
The plugin code would need to be compiled before execution because Wasmtime is not an interpreter. Since compilation is costly, it would make sense to compile once and reuse the code for multiple executions.
The plugin code would need to have its own Wasm memory and Wasm instance.
With all these pieces together, we will get close to a canister. It might be more efficient to take canisters and adjust them for this use case rather than to implement something that is similar to canisters from scratch.