内容简介:在前面的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应用。
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
JAVA 2核心技术 卷Ⅰ
[美] 霍斯特曼、[美] 科奈尔 / 叶乃文、邝劲筠 等 / 机械工业出版社 / 2006-5 / 88.00元
本书是Java技术经典参考书,多年畅销不衰,第7版在保留以前版本风格的基础上,涵盖Java2开发平台标准版J2SE5.0的基础知识,主要内容包括面各对象程序设计、反射与代理、接口与内部类、事件监听器模型、使用Swing UI工具箱进行图形用户界面设计,异常处理、流输入/输出和对象序列化、泛型程序设计等。 本书内容翔实、深入浅出,附有大量程序实例,极具实用价值,是Java初学者和Java程序员......一起来看看 《JAVA 2核心技术 卷Ⅰ》 这本书的介绍吧!