eWASM is a flavor of WebAssembly that’s being proposed to replace the EVM as execution engine in ETH2, and possibly also in Eth1.x. Basically, it’s a virtual machine like the JVM but with some properties that make it attractive to use in low-trust scenarios like web browsers, most of which support it by now.
The general flow to get from source code to running is to compile programs using a compiler, then run the code in a WebAssembly interpreter - your browser can most likely do it, and there are standalone environments as well like Wasmer.
The compiler translates the code from the source language, for example rust
, C
or Nim
to WASM VM instructions, optimizing along the way. The runtime then translates the WASM VM instructions to the CPU instructions of the machine it’s executing on, allowing the same WASM code to run on any / most hardware, fairly efficiently.
While developing eWASM, a challenge was posted to create some smart contract code that would run on eWASM, but instead of being written in the EVM-specific languages like Solidity or Vyper, any language could be used that supported WASM. rust
is on the forefront here thanks to Mozilla and Parity, but support for other languages is steadily growing.
From our brave Nimbus team, @yuriy stepped up and wrote a Nim version of the challenge back when it was posted - Nim compiles to C, and by using a specially compiled version of clang
we could get support for eWASM going.
I’ve been playing around with a compiler for Nim that’s based on LLVM, and with them adding a complete WASM tool chain recently, I went ahead and tried it on the WRC20 contract - results are promising, for a first pass In a single step, we can get from Nim to some fairly compact WASM!
Let’s have a look at how it works.
First of all, you need a copy of the nim-eth-contracts repo, and nlvm
itself - it’s linux only, but if someone is interested in learning about compilers, I’m happy to mentor a port to Mac/Windows/ARM etc.
# Clone repo
git clone https://github.com/status-im/nim-eth-contracts.git
cd nim-eth-contracts/examples
# Remove build config that's used for the special clang version
rm config.nims
# Grab latest nlvm
curl -L https://github.com/arnetheduck/nlvm/releases/download/continuous/nlvm-x86_64.AppImage -o nlvm; chmod +x nlvm
Next up, we compile the nim code to WASM - we need to add a few flags - turn off garbage collection, make sure we run the optimized version and allow symbols that come from the eWASM runtime environment to remain undefined at link time:
# compile to 32-bit wasm
./nlvm c -d:release --nlvm.target=wasm32 --gc:none -l:--no-entry -l:--allow-undefined wrc20
# Is it there??
[arnetheduck@tempus examples]$ ls -l wrc20.wasm wrc20.nim
-rw-rw-r--. 1 arnetheduck arnetheduck 1537 11 apr 13.09 wrc20.nim
-rwxrwxr-x. 1 arnetheduck arnetheduck 2097 11 apr 13.49 wrc20.wasm
# Yay!
# optionally, convert to text format: wasm2wat wrc20.wasm > wrc20.wat
There’s an online tool to convert binary wasm files to their text representation, or you can get a converter from wabt. Let’s have a look at a few pieces - Wasm code is divided into modules, and we start with a few type definitions that we’ll use later - full code also available:
(module
(type (;0;) (func (result i32)))
(type (;1;) (func (param i32 i32)))
(type (;2;) (func (param i32 i32 i32)))
(type (;3;) (func (param i32)))
(type (;4;) (func))
(type (;5;) (func (param i32 i32) (result i32)))
Next up, we have imports - these are functions that the runtime environment proves, so that the wasm code that interact with the outside world. eWASM - the ethereum flavor of WASM, specifies what this environment should look like.
(import "env" "getCallDataSize" (func $getCallDataSize (type 0)))
(import "env" "revert" (func $revert (type 1)))
(import "env" "callDataCopy" (func $callDataCopy (type 2)))
(import "env" "finish" (func $finish (type 1)))
(import "env" "storageLoad" (func $storageLoad (type 1)))
(import "env" "getCaller" (func $getCaller (type 3)))
(import "env" "storageStore" (func $storageStore (type 1)))
Finally, we have the code itself. The WASM vm is a fairly simple, stack-based machine, but I’m no WASM expert, so I’m mostly guessing what’s going on . This is the
main
function that selects which operation to perform - we see here some calls to the external interface, and calls into some of the functions we defined - first in Nim:
proc main() {.exportwasm.} =
if getCallDataSize() < 4:
revert(nil, 0)
var selector: uint32
callDataCopy(selector, 0)
case selector
of 0x9993021a'u32:
do_balance()
of 0x5d359fbd'u32:
do_transfer()
else:
revert(nil, 0)
… and the corresponding WASM code:
(func $main.1 (type 4)
(local i32 i32)
global.get 0
i32.const 16
i32.sub
local.tee 0
global.set 0
block ;; label = @1
block ;; label = @2
call $getCallDataSize
i32.const 3
i32.le_s
br_if 0 (;@2;)
local.get 0
i32.const 0
i32.store offset=12
local.get 0
i32.const 12
i32.add
call $callDataCopy_2O1SXmzMBKQV9cWGnElsimg
block ;; label = @3
local.get 0
i32.load offset=12
local.tee 1
i32.const 1563795389
i32.ne
br_if 0 (;@3;)
call $do_transfer_E82kPpU5OcEfVOGiDsEd5g_2
local.get 0
i32.const 16
i32.add
global.set 0
return
end
local.get 1
i32.const -1718418918
i32.ne
br_if 1 (;@1;)
call $do_balance_E82kPpU5OcEfVOGiDsEd5g
unreachable
end
i32.const 0
i32.const 0
call $revert
unreachable
end
i32.const 0
i32.const 0
call $revert
unreachable)
Turns out the WebAssembly folks are not lying - it looks stack-based indeed - operations like add
and le_s
(compare) generally lack operands - they’re popped a stack with the result being pushed back.
In generating WASM code, nlvm
will first generate LLVM IR, which is a similar, but slightly more high-level representation of the same code. Notable differences include the LLVM IR being target-dependent (compiling the same Nim code for x86_64 would look different) and register-based:
# Add -c to produce LLVM IR:
./nlvm c -d:release --nlvm.target=wasm32 --gc:none -l:--no-entry -l:--allow-undefined -c wrc20
define void @main.1() local_unnamed_addr #1 {
secAlloca:
%selector = alloca i32, align 4
%call.res.wrc20.53.20 = tail call i32 @getCallDataSize()
%icmp.IntSLT.wrc20.53.23 = icmp slt i32 %call.res.wrc20.53.20, 4
br i1 %icmp.IntSLT.wrc20.53.23, label %if.true.wrc20.53.2, label %if.end.wrc20.53.2
if.true.wrc20.53.2: ; preds = %secAlloca
tail call void @revert(i8* null, i32 0)
unreachable
if.end.wrc20.53.2: ; preds = %secAlloca
store i32 0, i32* %selector, align 4
call fastcc void @callDataCopy_2O1SXmzMBKQV9cWGnElsimg(i32* nonnull %selector)
%load.selector = load i32, i32* %selector, align 4
switch i32 %load.selector, label %case.else.do.wrc20.57.2 [
i32 -1718418918, label %case.of.1.do.wrc20.57.2
i32 1563795389, label %secReturn
]
case.of.1.do.wrc20.57.2: ; preds = %if.end.wrc20.53.2
call fastcc void @do_balance_E82kPpU5OcEfVOGiDsEd5g()
unreachable
case.else.do.wrc20.57.2: ; preds = %if.end.wrc20.53.2
call void @revert(i8* null, i32 0)
unreachable
secReturn: ; preds = %if.end.wrc20.53.2
call fastcc void @do_transfer_E82kPpU5OcEfVOGiDsEd5g_2()
ret void
}
In register-based VM’s operations take arguments in the form of registers or memory locations - like the icmp
.
We can see that the optimizer has made a pass over the code already (-d:release
flag) - cases are reordered and simplified a little - one of the advantages of using WASM is that we can reuse the tooling that’s developed for WASM, including compilers, debuggers etc.
Of course, the support above is very bare-bones and primitive - there are parts missing and more optimizations could be done. During the Eth2 meetup, Vitalik for example raised a concern that WASM bytecode might be less compact than EVM.
Presently, nlvm
leaves some cruft around that could be removed to produce a smaller file but take the numbers with a grain of salt - this is not production ready by any means . A simple optimization is to remove some of the debugging information that normally gets added:
[arnetheduck@tempus examples]$ nlvm c -d:release --nlvm.target=wasm32 --gc:none -l:--no-entry -l:--allow-undefined -d:clang -l:--strip-all wrc20.nim
[arnetheduck@tempus examples]$ ls -l wrc20.wasm
-rwxrwxr-x. 1 arnetheduck arnetheduck 1593 11 apr 14.35 wrc20.wasm
# Yay, 25% less!
We’ll see how it goes, but WASM has plenty of things going for it right now - WASM engines are popping up everywhere - in browsers (Nim/WASM web example), phones - and will likely make their way into embedded systems also.
Nim on the blockchain next? Who knows …