Как вернуть строку (или аналогичную) из Rust в WebAssembly?

Я создал небольшой файл WASM из этого кода Rust:

#[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 но он не работает (он по-прежнему отображает "неопределенный").

Есть ли способ вернуть строку из Rust в WebAssembly? Какой тип я должен использовать?

4 ответа

Решение

WebAssembly поддерживает только несколько числовых типов, и это все, что может быть возвращено через экспортированную функцию.

Когда вы компилируете в WebAssembly, ваша строка будет храниться в линейной памяти модуля. Чтобы прочитать эту строку из JavaScript хоста, вам нужно вернуть ссылку на ее местоположение в памяти и длину строки, то есть два целых числа. Это позволяет читать строку из памяти.

Вы используете эту же технику независимо от того, какой язык вы компилируете в WebAssembly. Как я могу вернуть строку JavaScript из функции WebAssembly, дает подробное описание проблемы.

В частности, для 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 можно увидеть в действии в WasmFiddle.

Вернуть строку из Rust fn в ReactApp

TLDR:
добавить в main.rsuse wasm_bindgen::prelude::*;
ИспользоватьJsValueкак возвращаемый тип fn.
Возврат из фнJSValue::from_str("string")


Создать библиотеку Rust для функции

      mkdir ~/hello-from-rust-demo \
cd ~/hello-from-rust-demo \
cargo new --lib hello-wasm \
cargo add wasm-bindgen \
code ~/hello-from-rust-demo/hello-wasm/src/lib.rs
      use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub fn hello(name: &str) -> JsValue {
    JsValue::from_str(&format!("Hello from rust, {}!", name))
}
      cargo install wasm-pack \
wasm-pack build --target web

Создайте приложение React для демонстрации функции Rust

      cd ~/hello-from-rust-demo \
yarn create react-app hello \
cd hello \
yarn add ../hello-wasm/pkg \
code ~/hello-from-rust-demo/hello/src/App.js

App.js

      import init, { hello } from 'hello-wasm';
import { useState, useEffect } from 'react';

function App() {
  const [hello, setHello] = useState(null);
  useEffect(() => {
    init().then(() => {
      setHello(()=>hello);
    })
  }, []);

  return (
    hello("Human")
  );
}

export default App;

Запустить приложение

      yarn start

Hello from rust, Human!

Вы не можете напрямую вернуть Rust String или &str, Вместо этого выделите и верните необработанный байтовый указатель, содержащий данные, которые затем должны быть закодированы в виде строки JS на стороне JavaScript.

Вы можете взглянуть на пример SHA1 здесь.

Интересующие функции находятся в

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

Дополнительные примеры: https://www.hellorust.com/demos/sha1/index.html

Большинство примеров, которые я видел, копируют строку дважды. Сначала на стороне WASM, в CString или сжимая Vec до его емкости, а затем на стороне JS при декодировании UTF-8.

Учитывая, что мы часто используем 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, который может увеличить размер, если HashMap уже требуется.

Интересной альтернативой будет использование таблиц для совместного использования триплета (полезная нагрузка, длина, емкость) с JavaScript и его возврат, когда придет время освободить строку. Но я пока не знаю, как пользоваться таблицами.

PS Иногда мы не хотим выделять Vec на первом месте.
В этом случае мы можем переместить отслеживание памяти в JavaScript:

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}))
Другие вопросы по тегам