Wasm Functions
撰写时间:2025-01-13
修订时间:2025-01-18
概述
本章集中阐述Wasm函数。
函数是Wasm模块中最基础的基石。当我们了解了Wasm模块中函数的各种变体后,我们就可以直接使用Wasm Text来编写代码,无需再导入任何第三方编程语言的代码,从而得以将注意力完全集中在Wasm的源代码上面。
从Wasm导出至Web
在Wasm内编写一个名为sum的函数,然后导出到Web环境中进行调用。
从Web导入至Wasm
基本形式
第一步,创建一个对象,承载要导入到Wasm的内容。在该对象中,要导入的具体对象须有namespace.name
的2级的对象形式:
上面,MyNameSpace.log
返回一个函数,从而满足了声明要求。
第二步,在Wasm Text代码中,使用以下语句导入该对象:
import
语句后须分别跟有上面的namespace及name,且使用func来将其声明为一个名为$log的函数。
import
语句也可以嵌入到func
的原型声明中:
第三步,在Wasm的$sum函数中,求和后,调用$log函数。
在此步骤中,在Wasm内部,对两数值求和后,调用了从JavaScript客户端传进来的log函数用以在Web中打印结果。
最后一步,在初始化Instance时,传入上面的importObject对象:
Wasm的中转作用
上节演示了Wasm的高效、与平台无关的特点。
JavaScript客户端代码:
用以从JavaScript客户端接收两个数值,接着在Wasm中使用汇编语言求和,然后再通过调用所导入函数,在Web中输出结果。求和部分体现了Wasm的高效,而导入与导出功能体现了与特定平台、特定编程语言无关的特性,在哪个环境中使用,只需从相应环境导入相关对象、将结果导出至相应环境中即可。
而这也是各种平台与Web实现无缝链接的方式,将Python、C语言等各种编程语言的代码分别编译为Wasm代码,就可以直接在Web平台上应用。
而Wasm都有哪些指令,能高效地实现什么样的效果,另有专门的章节。
导入函数指针
上节导入以下对象:
这是一个只有1个参数的函数,而在此函数体中,调用了pc的log方法。
而本节中导入对象:
则将pc的log方法作为函数指针导入,因此在Wasm中将更为灵活。
取决于print函数在客户端中如何应用,在Wasm中可自行定义使用多少个函数参数。
bind方法的应用,参见call, apply and bind一节。
在函数中使用局域变量
参数列表与局域变量共用一个Local Stack。
使用局域变量的代码
其效果等同于以下JavaScript代码:
当使用函数局域变量,函数语法如下:
需注意的是,局域变量的声明部分并不属于函数体,应紧随着函数原型的声明。
栈操作示意图
在求和后,代码i32.const 10
将常量10压进Result Stack:
Index | Local Stack | ||
...... | |||
2 | &factor | ||
1 | &num2 | ||
0 | &num1 |
10 | ||
&sum | ||
Result Stack |
之后,代码local.set $factor
从Result Stack弹出常量值10,并赋值于局域变量&factor:
Index | Local Stack | ||
...... | |||
2 | &factor | ||
1 | &num2 | ||
0 | &num1 |
10 | ||
&sum | ||
Result Stack |
代码local.get $factor
将变量值压进Result Stack中:
Index | Local Stack | ||
...... | |||
2 | &factor | ||
1 | &num2 | ||
0 | &num1 |
&factor | ||
&sum | ||
Result Stack |
最后,代码i32.mul
将Result Stack的两个数值相乘。result i32
返回该结果。
从上面图示也可看出,如果局域变量&factor无需过多操作,则该变量也可省略:
参考资源
函数调用机制
最简单的函数调用
call $log
调用了$log函数,没有参数,没有返回值。
带有参数的函数调用
在$print函数内,i32.const 35
的栈操作如下:
即,将常量数值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。此时栈示意图如下:
20 | ||
35 | ||
$log Frame Stack |
20 | ||
35 | ||
$print Result Stack |
Wasm自带校验流程。由于$log函数有2个参数,在从$print函数调用$log函数时,如果$print函数的Result Stack的数量不等于2个,将导致抛出校验异常。
上面的call $log
语句也可改写为更接近于高级编程语言调用函数的方式:
这种方式称为行内表达式 (inline expression),call指令需用()
包围起来。
相应完整代码:
当有多个参数,又有多行进栈操作及行内进栈操作时,依代码顺序进栈。
参考资源
函数返回值
单个返回值
Wasm的0x00 | ... | 0xFF
可用于表示byte类型的数据。
i32.xor
求出两个操作数中相异的数位。
多个返回值
Wasm的函数可以返回多个数值,但在JavaScript中,则自动打包为数组的形式。
当有多个返回值时,尽管进栈顺序是FILO (First in, last out),但result指令将结果改为FIFO (First in, first out)。
下面的minMax函数同时返回最小值与最大值。
min及max指令都是二元指令 (binop, binary operation),消耗2个操作数,产生1个结果。
此外,只有浮点数有min及max指令。所以这些指令均属于fbinop (floating binary operation)。
利用多个返回值的特点,可快捷地实现交换数值的功能。
函数指针
第一步,先声明2个函数:
这2个函数都带有2个参数,返回一个i32数值。
第二步,声明2个函数指针:
table是特定引用类型的集合,可通过索引值来引用集合中的元素。上面代码声明了一个名为$funcTable
的table,其元素数量的最小值为2,类型为函数引用。未声明元素数量的最大值,因此该表可自由拓展。
若需要限制元素数量的最大值,可使用以下代码:
第三步,使用elem来初始化表$funcTable:
i32.const 0
指定在表中的起始偏移值,须以常量表达式的方式来指定。且分别将$add及$sub函数的引用添加进该表中。
第四步,声明函数指针的签名:
该函数指针带有2个类型均为i32的参数,返回一个i32的数值。
第五步,调用函数指针:
调用时,先将函数指针所需的2个实参压进Result Stack中,然后再调用call_indirect指令,根据客户端所传来的索引值,调用具体的函数。