EOSIO programs are typically written in C++ and then compiled into a smart contract for execution on an EOSIO blockchain.

But did you know that C++ is not the only programming language that EOSIO programs can be written in? Some projects are working on Python and even Solidity based programs. These are all compiled down into platform independent code that can be run on the EOSIO WASM Virtual Machine (WAVM).

So the WAVM does not need to know anything at all about the high level programming language (C++, Python, Solidity etc). All it knows how to do is to execute its own binary instruction set called WebAssembly.

What is WebAssembly?

The project to develop a new portable, size efficient binary format for the web was driven by the major browser vendors (Mozilla, Google, Apple and Microsoft). Apart from portability, WebAssembly's other design goals were for it to be:

  • Fast: Able to execute at near native speed
  • Safe: code executes in a sandboxed environment and eliminating dangerous features
  • Hardware independent: Implementable by web browsers and other completely different execution environments such as datacenters, IoT devices, mobile/desktops etc
  • Not break the web (allow calls/bindings to JavaScript)
  • A human-editable text format that is convertible to/from the binary format

These goals, as well as the large browser ecosystem working to continuously improve it, make WASM a great choice for smart contract execution platforms. Apart from EOSIO, WASM is also planned to be used by Ethereum.

The EOSIO WAVM

EOSIO's WASM Virtual Machine (WAVM) is based on Andrew Scheidecker's standalone WAVM.

Stack Machine

The WAVM is a stack machine with two main components:

  1. a last-in, first-out stack is used to hold temporary values. Most of the WAVM instructions therefore assume that operands will be taken from the stack and results placed back on the stack.
  2. A program counter that controls the execution of the program and is modified explicitly by control instructions and implicitly advanced when non-control instructions execute.

This is in contrast to most modern computers that implement a register machine where temporary values are held in a set of registers.Advantages of using a stack machine over a register machine are that stack machines allow for:

  • Compact object code: as instructions deal with values from the stack and do not need to select which registers to use
  • Simple compilers: as no register management is needed making code generation fairly trivial
  • Simple interpreters: particularly for Virtual Machines interpreters for stack machines are simpler as the logic for handling memory accesses is just in one place
  • Easier control flow: control flow is expressed as structured constructs like blocks, if and loops that have clear labels at their beginning and end. The WAVM has  Implementing control flow like this makes it very easy to compile and manipulate WASM and verify the correctness of control flow in a WASM program.

A simple execution example

We can now go review a simple example to illustrate the whole flow from source code to WASM to execution of that code in the WAVM.As prerequisites we assume that you have:

  • Installed the EOSIO Contract Development Toolkit (CDT)
  • Have access to the Web Assembly Binary Toolkit (WABT pronounced wabbit). You can do this by running the eosio-wasm2wast/eosio-wast2wasm binaries that come with CDT. Alternatively you can also download and build the generic WABT version from https://github.com/WebAssembly/wabt. (A cursory examination seems to indicate that this versions are compatible if you disable "Generate Names" and "Fold Expressions")

Compiling Hello

For this example we'll be compiling the basic Hello World program:

#include <eosiolib/eosio.hpp>
#include <eosiolib/print.hpp>


using namespace eosio;


class hello : public contract {
  public:
      using contract::contract;


      [[eosio::action]]
      void hi( name user ) {
         require_auth( user );
         print( "Hello, ", user);
      }
};


EOSIO_DISPATCH( hello, (hi))

Running the eosio C++ compiler eosio-cpp:

eosio-cpp -o hello.wasm hello.cpp
eosio-cpp -o hello.wasm hello.cpp

This takes the C++ source file and converts it into an intermediate representation (IR) using a compiler tool chain called LLVM. LLVM performs some optimisations and then a back-end converts from the LLVM IR into WASM.

WASM Compilation flow.
C++ to WASM compilation flow

We get a WASM file, hello.wasm which is in binary. To make it readable we need to convert it into a WASM text format as follows:

eosio-wasm2wast hello.wasm -o hello.wast

This WAST file is typically huge (approximately 900 lines) so we will only concentrate on the first 50 or so lines here to illustrate some important concepts.

WASM sections

The WebAssembly module is composed of several sections. Some sections are required in all WASM modules and some are optional (you may see the optional modules appearing at the end of the WAST file).Required:

  1. Type. Contains the function signatures for functions defined in this module and any imported functions.
  2. Function. Gives an index to each function defined in this module.
  3. Code. The actual function bodies for each function in this module.

Optional:

  1. Export. Makes functions, memories, tables, and globals available to other WebAssembly modules and JavaScript. This allows separately-compiled modules to be dynamically linked together. This is WebAssembly’s version of a .dll.
  2. Import. Specifies functions, memories, tables, and globals to import from other WebAssembly modules or JavaScript.
  3. Start. A function that will automatically run when the WebAssembly module is loaded (basically like a main function).
  4. Global. Declares global variables for the module.
  5. Memory. Defines the memory this module will use.
  6. Table. Makes it possible to map to values outside of the WebAssembly module, such as JavaScript objects. This is especially useful for allowing indirect function calls.
  7. Data. Initializes imported or local memory.
  8. Element. Initializes an imported or local table.

Section examples

For our Hello.wast file this looks like so:

(module
  (type (;0;) (func (param i32 i64)))
  (type (;1;) (func (result i32)))
  (type (;2;) (func (param i32 i32) (result i32)))
  (type (;3;) (func (param i32 i32)))
  (type (;4;) (func (param i32 i32 i32) (result i32)))
  (type (;5;) (func (param i64)))
  (type (;6;) (func (param i32)))
  (type (;7;) (func))
  (type (;8;) (func (param i64 i64 i64)))
  (type (;9;) (func (param i64 i64 i32) (result i32)))
  (type (;10;) (func (param i32) (result i32)))
  (import "env" "action_data_size" (func (;0;) (type 1)))
  (import "env" "read_action_data" (func (;1;) (type 2)))
  (import "env" "eosio_assert" (func (;2;) (type 3)))
  (import "env" "memcpy" (func (;3;) (type 4)))
  (import "env" "require_auth" (func (;4;) (type 5)))
  (import "env" "prints" (func (;5;) (type 6)))
  (import "env" "printn" (func (;6;) (type 5)))
  (import "env" "set_blockchain_parameters_packed" (func (;7;) (type 3)))
  (import "env" "get_blockchain_parameters_packed" (func (;8;) (type 2)))
  (import "env" "memset" (func (;9;) (type 4)))


  (table (;0;) 2 2 anyfunc)
  (memory (;0;) 1)
  (global (;0;) (mut i32) (i32.const 8192))
  (global (;1;) i32 (i32.const 16700))
  (global (;2;) i32 (i32.const 16700))
  (export "memory" (memory 0))
  (export "__heap_base" (global 1))
  (export "__data_end" (global 2))
  (export "apply" (func 11))
  (elem (i32.const 1) 12)
  (data (i32.const 8192) "Hello, \00")
  (data (i32.const 8202) "read\00malloc_from_freed was designed to only be called after _heap was completely allocated\00"))

Here we see that there are 10 function signatures and indices for the functions in this WASM file

WASM function signature example.

These signatures are similar to function prototypes and define the expected inputs and outputs for each type of function.

There are also some imported functions  such as

(import "env" "read_action_data" (func (;1;) (type 2)))  
(import "env" "require_auth" (func (;4;) (type 5)))
(import "env" "prints" (func (;5;) (type 6)))
Imported type 2 function

We can also see our print statement being initialised in memory:  

(data (i32.const 8192) "Hello, \00")

The beginning of the code section contains the actual function bodies for each function in this module. Looking back at the function definitions we see that there is one function exported which has an index of function 11:

(export "apply" (func 11))

This is the apply function that is called when an action is issued to this smart contract:

  (func (;11;) (type 8) (param i64 i64 i64)
    (local i32)
    get_global 0
    i32.const 16
    i32.sub
    tee_local 3
    set_global 0
    call 10
    block  ;; label = @1
      get_local 1
      get_local 0
      i64.ne
      br_if 0 (;@1;)
      get_local 2
      i64.const 7746191359077253120
      i64.ne
      br_if 0 (;@1;)
      get_local 3
      i32.const 0
      i32.store offset=12
      get_local 3
      i32.const 1
      i32.store offset=8
      get_local 3
      get_local 3
      i64.load offset=8
      i64.store
      get_local 1
      get_local 1
      get_local 3
      call 13
      drop
    end

Deploy your smart contract!

The last thing to do after compiling your smart contract is to send it to the blockchain by issuing a set code transaction using the cleos command line tool.

cleos set contract hello $YOURDIRECTORY/hello -p hello@active

When the block producer (BP) node receives the set code action it proceeds to create an instance of the smart contract WebAssembly module in the EOSIO blockchain's RAM. Instantiating the WebAssembly module is what makes it functional and the BP node does so by resolving memory references, linking external C++ functions and giving access to the exported functions like the apply() function.

Once the BP node completes this step the smart contract WebAssembly module will be on the chain's RAM and any BP node can then access actions intended for the smart contract.

Trigger an action!

Now that the smart contract is on chain, you can trigger it by sending an action to the smart contract using cleos:

cleos push action hello hi '["bob"]' -p bob@active

The message will be sent to the BP node which will load the smart contract WebAssembly instance and call the apply() function.

If you are curious, here's how the WebAssembly virtual machine would execute the first few instructions in the function:

  (func (;11;) (type 8) (param i64 i64 i64)
    (local i32)
    get_global 0
    i32.const 16
    i32.sub
  • get_global 0: gets the value of the global variable at index 0 and pushes it onto the stack
  • i32.const 16: Pushes the constant value sixteen onto the stack
  • i32.sub: remember we said WebAssembly was a stack machine earlier? With a stack machine all values an operand needs are pushed onto the stack before the operation is performed. The sub instruction knows that it needs two operands and will simply take the last two values from the top of the stack and return the result to the top of the stack. This means that instructions can be short (one byte for sub) as the instructions don't need to specify source or destination addresses which makes for more compact WASM files.

Finally after the whole WebAssembly module has been executed, you should then see the smart contract reply:

Hello, bob

Closing thoughts

WebAssembly allows smart contract developers to write EOSIO applications in C++ (and in future other languages such as Solidity and Python) and ensures that the resulting code can run efficiently across different BP nodes.

It is perfectly possible for a smart contract developer to deply a distributed application (dApp) without knowledge of WebAssembly or how the WAVM works. However understanding how it works may help you to tackle difficult problems from time to time and deal better with security vulnerabilities in your smart contract.