# AssemblyScript介紹1

買賣虛擬貨幣

AssemblyScript並不是一門全新的程式語言,它的語法是目前非常流行的TypeScript語言語法的嚴格子集,專門針對WebAssembly(後面簡稱Wasm)進行了裁剪和定製。下面是JavaScript、TypeScript和AssemblyScript這三種語言的語法關係圖。

本文的重點是討論AssemblyScript程式如何編譯為Wasm模組,如果想要了解AssemblyScript語言的基本語法和用法,可以參考AssemblyScript教程。在前面的文章中,我們已經詳細的討論了Wasm二進位制格式以及Wasm指令集。我們已經知道,Wasm二進位制模組是按段(Section)來組織內容的,目前一共有12種不同型別的段。我們簡單回顧一下這些段的內容:

自定義段(ID是0),存放函式名等輔助資訊。這些資訊不影響Wasm執行語義,即使完全丟棄也問題不大。

型別段(ID是1),存放函式型別(也叫函式簽名)和塊型別

匯入段(ID是2),存放匯入資訊。可以匯入的專案有四種:函式、表、記憶體、全域性變數。

函式段(ID是3),存放內部定義的函式簽名資訊。這是一個索引表,裡面存放的是內部定義的函式的簽名在型別段中的索引。

表段(ID是4),存放內部定義的表資訊。Wasm規範限制模組只能匯入或者定義一張表。

記憶體段(ID是5),存放內部定義的記憶體資訊。Wasm規範限制模組只能匯入或者定義一塊記憶體。

全域性段(ID是6),存放內部定義的全域性變數資訊。

匯出段(ID是7),存放匯出資訊。和匯入段一樣,可以匯出的專案也有四種:函式、表、記憶體、全域性變數。

起始段(ID是8),存放起始函式索引。

元素段(ID是9),存放表的初始化資料。

程式碼段(ID是10),存放內部定義函式的區域性變數資訊和位元組碼。

資料段(ID是11),存放記憶體初始化資料。

下面我們就來看看AssemblyScript程式是如何被編譯成Wasm模組的,更具體的說,是看看程式執行所需要的關鍵資訊是如何被存放在各種段裡的。我們將使用WABT提供的wasm2wat和wasm-objdump命令來觀察AssemblyScript編譯器生成的二進位制模組。

型別段

程式中的所有函式簽名會被編譯器收集起來放進二進位制模組的型別段中,下面請看一個例子:

declare function f1(x: i32): i32;declare function f2(x: f32, y: f32): f32;declare function f3(x: f32, y: f32): f32;export function f4(a: i32, b: i32): i32 {  return f1(b) + f1(b);}export function f5(a: f32, b: f32, c: f32): f32 {  return f2(a, b) + f3(b, c);}

上面的例子宣告瞭三個外部函式,並且定義了兩個內部函式。注意我們需要將內部函式標記為匯出(或者關閉編譯器最佳化),否則編譯器可能會把它們最佳化掉。編譯上面的程式,然後用wasm-objdump命令將生成的二進位制模組轉換為文字格式,結果如下所示:

(module  (type (;0;) (func (param f32 f32) (result f32)))  (type (;1;) (func (param i32) (result i32)))  (type (;2;) (func (param i32 i32) (result i32)))  (type (;3;) (func (param f32 f32 f32) (result f32)))  (import "index" "f1" (func (;0;) (type 1)))  (import "index" "f2" (func (;1;) (type 0)))  (import "index" "f3" (func (;2;) (type 0)))  (func (;3;) (type 2) (;程式碼省略;) )  (func (;4;) (type 3) (;程式碼省略;) )  ;; 其他程式碼省略)

由於f2()和f3()的簽名一樣,所以總共有四個函式簽名。可以看到,編譯器將f2()和f3()的簽名放在了最前面,然後是f1()、f4()和f5()的簽名。

匯入和匯出段

如前面所說,模組可以匯入或匯出四種專案:函式、表、記憶體、全域性變數。從上面的例子可以看到,如果不考慮編譯器最佳化,那麼AssemblyScript語言中的函式將被編譯成Wasm函式。我們馬上還會看到,全域性變數也有類似的對應關係。下面的例子展示瞭如何宣告外部的函式和全域性變數:

declare function add(a: i32, b: i32): i32;@external("sub2")declare function sub(a: i32, b: i32): i32;@external("math", "mul2")declare function mul(a: i32, b: i32): i32;@external("math", "pi")declare const pi: f32;export function main(): void {  add(1, 2);  sub(1, 2);  mul(1, pi as i32);}

在預設情況下,AssemblyScript編譯器會把被編譯程式的檔名當作外部模組名,把函式或全域性變數名當作成員名。但也可以使用@external註解明確單獨指定成員名,或者同時指定外部模組名和成員名。表和記憶體比較特殊,我們將在後面的文章中詳細討論。AssemblyScript編譯器提供了--importTable和--importMemory選項,如果在編譯時指定了這兩個選項,將會在模組的匯入段中生成表和記憶體的匯入項(env.table和env.memory)。將上面的例子儲存為index.ts,然後使用這兩個選項編譯,下面是編譯結果(已經轉換為文字格式):

(module  (type (;0;) (func (param i32 i32) (result i32)))  (type (;1;) (func))  (import "index" "add" (func (;0;) (type 0)))  (import "index" "sub2" (func (;1;) (type 0)))  (import "math" "mul2" (func (;2;) (type 0)))  (import "math" "pi" (global (;0;) f32))  (import "env" "memory" (memory (;0;) 0))  (import "env" "table" (table (;0;) 1 funcref))  (func (;3;) (type 1) (;程式碼省略;) )  ;; 其他程式碼省略)

在AssemblyScript語言中被標記為匯出的函式和全域性變數會被編譯器登記到模組的匯出段中;記憶體是預設匯出的,但是可以透過--noExportMemory選項關閉;表預設不匯出,但是可以透過--exportTable開啟。下面來看另一個例子:

export const pi: f32 = 3.14;export function add(a: i32, b: i32): i32 { return a + b; }export function sub(a: i32, b: i32): i32 { return a - b; }export function mul(a: i32, b: i32): i32 { return a * b; }

使用--exportTable選項編譯上面的例子,下面是編譯後的模組(已經轉換為文字格式):

(module  (type (;0;) (func (param i32 i32) (result i32)))  (func (;0;) (type 0) (;程式碼省略;) )  (func (;1;) (type 0) (;程式碼省略;) )  (func (;2;) (type 0) (;程式碼省略;) )  (table (;0;) 1 funcref)  (memory (;0;) 0)  (global (;0;) f32 (f32.const 0x1.91eb86p+1 (;=3.14;)))  (export "memory" (memory 0))  (export "table" (table 0))  (export "pi" (global 0))  (export "add" (func 0))  (export "sub" (func 1))  (export "mul" (func 2))  (elem (;0;) (i32.const 1) func))

函式和程式碼段

如前文所述,模組內定義的函式資訊被分開放在兩個段裡:函式的簽名資訊在型別段裡,函式的區域性變數資訊和位元組碼在程式碼段裡。如果完全關閉最佳化,那麼AssemblyScript語言中定義的函式和Wasm模組中的函式應該有一個直接的對應關係。也就是說,語言中定義的每一個函式都會在模組的函式段和程式碼段中各產生一個條目。下面來看一個例子:

function add(a: i32, b: i32): i32 { return a + b; }function sub(a: i32, b: i32): i32 { return a - b; }function mul(a: i32, b: i32): i32 { return a * b; }export function main(): void {  add(1, 2);  sub(1, 2);  mul(1, 2);}

當編譯器最佳化開啟時,對於非匯出的內部函式,這一對應關係就可能會被打破。為了便於觀察,我們可以在編譯時指定-O0選項關閉最佳化,下面是編譯後的模組(已經轉換為文字格式):

(module  (type (;0;) (func (param i32 i32) (result i32)))  (type (;1;) (func))  (func (;0;) (type 0) (;程式碼省略;) )  (func (;1;) (type 0) (;程式碼省略;) )  (func (;2;) (type 0) (;程式碼省略;) )  (func (;3;) (type 1) (;程式碼省略;) )  (table (;0;) 1 funcref)  (memory (;0;) 0)  (export "memory" (memory 0))  (export "main" (func 3))  (elem (;0;) (i32.const 1) func))

用wasm-objdump命令觀察函式段和程式碼段更直觀一些,下面是輸出結果(省略了無關內容):

...Section Details:Type[2]: - type[0] (i32, i32) -> i32 - type[1] () -> nilFunction[4]: - func[0] sig=0 - func[1] sig=0 - func[2] sig=0 - func[3] sig=1 <main>Table[1]: ...Memory[1]: ...Export[2]: ...Elem[1]: ...Code[4]: - func[0] size=7 - func[1] size=7 - func[2] size=7 - func[3] size=23 <main>Custom: ...

表和元素段

Wasm中的表主要用來實現C/C++等語言中的函式指標。AssemblyScript語言和JavaScript/TypeScript語言一樣,都支援一等函式,這一特性也是透過Wasm表來實現的。下面來看一個例子:

type op = (a: i32, b: i32) => i32;function add(a: i32, b: i32): i32 { return a + b; }function sub(a: i32, b: i32): i32 { return a - b; }function mul(a: i32, b: i32): i32 { return a * b; }export function calc(a: i32, b: i32, op: (x:i32, y:i32) => i32): i32 {  return op(a, b);}export function main(a: i32, b: i32): void {  calc(a, b, add);  calc(a, b, sub);  calc(a, b, mul);}

下面是編譯後的模組(已經轉換為文字格式),請注意觀察表段和元素段:

(module  (type (;0;) (func (param i32 i32) (result i32)))  (type (;1;) (func (param i32 i32)))  (type (;2;) (func (param i32 i32 i32) (result i32)))  (func (;0;) (type 2) (param i32 i32 i32) (result i32)    (call_indirect (type 0)      (local.get 0) (local.get 1)      (block (result i32)  ;; label = @1        (global.set 0 (i32.const 2))        (local.get 2)      )    )  )  (func (;1;) (type 0) (;程式碼省略;) )  (func (;2;) (type 0) (;程式碼省略;) )  (func (;3;) (type 0) (;程式碼省略;) )  (func (;4;) (type 1) (param i32 i32)    (drop (call 0 (local.get 0) (local.get 1) (i32.const 1)))    (drop (call 0 (local.get 0) (local.get 1) (i32.const 2)))    (drop (call 0 (local.get 0) (local.get 1) (i32.const 3)))  )  (table (;0;) 4 funcref)  (memory (;0;) 0)  (global (;0;) (mut i32) (i32.const 0))  (export "memory" (memory 0))  (export "calc" (func 0))  (export "main" (func 4))  (elem (;0;) (i32.const 1) func 1 2 3))

記憶體和資料段

我們會在後面的文章中詳細討論AssemblyScript記憶體管理,這裡先來看一個簡單的例子:

declare function printChar(c: i32): void;export function main(): void {  const str = "Hello, World!\n";  for (let i = 0; i < str.length; i++) {    printChar(str.charCodeAt(i));  }}

AssemblyScript字串內部使用UTF-16編碼,字串字面量會被放在資料段中。下面是編譯後的模組(開啟了編譯器最佳化),請注意觀察記憶體段和資料段:

(module  (type (;0;) (func))  (type (;1;) (func (param i32)))  (import "index" "printChar" (func (;0;) (type 1)))  (func (;1;) (type 0) (;程式碼省略;) )  (memory (;0;) 1)  (export "memory" (memory 0))  (export "main" (func 1))  (data (;0;) (i32.const 1024) "\1c\00\00\00\01\00\00\00\01\00\00\00\1c\00\00\00H\00e\00l\00l\00o\00,\00 \00W\00o\00r\00l\00d\00!\00\0a"))

AssemblyScript編譯器還提供了--initialMemory和--maximumMemory這兩個選項,允許我們顯式控制記憶體的初始和最大頁數,這裡就不詳細介紹了。

全域性段

由前文可知,AssemblyScript語言使用Wasm全域性變數來實現語言中的全域性變數。在完全關閉編譯器最佳化時,AssemblyScript語言中定義的每一個全域性變數都會在生成模組的全域性段中佔據一個專案。我們來看一個例子:

var g1: i32 = 100;export var g2: i32 = 200;export var g3: i64 = 300;export const pi: f32 = 3.14;export function main(): i32 {  return g1;}

下面是編譯後的模組(關閉編譯器最佳化),可以看到,四個全域性變數全部出現在了全域性段中:

(module  (type (;0;) (func (result i32)))  (func (;0;) (type 0) (result i32) (global.get 0))  (table (;0;) 1 funcref)  (memory (;0;) 0)  (global (;0;) (mut i32) (i32.const 100))  (global (;1;) (mut i32) (i32.const 200))  (global (;2;) (mut i64) (i64.const 300))  (global (;3;) f32 (f32.const 0x1.91eb86p+1 (;=3.14;)))  (export "memory" (memory 0))  (export "g2" (global 1))  (export "g3" (global 2))  (export "pi" (global 3))  (export "main" (func 0))  (elem (;0;) (i32.const 1) func))

起始段

起始段的作用是指定一個起始函式索引,被指定的函式將在模組例項化之後被自動執行,從而進行一些額外的初始化工作。下面來看一個例子:

declare function max(a: i32, b: i32): i32;declare function printI32(n: i32): void;var x = max(123, 456);export function main(): void {  printI32(x);}

這個例子宣告瞭兩個外部函式,並且定義了一個全域性變數x和一個函式main()。AssemblyScript編譯器需要把全域性變數x的初始化邏輯放到一個函式中,並將該函式的索引放在起始段中。下面請看編譯後的模組(起始函式的索引是4):

(module  (type (;0;) (func))  (type (;1;) (func (param i32)))  (type (;2;) (func (param i32 i32) (result i32)))  (import "index" "max" (func (;0;) (type 2)))  (import "index" "printI32" (func (;1;) (type 1)))  (func (;2;) (type 0)    (global.set 0 (call 0 (i32.const 123) (i32.const 456)))  )  (func (;3;) (type 0) (call 1 (global.get 0)))  (func (;4;) (type 0) (call 2))  (table (;0;) 1 funcref)  (memory (;0;) 0)  (global (;0;) (mut i32) (i32.const 0))  (export "memory" (memory 0))  (export "main" (func 3))  (start 4)  (elem (;0;) (i32.const 1) func))

自定義段

如前面所述,自定義段主要是存放一些附加資訊,例如函式名等除錯資訊。Wasm規範只定義了一個標準的“name”自定義段,專門用來存放名字資訊。預設情況下,AssemblyScript編譯器不生成“name”自定義段,但是可以透過--debug選項開啟。讓我們加上--debug選項來重新編譯上面的例子,並透過wasm-objdump -x build/optimized.wasm命令觀察生成的二進位制模組(省略了部分無關內容):

...Section Details:Type[3]: - type[0] () -> nil - type[1] (i32) -> nil - type[2] (i32, i32) -> i32Import[2]: - func[0] sig=2 <assembly/index/max> <- index.max - func[1] sig=1 <assembly/index/printI32> <- index.printI32Function[3]: - func[2] sig=0 <start:assembly/index> - func[3] sig=0 <assembly/index/main> - func[4] sig=0 <~start>Table[1]: ...Memory[1]: ...Global[1]: - global[0] i32 mutable=1 - init i32=0Export[2]: ...Start: - start function: 4Elem[1]: ...Code[3]: - func[2] size=12 <start:assembly/index> - func[3] size=6 <assembly/index/main> - func[4] size=4 <~start>Custom: - name: "name" - func[0] <assembly/index/max> - func[1] <assembly/index/printI32> - func[2] <start:assembly/index> - func[3] <assembly/index/main> - func[4] <~start>Custom: - name: "sourceMappingURL"

總結

在這篇文章裡,我們討論了AssemblyScript程式是如何編譯成Wasm模組的,重點討論了Wasm模組的各個段中存放了哪些資訊。在下一篇文章裡,我們將討論AssemblyScript語言如何利用Wasm指令集實現各種語法要素。

*本文由CoinEx Chain開發團隊成員Chase撰寫。CoinEx Chain是全球首條基於Tendermint共識協議和Cosmos SDK開發的DEX專用公鏈,藉助IBC來實現DEX公鏈、智慧合約鏈、隱私鏈三條鏈合一的方式去解決可擴充套件性(Scalability)、去中心化(Decentralization)、安全性(security)區塊鏈不可能三角的問題,能夠高效能的支援數字資產的交易以及基於智慧合約的Defi應用。

免責聲明:

  1. 本文版權歸原作者所有,僅代表作者本人觀點,不代表鏈報觀點或立場。
  2. 如發現文章、圖片等侵權行爲,侵權責任將由作者本人承擔。
  3. 鏈報僅提供相關項目信息,不構成任何投資建議

推荐阅读

;