web-dev-qa-db-fra.com

Comment renvoyer une chaîne (ou similaire) depuis Rust dans WebAssembly?

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

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

Il 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 "non défini". Si je retourne un i32, cela fonctionne et affiche le i32. J'ai également essayé de renvoyer un String mais cela ne fonctionne pas (il affiche toujours "non défini").

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

12
rap-2-h

WebAssembly ne prend en charge que quelques types numériques , ce qui 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 mémoire.

Vous utilisez cette même technique quelle que soit la langue que vous compilez vers WebAssembly. Comment puis-je renvoyer une chaîne JavaScript à partir d'une fonction WebAssembly fournit un arrière-plan détaillé du problème.

Avec Rust spécifiquement, 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
ColinE

Vous ne pouvez pas retourner directement un Rust String ou un &str. Au lieu de cela, allouez et renvoyez un pointeur d'octets bruts contenant les données qui doivent ensuite être codées en tant que chaîne JS du côté JavaScript.

Vous pouvez jeter un œil à l'exemple SHA1 ici .

Les fonctions d'intérêt sont

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

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

4
letmutx

La plupart des exemples que j'ai vus copier la chaîne deux fois. D'abord du côté WASM, dans CString ou en réduisant le Vec à sa capacité, puis du 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 pourrait 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 le 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