iT邦幫忙

2018 iT 邦幫忙鐵人賽
DAY 8
1
Modern Web

謙虛,踏實的Web Assembly練習系列 第 8

[練習 07] 使用Memory來雙向溝通

Memory使用的一些限制

本來在幻想,如果WebAssembly程式要做出Console程式的功能,那只要用一個Memory做標準輸出,一個做標準輸入,還有一個做錯誤輸出不就好了?不過看了一下規格跟指令...雖然記憶體區塊可以加上Identifier(就是之前在程式中錢號$開頭的名字,不過這只是個語法糖就是了,編譯時這些都會變成索引),但是指令的語法裡面沒有...看起來就是只能有一個記憶體區塊的樣子XD

所以,雖然可以同時由Javascript端,以及WebAssembly程式端寫入及讀出Memory,但是需要做一下記憶體的規劃

規劃Memory的使用

簡單的做法,就是透過importObject,讓WebAssembly程式的Instance可以透過輸入global變數的方式,設定好in/out使用記憶體的基底。例如在importObject中這樣指定:

let importObject = {
    js: {
        inBase: 0,
        outBase: 1024
    }
};

然後WebAssembly程式這樣寫:

(module
	(global $outbase (import "js" "outBase") i32)
	(global $inbase (import "js" "inBase") i32)
    ...
	(data (get_global $outbase) "Hello, ")
)

這樣就可以知道要從$inBase開始的基底位址讀入輸入的資料,然後從$outBase開始的基底位址寫入資料。例如底下的資料區段,就可以設定成從$outBase開始填入資料。

因為程式會依賴從import輸入的資料,所以wabt在編譯時會檢查使用到import的指令以及區段是否在其他區段的前面。

互動的Hello

這其實是很基本的網頁程式,就是從文字輸入框輸入一個名字,按下submit按鈕,然後出現一段招呼的訊息。

網頁端程式:

<html>
<head>
	<meta charset="UTF-8">
</head>
<body>
	Name: <input id="in_name" type="text" /><button id="btn_submit" onclick="say();return false">Submit</button><br><br>
	<div id="panel"></div>
	<script src="../wasm_util.js"></script>
	<script>
	let out = new WebAssembly.Memory({initial: 1});
	let importObjects = {
		js: {
			out: out,
			log: log,
			outBase: 1024,
			inBase: 0
		}
	}
	let ginstance = null;
	let s = new Wasm('test005.wasm')
	.getInstance(importObjects);
	function say(obj) {
		if(ginstance === null) {
			s
			.then(instance => {
				process(ginstance=instance);
			});
		} else {
			process(ginstance);
		}
	}
	function process(instance) {
		let name = document.getElementById('in_name').value;
		name.split('').forEach(s => console.log(s.charCodeAt(0)));
		let view = new Uint8Array(out.buffer, 0, name.length);
		for(let i=0; i<name.length; i++) {
			view[i] = name.charCodeAt(i);
		}
		console.log(view);
		instance.exports.hello(0, name.length);
	}
	function log(offset, length) {
		let view = new Uint8Array(out.buffer, offset, length);
		console.log(view);
		document.getElementById('panel').innerHTML = String.fromCharCode.apply(null, view);
	}
	</script>
</body>
</html>

先用簡單的String.prototype.charCodeAt()把字串轉成Uint8Array,然後用String.fromCharCode()把Uint8Array轉成字串。

WebAssembly程式如下:

(module
	(func $log (import "js" "log") (param i32) (param i32))
	(memory (import "js" "out") 1)
	(global $outbase (import "js" "outBase") i32)
	(global $inbase (import "js" "inBase") i32)
	(func (export "hello") (param $start i32) (param $len i32)
		(local $idx_in i32)
		(local $idx_out i32)
		(local $i i32)
		(set_local $idx_in (i32.add (get_global $inbase) (get_local $start)))
		(set_local $idx_out (i32.add (get_global $outbase) (i32.const 7)))
		(set_local $i (i32.const 0))
		(block $break
			(loop $while1
				(br_if $break (i32.eq (get_local $i) (get_local $len)))
				(i32.store8 (get_local $idx_out) (i32.load8_u (get_local $idx_in)))
				(set_local $i (i32.add (i32.const 1) (get_local $i)))
				(set_local $idx_in (i32.add (get_local $idx_in) (i32.const 1)))
				(set_local $idx_out (i32.add (get_local $idx_out) (i32.const 1)))
				(br $while1)
			)
		)
		(call $log (get_global $outbase) (i32.add (i32.const 7) (get_local $len)))
	)
	(data (get_global $outbase) "Hello, ")
)

指令多的時候,大腦會Stack Overflow,所以改用S-Expression的語法來寫,這樣比較不需要去想現在指令用的參數在堆疊的哪裡XD

這裡使用了幾個流程控制的指令,包含block、loop、br_if還有br。主要其實只是在控制一個迴圈,來逐個把資料從Memory中的輸入資料的位置,複製到輸出資料的位置。

  • block:區塊,他的$label邏輯上是在區塊結束的地方
  • loop:迴圈,也是一種區塊,他的$label邏輯上是在迴圈開始的地方
  • br:branch,白話叫goto $label
  • br_if:條件式的br

所以上面程式中,br $while1會跳到迴圈開頭重新執行,而br_if $break會離開區塊,也就跳離迴圈。

然後可以試試看,輸入fillano,按下submit按鈕,頁面會出現Hello, fillano
Imgur

不過...如果輸入中文,會跑出亂碼:
Imgur

因為輸入時直接用charCodeAt()取出直接塞Uint8Array,多出的位元都被截掉了XD。網頁有指定編碼是UTF-8,但是不想自己寫UTF-8轉換成Uint8Array的程式,所以上網找了一個:

JavaScript UTF-8 encoding and decoding with TypedArray

把他加到網頁,然後調整一下網頁的程式:

<html>
<head>
	<meta charset="UTF-8">
</head>
<body>
	Name: <input id="in_name" type="text" /><button id="btn_submit" onclick="say();return false">Submit</button><br><br>
	<div id="panel"></div>
	<script src="../wasm_util.js"></script>
	<script src="../utf8.js"></script>
	<script>
	let out = new WebAssembly.Memory({initial: 1});
	let importObjects = {
		js: {
			out: out,
			log: log,
			outBase: 1024,
			inBase: 0
		}
	}
	let ginstance = null;
	let s = new Wasm('test006.wasm')
	.getInstance(importObjects);
	function say(obj) {
		if(ginstance === null) {
			s
			.then(instance => {
				process(ginstance=instance);
			});
		} else {
			process(ginstance);
		}
	}
	function process(instance) {
		let name = document.getElementById('in_name').value;
		let enc = encodeUTF8(name);
		let view = new Uint8Array(out.buffer, 0, enc.length);
		for(let i=0; i<enc.length; i++) {
			view[i] = enc[i];
		}
		console.log(view);
		instance.exports.hello(0, enc.length);
	}
	function log(offset, length) {
		let view = new Uint8Array(out.buffer, offset, length);
		console.log(view);
		document.getElementById('panel').innerHTML = decodeUTF8(view);
	}
	</script>
</body>
</html>

WebAssembly端的程式就不需要改了,因為邏輯沒變。

執行看看,輸入中文也可以正確顯示了:

Imgur


關於Memory的部份先嘗試到這裡,後面如果用到再來深究。明天先來看看怎麼使用Table。


上一篇
[練習 06] 使用Memory來實做Hello, World.
下一篇
[練習 08] 使用Table來分享資源
系列文
謙虛,踏實的Web Assembly練習20
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言