估计阅读时长: 6 分钟

https://github.com/dotvanilla/vanilla

WebAssembly是一种运行在浏览器端的二进制程序集文件。和普通的应用程序开发一样,WebAssembly需要基于一定的源代码文本进行编译。这个编译所需要的源代码文本就是WAST文件。

Understanding WebAssembly text format(https://developer.mozilla.org/en-US/docs/WebAssembly/Understanding_the_text_format)

WAST文件主要是通过一系列的S-Expression表达式来构成的。S-Expression最基本的单元为圆括号组成的元表达式,最基本的编译单位为一个模块。例如下面展示了最小的一个合法的S-Expression,可以正常的通过编译,生成WebAssembly文件:

(module)

在WebAssembly之中,所有的表达式都会产生值。每一个S-Expression的基本格式为:(<operation> <argument1> <argument2> ...)。在这个基本的表达式格式之中,operation部分就是一些具有特殊含义的关键词,例如

关键词 含义 示例
call 函数调用 (call $func (...) (...))
return 函数返回值 (return (...))
i32.add 加法运算 (i32.add (i32.const 1) (i32.const 2))

可以看出,在S-Expression之中,求值的基本单元就是一个括号表达式。我们可以在参数中递归的添加任意层数的括号表达式,从而产生了语法树。基于此结构我们可以构建出任意复杂度规模的WebAssembly应用。

当然,我们如果通过手写的方式编写WebAssembly程序,显然是不现实的。我们一般是将一些高级编程语言(例如VisualBasic/Rust/JavaScript/C++)转换为WAST源代码之后,再对WAST做编译得到WebAssembly程序模块的。在这里理解和学习WebAssembly文本格式,可以帮助我们理解webAssembly,开发自己的WebAssembly编译器程序。

代码注释

在WAST之中,代码注释与VisualBasic代码或者R代码类似,仅有单行注释。WAST之中的注释是以两个分号开始的,例如:

;; WAST code comment

基础数据类型

在WebAssembly之中,仅存在有4种基础的数据类型,分别为:

  • i32 (System.Int32) 32位整型数
  • i64 (System.Int64) 64位整型数
  • f32 (System.Single) 32位单精度实数
  • f64 (system.Double) 64位双精度实数

可能你会问到,没有其他的数据类型了么?例如字符串呢?没错,在WebAssembly之中,只有上面的4种基础数据类型,没有其他的类型了。因为webAssembly所主要解决的是在浏览器端实现高性能的计算操作,所以目前只支持上面的4种数值类型。对于字符串而言,则需要借助JavaScript来完成。我们如果需要实现其他的数据类型,例如class,array等,则需要自己设计内存数据结构通过搭积木的方式建立起来。

常量数据

对于常数而言,其申明的语法为

;; <wasm_type>.const <literal>
(i32.const 1)

上面的代码就声明了一个整型数1。

变量数据

在WebAssembly之中,变量会分为两种类型:局部变量和全局变量,分别使用local和global关键词来描述,例如

(global $Math.E (mut f64) (f64.const 2.7182818284590451))
(local $text i32)

值得注意的是,对于全局变量,可以设置初始值。但是对于局部变量则不行。你只能够首先声明变量,然后设置变量的值。对于全局变量而言,使用get/set_global获取或者设置值,对于局部变量而言,则只需要将global修改为local即可。

;; offset = global.ObjectManager
;; global.ObjectManager = offset + sizeof
(set_local $offset (get_global $global.ObjectManager))
(set_global $global.ObjectManager (i32.add (get_local $offset) (get_local $sizeof)))

可以看得出来,在WebAssembly语言之中,并没有我们在平常使用的编程语言中常见的直观的操作符,例如赋值等号,加法运算符号等。而是使用一些关键词来作为替代。初期学习WebAssembly可能会在这方面有一些不适应,但是学习到后面习惯了就好。

二元运算符

在WebAssembly之中,二元运算符不像其他的高级编程语言一样直接使用数学符号来表示。对于WAST而言,二元运算符更加类似于一个函数。并且,在WebAssembly之中,并不像JavaScript/R/VB.NET这类高级编程语言一样存在自动类型转换。在WebAssembly之中使用二元运算符必须遵循着一个很严格的类型匹配规则:即二元运算符仅能够在同类型的数据之间发生。例如,假若需要进行i32和f32类型之间的运算,必须要将i32转换为f32类型之后才可以执行。

在VB.NET语言之中,我们可以很自然的写出一个基于二元运算符的四则运算表达式,例如:

1 + 1

但是,在WebAssembly之中,这个变得不一样了。因为WebAssembly之中所有的操作都是基于S-Expression的栈操作。所以我们在WebAssembly之中进行基于二元表达式的四则运算,会需要进行一些堆栈操作,例如:

;; 1 + 1
(
    i32.const 1
    i32.const 1
    i32.add
)

从上面的示例代码我们可以看出,在WebAssembly之中进行一个最基本的二元运算,会首先需要将两个目标数值压入栈空间,然后通过二元运算符从栈中取出我们前面压入栈的两个数据,完成计算操作。最后,S-Expression将计算结果出栈,就可以得到1+1表达式的结果值了。

因为我们在上面的计算操作之中,首先堆栈了两个元素,之后出栈了两个元素,所以我们的S-Expression的最终栈空间应该是空栈来的。假若检测到执行完一个S-Expression之后,栈空间不为空,那么WebAssembly编译器会报错。

字符串常量

对于字符串常量数据

(data (i32.const intptr) "string\00")

在编译器中,我们可以通过下面的一个对象用来表示WASM之中的一个字符串常量:

''' <summary>
''' a string literal value
''' </summary>
''' <remarks>
''' just supports ASCII chars!
''' </remarks>
Public Class StringLiteral : Inherits MemoryObject

    ''' <summary>
    ''' the string literal value
    ''' </summary>
    ''' <returns></returns>
    Public Property Value As String

    Public Overrides ReadOnly Property sizeOf As WATSyntax
        Get
            Return New LiteralValue(Value.Length)
        End Get
    End Property

    Public Function ToSExpression() As String
        Dim lines As New List(Of String)
        Dim nchars As Integer = Strings.Len(Value)

        lines += $""
        lines += $";; String from {MemoryPtr} with {nchars} bytes in memory"

        If Not Annotation.StringEmpty Then
            lines += ";;"
            lines += ";; " & Annotation
            lines += ";;"
        End If

        lines += $"(data (i32.const {MemoryPtr}) ""{Value}\00"")"

        Return lines _
            .Select(Function(line) "    " & line) _
            .JoinBy(ASCII.LF)
    End Function
End Class

函数

在WAST之中,函数的申明格式为:

( func <signature> <locals> <body> )

例如声明一个不带名字的函数:

(func (param $p i32) (result i32)
   local.get $p
   local.get $p
   i32.add
)

将上面的S-Expression翻译成VB.NET语言就是:

Function(p As Integer) As Integer
   Return p + p
End Function

从上面的例子中可以看得到,S-Expression在函数值返回方面与R/R#语言有些类似。在R/R#语言之中,函数的返回值可以是最后一条表达式的计算结果,不需要特别的指定return关键词;而对于WebAssembly而言,函数的返回值则是当前栈中的结果值。

其实,我们可以为上面的函数添加名称,例如

;; functions in [SimpleHelloWorld.DEMO]

(func $SimpleHelloWorld.DEMO.Main (param $args i32) (result i32)
    ;; Public Function SimpleHelloWorld.DEMO.Main(args As System.Int32) As System.Int32
    (call $Console.WriteLine (i32.const 13))
    (return (i32.const 0))
)

如果要调用上面所申明的函数呢?我们会需要通过call关键词来完成:

;; --------------------------------------------------
;; Microsoft.VisualBasic.My.Application_Startup Event
(func $MyApplication_Startup
    (call $SimpleHelloWorld.DEMO.SubNew )
)
;; --------------------------------------------------

需要特别注意的是,WebAssembly从他的名字就可以看出,这是一门比较低级的汇编语言。只不过WebAssembly是运行在浏览器之中的。所以WebAssembly并不会像其他的高级语言一样,带有自动GC。WebAssembly与R语言类似,并不带有自动GC的功能,我们会需要自己做一些手动的内存维护管理。在WebAssembly之中会需要开发人员手动维护函数的栈。如果编译器检测到了函数在没有清空栈的情况下返回了数据,则WebAssembly的编译将会失败。这个时候drop关键词就派上用场了。这个drop关键词有点类似于VB.NET之中的Call关键词。VB之中的Call关键词可以帮助开发者自动清空函数调用所使用的栈信息,从而提升程序的执行效率。在WebAssembly之中,drop关键词可以帮助开发人员自动清空函数调用产生的未释放的栈信息,从而避免出现内存问题。

我们基于下面的代码进行栈问题的自动检测,并自动添加上drop操作:

''' <summary>
''' drop unused stack value
''' </summary>
''' <remarks>
''' Your imported function $array_push has a return value of i32. You does not consume 
''' this value in the then block. Your if block is declared as void or []. You need to 
''' consume the i32 (for example with a drop) or change the return type of the if 
''' expression to (result i32).
''' 
''' https://github.com/WebAssembly/wabt/issues/1067
''' </remarks>
Public Class Drop : Inherits WATSyntax

    Public Overrides ReadOnly Property Type As WATType
        Get
            Return WATType.void
        End Get
    End Property

    Public Property Value As WATSyntax

    Public Overrides Function ToSExpression(env As Environment, indent As String) As String
        Return $"(drop {Value.ToSExpression(env, indent)})"
    End Function

    Public Shared Function AutoDropValueStack(line As WATSyntax) As WATSyntax
        If Not TypeOf line Is ReturnValue AndAlso (line.Type Is WATType.void OrElse line.Type.UnderlyingWATType = WATElements.void) Then
            ' https://github.com/WebAssembly/wabt/issues/1067
            '
            ' required a drop if target produce values
            Return New Drop With {.Value = line}
        Else
            Return line
        End If
    End Function
End Class

One response

Leave a Reply

Your email address will not be published.