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

1,439 阅读3分钟

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也能够使用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:

嗯??,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:

yeap! 一切如自己所料的感觉真不错!😙

但是!

memory对象应该是共享的才对!这样JS端也能够向WebAssembly传值...

算了,还是留待下回分解吧 😋