facebook / starlark-rust

A Rust implementation of the Starlark language

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

WASM / JS Support

pspeter3 opened this issue · comments

Would it be possible to run Starlark from Node or the Browser using this library?

Presumably if you compiled it with the WASM target it should be possible, but I've never done so. If you have instructions it would be great to add them to the docs.

I don't yet but I can try at some point!

According to my tests, this already compiles successfully for the wasm32-wasi target, but for the normal wasm32-unknown-unknown target the errno crate has no sys implementation and therefore the build fails. I was unable to find where this crate is used.

this already compiles successfully for the wasm32-wasi target

It even passes most of tests (test run internally, but I forgot to setup a job on GitHub).

for the normal wasm32-unknown-unknown target the errno crate has no sys implementation and therefore the build fails

This setup was not tested.

Not sure if this is helpful information but for anyone lost like me, I got this sort of working. I don't know Rust so please forgive the syntax.

First I patched starlark-rust to remove the Instant::now() call since it panics. Should this be patched out under wasm? I don't personally care about timing.

diff --git a/starlark/src/eval.rs b/starlark/src/eval.rs
index 4317b49e..02e7c552 100644
--- a/starlark/src/eval.rs
+++ b/starlark/src/eval.rs
@@ -25,7 +25,6 @@ pub(crate) mod soft_error;
 
 use std::collections::HashMap;
 use std::mem;
-use std::time::Instant;
 
 use dupe::Dupe;
 pub use runtime::arguments::Arguments;
@@ -62,8 +61,6 @@ impl<'v, 'a, 'e> Evaluator<'v, 'a, 'e> {
     /// Evaluate an [`AstModule`] with this [`Evaluator`], modifying the in-scope
     /// [`Module`](crate::environment::Module) as appropriate.
     pub fn eval_module(&mut self, ast: AstModule, globals: &Globals) -> crate::Result<Value<'v>> {
-        let start = Instant::now();
-
         let (codemap, statement, dialect, typecheck) = ast.into_parts();
 
         let codemap = self.module_env.frozen_heap().alloc_any(codemap.dupe());
@@ -135,8 +132,6 @@ impl<'v, 'a, 'e> Evaluator<'v, 'a, 'e> {
 
         self.module_def_info = old_def_info;
 
-        self.module_env.add_eval_duration(start.elapsed());
-
         // Return the result of evaluation
         res.map_err(|e| e.into_error())
     }

Then I made a simple project:

[package]
name = "starlark-js"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
anyhow = "1.0.86"
starlark = {path = "../starlark-rust/starlark", version = "0.12.0"}

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

And put some code to call starlark in src/lib.rs:

use std::mem;

use anyhow::anyhow;
use starlark::environment::Globals;
use starlark::environment::Module;
use starlark::ErrorKind::Internal;
use starlark::eval::Evaluator;
use starlark::syntax::AstModule;
use starlark::syntax::Dialect;
use starlark::values::Value;

#[no_mangle]
pub extern "C" fn evaluate() -> *mut u8 {
    let result = execute();
    let success = result.is_ok();
    let message = result.unwrap_or_else(|e| e.into_anyhow().to_string());
    let bytes = message.as_bytes();
    let len = message.len();
    let mut buffer = Vec::with_capacity(len + 5);
    buffer.extend_from_slice(&(len as u32).to_le_bytes());
    buffer.push(if success { 1 } else { 0 });
    buffer.extend_from_slice(bytes);
    let mut this = mem::ManuallyDrop::new(buffer);
    this.as_mut_ptr()
}

fn execute() -> Result<String, starlark::Error> {
    let content = r#"
def hello():
   return "hello"

hello() + " world!"
    "#;
    
    let ast: AstModule =
        AstModule::parse("hello_world.star", content.to_owned(), &Dialect::Standard)?;
    
    // We create a `Globals`, defining the standard library functions available.
    // The `standard` function uses those defined in the Starlark specification.
    let globals: Globals = Globals::standard();
    
    // We create a `Module`, which stores the global variables for our calculation.
    let module: Module = Module::new();
    
    // We create an evaluator, which controls how evaluation occurs.
    let mut eval: Evaluator = Evaluator::new(&module);
    
    // And finally we evaluate the code using the evaluator.
    let res: Value = eval.eval_module(ast, &globals)?;
    let result = res.unpack_str().ok_or(starlark::Error::new(Internal(anyhow!("Can't get str"))))?;
    return Ok(String::from(result));
}

Ran cargo build --target wasm32-unknown-unknown --release and then opened a simple HTML test page under python3 -m http.server:

<!doctype html>
<html lang="en-US">
  <head>
    <meta charset="utf-8" />
    <title>hello-wasm example</title>
  </head>
  <body>
    <script type="module">
      WebAssembly.instantiateStreaming(fetch("target/wasm32-unknown-unknown/release/starlark_js.wasm"), {}).then(({instance}) => {
        const memory = instance.exports.memory;
	const offset = instance.exports.evaluate();
        const length = new Uint32Array(memory.buffer, offset, 1)[0];
        const result = new Uint8Array(memory.buffer, offset + 4, 1)[0] != 0;
        const characters = new Uint8Array(memory.buffer, offset + 5, length);
        if (result) {
          console.log(new TextDecoder().decode(characters));
        } else {
          console.error(new TextDecoder().decode(characters));
        }
      });
    </script>
  </body>
</html>

And it seems to work!


Edited 7/13 to clean it up a little:

#![feature(str_from_raw_parts)]

use std::mem;

use anyhow::anyhow;
use starlark::environment::Globals;
use starlark::environment::Module;
use starlark::ErrorKind::Internal;
use starlark::eval::Evaluator;
use starlark::syntax::AstModule;
use starlark::syntax::Dialect;
use starlark::values::Value;

#[no_mangle]
pub extern "C" fn malloc(n: usize) -> *mut u8 {
    mem::ManuallyDrop::new(Vec::with_capacity(n)).as_mut_ptr()
}

#[no_mangle]
pub extern "C" fn evaluate(s: *const u8) -> *mut u8 {
    let input = unsafe {
        let length = u32::from_le_bytes(*(s as *const [u8; 4])) as usize;
        std::str::from_raw_parts(s.offset(4), length)
    };
    let result = execute(input);
    let success = result.is_ok();
    let message = result.unwrap_or_else(|e| e.into_anyhow().to_string());
    let len = message.len();
    let mut buffer = Vec::with_capacity(len + 8);
    buffer.push(if success { 1 } else { 0 });
    buffer.extend(vec![0; 3]);
    buffer.extend_from_slice(&(len as u32).to_le_bytes());
    buffer.extend_from_slice(message.as_bytes());
    mem::ManuallyDrop::new(buffer).as_mut_ptr()
}

fn execute(content: &str) -> Result<String, starlark::Error> {
    let ast: AstModule =
        AstModule::parse("hello_world.star", content.to_owned(), &Dialect::Standard)?;
    let globals = Globals::standard();
    let module: Module = Module::new();
    let mut eval: Evaluator = Evaluator::new(&module);
    let res: Value = eval.eval_module(ast, &globals)?;
    let result = res.unpack_str().ok_or(starlark::Error::new(Internal(anyhow!("Can't unpack as string"))))?;
    return Ok(String::from(result));
}
<!doctype html>
<html lang="en-US">
  <head>
    <meta charset="utf-8" />
    <title>hello-wasm example</title>
  </head>
  <body>
    <script type="module">
      WebAssembly.instantiateStreaming(fetch("target/wasm32-unknown-unknown/release/starlark_js.wasm"), {}).then(({instance}) => {
        const readString = (offset) => {
          const memory = instance.exports.memory.buffer;
          const length = new Uint32Array(memory, offset, 1)[0];
          const characters = new Uint8Array(memory, offset + 4, length);
          return new TextDecoder().decode(characters);
        };

        const readU8 = (offset) => {
          return new Uint8Array(instance.exports.memory.buffer, offset, 1)[0];
        };

        const writeString = (s) => {
          const encoded = new TextEncoder().encode(s.trim());
          const offset = instance.exports.malloc(4 + encoded.byteLength);
          // TODO(april): this probably isn't guaranteed to be 4-byte aligned? Might need to fix.
          const memory = instance.exports.memory.buffer;
          const uint32s = new Uint32Array(memory, offset, 1);
          uint32s[0] = encoded.byteLength;
          const uint8s = new Uint8Array(memory, offset + 4, encoded.byteLength);
          uint8s.set(encoded);
          return offset;
        };

        const content = `
def hello(name):
    return "hello " + name

hello("friend")
`;
	const offset = instance.exports.evaluate(writeString(content));
        const ok = readU8(offset) != 0;
        const result = readString(offset + 4);
        if (ok) {
          console.log(result);
        } else {
          console.error(result);
        }
      });
    </script>
  </body>
</html>

That's awesome!