WebGL Tutorial
and more

Control Instructions

撰写时间:2025-01-24

修订时间:2025-01-28

概述

程序流程控制指令包含以下指令:

  • nop
  • unreachable
  • block
  • loop
  • if
  • br
  • br_if
  • br_table
  • return
  • call
  • call_indirect

block, loopif指令为结构化指令 (structured instructions)。用在语句块中。语句块以endelse伪指令来结束或分隔。

根据语句块的类型,结构化指令可以消耗栈中数据,并在操作数栈中产生结果。

每个结构化指令产生一个隐式的标签。标签是分支指令 (branch instructions)的目标。可使用label 0来引用第0个标签,其中0表示最里层的语句块。

brbr_if是直接跳转(类似于C语言中的break语句),而loop则是跳到循环的开始位置(类似于C语言中的continue语句)。

br执行无条件分支,br_if执行有条件分支,br_table执行一个间接分支,return是一个无条件分支的快捷指令 (shortcut),跳转到默认为当前函数的最外层语句块。

向前指令 (forward branches) 消耗整个语句块最后所生成的结果,而向后指令 (backward branches) 消耗整个语句块最初所依赖的在栈中的操作数。

call指令调用另一个函数,消耗栈中的参数,返回调用的结果。call_indirect通过table索引值来间接调用另一函数。

return 指令

Wasm自带检验功能,包括自动检查函数内Result Stack的状态是否与返回值一致。

而这又分为几种情况。

第一种情况,函数没有返回值,但Result Stack非空。此时将抛出校验异常。

let watSrc = ` (module (func i32.const 5 ) (start 0) ) `; WabtUtils.RunWat(watSrc) .catch(e => { pc.error('%s', e.message); });

异常信息为,函数没有返回值,却在Result Stack中发现了一个i32的元素。

return指令将Result Stack中的所有元素依序弹出。因此可解决上面出现异常的问题。

let watSrc = ` (module (func i32.const 15 i32.const 25 i32.const 35 return ) (start 0) ) `; WabtUtils.RunWat(watSrc) .then(() => pc.log('pass')) .catch(e => { pc.error('%s', e.message); });

栈中原本有3个元素,一个return指令将所有元素都弹出栈,栈变空,通过了检验。

当特定函数没有返回值时,可调用start指令来自动运行该函数。

第二种情况,函数通过result指令声明有具体数量的返回值,且Result Stack中的元素数量正好与所声明的返回值的数量相等。

let watSrc = ` (module (func (export "checkReturnValue") (result i32 i32) i32.const 15 i32.const 25 ) ) `; const {checkReturnValue} = await WabtUtils.RunWat(watSrc); pc.log(checkReturnValue());

返回值的顺序为依序进栈的顺序。

因为函数带有result指令,函数体内实际上是隐式地调用了return指令。因此也上面代码也可编写为:

let watSrc = ` (module (func (export "checkReturnValue") (result i32 i32) i32.const 15 i32.const 25 return ) ) `; const {checkReturnValue} = await WabtUtils.RunWat(watSrc); pc.log(checkReturnValue());

也可改写为一行语句的方式:

let watSrc = ` (module (func (export "checkReturnValue") (result i32 i32) (return (i32.const 15) (i32.const 25)) ) ) `; const {checkReturnValue} = await WabtUtils.RunWat(watSrc); pc.log(checkReturnValue());

这种方式更加直观。

第三种情况,如果return实际返回的数量大于result所声明的数量,则前面多余的返回值将被舍弃。

let watSrc = ` (module (func (export "checkReturnValue") (result i32 i32) (return (i32.const 15) (i32.const 25) (i32.const 35)) ) ) `; const {checkReturnValue} = await WabtUtils.RunWat(watSrc); pc.log(checkReturnValue());

第四种情况,如果return实际返回的数量小于result所声明的数量,则抛出校验异常。

let watSrc = ` (module (func (export "checkReturnValue") (result i32 i32) (return (i32.const 15)) ) ) `; const {checkReturnValue} = await WabtUtils.RunWat(watSrc); pc.log(checkReturnValue());

if 指令

条件语句的基本语法如下:

let watSrc = ` (module (func (export "judge") (result i32) (if (i32.const 1) (then (return (i32.const 25))) (else (return (i32.const 35))) ) (return (i32.const 0)) ) ) `; const { judge } = await WabtUtils.RunWat(watSrc); pc.log(judge());

默认情况下,if语句将对Result Stack的栈顶元素进行判断。如果调用if语句时,栈为空,则抛出检验错误的异常。

上面代码i32.const 1先将数值1压进栈顶,以供if语句使用(消耗)。当此值为非零值时,执行then语句分支;当此值为零值时,执行else语句分支。

当有result声明时,为通过检验,函数体内最后一条语句需显式地返回相应的值,尽管上面的else指令已实际达成此效果。因此,上面代码也可改写为:

let watSrc = ` (module (func (export "judge") (result i32) (if (i32.const 0) (then (return (i32.const 25))) ) (return (i32.const 35)) ) ) `; const { judge } = await WabtUtils.RunWat(watSrc); pc.log(judge());

block 指令

block指令为一个语句块附上一个名称,以供br等指令跳出此段代码。block指令只能用于func中。其基本使用方法如下:

let watSrc = ` (module (func (block $block_a ;; define a named block br $block_a ;; jump out of the block unreachable ;; trap ) ;; codes here would be run ) (start 0) ) `; await WabtUtils.RunWat(watSrc);

定义了一个名为$block_a的语句块,然后使用br指令跳出该语句块,转到执行函数中该语句块后面的语句。

br指令后面的标签是一个block时,程序流程跳转到该语句块的后面,形成了跳出的效果。相对应的,当br指令后面的标签是一个loop时,程序流程跳转到该语句块的前面,形成了循环的效果。

unreachable指令用于定义一个不应被执行到的陷阱 (trap),如果该处的语句被执行到,则会抛出一个异常。因此下面代码将抛出异常:

let watSrc = ` (module (func (block $block_a unreachable ;; fall off trap ) ) (start 0) ) `; await WabtUtils.RunWat(watSrc);

loop 指令

loop指令为一个语句块附上一个名称,以供br等指令跳到此段代码的起始处,从而形成循环执行语句块代码的效果。loop指令只能用于func中。其基本使用方法如下:

let watSrc = ` (module (func (loop $loop_a (if (...) (then (br $loop_a)) ) ) ) (start 0) ) `; await WabtUtils.RunWat(watSrc);

loop指令自身不会自动循环执行语句,需配合br等跳转分支语句,才会形成循环语句的效果。

在使用loop指令时,最重要的一点是需保证确实存在不再重复执行语句块的条件,以免程序流程陷入进入无尽的循环。

br 指令

br指令,branch的缩写,直接跳转至相应的block语句块或loop语句块。

跳转至block分支

const importObject = { js: {log: pc.log.bind(pc)} }; let watSrc = ` (module (import "js" "log" (func $log (param i32))) (func (export "leaveTen") (param $arg i32) (block $block_a local.get $arg i32.const 10 i32.eq (if (then br $block_a) ) (local.set $arg (i32.const 5)) ) (call $log (local.get $arg)) ) ) `; const { leaveTen } = await WabtUtils.RunWat(watSrc, importObject); leaveTen(2); leaveTen(10);

leaveTen函数将值不为10的参数值统一设置为5

在代码中,如果参数值等于10,则跳转到$block_a语句块的结尾;否则,继续执行$block_a语句块中后续代码,将参数值统一设置为5

跳转至loop分支

const importObject = { js: {log: pc.log.bind(pc)} }; let watSrc = ` (module (import "js" "log" (func $log (param i32))) (func (export "stopAtTen") (param $arg i32) (loop $loop_a local.get $arg i32.const 10 i32.lt_u (if (then local.get $arg i32.const 1 i32.add local.set $arg br $loop_a ) ) ) (call $log (local.get $arg)) ) ) `; const { stopAtTen } = await WabtUtils.RunWat(watSrc, importObject); stopAtTen(2); stopAtTen(10);

stopAtTen函数将参数值持续加1,直至其值等于10为止。

在代码中,如果参数值小于10,则加1后,跳转到$loop_a语句块的开始处重复执行该语句块;否则,退出循环。

小结

br指令实际跳转,而blockloop指令规范了跳转的目的地。

br_table 指令

br_table可从嵌套的blocks中直接跳转到指定的block

const importObject = { js: {log: pc.log.bind(pc)} }; let watSrc = ` (module (import "js" "log" (func $log (param i32))) (func (export "nestedBlock") (param $arg i32) (block (block (block (call $log (i32.const 5)) local.get $arg br_table 0 1 2 ) (call $log (i32.const 10)) ) (call $log (i32.const 15)) ) ) ) `; const { nestedBlock } = await WabtUtils.RunWat(watSrc, importObject); pc.group('block 0'); nestedBlock(0); pc.groupEnd(); pc.group('block 1'); nestedBlock(1); pc.groupEnd(); pc.group('block 2'); nestedBlock(2); pc.groupEnd(); pc.group('block 7'); nestedBlock(7); pc.groupEnd();

定义了3blocks,每一个block都分别打印了一个数值。

最里层的代码:

(block (call $log (i32.const 5)) local.get $arg br_table 0 1 2 )

根据所传进的参数,决定跳转到第几层block

首先,代码br_table 0 1 2必须放置在最里层中。其次,从最里层到最外层,分别用数字0, 1, 2表示。第三,在调用br_table指令前,需在栈中设置一个值而成为其立即参数,上面的local.get即为此作用。当此立即参数超出层数的范围时,则跳转至最外层。

br_if 指令

br_if指令,branch if的缩写,可在满足特定条件下跳转至相应的loop语句块或block语句块。

先看下面的JavaScript代码:

function iter(num) { let tmp = num; do { pc.log(tmp); tmp++; } while (tmp <= 5) pc.log(-999); } iter(3);

调用do ... while语句,只要参数num的值小于等于5,就打印其值;否则,其值加1后重复循环。循环结束后,打印-999以示程序结束。

编写相对应的Wat代码如下:

const importObject = { js: {log: pc.log.bind(pc)} }; let watSrc = ` (module (import "js" "log" (func $log (param i32))) (func (export "iter") (param $arg i32) (local $tmp i32) ;; store the increasing value local.get $arg ;; update $tmp local.set $tmp (loop $iter_loop ;; define looping label local.get $tmp ;; print current $temp value call $log local.get $tmp ;; increase the value of $tmp by 1 i32.const 1 i32.add local.set $tmp ;; and update local.get $tmp i32.const 5 i32.le_u ;; $tmp <= 5 ? br_if $iter_loop ;; if true, jump to branch $iter_loop ) i32.const -999 ;; $tmp > 5, jump out of branch $iter_loop call $log ) ) `; const { iter } = await WabtUtils.RunWat(watSrc, importObject); iter(3);

end 指令

当以块指令 (block instructions) 的方式来编写if, loop, block等语句块代码时,须用end指令来标记该语句快的结束。

标记 if 代码块

const importObject = { js: { log: pc.log.bind(pc), prtZero() { pc.log('Zero.'); }, prtNonZero() { pc.log('Non zero.'); }, prtEnd() { pc.log('End of testing.'); } } }; let watSrc = ` (module (import "js" "log" (func $log (param i32))) (import "js" "prtZero" (func $prtZero)) (import "js" "prtNonZero" (func $prtNonZero)) (import "js" "prtEnd" (func $prtEnd)) (func (export "isZero") (param i32) local.get 0 i32.eqz if call $prtZero else call $prtNonZero end call $prtEnd ) ) `; const { isZero } = await WabtUtils.RunWat(watSrc, importObject); isZero(0); isZero(1);

这种方式的if语句块中不能再调用then指令,但更接近于其他高级语言的形式。

标记 block 代码块

const importObject = { js: {log: pc.log.bind(pc)} }; let watSrc = ` (module (import "js" "log" (func $log (param f32))) (func (export "nonNeg") (param f32) block $block_a local.get 0 f32.const 0 f32.ge br_if $block_a local.get 0 f32.neg local.set 0 end local.get 0 call $log ) ) `; const { nonNeg } = await WabtUtils.RunWat(watSrc, importObject); nonNeg(-5); nonNeg(5);

当函数参数大于等于0时,跳出$block_a语句块,否则将其符号反转,从而得到一个非负数。

标记 loop 代码块

const importObject = { js: {log: pc.log.bind(pc)} }; let watSrc = ` (module (import "js" "log" (func $log (param i32))) (func (export "prtToTen") (param i32) loop $loop_a local.get 0 call $log local.get 0 i32.const 10 i32.lt_u if local.get 0 i32.const 1 i32.add local.set 0 br $loop_a end end ) ) `; const { prtToTen } = await WabtUtils.RunWat(watSrc, importObject); prtToTen(8);

打印函数参数值,直至其值等于10为止。

参考资源

  1. Control Instructions - Block Instructions

nop 指令

nop指令是一个空指令,不会执行任何操作。

let watSrc = ` (module (func nop ) ) `; await WabtUtils.RunWat(watSrc);

call 指令

call指令用以调用函数。

const importObject = { js: {log: pc.log.bind(pc)} }; let watSrc = ` (module (import "js" "log" (func $log (param i32))) (func i32.const 10 call $log ) (start 1) ) `; await WabtUtils.RunWat(watSrc, importObject);

所导入的$log函数的索引值为0,所定义的匿名函数的索引值为1

call_indirect 指令

call_indirect指令用以调用存储在函数表中的函数,可用于实现函数指针。

let watSrc = ` (module (func $add (param i32 i32) (result i32) local.get 0 local.get 1 i32.add ) (func $sub (param i32 i32) (result i32) local.get 0 local.get 1 i32.sub ) (table $funcTable 2 funcref) (elem $funcTable (i32.const 0) $add $sub) (type $funcType (func (param i32 i32) (result i32))) (func (export "funcPointer") (param $funcIdx i32) (param $num1 i32) (param $num2 i32) (result i32) local.get $num1 local.get $num2 (call_indirect $funcTable (type $funcType) (local.get $funcIdx)) ) ) `; const { funcPointer } = await WabtUtils.RunWat(watSrc); pc.log(funcPointer(0, 2, 3)); // invoke the 0th function pc.log(funcPointer(1, 5, 2)); // invoke the 1st function

参考资源

Main

  1. W3C version (single page)
  2. Doc version (webassembly.org)

Control Instructions

  1. Control Instructions
  2. Control Instructions
  3. Folded Instructions
  4. WebAssembly control flow instructions (MDN)