How to return a string (or similar) from Rust in WebAssembly?
Asked Answered
B

4

30

I created a small Wasm file from this Rust code:

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

It builds and the hello function can be called from 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>

My problem is that the alert displays "undefined". If I return a i32, it works and displays the i32. I also tried to return a String but it does not work (it still displays "undefined").

Is there a way to return a string from Rust in WebAssembly? What type should I use?

Briarroot answered 28/11, 2017 at 10:47 Comment(3)
pub fn hello() -> String { "hello from rust".to_string() } returns the same "undefined"Briarroot
Possible duplicate of How can I return a JavaScript string from a WebAssembly functionInnocuous
Never, ever return Rust types across an FFI boundary. Check out my Rust FFI Omnibus. While it doesn't have anything for WebAssembly (yet), the concepts are all still valid.Marsha
A
26

WebAssembly only supports a few numeric types, which is all that can be returned via an exported function.

When you compile to WebAssembly, your string will be held in the module's linear memory. In order to read this string from the hosting JavaScript, you need to return a reference to its location in memory, and the length of the string, i.e. two integers. This allows you to read the string from memory.

You use this same technique regardless of whichever language you are compiling to WebAssembly. How can I return a JavaScript string from a WebAssembly function provides a detailed background to the problem.

With Rust specifically, you need to make use of the Foreign Function Interface (FFI), using the CString type as follows:

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

The above code exports two functions, get_hello which returns a reference to the string, and get_hello_len which returns its length.

With the above code compiled to a wasm module, the string can be accessed as follows:

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

The C equivalent can be seen in action in a WasmFiddle.

Ariosto answered 6/12, 2017 at 14:36 Comment(4)
Thank you for posting an answer that contains actual code, as opposed to just links saying "go elsewhere and read this".Marsha
It should be noted that CString::new allocates memory on the WASM heap (in order to generate a null-terminated copy of the string). CString::into_raw transfers the "ownership" of this memory out, but the memory can not be readily freed by anything other than the Rust allocator. The given example provides no ways to free that memory, so if used "as is" it will memory-leak.Mcleod
I heard this is outdated?Frierson
Yes, the answer is outdated. See the greg-weigner answer below: https://mcmap.net/q/475619/-how-to-return-a-string-or-similar-from-rust-in-webassemblyEngelbert
U
8

Return String from Rust fn to ReactApp

TLDR:
Add to main.rs use wasm_bindgen::prelude::*;
Use JsValue as the return type of fn.
Return from fn JSValue::from_str("string")


Create Rust Library for Function

mkdir ~/hello-from-rust-demo \
cd ~/hello-from-rust-demo \
cargo new --lib hello-wasm \
cd 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

Create React App to Demo Rust Function

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;

Start App

yarn start

Hello from rust, Human!

Unruh answered 10/8, 2022 at 17:56 Comment(1)
this doesn't work for me, I got "TypeError: hello is not a function"Pale
C
5

You cannot directly return a Rust String or an &str. Instead allocate and return a raw byte pointer containing the data which has to be then encoded as a JS string on the JavaScript side.

You can take a look at the SHA1 example here.

The functions of interest are in

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

For more examples: https://www.hellorust.com/demos/sha1/index.html

Castora answered 29/11, 2017 at 14:22 Comment(1)
One more example arkada38.github.io/2017/12/04/rust-wasm-string-to-uppercasePapiamento
M
4

Most examples I saw copy the string twice. First on the WASM side, into CString or by shrinking the Vec to its capacity, and then on the JS side while decoding the UTF-8.

Given that we often use WASM for the sake of the speed, I sought to implement a version that would reuse the Rust vector.

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

And the JavaScript part:

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

The trade-off here is that we're using HashMap in the WASM which might increase the size unless HashMap is already required.

An interesting alternative would be to use the tables to share the (payload, length, capacity) triplet with the JavaScript and get it back when it is time to free the string. But I don't know how to use the tables yet.

P.S. Sometimes we don't want to allocate the Vec in the first place.
In this case we can move the memory tracking to 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}))
Mcleod answered 11/3, 2018 at 16:15 Comment(1)
Similar ideas can be found in the converse of this question.Marsha

© 2022 - 2024 — McMap. All rights reserved.