WebGL Tutorial
and more

Wasm Functions

撰写时间:2025-01-13

修订时间:2025-01-18

概述

本章集中阐述Wasm函数。

函数是Wasm模块中最基础的基石。当我们了解了Wasm模块中函数的各种变体后,我们就可以直接使用Wasm Text来编写代码,无需再导入任何第三方编程语言的代码,从而得以将注意力完全集中在Wasm的源代码上面。

从Wasm导出至Web

Wasm内编写一个名为sum的函数,然后导出到Web环境中进行调用。

const { WabtUtils } = await import('/js/esm/WabtUtils.js'); let watSrc = ` (module (func $sum (export "sum") (param i32 i32) (result i32) local.get 0 local.get 1 i32.add ) ) `; let wasmModule = await WabtUtils.WatToWasmModule(watSrc); const wasmInstance = new WebAssembly.Instance(wasmModule, {}); const {sum} = wasmInstance.exports; pc.log(sum(30, 10));

从Web导入至Wasm

基本形式

const { WabtUtils } = await import('/js/esm/WabtUtils.js'); const importObject = { MyNameSpace: { log: function(arg) { pc.log(arg); } } }; let watSrc = ` (module (import "MyNameSpace" "log" (func $log (param i32))) (func $sum (export "sum") (param i32 i32) local.get 0 local.get 1 i32.add call $log ) ) `; let wasmModule = await WabtUtils.WatToWasmModule(watSrc); const wasmInstance = new WebAssembly.Instance(wasmModule, importObject); const {sum} = wasmInstance.exports; sum(50, 20);

第一步,创建一个对象,承载要导入到Wasm的内容。在该对象中,要导入的具体对象须有namespace.name2级的对象形式:

const importObject = { MyNameSpace: { log: function(arg) { pc.log(arg); } } };

上面,MyNameSpace.log返回一个函数,从而满足了声明要求。

第二步,在Wasm Text代码中,使用以下语句导入该对象:

(import "MyNameSpace" "log" (func $log (param i32)))

import语句后须分别跟有上面的namespacename,且使用func来将其声明为一个名为$log的函数。

import语句也可以嵌入到func的原型声明中:

(func $log (import "MyNameSpace" "log") (param i32))

第三步,在Wasm$sum函数中,求和后,调用$log函数。

(func $sum (export "sum") (param i32 i32) local.get 0 local.get 1 i32.add call $log )

在此步骤中,在Wasm内部,对两数值求和后,调用了从JavaScript客户端传进来的log函数用以在Web中打印结果。

最后一步,在初始化Instance时,传入上面的importObject对象:

const wasmInstance = new WebAssembly.Instance(wasmModule, importObject);

Wasm的中转作用

上节演示了Wasm高效与平台无关的特点。

JavaScript客户端代码:

sum(50, 20)

用以从JavaScript客户端接收两个数值,接着在Wasm中使用汇编语言求和,然后再通过调用所导入函数,在Web中输出结果。求和部分体现了Wasm的高效,而导入与导出功能体现了与特定平台、特定编程语言无关的特性,在哪个环境中使用,只需从相应环境导入相关对象、将结果导出至相应环境中即可。

而这也是各种平台与Web实现无缝链接的方式,将PythonC语言等各种编程语言的代码分别编译为Wasm代码,就可以直接在Web平台上应用。

Wasm都有哪些指令,能高效地实现什么样的效果,另有专门的章节。

导入函数指针

const { WabtUtils } = await import('/js/esm/WabtUtils.js'); const importObject = { pc: {log: pc.log.bind(pc)} }; let watSrc = ` (module (import "pc" "log" (func $log (param i32 i32))) (func (export "print") (param i32 i32) local.get 0 local.get 1 call $log ) ) `; let wasmModule = await WabtUtils.WatToWasmModule(watSrc); const wasmInstance = new WebAssembly.Instance(wasmModule, importObject); const {print} = wasmInstance.exports; print(1, 2);

上节导入以下对象:

const importObject = { MyNameSpace: { log: function(arg) { pc.log(arg); } } };

这是一个只有1个参数的函数,而在此函数体中,调用了pclog方法。

而本节中导入对象:

const importObject = { pc: {log: pc.log.bind(pc)} };

则将pclog方法作为函数指针导入,因此在Wasm中将更为灵活。

取决于print函数在客户端中如何应用,在Wasm中可自行定义使用多少个函数参数。

bind方法的应用,参见call, apply and bind一节。

在函数中使用局域变量

参数列表与局域变量共用一个Local Stack

使用局域变量的代码

const { WabtUtils } = await import('/js/esm/WabtUtils.js'); let watSrc = ` (module (func (export "mulTen") (param $num1 i32) (param $num2 i32) (result i32) (local $factor i32) local.get $num1 local.get $num2 i32.add (local.set $factor (i32.const 10)) local.get $factor i32.mul ) ) `; let wasmModule = await WabtUtils.WatToWasmModule(watSrc); const wasmInstance = new WebAssembly.Instance(wasmModule); const {mulTen} = wasmInstance.exports; let result = mulTen(2, 5); pc.log(result);

其效果等同于以下JavaScript代码:

function mulTen(num1, num2) { let factor; let temp = num1 + num2; factor = 10; return factor * temp; } pc.log(mulTen(2, 5));

当使用函数局域变量,函数语法如下:

(func (param) (param) (result) // function signature (local) // locals ... // body start )

需注意的是,局域变量的声明部分并不属于函数体,应紧随着函数原型的声明。

栈操作示意图

在求和后,代码i32.const 10将常量10压进Result Stack

digraph { graph [ splines=spline; rankdir=TB; ] edge [ fontcolor=gray; ] local_stack [shape=plain, fillcolor=invis, label=<
IndexLocal Stack
......
2&factor
1&num2
0&num1
>]; result_stack [shape=plain, fillcolor=invis, label=<
10
&sum
Result Stack
>]; a[label="i32.const 10", shape=note]; a -> result_stack:param2 [label="push"]; }

之后,代码local.set $factorResult Stack弹出常量值10,并赋值于局域变量&factor

digraph { graph [ splines=spline; rankdir=TB; ] edge [ fontcolor=gray; ] local_stack [shape=plain, fillcolor=invis, label=<
IndexLocal Stack
......
2&factor
1&num2
0&num1
>]; result_stack [shape=plain, fillcolor=invis, label=<
10
&sum
Result Stack
>]; local_stack:param3:e -> result_stack:param2 [dir=back, label="pop"]; }

代码local.get $factor将变量值压进Result Stack中:

digraph { graph [ splines=spline; rankdir=TB; ] edge [ fontcolor=gray; ] local_stack [shape=plain, fillcolor=invis, label=<
IndexLocal Stack
......
2&factor
1&num2
0&num1
>]; result_stack [shape=plain, fillcolor=invis, label=<
&factor
&sum
Result Stack
>]; local_stack:param3:e -> result_stack:param2 [label="Push"]; }

最后,代码i32.mulResult Stack的两个数值相乘。result i32返回该结果。

从上面图示也可看出,如果局域变量&factor无需过多操作,则该变量也可省略:

const { WabtUtils } = await import('/js/esm/WabtUtils.js'); let watSrc = ` (module (func (export "mulTen") (param $num1 i32) (param $num2 i32) (result i32) local.get $num1 local.get $num2 i32.add i32.const 10 i32.mul ) ) `; let wasmModule = await WabtUtils.WatToWasmModule(watSrc); const wasmInstance = new WebAssembly.Instance(wasmModule); const {mulTen} = wasmInstance.exports; let result = mulTen(2, 5); pc.log(result);

参考资源

  1. Funcitons

函数调用机制

最简单的函数调用

const { WabtUtils } = await import('/js/esm/WabtUtils.js'); const importObject = { pc: { log: function() { pc.log('Hello'); } } }; let watSrc = ` (module (import "pc" "log" (func $log)) (func (export "print") call $log ) ) `; let wasmModule = await WabtUtils.WatToWasmModule(watSrc); const wasmInstance = new WebAssembly.Instance(wasmModule, importObject); const {print} = wasmInstance.exports; print();

call $log调用了$log函数,没有参数,没有返回值。

带有参数的函数调用

const { WabtUtils } = await import('/js/esm/WabtUtils.js'); const importObject = { pc: {log: pc.log.bind(pc)} }; let watSrc = ` (module (import "pc" "log" (func $log (param i32 i32))) (func $print (export "print") i32.const 35 i32.const 20 call $log ) ) `; let wasmModule = await WabtUtils.WatToWasmModule(watSrc); const wasmInstance = new WebAssembly.Instance(wasmModule, importObject); const {print} = wasmInstance.exports; print();

$print函数内,i32.const 35的栈操作如下:

t.const c : [] → [t]

即,将常量数值35压进$print函数的Result Stack中。

同理,i32.const 20将常量数值20再压进Result Stack中。

local.get从参数列表或函数的局域变量所在的栈中取出数值并压进Result Stack,而i32.const将一个常量直接压进Result Stack

而当执行代码call $log时,则为$log函数先创建一个栈帧 (frame stack),再从Result Stack的两个数值作为整体弹出,并压进栈帧中。此时栈帧即成为$log函数的Local Stack。此时栈示意图如下:

digraph { graph [ splines=spline; rankdir=TB; ] edge [ fontcolor=gray; ] local_stack [shape=plain, fillcolor=invis, label=<
20
35
$log Frame Stack
>]; result_stack [shape=plain, fillcolor=invis, label=<
20
35
$print Result Stack
>]; {rank=same; local_stack, result_stack} result_stack:param2:n -> local_stack:param2:n [label="call $log: pop and push"]; }

Wasm自带校验流程。由于$log函数有2个参数,在从$print函数调用$log函数时,如果$print函数的Result Stack的数量不等于2个,将导致抛出校验异常。

上面的call $log语句也可改写为更接近于高级编程语言调用函数的方式:

(func $print (export "print") (call $log (i32.const 35) (i32.const 20)) )

这种方式称为行内表达式 (inline expression),call指令需用()包围起来。

相应完整代码:

const { WabtUtils } = await import('/js/esm/WabtUtils.js'); const importObject = { pc: {log: pc.log.bind(pc)} }; let watSrc = ` (module (import "pc" "log" (func $log (param i32 i32))) (func $print (export "print") (call $log (i32.const 35) (i32.const 20)) ) ) `; let wasmModule = await WabtUtils.WatToWasmModule(watSrc); const wasmInstance = new WebAssembly.Instance(wasmModule, importObject); const {print} = wasmInstance.exports; print();

当有多个参数,又有多行进栈操作及行内进栈操作时,依代码顺序进栈。

const { WabtUtils } = await import('/js/esm/WabtUtils.js'); const importObject = { pc: {log: pc.log.bind(pc)} }; let watSrc = ` (module (import "pc" "log" (func $log (param i32 i32 i32))) (func $print (export "print") (i32.const 10) (call $log (i32.const 20) (i32.const 30)) ) ) `; let wasmModule = await WabtUtils.WatToWasmModule(watSrc); const wasmInstance = new WebAssembly.Instance(wasmModule, importObject); const {print} = wasmInstance.exports; print();

参考资源

  1. Funciton Calls

函数返回值

单个返回值

const { WabtUtils } = await import('/js/esm/WabtUtils.js'); const { getBinStr } = await import('/js/esm/BinUtils.js'); let watSrc = ` (module (func (export "getXor") (result i32) i32.const 0x25 i32.const 0x35 i32.xor ) ) `; let wasmModule = await WabtUtils.WatToWasmModule(watSrc); const wasmInstance = new WebAssembly.Instance(wasmModule); const {getXor} = wasmInstance.exports; let num1 = 0x25; let num2 = 0x35; pc.log("0x%s: %s", num1.toString(16), getBinStr(num1, 4)); pc.log("0x%s: %s", num2.toString(16), getBinStr(num2, 4)); const result = getXor(); pc.log("0x%s: %s", result.toString(16), getBinStr(result, 4));

Wasm0x00 | ... | 0xFF可用于表示byte类型的数据。

i32.xor求出两个操作数中相异的数位。

多个返回值

Wasm的函数可以返回多个数值,但在JavaScript中,则自动打包为数组的形式。

const { WabtUtils } = await import('/js/esm/WabtUtils.js'); let watSrc = ` (module (func (export "getResults") (result i32 i32 f32) i32.const 10 i32.const 20 f32.const 30.16 ) ) `; let wasmModule = await WabtUtils.WatToWasmModule(watSrc); const wasmInstance = new WebAssembly.Instance(wasmModule); const {getResults} = wasmInstance.exports; const results = getResults(); pc.log(results);

当有多个返回值时,尽管进栈顺序是FILO (First in, last out),但result指令将结果改为FIFO (First in, first out)。

下面的minMax函数同时返回最小值与最大值。

const { WabtUtils } = await import('/js/esm/WabtUtils.js'); let watSrc = ` (module (func (export "minMax") (param f32 f32) (result f32 f32) local.get 0 local.get 1 f32.min local.get 0 local.get 1 f32.max ) ) `; let wasmModule = await WabtUtils.WatToWasmModule(watSrc); const wasmInstance = new WebAssembly.Instance(wasmModule); const {minMax} = wasmInstance.exports; const [min, max] = minMax(15, 35); pc.log(min, max);

minmax指令都是二元指令 (binop, binary operation),消耗2个操作数,产生1个结果。

此外,只有浮点数有minmax指令。所以这些指令均属于fbinop (floating binary operation)。

利用多个返回值的特点,可快捷地实现交换数值的功能。

let watSrc = ` (module (func (export "swap") (param i32 i32) (result i32 i32) local.get 1 local.get 0 ) ) `; const { swap } = await WabtUtils.RunWat(watSrc); let result = swap(3, 7); pc.log(result);

函数指针

const { WabtUtils } = await import('/js/esm/WabtUtils.js'); 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)) ) ) `; let wasmModule = await WabtUtils.WatToWasmModule(watSrc); const wasmInstance = new WebAssembly.Instance(wasmModule); const { funcPointer } = wasmInstance.exports; pc.log(funcPointer(0, 45, 30)); pc.log(funcPointer(1, 45, 30));

第一步,先声明2个函数:

(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 )

2个函数都带有2个参数,返回一个i32数值。

第二步,声明2个函数指针:

(table $funcTable 2 funcref)

table是特定引用类型的集合,可通过索引值来引用集合中的元素。上面代码声明了一个名为$funcTabletable,其元素数量的最小值为2,类型为函数引用。未声明元素数量的最大值,因此该表可自由拓展。

若需要限制元素数量的最大值,可使用以下代码:

(table $funcTable 2 5 funcref)

第三步,使用elem来初始化表$funcTable

(elem $funcTable (i32.const 0) $add $sub)

i32.const 0指定在表中的起始偏移值,须以常量表达式的方式来指定。且分别将$add$sub函数的引用添加进该表中。

第四步,声明函数指针的签名:

(type $funcType (func (param i32 i32) (result i32)))

该函数指针带有2个类型均为i32的参数,返回一个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)) )

调用时,先将函数指针所需的2个实参压进Result Stack中,然后再调用call_indirect指令,根据客户端所传来的索引值,调用具体的函数。

参考资源

  1. Numeric Instructions
  2. call_indirect
  3. Tables
  4. Element Segments

参考资源

WebAssembly

Specifications

Main Entrance

  1. WebAssembly Specifications (github)
  2. WebAssembly Specifications (webassembly.org)

Core Specification

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

Embedder Specifications

  1. JavaScript API
  2. Web API
  3. WASI API

Others

  1. webassembly.org
  2. Wasm Modules
  3. Wasm Instructions
  4. MDN: Using the JavaScript API
  5. WASM汇编入门教程