Wasm/web-sys: How to manipulate JS objects from rust?

Please excuse the beginner question but I can't seem to figure out how to access and manipulate javascript objects from my wasm_bindgen code.
Say I have the following object attached to window on the javascript side, how do I call window.greeting.greet() from the rust side? How can I access the properties of the object?

class Greeting {
    constructor(msg, recipient) {
        this.msg = msg
        this.recipient = recipient
    }

   greet() {
       console.log(`${this.msg) ${this.recipient}`)
   }
 }

window.greeting = new Greeting("Hello", "World")

This is described in sections 1.6, 2.15.1.2, and 2.15.1.4 of the wasm-bindgen book. In short, you need to create an extern "C" block with a few extra attributes attached to the functions inside. It should look something like this:

use wasm_bindgen::prelude::*;

#[wasm_bindgen(module = "/my-js-file.js")]
extern "C" {
    type Greeting;

    #[wasm_bindgen(constructor)]
    fn new(msg: String, recipient: String) -> Greeting;

    #[wasm_bindgen(method, getter)]
    fn msg(this: &Greeting) -> String;
    #[wasm_bindgen(method, setter)]
    fn set_msg(this: &Greeting, msg: String);
    #[wasm_bindgen(method, getter)]
    fn recipient(this: &Greeting) -> String;
    #[wasm_bindgen(method, setter)]
    fn set_recipient(this: &Greeting, recipient: String);

    #[wasm_bindgen(method)]
    fn greet(this: &Greeting);
}

#[wasm_bindgen(start)]
pub fn run() {
    let x = Greeting::new("Hello", "world");
    x.set_recipient("WebAssembly");
    x.greet();
}
1 Like

Strangely enough getting the instance from the Window object seems to require an unchecked cast.

// file: greeting/greeting.js

export class Greeting {
  constructor(msg, recipient) {
    this.msg = msg;
    this.recipient = recipient;
  }

  greet() {
     console.log(`${this.msg} ${this.recipient}`);
  }
}
<html>
  <!--
    greeting/index.html
    Based on:
    https://rustwasm.github.io/docs/wasm-bindgen/examples/without-a-bundler.html

    With Chrome Version 79.0.3945.88 (Official Build) (64-bit)
  -->
  <head>
    <meta content="text/html;charset=utf-8" http-equiv="Content-Type"/>
  </head>
  <body>
    <script type="module">
     import { Greeting } from './greeting.js';
     import init from './pkg/greeter.js';

     window.greeting = new Greeting("Hello", "World");

     init().then(module => {
       module.greet();

     }).catch(error => {
       console.log(error);

     });
    </script>
  </body>
</html>
// file: greeting/src/lib.rs

use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;
use js_sys::{Error, Reflect};
use web_sys::{console};

// A macro to provide `println!(..)`-style syntax for `console.log` logging.
#[allow(unused_macros)]
macro_rules! log {
    ( $( $t:tt )* ) => {
        console::log_1(&format!( $( $t )* ).into());
    }
}

#[wasm_bindgen(module = "/greeting.js")]
extern "C" {
    type Greeting;

    #[wasm_bindgen(constructor)]
    fn new(msg: &str, recipient: &str) -> Greeting;

    #[wasm_bindgen(method)]
    fn greet(this: &Greeting);
}

#[wasm_bindgen]
pub fn greet() -> Result<(),JsValue> {
    log!("greet");
    demo_it();

    if let Some(window) = web_sys::window() {
        match Reflect::get(&window, &JsValue::from_str("greeting")) {
            Ok(value) if Ok(true) == Reflect::has(&value, &JsValue::from_str("greet")) => {
                check_it("two", &value);
                let x = value.unchecked_ref::<Greeting>();
                x.greet();
                Ok(())
            },
            _ =>
                Err(Error::new("Window object doesn't have a suitable \"greeting\" property").into())
        }
    } else {
        Err(Error::new("Can't access Window object").into())
    }
}

fn check_it(label: &str, value: &JsValue) {
    let result =
        if value.has_type::<Greeting>() {
            "checked cast will succeed"
        } else {
            "checked cast will fail"
        };

    console::log_2(
        &format!("{}: {} ", label, result).into(),
        value
    );
}

fn demo_it() {
    let x = Greeting::new("Hi", "there!");
    check_it("one", &x);
    x.greet();
}

/*
    https://rustwasm.github.io/docs/wasm-bindgen/

    https://rustwasm.github.io/wasm-bindgen/api/web_sys/fn.window.html

    https://rustwasm.github.io/docs/wasm-bindgen/reference/types/result.html

    https://rustwasm.github.io/wasm-bindgen/api/wasm_bindgen/struct.JsValue.html

    https://rustwasm.github.io/wasm-bindgen/api/js_sys/struct.Error.html

    https://rustwasm.github.io/docs/wasm-bindgen/contributing/design/importing-js-struct.html

    https://rustwasm.github.io/wasm-bindgen/api/wasm_bindgen/trait.JsCast.html
 */
# file: greeting/Cargo.toml
[package]
name = "greeter"
version = "0.1.0"
edition = "2018"

[lib]
crate-type = ["cdylib"]

[dependencies]
wasm-bindgen = "0.2.56"
js-sys = "0.3.33"

[dependencies.web-sys]
version = "0.3.4"
features = [
  'console',
  'Window',
]
greeting$ wasm-pack build --target web
[INFO]: 🎯  Checking for the Wasm target...
[INFO]: 🌀  Compiling to Wasm...
   Compiling proc-macro2 v1.0.7
   Compiling unicode-xid v0.2.0
   Compiling log v0.4.8
   Compiling wasm-bindgen-shared v0.2.56
   Compiling syn v1.0.13
   Compiling cfg-if v0.1.10
   Compiling lazy_static v1.4.0
   Compiling bumpalo v2.6.0
   Compiling memchr v2.2.1
   Compiling version_check v0.1.5
   Compiling anyhow v1.0.26
   Compiling unicode-segmentation v1.6.0
   Compiling wasm-bindgen v0.2.56
   Compiling sourcefile v0.1.4
   Compiling heck v0.3.1
   Compiling nom v4.2.3
   Compiling quote v1.0.2
   Compiling weedle v0.10.0
   Compiling wasm-bindgen-backend v0.2.56
   Compiling wasm-bindgen-macro-support v0.2.56
   Compiling wasm-bindgen-webidl v0.2.56
   Compiling wasm-bindgen-macro v0.2.56
   Compiling js-sys v0.3.33
   Compiling web-sys v0.3.33
   Compiling greeter v0.1.0 (/Users/wheatley/sbox/rasm/greeting)
    Finished release [optimized] target(s) in 46.47s
⚠️   [WARN]: origin crate has no README
[INFO]: ⬇️  Installing wasm-bindgen...
[INFO]: Optional fields missing from Cargo.toml: 'description', 'repository', and 'license'. These are not necessary, but recommended
[INFO]: ✨   Done in 46.58s
[INFO]: 📦   Your wasm pkg is ready to publish at ./pkg.

greeting$ http-server ./ -c-1

Starting up http-server, serving ./
Available on:
  http://127.0.0.1:8080
  http://192.168.1.187:8080
Hit CTRL-C to stop the server

Developer console for localhost:8080/index.html:

greet
greeter.js:94 one: checked cast will succeed    Greeting {msg: "Hi", recipient: "there!"}
greeting.js:10 Hi there!
greeter.js:94 two: checked cast will fail    Greeting {msg: "Hello", recipient: "World"}
greeting.js:10 Hello World
1 Like

Thank you both so much, that was very helpful.

Some variations:

<html>
  <!--
    greeting/index.html
    Based on:
    https://rustwasm.github.io/docs/wasm-bindgen/examples/without-a-bundler.html

    With Chrome Version 79.0.3945.88 (Official Build) (64-bit)
  -->
  <head>
    <meta content="text/html;charset=utf-8" http-equiv="Content-Type"/>
  </head>
  <body>
    <script type="module">
     import init, { greet } from './pkg/greeter.js';

     class Greeting {
       constructor(msg, recipient) {
         this.msg = msg;
         this.recipient = recipient;
       }

       greet() {
         console.log(`${this.msg} ${this.recipient}`);
       }
     }

     window.greeting = new Greeting("Hello", "World");

     run();

     // ---

     async function run() {
       // i.e. wait until WebAssembly module
       // has finished loading asynchronously
       await init();

       try {
         greet("greeting", "greet");

       } catch(error) {
         console.log(error);

       }
     }
    </script>
  </body>
</html>
// file: greeting/src/lib.rs

use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;
use js_sys::{Error, JsString, Reflect};
use web_sys::{console};

// A macro to provide `println!(..)`-style syntax for `console.log` logging.
#[allow(unused_macros)]
macro_rules! log {
    ( $( $t:tt )* ) => {
        console::log_1(&format!( $( $t )* ).into());
    }
}

macro_rules! make_error {
    ( $( $t:tt )* ) => {
        // js_sys::Error => wasm_bindgen::JsValue
        Error::new( &format!( $( $t )* ) ).unchecked_into::<JsValue>()
    }
}

fn extract_name(key: &JsString) -> String {
    match key.as_string() {
        Some(name) => name,
        None => "?".into()
    }
}

#[wasm_bindgen]
pub fn greet(property_key: &JsString, method_key: &JsString) -> Result<(), JsValue> {

    let self_ =
        web_sys::window().ok_or_else(|| {
            make_error!("Can't access Window object")
        })?;

    let object =
        match Reflect::get(&self_, property_key) {
            Ok(value) if value.is_object() => {
                Ok(value)
            },
            _ => Err(make_error!("Window object doesn't have a suitable \"{}\" property", extract_name(property_key)))
        }?;

    let method: js_sys::Function =
        match Reflect::get(&object, method_key) {
            Ok(value) if value.is_function() => {
                // wasm_bindgen::JsValue => js_sys::Function
                Ok(value.into())
            },
            _ => Err(make_error!("\"{}\" object doesn't have a suitable \"{}\" method", extract_name(property_key), extract_name(method_key)))
        }?;

    let arguments = js_sys::Array::new();
    match Reflect::apply(&method, &object, &arguments) {
        Ok(_result) => {
            log!("Applied method successfully.");
            Ok(())
        },
        Err(error) => {
            log!("Attempt to apply method failed.");
            Err(error)
        }
    }
}

/*
  Note: Window, WorkerGlobalScope, SharedWorkerGlobalScope, ServiceWorkerGlobalScope **all** have `self`;
  start thinking of `self` as the global object rather than `window` (or `this`):

  https://rustwasm.github.io/wasm-bindgen/api/web_sys/struct.Window.html#method.self_

  https://developer.mozilla.org/en-US/docs/Web/API/Window/self

  https://rustwasm.github.io/wasm-bindgen/api/web_sys/struct.WorkerGlobalScope.html#method.self_

  https://rustwasm.github.io/wasm-bindgen/api/web_sys/struct.SharedWorkerGlobalScope.html#method.self_

  https://rustwasm.github.io/wasm-bindgen/api/web_sys/struct.ServiceWorkerGlobalScope.html#method.self_

  https://developer.mozilla.org/en-US/docs/Web/API/WorkerGlobalScope/self

  FYI: webpack will use `window` for `target: 'web'` (the default) and `self` for `target: 'webworker'`.

  https://webpack.js.org/configuration/target/


  https://doc.rust-lang.org/book/ch19-06-macros.html

  https://doc.rust-lang.org/book/ch09-02-recoverable-errors-with-result.html#a-shortcut-for-propagating-errors-the--operator
  
  https://rustwasm.github.io/docs/wasm-bindgen/reference/types.html

  https://doc.rust-lang.org/std/option/enum.Option.html#method.ok_or_else

  https://rustwasm.github.io/wasm-bindgen/api/wasm_bindgen/struct.JsValue.html#method.is_object

  https://rustwasm.github.io/wasm-bindgen/api/wasm_bindgen/struct.JsValue.html#method.is_function

  https://rustwasm.github.io/wasm-bindgen/api/js_sys/struct.Array.html#method.new

  https://rustwasm.github.io/wasm-bindgen/api/js_sys/Reflect/fn.apply.html

 */

Developer console for localhost:8080/index.html:

Hello World
greeter.js:170 Applied method successfully.