代码之家  ›  专栏  ›  技术社区  ›  rap-2-h

如何在WebAssembly中从Rust返回字符串(或类似内容)?

  •  24
  • rap-2-h  · 技术社区  · 7 年前

    我根据这个Rust代码创建了一个小Wasm文件:

    #[no_mangle]
    pub fn hello() -> &'static str {
        "hello from rust"
    }
    

    它构建和 hello 可以从JS调用函数:

    <!DOCTYPE html>
    <html>
    <body>
      <script>
        fetch('main.wasm')
        .then(response => response.arrayBuffer())
        .then(bytes => WebAssembly.instantiate(bytes, {}))
        .then(results => {
          alert(results.instance.exports.hello());
        });
      </script>
    </body>
    </html>
    

    我的问题是 alert i32 ,它工作并显示 i32 . 我还试着返回一个 String 但它不起作用(仍显示“未定义”)。

    有没有办法在WebAssembly中从Rust返回字符串?我应该使用哪种类型?

    3 回复  |  直到 5 年前
        1
  •  21
  •   Shepmaster Tim Diekmann    7 年前

    WebAssembly只支持少数几个 numeric types ,这是通过导出函数可以返回的全部内容。

    编译到WebAssembly时,字符串将保存在模块的线性内存中。为了从宿主JavaScript中读取该字符串,需要返回对其在内存中位置的引用,以及字符串的长度,即两个整数。这允许您从内存中读取字符串。

    无论编译到WebAssembly的是哪种语言,都使用相同的技术。 How can I return a JavaScript string from a WebAssembly function 提供问题的详细背景。

    对于Rust,您需要使用外部函数接口(FFI),使用 CString 类型如下:

    use std::ffi::CString;
    use std::os::raw::c_char;
    
    static HELLO: &'static str = "hello from rust";
    
    #[no_mangle]
    pub fn get_hello() -> *mut c_char {
        let s = CString::new(HELLO).unwrap();
        s.into_raw()
    }
    
    #[no_mangle]
    pub fn get_hello_len() -> usize {
        HELLO.len()
    }
    

    上面的代码导出了两个函数, get_hello 返回对字符串的引用,并且 get_hello_len 返回其长度。

    通过将上述代码编译成wasm模块,可以按如下方式访问字符串:

    const res = await fetch('chip8.wasm');
    const buffer = await res.arrayBuffer();
    const module = await WebAssembly.compile(buffer);
    const instance = await WebAssembly.instantiate(module);
    
    // obtain the module memory
    const linearMemory = instance.exports.memory;
    
    // create a buffer starting at the reference to the exported string
    const offset = instance.exports.get_hello();
    const stringBuffer = new Uint8Array(linearMemory.buffer, offset,
      instance.exports.get_hello_len());
    
    // create a string from this buffer
    let str = '';
    for (let i=0; i<stringBuffer.length; i++) {
      str += String.fromCharCode(stringBuffer[i]);
    }
    
    console.log(str);
    

    C等价物 can be seen in action in a WasmFiddle .

        2
  •  5
  •   letmutx    7 年前

    你不能直接退回铁锈 String 或者 &str . 相反,分配并返回一个原始字节指针,其中包含必须在JavaScript端编码为JS字符串的数据。

    您可以看一看SHA1示例 here .

    感兴趣的功能是

    • demos/bundle.js - copyCStr
    • demos/sha1/sha1-digest.rs - digest

    更多示例: https://www.hellorust.com/demos/sha1/index.html

        3
  •  3
  •   ArtemGr    5 年前

    我看到的大多数示例都会复制字符串两次。首先在WASM一侧,进入 CString 或者通过缩小 Vec 然后在解码UTF-8的同时在JS端。

    考虑到我们经常使用WASM是为了提高速度,我试图实现一个可以重用Rust向量的版本。

    use std::collections::HashMap;
    
    /// Byte vectors shared with JavaScript.
    ///
    /// A map from payload's memory location to `Vec<u8>`.
    ///
    /// In order to deallocate memory in Rust we need not just the memory location but also it's size.
    /// In case of strings and vectors the freed size is capacity.
    /// Keeping the vector around allows us not to change it's capacity.
    ///
    /// Not thread-safe (assuming that we're running WASM from the single JavaScript thread).
    static mut SHARED_VECS: Option<HashMap<u32, Vec<u8>>> = None;
    
    extern "C" {
        fn console_log(rs: *const u8);
        fn console_log_8859_1(rs: *const u8);
    }
    
    #[no_mangle]
    pub fn init() {
        unsafe { SHARED_VECS = Some(HashMap::new()) }
    }
    
    #[no_mangle]
    pub fn vec_len(payload: *const u8) -> u32 {
        unsafe {
            SHARED_VECS
                .as_ref()
                .unwrap()
                .get(&(payload as u32))
                .unwrap()
                .len() as u32
        }
    }
    
    pub fn vec2js<V: Into<Vec<u8>>>(v: V) -> *const u8 {
        let v = v.into();
        let payload = v.as_ptr();
        unsafe {
            SHARED_VECS.as_mut().unwrap().insert(payload as u32, v);
        }
        payload
    }
    
    #[no_mangle]
    pub extern "C" fn free_vec(payload: *const u8) {
        unsafe {
            SHARED_VECS.as_mut().unwrap().remove(&(payload as u32));
        }
    }
    
    #[no_mangle]
    pub fn start() {
        unsafe {
            console_log(vec2js(format!("Hello again!")));
            console_log_8859_1(vec2js(b"ASCII string." as &[u8]));
        }
    }
    

    JavaScript部分:

    (function (iif) {
    
      function rs2js (mod, rs, utfLabel = 'utf-8') {
        const view = new Uint8Array (mod.memory.buffer, rs, mod.vec_len (rs))
        const utf8dec = new TextDecoder (utfLabel)
        const utf8 = utf8dec.decode (view)
        mod.free_vec (rs)
        return utf8}
    
      function loadWasm (cache) {
        // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WebAssembly/instantiateStreaming
        WebAssembly.instantiateStreaming (fetch ('main.wasm', {cache: cache ? "default" : "no-cache"}), {env: {
          console_log: function (rs) {if (window.console) console.log ('main]', rs2js (iif.main, rs))},
          console_log_8859_1: function (rs) {if (window.console) console.log ('main]', rs2js (iif.main, rs, 'iso-8859-1'))}
        }}) .then (results => {
          const exports = results.instance.exports
          exports.init()
          iif.main = exports
          iif.main.start()})}
    
      // Hot code reloading.
      if (window.location.hostname == '127.0.0.1' && window.location.port == '43080') {
        window.setInterval (
          function() {
            // Check if the WASM was updated.
            fetch ('main.wasm.lm', {cache: "no-cache"}) .then (r => r.text()) .then (lm => {
              lm = lm.trim()
              if (/^\d+$/.test (lm) && lm != iif.lm) {
                iif.lm = lm
                loadWasm (false)}})},
          200)
      } else loadWasm (true)
    
    } (window.iif = window.iif || {}))
    

    这里的权衡是我们正在使用 HashMap 在WASM中,可能会增加尺寸,除非 哈希图 已需要。

    一个有趣的替代方法是使用 tables 与JavaScript共享(负载、长度、容量)三元组,并在释放字符串时将其取回。但我还不知道如何使用这些桌子。

    P、 有时我们不想分配 Vec公司 首先。

    extern "C" {
        fn new_js_string(utf8: *const u8, len: i32) -> i32;
        fn console_log(js: i32);
    }
    
    fn rs2js(rs: &str) -> i32 {
        assert!(rs.len() < i32::max_value() as usize);
        unsafe { new_js_string(rs.as_ptr(), rs.len() as i32) }
    }
    
    #[no_mangle]
    pub fn start() {
        unsafe {
            console_log(rs2js("Hello again!"));
        }
    }
    
    (function (iif) {
      function loadWasm (cache) {
        WebAssembly.instantiateStreaming (fetch ('main.wasm', {cache: cache ? "default" : "no-cache"}), {env: {
          new_js_string: function (utf8, len) {
            const view = new Uint8Array (iif.main.memory.buffer, utf8, len)
            const utf8dec = new TextDecoder ('utf-8')
            const decoded = utf8dec.decode (view)
            let stringId = iif.lastStringId
            while (typeof iif.strings[stringId] !== 'undefined') stringId += 1
            if (stringId > 2147483647) {  // Can't easily pass more than that through WASM.
              stringId = -2147483648
              while (typeof iif.strings[stringId] !== 'undefined') stringId += 1
              if (stringId > 2147483647) throw new Error ('Out of string IDs!')}
            iif.strings[stringId] = decoded
            return iif.lastStringId = stringId},
          console_log: function (js) {
            if (window.console) console.log ('main]', iif.strings[js])
            delete iif.strings[js]}
        }}) .then (results => {
          iif.main = results.instance.exports
          iif.main.start()})}
    
      loadWasm (true)
    } (window.iif = window.iif || {strings: {}, lastStringId: 1}))