LOADING...
LOADING...
LOADING...
当前位置: 玩币族首页 > 行情分析 > # Wasm介绍之6:间接函数调用

# Wasm介绍之6:间接函数调用

2020-03-12 CoinEx公链Talk 来源:火星财经

在前面的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应用。

本文来源:CoinEx公链Talk
原文标题:# Wasm介绍之6:间接函数调用

—-

编译者/作者:CoinEx公链Talk

玩币族申明:玩币族作为开放的资讯翻译/分享平台,所提供的所有资讯仅代表作者个人观点,与玩币族平台立场无关,且不构成任何投资理财建议。文章版权归原作者所有。

知识 WASM 指令
LOADING...
LOADING...