在2017年2月28日,Google、微軟、Firefox、蘋果這四個主流瀏覽器供應商,共同宣布了WebAssembly最小可行版本(Minimum Viable Product,MVP),而從MVP可看到幾個可存取資料的地方,其中包含了memory這令人驚悚的儲存空間,然後各種想像就隨之而出了!

從堆疊開始

WebAssembly的指令執行於一個概念上的機器,這個機器有個堆疊空間,裡頭可以放入的整數為i32、i64,可以放入的浮點數為f32、f64,型態名稱上的數字,表示數值使用的位元組長度,在進行任何運算前,必須先將數值放到堆疊之中,然後執行指令,指令會取出對應數量的數值(後進先出)進行運算,有的指令會將結果放入堆疊,有的不會,例如,想要進行i32整數的1+2,使用WebAssembly文字格式,須依照下列方式撰寫:

i32.const 1
i32.const 2
i32.add

i32.const指令會將數值置入堆疊,如果想寫得比較像個高階語言,可以寫成(i32.add (i32.const 1) (i32.const 2)),像i32.add這樣的指令,不用指定來源暫存器、目的暫存器,因而編譯出來的.wasm可以比較小,載入.wasm也就可以快一些,而且這給予瀏覽器實際翻譯為機器碼時,自行分配暫存器的自由。

經常會有開發者問到的是,整數是有號還是無號?堆疊中保留的是位元形式,從堆疊中取出的也是一組位元,對於這組位元要以什麼觀點來看?其實,就是資料型態的概念。有了資料型態的概念,就可以用具體的概念來操作一組位元,而不是直接處理0101的運算。

堆疊操作指令會使用某個資料型態的觀點,將一組位元置入堆疊,或者從堆疊中取出一組位元,因此問題不在於整數是有號或無號,而是要看使用什麼指令,比方說相除就區分為有號的i32.div_s,以及無號的i32.div_u。

索引空間

只有堆疊的話,可進行的任務有限,因而還會需要其他儲存空間,接著該認識變數?確實,可以使用local $a i32來定義變數,可以使用set_local $a、get_local $a來存取變數,不過,$a名稱只是撰寫、閱讀時方便,變數會按照定義的順序給予從0開始的索引,指令操作實際上會使用索引,也就是set_local 0、get_local 0的形式。

索引其實是種定址的方式,就目前來說,WebAssembly的模組定義的索引空間(index space)有全域、函式、表格、線性記憶體等,存取這些空間中的元素,必須透過各自不同的指令,以及各自被賦予的索引來定址,例如呼叫模組中首個被定義的函式,在堆疊中置入函式需要的引數後,必須使用call 0來呼叫,0是寫死的索引,不能使用call (get_local $f)。

區域變數基本上也是位於某個索引空間,不過目前規格中,只提到會是位於函式的某個索引空間中,細節將在未來的規格中制訂。然而,索引雖然用來搭配對應的指令進行定址,實際上索引不是真正的記憶體位址,變數、函式等使用的記憶體彼此都是隔離的,這確保了位元組碼被直接存取、溢位等記憶體安全問題。

函式也有索引空間,實際上函式也有對應的型態,那函式是個值嗎?可以儲存嗎?可以!但是不能存放到堆疊,只能存放到表格,對於存入表格的函式,會依照順序給予用於表格的索引,然而就WebAssembly來說,不能直接使用索引從表格中取得函式,只能使用call_indirect指令,它會從堆疊中取得一個數值作為索引,查找表格以間接呼叫對應的函式。

這就使得表格上的索引,有了類似函式指標的概念(然而索引並不是記憶體位址),例如,高階語言中傳遞函式給另一個函式,就可以透過在表格上存放函式,然後透過傳遞索引的方式來實作,像是底下的$f,就相當於接受函式指標的角色:

(func $interest (param $m f32) (param $f i32) (result f32)
(f32.mul
(call_indirect (type $rate) (get_local $f))
(get_local $m)))

線性記憶體

在目前的MVP中,每個模組可以使用memory定義一個線性儲存空間,就直接稱它是記憶體吧!建立時,以page為單位,每個page為64KiB(也就是65536位元組),可以使用像是i32.load、i32.store等來存取記憶體,執行時會從堆疊中取出一個數值,作為位元組偏移量,表示要從記憶體中哪個位元組開始讀出或存入位元組資料。

memory乍看名稱頗為嚇人,直接存取記憶體?這安全嗎?建立的記憶體彼此之間是隔離的,與變數、函式等使用的空間也是隔離的,指定的偏移量只是位元組偏移量,不是真正的記憶體位址。

線性記憶體存在的目的,是因為WebAssembly的堆疊中,目前只能有i32、i64、f32、f64四種型態,其他高階語言中的資料結構,像是陣列、字串等,就得在記憶體中實作了,開發者必須在記憶體中規畫適當的結構,將位元組讀出、置入堆疊中操作後,從堆疊中取出再存入記憶體,以這種模式來實現高階資料結構的操作。

例如,我在〈Wasm Array〉(https://goo.gl/h1dWnk)中,就使用WebAssembly文字格式,實作了類似C陣列的建立、指定索引存取的概念。

WebAssembly只是個低階語言,要如何實現陣列、字串甚至物件等結構,端視想實作的語言功能而定,或者視想要互通的語言是哪個而定,WebAssembly的記憶體若匯出至JavaScript環境,實際上就是ArrayBuffer,若它代表著某個陣列結構,想轉為JavaScript的Array,就要使用JavaScript(透過TypedArray)存取ArrayBuffer,取得每個元素設定給JavaScript陣列。

至於高階語言中的資料結構,也要有個居中轉換的函式,將陣列之類的東西轉為ArrayBuffer,再匯入WebAssembly;不少高階語言,現在都可轉換為WebAssembly,基本上應該會提供某種包裹程式庫,讓編譯出來的WebAssembly與JavaScript間,在高階資料結構方面可以便於轉換。

WebAssembly的記憶體,在JavaScript中是個ArrayBuffer,這意味著JavaScript環境的垃圾收集可以處理它,當然,若沒有寫好,還是會造成記憶體洩漏,只不過等級上,就像沒寫好JavaScript,造成DOM記憶體洩漏之類的問題。

實際操作的可行性越來越高

在MVP之前,不少人對WebAssembly有著不少想像,也充滿好奇,在MVP推出的一年多之後,相關工具也逐漸齊全,語言支援也有了不少,其實可以試著使用WebAssembly文字格式操作看看,可以澄清不少的傳言與誤解。

就目前來說,若只是打算試著將某個語言編譯為WebAssembly,搭配JavaScript來進行操作,認識WebAssembly文字格式也有很大的幫助,而且,在Firefox、Chrome的開發者工具上,已經能夠將.wasm轉為文字格式以進行除錯,Chrome上甚至可以看到各個儲存空間的內容,不再只是憑藉想像。

專欄作者

熱門新聞

Advertisement