内容简介:在前面的5篇文章里,我们详细讨论了WebAssembly(简称Wasm)二进制格式和除为了更好的理解
在前面的5篇文章里,我们详细讨论了WebAssembly(简称Wasm)二进制格式和除 call_indirect
之外的所有指令。这篇文章将详细介绍Wasm间接函数调用机制和 call_indirect
指令。
call_indirect指令
为了更好的理解 call_indirect
指令,我们首先来回顾一下 call
指令的工作方式。根据之前文章的介绍 可知, call
指令带有一个立即数参数,指定被调用函数的索引。在Wasm实现执行 call
指令之前,必须保证要传递给被调用函数的参数已经在栈顶,且参数的顺序和类型必须完全匹配被调函数的签名。 call
指令执行完毕之后,参数已经从栈顶弹出,函数的返回值(如果有的话)会出现在栈顶。我们假设被调用函数接收两个参数,类型分别是 f32
和 f64
,返回值类型是 i64
,下面是 call
指令的示意图:
bytecode:
...][ call ][ func_idx ][...
stack:
| | | |
| | | |
| d(f64) |➚ | |
| c(f32) |➚ ➘| r(i64) | # funcs[func_idx](c,d)
| b | | b |
| a | | a |
└───────────┘ └───────────┘
call_indirect
指令主要是用来实现C/C++、Rust等语言中的函数指针的。顾名思义, call_indirect
指令给函数调用引入了间接层。 call_indirect
指令在格式上和 call
指令一致,但是调用语义有很大不同。第一,被调用函数并不是通过存储在立即数里的函数索引直接定位,而是从表间接定位。表索引和参数一起放在操作数栈顶,位于所有参数之上。第二,由于具体要调用的是哪个函数在编译期并不知道,只要在运行时才能知道,所以没办法像 call
指令那样通过函数索引拿到函数签名。但是被调用函数的签名在编译期就已经是知道的了,所以可以把函数签名的索引放在立即数里。假设被调用函的签名和上图一样,下面是 call_indirect
指令的示意图:
bytecode:
...][ call_indirect ][ type_idx ][...
stack:
| | | |
| i(i32) |➚ | |
| d(f64) |➚ | |
| c(f32) |➚ ➘| r(i64) | # table[i](c,d)
| b | | b |
| a | | a |
└───────────┘ └───────────┘
根据之前文章的介绍可知,Wasm模块可以定义或导入表,表的初始数据放在元素段里。Wasm1.0规范对于表有诸多限制。第一、每个Wasm模块最多可以导入或定义一个表。第二、表只支持一种元素,也就是函数引用(funcref)。在未来的版本中,可能会放开这些限制。由上图可知, call_indirect
指令首先要根据栈顶操作数得到元素索引,然后通过元素索引拿到函数引用(或者函数地址),最后通过函数引用调用函数。在定位到具体函数之后,Wasm实现会校验实际函数的签名,确保它和指令立即数指定的签名一致。介绍了这么多,可能还是不太好理解,下面通过一个具体的例子进行说明。
实例分析
我们写一个简单的Rust例子来说明 call_indirect
指令。请读者创建一个Cargo项目,把下面的Rust代码复制到src/main.rs文件里:
#![no_std]
#![no_main]
#[panic_handler]
fn panic(_: &core::panic::PanicInfo) -> ! {
loop {}
}
type Binop = fn(f32, f32) -> f32;
fn add(a: f32, b: f32) -> f32 { a + b }
fn sub(a: f32, b: f32) -> f32 { a - b }
fn mul(a: f32, b: f32) -> f32 { a * b }
fn div(a: f32, b: f32) -> f32 { a / b }
#[no_mangle]
pub extern "C" fn main(op: usize, a: f32, b: f32) -> f32 {
let ops: [Binop; 4] = [add, sub, mul, div];
if op < 4 {
ops[op](a, b)
} else {
0.0
}
}
上面的例子非常简单,定义了
add()
、
sub()
、
mul()
、
div()
四个函数,然后在
main()
函数里通过函数指针调用其中一个。
可以执行
cargo build
命令把项目编译成Wasm二进制格式,然后可以通过
WABT 提供的
wasm2wat
命令把Wasm二进制格式转成文本格式(预告,Wasm
文本格式 将在下一篇文章中详细介绍)以便于观察。
下面是需要用到的全部命令:
$ # install rustup & wabt
$ rustup target add wasm32-unknown-unknown
$ cargo new table_demo
$ cd table_demo/
$ # edit src/main.rs
$ cargo build --target wasm32-unknown-unknown --release
$ wasm2wat target/wasm32-unknown-unknown/release/table_demo.wasm
让我们来看看编译后的Wasm模块:
(module
(type (;0;) (func (param f32 f32) (result f32)))
(type (;1;) (func (param i32 f32 f32) (result f32)))
(func $add (type 0) (f32.add (local.get 0) (local.get 1)))
(func $sub (type 0) (f32.sub (local.get 0) (local.get 1)))
(func $mul (type 0) (f32.mul (local.get 0) (local.get 1)))
(func $div (type 0) (f32.div (local.get 0) (local.get 1)))
(func $main (type 1) (param i32 f32 f32) (result f32)
...
)
(table (;0;) 5 5 funcref)
(elem (;0;) (i32.const 1) funcref $div $mul $sub $add)
(memory (;0;) 16)
(global (;0;) (mut i32) (i32.const 1048576))
(global (;1;) i32 (i32.const 1048576))
(global (;2;) i32 (i32.const 1048576))
(export "memory" (memory 0))
(export "__data_end" (global 1))
(export "__heap_base" (global 2))
(export "main" (func $main))
)
main()
函数稍微有点长,稍后给出。可以看到,Rust编译器的确生成了表和元素段,而且看起来也的确是把
div()
、
mul()
、
sub()
、
add()
这四个函数(注意顺序)填入了表里,索引分别是1、2、3、4:
(table (;0;) 5 5 funcref)
(elem (;0;) (i32.const 1) funcref $div $mul $sub $add)
下面来看一下 main()
函数(格式进行了适当调整,并且添加了注释):
(func $main (type 1)
(param $op i32) (param $a f32) (param $b f32) (result f32)
(local $l3 i32) (local $l4 f32)
(i32.sub (global.get 0) (i32.const 16)) ;; $tmp0 = $g0 - 16
(local.tee 3) ;; $l3 = $tmp0
(global.set 0) ;; $g0 = $tmp0
(i32.store offset=12 (local.get 3) (i32.const 1)) ;; $mem[$g0 - 4] = 1
(i32.store offset=8 (local.get 3) (i32.const 2)) ;; $mem[$g0 - 8] = 2
(i32.store offset=4 (local.get 3) (i32.const 3)) ;; $mem[$g0 - 12] = 3
(i32.store (local.get 3) (i32.const 4)) ;; $mem[$g0 - 16] = 4
(local.set 4 (f32.const 0x0p+0)) ;; $l4 = 0.0
(block
(br_if 0 (i32.gt_u (local.get 0) (i32.const 3))) ;; $op > 3 ? br
(local.get 1) (local.get 2) ;; $tmp0 = $a, $tmp1 = $b
(local.get 3) (local.get 0) ;; $tmp2 = $l3, $tmp3 = $op
(i32.const 2) ;; $tmp4 = 2
(i32.shl) ;; $tmp3 = $op * 4
(i32.add) ;; $tmp2 = $l3 + $op*4
(i32.load) ;; $tmp2 = $mem[$g0 - 16 + $op*4]
(call_indirect (type 0) ) ;; $tmp0 = call_indirect($tmp0, $tmp1, $tmp2)
(local.set 4) ;; $l4 = $tmp0
)
(i32.add (local.get 3) (i32.const 16)) ;; $tmp0 = $l3 + 16
(global.set 0) ;; $g0 = $tmp0
(local.get 4) ;; return $l4
)
由于Rust编译器用了全局变量和内存来操作表索引,所以 main()
函数看起来比想象中要复杂一些。如果把这些多余的因素去掉,那么模块看起来应该是下面这样:
(module
(type (;0;) (func (param f32 f32) (result f32)))
(type (;1;) (func (param i32 f32 f32) (result f32)))
(func $add (type 0) (f32.add (local.get 0) (local.get 1)))
(func $sub (type 0) (f32.sub (local.get 0) (local.get 1)))
(func $mul (type 0) (f32.mul (local.get 0) (local.get 1)))
(func $div (type 0) (f32.div (local.get 0) (local.get 1)))
(func $main (type 1) (param i32 f32 f32) (result f32)
(block (result f32)
(f32.const 0x0p+0)
(br_if 0 (i32.gt_u (local.get 0) (i32.const 3)))
(drop)
(local.get 1) (local.get 2) (local.get 0)
(call_indirect (type 0) )
)
)
(table (;0;) 5 5 funcref)
(elem (;0;) (i32.const 1) func $add $sub $mul $div)
(export "main" (func $main))
)
*本文由CoinEx Chain开发团队成员Chase撰写。CoinEx Chain是全球首条基于Tendermint共识协议和Cosmos SDK开发的DEX专用公链,借助IBC来实现DEX公链、智能合约链、隐私链三条链合一的方式去解决可扩展性(Scalability)、去中心化(Decentralization)、安全性(security)区块链不可能三角的问题,能够高性能的支持数字资产的交易以及基于智能合约的Defi应用。
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Code Reading
Diomidis Spinellis / Addison-Wesley Professional / 2003-06-06 / USD 64.99
This book is a unique and essential reference that focuses upon the reading and comprehension of existing software code. While code reading is an important task faced by the vast majority of students,......一起来看看 《Code Reading》 这本书的介绍吧!