J'ai créé un petit fichier Wasm à partir de ce code Rust:

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

Il se construit et la fonction hello peut être appelée depuis 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>

Mon problème est que le alert affiche "indéfini". Si je retourne un i32, cela fonctionne et affiche le i32. J'ai aussi essayé de renvoyer un String mais cela ne fonctionne pas (il affiche toujours "undefined").

Existe-t-il un moyen de renvoyer une chaîne de Rust dans WebAssembly? Quel type dois-je utiliser?

14
rap-2-h 28 nov. 2017 à 13:47

3 réponses

Meilleure réponse

WebAssembly ne prend en charge que quelques types numériques, c'est tout ce qui peut être renvoyé via une fonction exportée.

Lorsque vous compilez vers WebAssembly, votre chaîne sera conservée dans la mémoire linéaire du module. Afin de lire cette chaîne à partir du JavaScript d'hébergement, vous devez renvoyer une référence à son emplacement en mémoire et à la longueur de la chaîne, c'est-à-dire deux entiers. Cela vous permet de lire la chaîne de la mémoire.

Vous utilisez cette même technique quel que soit le langage que vous compilez dans WebAssembly. Comment puis-je renvoyer une chaîne JavaScript à partir d'un WebAssembly La fonction fournit un contexte détaillé du problème.

Avec Rust en particulier, vous devez utiliser l'interface de fonction étrangère (FFI), en utilisant le type CString comme suit:

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()
}

Le code ci-dessus exporte deux fonctions, get_hello qui renvoie une référence à la chaîne, et get_hello_len qui renvoie sa longueur.

Avec le code ci-dessus compilé dans un module wasm, la chaîne est accessible comme suit:

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);

L'équivalent C peut être vu en action dans un WasmFiddle.

13
Shepmaster 6 déc. 2017 à 14:38

Vous ne pouvez pas renvoyer directement un Rust String ou un &str. À la place, allouez et retournez un pointeur d'octet brut contenant les données qui doivent ensuite être codées sous forme de chaîne JS côté JavaScript.

Vous pouvez consulter l'exemple SHA1 ici.

Les fonctions d'intérêt sont dans

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

Pour plus d'exemples: https://www.hellorust.com/demos/sha1/index. html

4
letmutx 29 nov. 2017 à 14:22

La plupart des exemples que j'ai vus copier la chaîne deux fois. D'abord côté WASM, en CString ou en réduisant le Vec à sa capacité, puis côté JS lors du décodage de l'UTF-8.

Étant donné que nous utilisons souvent WASM pour des raisons de vitesse, j'ai cherché à implémenter une version qui réutiliserait le vecteur 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]));
    }
}

Et la partie 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 || {}))

Le compromis ici est que nous utilisons HashMap dans le WASM, ce qui peut augmenter la taille à moins que HashMap ne soit déjà requis.

Une alternative intéressante serait d'utiliser les tables pour partager le triplet (charge utile, longueur, capacité) avec JavaScript et le récupérer lorsqu'il est temps de libérer la chaîne. Mais je ne sais pas encore comment utiliser les tableaux.

P.S. Parfois, nous ne voulons pas allouer le Vec en premier lieu.
Dans ce cas, nous pouvons déplacer le suivi de la mémoire vers 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}))
2
ArtemGr 11 juil. 2019 à 21:13
47529643