[WebAssembly 入門] 第二次的 Hello, world!

Yiniau發表於2018-03-30

title: [WebAssembly 入門] 第二次的 Hello, world!

date: 2018-3-29 14:45:00

categories: WebAssembly, 筆記

tags: WebAssembly, JavaScript, Rust, LLVM toolchain

Reference link: 參考連結

auther: Yiniau


[WebAssembly 入門] 第二次的 Hello, world!


上一次Hello,world做了最基礎的實現,主要的程式碼如下

src/lib.rs

//! a WebAssembly module with Rust

extern {
    fn hello_insert_dom();
}

/// return "Hello, world"'s bytes array
#[no_mangle]
pub extern fn hello_call_js() { // equal pub extern "C" fn ...
    unsafe {
        hello_insert_dom();
    }
}
複製程式碼

main.js

const imports = {
  env: {
    hello_insert_dom: () => {
      const h1 = document.createElement('h1');
      h1.innerHTML = 'Hello, world';
      const body = document.querySelector('body');
      body.appendChild(h1);
    },
  }
};

fetch('build/hello.wasm')
  .then(response => response.arrayBuffer())
  .then(bytes => WebAssembly.instantiate(bytes, imports))
  .then(results => {
    console.log(results);
    const exports = results.instance.exports;
    exports.hello_call_js();
  });
複製程式碼

ok, 現在讓我們來換一個方式實現這個hello,world ———— 通過在WebAssembly中向JS函式傳遞引數來實現

傳遞引數

但是傳參在 MVP 版本的WebAssebly中並不是一件容易的事, WebAssembly只支援整數或浮點數 (支援 i32/i64/f32/f64值型別以及 i8/i16 儲存。), 並沒有對String的原生支援

想要與例項互動, 可以通過以下屬性

  • exports - JS呼叫WebAssembly函式的地方, WebAssembly返回的值
  • imports - 當WebAssembly呼叫JS時, 具有任意數量的值型別(請注意: 這即不是陣列也不是可變引數, 長度必須在Module編譯時已知)
  • Memory.buffer - 這是一個ArrayBuffer,可以使用Uint8Array(等等)進行序列化

所有的方法中似乎直接訪問緩衝區是最簡單的: 首先我們要在JS中建立一個記憶體物件並序列化它:

// create a memory, Size is in pages.
const memory = new WebAssembly.Memory({ initial: 2 }); // 建立了2頁的記憶體,每個page 65536B -> 64KiB
const buffer = new Uint8Array(memory.buffer); // index arrayBuffer
                                              // 當字元全部是英文時u8足夠使用
                                              // 即只有0~255
複製程式碼

然後在env屬性中建立存值函式:

env: {
  ...
  save_char_to_memory(num, index) {
    buffer[index] = num;
  },
  ...
}
複製程式碼

以及hello函式:

env: {
  save_char_to_memory(num, index) {
    buffer[index] = num;
  },
  log_string(size, index) {
    let s = "";
    for (let i = index; i < index + size; ++i) {
      s += String.fromCodePoint(buffer[i]);
    }
    console.log(s);
  }
}
複製程式碼

接下來在rust中完成函式

extern {
    fn save_char_to_memory(num: u8, index: usize);
    fn log_string(size: usize, index: usize);
}

/// return "Hello, world"'s bytes array
#[no_mangle]
pub extern fn pass_str() { // equal pub extern "C" fn ...
    let s = b"Hello, world";
    unsafe {
        for i in 0..s.len() {
            save_char_to_memory(s[i], i);
        }
        log_string(s.len(), 0);
    }
}
複製程式碼

測試!

測試之前我們的程式碼應該長這樣:

src/lib.rs:

//! a WebAssembly module with Rust

extern {
    fn save_char_to_memory(num: u8, index: usize);
    fn log_string(size: usize, index: usize);
}

/// return "Hello, world"'s bytes array
#[no_mangle]
pub extern fn pass_str() { // equal pub extern "C" fn ...
    let s = b"Hello, world";
    unsafe {
        for i in 0..s.len() {
            save_char_to_memory(s[i], i);
        }
        log_string(s.len(), 0);
    }
}
複製程式碼

main.js:

// create a memory, Size is in pages.
const memory = new WebAssembly.Memory({ initial: 2 });
const buffer = new Uint8Array(memory.buffer); // index arrayBuffer

const imports = {
  memory: memory,
  env: {
    save_char_to_memory(num, index) {
      buffer[index] = num;
    },
    log_string(size, index) {
      let s = "";
      for (let i = index; i < index + size; i++) {
        s += String.fromCodePoint(buffer[i]);
      }
      console.log(s);
    }
  }
};

fetch('build/hello.wasm')
  .then(response => response.arrayBuffer())
  .then(bytes => WebAssembly.instantiate(bytes, imports))
  .then(results => {
    console.log(results);
    const exports = results.instance.exports;

    exports.pass_str();
  });
複製程式碼

編譯執行吧!

[WebAssembly 入門] 第二次的 Hello, world!

但是,沒有更優雅的方式嗎?

結果顯而易見,之前的文章中有講到WebAssembly也能夠使用memory,但是無法接觸到memory範圍之外的資料。

那麼讓我們先列印一下WebAssembly例項的內容

main.js:

fetch('build/hello.wasm')
  .then(response => response.arrayBuffer())
  .then(bytes => WebAssembly.instantiateStreaming(bytes, imports))
  .then(results => {
    console.log(results);
  });
複製程式碼

src/lib.rs:

//! a WebAssembly module with Rust

extern {
    fn log_string(ptr: *const u8, len: u16);
}

/// pass str | slice to js side
#[no_mangle]
pub extern fn pass_str() { // equal pub extern "C" fn ...
    ...
}
複製程式碼

result:

[WebAssembly 入門] 第二次的 Hello, world!

嗯??,memory? 看來這個就是WebAssembly自動建立的Memory例項,並且在附加在exports.instance中傳遞到了JS端

那麼我們就能夠自然地假設js直接從WebAssembly中獲取字串的裸指標和長度就能夠從memory中擷取一段buffer並使用Uint8Array序列化,再使用String.fromCharCode翻譯為js String

try:

main.js:

...
const imports = {
  memory: memory,
  env: {
    store_string_to_buffer(ptr, len) {
      // buffer slice
      let buf = new Uint16Array(mem.buffer, ptr, len);
      let msg = new TextDecoder('utf8').decode(buf);
      // 這是個新的api,你也可以用下面的方法
      // let msg = String.fromCharCode(...buf);
      console.log('msg: ', msg);
      console.log('mbuf: ', buf);
    }
  }
};
...
複製程式碼

src/lib.rs:

//! a WebAssembly module with Rust

extern {
    fn log_string(ptr: *const u8, len: u16);
}

/// return "Hello, world"'s bytes array
#[no_mangle]
pub extern fn pass_str() { // equal pub extern "C" fn ...
    let s = "Hello, world";
    unsafe {
        log_string(s.as_ptr(), s.len() as u16);
    }
}

複製程式碼

Second attempt:

[WebAssembly 入門] 第二次的 Hello, world!

yeap! 一切如自己所料的感覺真不錯!?

但是!

memory物件應該是共享的才對!這樣JS端也能夠向WebAssembly傳值...

算了,還是留待下回分解吧 ?

相關文章