Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WASM / JS Support #109

Open
pspeter3 opened this issue Feb 15, 2024 · 8 comments
Open

WASM / JS Support #109

pspeter3 opened this issue Feb 15, 2024 · 8 comments

Comments

@pspeter3
Copy link

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

@ndmitchell
Copy link
Contributor

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.

@pspeter3
Copy link
Author

pspeter3 commented Mar 6, 2024

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

@Snapstromegon
Copy link

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.

@stepancheg
Copy link
Contributor

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.

@aschleck
Copy link

aschleck commented Jul 12, 2024

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>

@pspeter3
Copy link
Author

That's awesome!

ndmitchell added a commit that referenced this issue Aug 25, 2024
Based on the code in #109 (comment), with a few minor changes to make it work with stable and work for types that aren't string. Plus a slightly nicer HTML page where you can edit the input.
ndmitchell added a commit that referenced this issue Aug 25, 2024
Based on the code in #109 (comment), with a few minor changes to make it work with stable and work for types that aren't string. Plus a slightly nicer HTML page where you can edit the input.
facebook-github-bot pushed a commit that referenced this issue Aug 28, 2024
Summary:
Based on the code in #109 (comment), with a few minor changes to make it work with stable and work for types that aren't string. Plus a slightly nicer HTML page where you can edit the input.

![image](https://github.com/user-attachments/assets/cc5a1f42-8e25-4284-9d9c-9ccffdeab318)

Credit for most of the code goes to aschleck.

Pull Request resolved: #129

Reviewed By: stepancheg

Differential Revision: D61774902

Pulled By: ndmitchell

fbshipit-source-id: 4babe21de199f194be3405cd7e22b543a3e9169d
facebook-github-bot pushed a commit to facebook/buck2 that referenced this issue Aug 28, 2024
Summary:
Based on the code in facebook/starlark-rust#109 (comment), with a few minor changes to make it work with stable and work for types that aren't string. Plus a slightly nicer HTML page where you can edit the input.

![image](https://github.com/user-attachments/assets/cc5a1f42-8e25-4284-9d9c-9ccffdeab318)

Credit for most of the code goes to aschleck.

X-link: facebook/starlark-rust#129

Reviewed By: stepancheg

Differential Revision: D61774902

Pulled By: ndmitchell

fbshipit-source-id: 4babe21de199f194be3405cd7e22b543a3e9169d
@ndmitchell
Copy link
Contributor

PR #129 gives us an example. Is that enough @pspeter3 - or anything else you were after?

@pspeter3
Copy link
Author

That's super cool, thank you!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants