Wasm Introduction (Part 6): Table & Indirect Call
Written by the CoinEx Chain lab, this article is the sixth one of the Wasm Introduction series and introduces Table & Indirect Call. CoinEx Chain is the world’s first public chain exclusively designed for DEX, and will also include a Smart Chain supporting smart contracts and a Privacy Chain protecting users’ privacy.
In the previous articles, we discussed the WebAssembly(Wasm for short) binary formatand all instructions except call_indirect
. This article will focus on the Wasm indirect function call mechanism and call_indirect
instruction in detail.
The call_indirect Instruction
To better understand the call_indirect
instruction, let's first review how the call
instruction works. According to the introduction in the previous articles, the call
instruction takes an immediate argument that specifies the index of the called function. Before Wasm executes the call
instruction, it must be ensured that the parameters to be passed to the called function are already on the stack, and the order and type of the parameters must exactly match the signature of the called function. After the call instruction is executed, the parameters have been popped from the top of the stack, and the return value of the function (if any) will appear on the top of the stack. We assume that the called function receives two parameters, the types of which are f32
and f64
respectively, and the return value type is i64
. The following diagram shows how call
instruction works:
call_indirect
instructions are mainly used to implement function pointers in languages such as C/C ++ and Rust. As the name implies, call_indirect
instructions introduce a layer of indirection to function calls. They have the same binary format as the call
instruction, but are quite different in the execution semantics. First, the called function is not directly located through the function index stored in the immediate argument, but is located indirectly using the table. The table index is placed with the parameters on top of the operand stack, above all parameters. Second, as we do not know at compile time which function is to be called until the runtime, there is no way to get the function signature through the function index like we do in the call
instruction. Fortunately, the signature of the called function is already known at compile time, so the index of the function signature can be placed in the immediate argument. Assuming the signature of the called function is the same as the figure above, the following diagram shows how call_indirect
instruction works:
According to the introduction of the previous articles, the Wasm module can define or import a table, and the initial data of the table is placed in the element segments. The Wasm1.0 specification places many restrictions on tables. First, each Wasm module can import or define at most one table. Second, the table supports only one element, which is funcref
(function reference, or function address). These restrictions may be released in the future. As can be seen from the above figure, the call_indirect
instruction first obtains the element index according to the top operand, then obtains the function reference through the element index, and finally calls the function through the function reference. After locating the specific function, the Wasm implementation verifies the signature of the actual function to ensure that it is consistent with the signature specified by the immediate argument of the instruction. You may still feel confused. Let's take a look at the specific example as below.
Case Analysis
Let’s write a simple Rust example to illustrate the call_indirect
instruction. Now create a Cargo project and copy the following Rust code into src/main.rs
file:
It is a simple example. The four functions of add()
, sub()
, mul()
, and div()
are defined, and then one of them is called through the function pointer in main()
function. You can use the cargo build
command to compile the project into the Wasm binary format, and then convert the Wasm binary format into a text format with the wasm2wat
command provided by WABT(We will introduce the Wasm text format in detail in the next article) for easy observation. Here are all the commands you need:
The main()
function is a bit long and will be given later. As you can see, the Rust compiler does generate table and element segments, and it does seem to fill the table with the four functions, div()
, mul()
, sub()
, and add()
, (note the order) in the table. The indexes are 1, 2, 3, and 4 respectively:
Since the Rust compiler manipulates table indexes with global variables and memory, the main()
function looks more complicated than expected. If we remove these extra factors, the module should look like this: