© The Author(s), under exclusive license to APress Media, LLC, part of Springer Nature 2022
S. M. JainWebAssembly for Cloudhttps://doi.org/10.1007/978-1-4842-7496-5_3

3. WebAssembly Text Toolkit and Other Utilities

Shashank Mohan Jain1  
(1)
Bangalore, India
 

The WebAssembly text toolkit is a toolkit to peek into a Wasm module in a textual format. Although the Wasm module is binary, this toolkit allows you to convert Wasm into a human-readable text format.

This chapter looks at some of the utilities that allow you to do the following.
  • Create a text file in WebAssembly text format (wat) and generate a Wasm file from it

  • Generate a wat file from a Wasm file

  • Generate a dump of the Wasm file

The wat2wasm Utility

This tool handcrafts Wasm files in a text format and then generates the actual Wasm file. Since this is a human-readable format, it allows for a deeper understanding of the Wasm layout under the hood.

It is a handy tool for beginners to WebAssembly. This chapter features a few examples of a wat file and shows you how to generate a Wasm file and then load it via a Node.js-based Wasm runtime.
Pre requisites
Git and Ubuntu VM
Cmake
sudo apt-get update && sudo apt-get install build-essential
Clone the WebAssembly Binary Toolkit (WABT) repository.
git clone --recursive https://github.com/WebAssembly/wabt
cd wabt
git submodule update --init
Once you have downloaded the WABT repo, it’s time to build the source.
mkdir build
cd build
cmake ..

Figure 3-1 is the build screenshot for the WABT make process.

cmake --build .
../images/520626_1_En_3_Chapter/520626_1_En_3_Fig1_HTML.jpg
Figure 3-1

Make WABT

Figure 3-2 shows the build process for the WABT utility.
../images/520626_1_En_3_Chapter/520626_1_En_3_Fig2_HTML.jpg
Figure 3-2

Building WABT

Once you have built and installed the toolkit, you see the different executables like wat2wasm and wasm2wat under the bin directory. Now let’s walk through a simple example of creating a wat file by hand. You can open any text editor like TextPad++ and create an example.wat file.
root@INLN34327424A:/home/ubuntu/wabt# cd ..
Create a directory named wat
root@INLN34327424A:/home/ubuntu# mkdir wat
root@INLN34327424A:/home/ubuntu# cd wat
Create an example.wat file
root@INLN34327424A:/home/ubuntu/wat# nano example.wat
Copy the following content. (I explain it a bit later.)
(module
  (func (result i32)
    (i32.const 100)
  )
  (export "hellowat2wasm" (func 0))
)
A wat file starts with the declaration of a module.
(module)
The next step is to define a function with the following signature.
(func <parameters/result> <local variables> <function body>)

It starts with the func keyword followed by parameters it accepts or the return type.

Wasm supports only numeric types, so the parameters are as follows.
  • i32: a 32-bit integer

  • i64: a 64-bit integer

  • f32: a 32-bit float

  • f64: a 64-bit float

The params for the functions are written as follows.
(param i32)
(param i64)
(param f32)
(param f64)
The result is written as follows.
(result i32)
(result i64)
(result f32)
(result f64)

In this example, you see a definition of the function with a i32 (32-bit integer) return type.

The next part of the function definition is the function body. Here the expression is i32.const 100. This expression pushes the value 100 onto the stack. (Recall from Chapter 1 that WebAssembly is a stack-based architecture.)

The last step is the export of the defined function. The export step is crucial because it makes the function visible to the host. The guest module exposes the function with index 0 (this module only defines one function) to the host runtime (Node.js in this case).

Name the hellowat2wasm function, which the host uses when invoking the function within the Wasm module.

Since there is a high-level understanding of the text format of the Wasm module, it’s time to create a Wasm module from the wat file. Let’s execute the wat2wasm executable (built as part of the cmake utilities you ran) and pass example.wat as input.
root@INLN34327424A:/home/ubuntu/wat# ../wabt/bin/wat2wasm example.wat
You see the generated Wasm file called example.wasm.
root@INLN34327424A:/home/ubuntu/wat# ls
example.wasm  example.wat

Time for loading and execution of the Wasm file.

Let’s use a Node.js-based runtime to load the Wasm file. Please make sure you have Node.js installed on the machine.

Create an index.js file in the wat directory.

Copy the following content into the file and save it.
const { readFileSync } = require("fs");
const run = async () => {
  const buffer = readFileSync("./example.wasm");
  const module = await WebAssembly.compile(buffer);
  const instance = await WebAssembly.instantiate(module);
  console.log(instance.exports.hellowat2wasm());
};
run();

In this code, you load the example.wasm file into a memory buffer and then instantiate the module. Once you have instantiated the module, invoke the hellowat2wasm function, which was exported by the Wasm module.

Execute the program using the following command.
node index.js

You should see 100 as the output.

Let’s proceed to a little more advanced wat program, where you create a wat file to add two integers.

Again, start with the module.
(module
Next, define the function signature. Here you can see that for parameters, $a and $b are the variable names, and a 32-bit integer is returned as a result.
(func (param $a i32) (param $b i32) (result i32)
The function body, get_local $a and get_local $b, pushes the a and b values on the stack.
   get_local $a
   get_local $b
The i32.add pops the two values from the stack and adds them, and pushes the result back on the stack.
   i32.add
)
Finally, it’s time to export the add function to the host runtime.
  (export "add" (func 0))
)
The following is the complete code.
(module
(func (param $a i32) (param $b i32) (result i32)
   get_local $a
   get_local $b
   i32.add
)
  (export "add" (func 0))
)

Next, use the wat2wasm tool to generate the add.wasm file.

Now it is time to consume this Wasm module from the Node.js runtime.

Create an add.js file.

Copy the following code into the file.
const { readFileSync } = require("fs");
const run = async () => {
  const buffer = readFileSync("./add.wasm");
  const module = await WebAssembly.compile(buffer);
  const instance = await WebAssembly.instantiate(module);
  console.log(instance.exports.add(34,76));
};
run();

The code loaded the add.wasm file into the memory buffer, created an instance, and invoked the add method on the exported Wasm module.

If all goes well, you should see 110 being printed on the console.

Now let’s build a small calculator using the wat file, which defines three functions and exposes all three functions to the host.

The calc.wat file is defined as follows.
(module
//Define the first function for addition as was done in previous example
(func (param $a i32) (param $b i32) (result i32)
   get_local $a
   get_local $b
   i32.add
)
Define the second function for the subtraction of the two numbers. The local variables are pushed to the stack, and i32.sub then pops the value from the stack, subtracts the two, and pushes the result again.
(func (param $a i32) (param $b i32) (result i32)
   get_local $a
   get_local $b
   i32.sub
)
Define the third function for multiplication. The i32.mul instruction pops the two values from the stack, multiplies them, and pushes the result back to the stack.
(func (param $a i32) (param $b i32) (result i32)
   get_local $a
   get_local $b
   i32.mul
)
  (export "add" (func 0))
  (export "subtract" (func 1))
  (export "multiply" (func 2))
)

Now use the wat2wasm tool to generate the Wasm file from the calc.wat file.

Once this is done, you create a simple Node.js program.
const { readFileSync } = require("fs");
const run = async () => {
  const buffer = readFileSync("./calc.wasm");
  const module = await WebAssembly.compile(buffer);
  const instance = await WebAssembly.instantiate(module);
var sum=  instance.exports.add(34,76);
var diff=instance.exports.subtract(76,34);
var mul=instance.exports.multiply(12,8);
console.log("sum of 34 and 76="+sum);
console.log("difference of 76 and 34="+diff);
console.log("product of 12 and 8="+mul);
};
run();

This program loads the calc.wasm file and invokes the exported functions one by one.

So far, you have seen how to create a wat file that exported three functions, which the host runtime can then consume. Next, let’s dig a bit more deeply into the WebAssembly text format, where you see how one function in a module can invoke another function in the same module.

Let’s start by defining a simple wat file with an addition function.
(module
(func (param $a i32) (param $b i32) (result i32)
   get_local $a
   get_local $b
   i32.add
)
  (export "add" (func 0))
)

You already know this function. Next, let’s create a wrapper around this function.

Create a file called wrap.wat.

Copy the following code into the file.
(module
(func  (param $a i32) (param $b i32) (result i32)
   get_local $a
   get_local $b
   i32.add
)
(func (result i32)
   i32.const 56
   i32.const 44
   call 0
)
  (export "add1" (func 1))
)

The preceding code defines an add function, which gets two integers from the stack, adds them, and pushes the result onto the stack. Next, wrap the function with another function, where you put 56 and 44 onto the stack and then call the wrapped add function. Finally, expose this wrapper as add1 to the host.

Next, create a Node.js file wrap.js and copy the following code into it.
const { readFileSync } = require("fs");
const run = async () => {
  const buffer = readFileSync("./wrap.wasm");
  const module = await WebAssembly.compile(buffer);
  const instance = await WebAssembly.instantiate(module);
  console.log(instance.exports.add1());
};
run();

You can see the add1 function is invoked, which prints the result as 100.

So far, you have seen how to consume an exported function from a Wasm module and how the Node.js runtime consumes it. Next, let’s import a function from the Node.js host and invoke it within the Wasm module.

Let’s define an add function at the host runtime (Node.js) and then invoke it via a function defined in the Wasm module. Create an export.wat file and copy the following code into the file.
(module
  (import "example" "add" (func $add (param i32) (param i32)))
  (func (export "add1")
    i32.const 56
    i32.const 44
    call $add))

The preceding code creates a Wasm module with an exported add1 function. This function puts two values (56 and 44) on the stack and makes a call to an add function, which, in turn, invokes the add function defined on the host under the namespace example. The import statement here is telling to import add function from an example namespace. Instead of calling $add, you can also invoke the add function by call 0 instruction as 0 is the index of the add function.

Generate the Wasm file (export.wasm) using the wat2wasm utility before using it in the JavaScript file.
const { readFileSync } = require("fs");
const run = async () => {
var importObject = {
  example: {
    add: function(arg1,arg2) {
     sum=arg1+arg2;
      console.log("sum="+sum);
    }
  }
};
  const buffer = readFileSync("./export.wasm");
  const module = await WebAssembly.compile(buffer);
  const instance = await WebAssembly.instantiate(module,importObject);
  instance.exports.add1();
};
run();

Let’s now explore WebAssembly memory. It is important to know how to isolate Wasm modules and deal with types other than integer and floats. As you know, Wasm only allows four basic types (i32, i64, f32, and f64) to be passed between the host and guest.

Knowledge of the Wasm memory model allows you to encode complex types between host and guest. You see some examples of this in later chapters.

In WebAssembly, memory is a linear array of bytes that can grow over time. WebAssembly provides instructions like i32.load and i32.store to read and write from the specific memory area. The host creates this linear memory array and provides it to the Wasm module. The Wasm module code only has visibility within that memory area, and it remains isolated from other modules running on the same host. This is how Wasm achieves memory sandboxing.

If the host is a JavaScript-based Node.js, think of this linear memory as an ArrayBuffer. To encode complex types like the string, you need to represent/encode this string into a byte array within the memory allocated for the Wasm module.

Here, a Wasm module is defined using the wat file. Create a string.wat file and copy the following content into that file.
(module
  (import "example" "log" (func $log (param i32 i32)))
  (import "js" "mem" (memory 1))
  (data (i32.const 0) "Hello Wat")
  (func (export "logme")
    i32.const 0  ;; pass offset 0 to log
    i32.const 9  ;; pass length 9 to log
    call $log))

Import one function and one variable from the runtime host. The function is the example namespaced log function, and the variable is the JavaScript namespaced mem variable. The idea here is that you create a string in Wasm memory (a linear memory provisioned by the host) from the guest and then pass the offset and length to the host. The host can then read from that offset to the length and then decode the string and print it.

Define a data segment in the wat file, and its content is “Hello Wat”. The data segment allows you to write a string into the Wasm memory at a given offset. A logme function exports two constants (offset and length) onto the stack and invokes the log function internally. This log function passes the offset and length to the host, which reads from the Wasm module memory at the offset until the length and prints the content.

Convert the string.wat file to a string.wasm file using the wat2wasm tool.

The following is the JavaScript code for defining the imports and invoking the logme function on the Wasm module.
const { readFileSync } = require("fs");
// this is one of the variable which will be imported by the guest
var memory = new WebAssembly.Memory({ initial : 1 });
const run = async () => {
var importObject = {
//the function and memory are defined as imports .These are imported by the Wasm module.
  example: {
    log: function(offset,length) {
var bytes = new Uint8Array(memory.buffer, offset, length);
  var string = new TextDecoder('utf8').decode(bytes);
  console.log(string);
    }
  }
,js: {
          mem: memory
        }
};
  const buffer = readFileSync("./string.wasm");
  const module = await WebAssembly.compile(buffer);
  const instance = await WebAssembly.instantiate(module,importObject);
  instance.exports.logme();
};
run();
The JavaScript file, when executed within Node.js, should print Hello Wat on the console. This file defines two imports.
  • The example.log function reads the memory array at a specific offset and to a specific length. Offset and length are provided by the guest, which has put the “Hello Wat” string into that array.

  • The memory array itself.

The host’s job is to pass this memory to the guest. This is done via importObject, which is imported into the Wasm module. The Wasm module puts the string into the memory and passes the offset and length of the memory the host has allocated it. The guest invokes the example.log function on the host. This function reads the memory at offset and length and prints the contents.

This is how you can achieve communication between the host and the guest.

Tables

Let’s look at the tables section in the wat file. You know that functions within a module can only be invoked by passing the function’s index to the call instruction. An instruction called call_indirect in Wasm provides a layer of indirection to the function calls. With the call instruction, the index passed is the index of the exact location of the function in memory, where call_indirect allows to invoke the function by an indirection by a structure called a table. The table holds the actual function location and maps this index to a virtual index used by the host code. This allows you to do late binding to the function call; for example, hosts that are compiled. For example, a Rust binary can decide at runtime which function to call by passing the index of the function to be invoked. This is a powerful mechanism provided by WebAssembly for the dynamic runtime invocation of a function.

Let’s look at a wat file.

Create a table.wat file and copy the following code.
(module
  (table 3 funcref)
(func $f1 (param $a i32) (param $b i32) (result i32)
   get_local $a
   get_local $b
   i32.add
)
(func $f2 (param $a i32) (param $b i32) (result i32)
   get_local $a
   get_local $b
   i32.sub
)
(func $f3 (param $a i32) (param $b i32) (result i32)
   get_local $a
   get_local $b
   i32.mul
)
  (elem (i32.const 0) $f1 $f2 $f3) // this refers to the index of the function in the function table
  (type $return_i32 (func (param i32 i32)(result i32)))
  (func (export "callByIndex") (param  i32 i32 i32)(result i32)
local.get 1
local.get 2
local.get 0
    call_indirect (type $return_i32)
)
)

In the wat file, you added (table 3 funcref) just below the module section. This tells the Wasm host to create a module of table size of 3. Here, funcref specifies that elements of this table are references to the functions.

The add, subtract, and multiply functions are defined as usual. The next section in the wat file is the elem, which defines the functions referenced by the table. In our case, the add, subtract, and multiply functions are part of the elem section.

The (type $return_i32 (func (param i32 i32)(result i32))) section specifies the type of the function to be invoked. In this case, since all three functions have similar signatures, you keep one type.

Finally, the following segment exports the callByIndex function to the host.
  (func (export "callByIndex") (param  i32 i32 i32)(result i32)
local.get 1
local.get 2
local.get 0
    call_indirect (type $return_i32)

This function takes three integer parameters. The first is the index of the function in the table. The other two are the parameters to be operated upon by the function to be invoked, like add, multiply, or subtract.

local.get 1, local.get 2, and local.get 0 push the index of the function and two other values on the stack. By default, the call_indirect instruction pops the top value from the stack. Because you push the function’s index on the stack, it picks up, and the actual function is invoked. The callee then pops the other two values from the stack and invokes the needed operation.

Create a Wasm file from the wat file using the wat2wasm utility.

Now let’s invoke the module function by index in the following JavaScript code.
const { readFileSync } = require("fs");
const run = async () => {
  const buffer = readFileSync("./table.wasm");
  const module = await WebAssembly.compile(buffer);
  const instance = await WebAssembly.instantiate(module);
var sum=  instance.exports.callByIndex(0,56,34);
var diff=instance.exports.callByIndex(1,56,34);
var mul=instance.exports.callByIndex(2,12,8);
console.log("sum of 34 and 56="+sum);
console.log("difference of 56 and 34="+diff);
console.log("product of 12 and 8="+mul);
};
run();

You can see how the individual functions are invoked using a specific index in a Wasm module table. You can take this index as input or based on some logic and dynamically call a function.

These tables can also be dynamically created by the host and can also be shared between different modules. However, this kind of dynamic linking is beyond the scope of this book.

The wasm2wat Utility

Now that you’ve seen the wat2wasm tool, let’s look at a tool that reverses the process, which means taking a Wasm file and generating a wat file. This can help debug a Wasm file.

Take the calc.wasm file generated in the previous section and use the wasm2wat executable against it. The wasm2wat executable is located in the same location as the wat2wasm binary (under the wabt/bin directory).

Run the following command.
../wabt/bin/wasm2wat calc.wasm -o calc1.wat
Next, let’s provide calc.wasm as input and calc1.wat as output. Inspect the calc1.wat file, as follows.
(module
  (type (;0;) (func (param i32 i32) (result i32)))
  (func (;0;) (type 0) (param i32 i32) (result i32)
    local.get 0
    local.get 1
    i32.add)
  (func (;1;) (type 0) (param i32 i32) (result i32)
    local.get 0
    local.get 1
    i32.sub)
  (func (;2;) (type 0) (param i32 i32) (result i32)
    local.get 0
    local.get 1
    i32.mul)
  (export "add" (func 0))
  (export "subtract" (func 1))
  (export "multiply" (func 2)))

This tool can help one to debug and look at some possible issues in the Wasm file.

Object Dump Using wasm-objdump

wasm-objdump is a utility that allows you to do a dump of the WebAssembly file. It prints information about the Wasm binary file. You can pass flags to print different details, like headers, full content, and function bodies.

The below prints the header information for the add.wasm file.
ubuntu@INLN34327424A:~/wat$ sudo ../wabt/bin/wasm-objdump add.wasm -h
add.wasm:       file format wasm 0x1
The following are the sections.
     Type start=0x0000000a end=0x00000011 (size=0x00000007) count: 1
 Function start=0x00000013 end=0x00000015 (size=0x00000002) count: 1
   Export start=0x00000017 end=0x0000001e (size=0x00000007) count: 1
     Code start=0x00000020 end=0x00000029 (size=0x00000009) count: 1
Passing the disassemble flag (-d) results in the following output.
ubuntu@INLN34327424A:~/wat$ sudo ../wabt/bin/wasm-objdump add.wasm -d
add.wasm:       file format wasm 0x1
The following shows the code disassembly.
000022 func[0] <add>:
 000023: 20 00                      | local.get 0
 000025: 20 01                      | local.get 1
 000027: 6a                         | i32.add
 000028: 0b                         | end
Run the following command (-x as the flag) to get the section information.
ubuntu@INLN34327424A:~/wat$ sudo ../wabt/bin/wasm-objdump add.wasm -x
add.wasm:       file format wasm 0x1
These are the section details.
Type[1]:
 - type[0] (i32, i32) -> i32
Function[1]:
 - func[0] sig=0 <add>
Export[1]:
 - func[0] <add> -> "add"
Code[1]:
 - func[0] size=7 <add>

Apart from the utilities, there are more utilities like wasm-interp, wasm-decompile, and wasm-strip. These are left for you to experiment with.

Summary

This chapter discussed utilities like wat2wasm, where you wrote the wat files by hand and learned their segments and structure. You also saw how functions could be exported and imported using the wat format. The chapter also covered other utilities like wasm2wat and utilities for generating the dump of the Wasm file.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.225.31.159