Skip to content

Commit

Permalink
Add a WASM/JS example
Browse files Browse the repository at this point in the history
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
  • Loading branch information
ndmitchell authored and facebook-github-bot committed Aug 28, 2024
1 parent 3ffdcc5 commit 1f720eb
Show file tree
Hide file tree
Showing 7 changed files with 188 additions and 0 deletions.
1 change: 1 addition & 0 deletions starlark-rust/starlark/src/environment/modules.rs
Original file line number Diff line number Diff line change
Expand Up @@ -517,6 +517,7 @@ impl Module {
self.docstring.replace(Some(docstring));
}

#[cfg(not(target_arch = "wasm32"))]
pub(crate) fn add_eval_duration(&self, duration: Duration) {
self.eval_duration.set(self.eval_duration.get() + duration);
}
Expand Down
3 changes: 3 additions & 0 deletions starlark-rust/starlark/src/eval.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ pub(crate) mod soft_error;

use std::collections::HashMap;
use std::mem;
#[cfg(not(target_arch = "wasm32"))]
use std::time::Instant;

use dupe::Dupe;
Expand Down Expand Up @@ -62,6 +63,7 @@ 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>> {
#[cfg(not(target_arch = "wasm32"))]
let start = Instant::now();

let (codemap, statement, dialect, typecheck) = ast.into_parts();
Expand Down Expand Up @@ -135,6 +137,7 @@ impl<'v, 'a, 'e> Evaluator<'v, 'a, 'e> {

self.module_def_info = old_def_info;

#[cfg(not(target_arch = "wasm32"))]
self.module_env.add_eval_duration(start.elapsed());

// Return the result of evaluation
Expand Down
16 changes: 16 additions & 0 deletions starlark-rust/starlark_js_example/BUCK
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
load("@fbcode_macros//build_defs:rust_library.bzl", "rust_library")
load("@fbsource//tools/build_defs:glob_defs.bzl", "glob")

oncall("build_infra")

# TODO: this code is meant to be compiled to wasm,
# but here we only check it compiles for linux/mac/etc.
rust_library(
name = "starlark_js_example",
srcs = glob(
["src/**/*.rs"],
),
deps = [
"//buck2/starlark-rust/starlark:starlark",
],
)
12 changes: 12 additions & 0 deletions starlark-rust/starlark_js_example/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[package]
description = "Example of running starlark-rust interpreter in browser"
edition = "2021"
name = "starlark_js_example"
publish = false
version = "0.0.0"

[dependencies]
starlark = { path = "../starlark", version = "0.12.0" }

[lib]
crate-type = ["cdylib", "rlib"]
13 changes: 13 additions & 0 deletions starlark-rust/starlark_js_example/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Starlark JS

This directory contains an example project making use of Starlark
WebAssembly/WASM. To try it:

```
rustup target add wasm32-unknown-unknown
cargo build --target wasm32-unknown-unknown --release
cp ../target/wasm32-unknown-unknown/release/starlark_js.wasm .
python -m http.server
```

Then visit [http://localhost:8000](http://localhost:8000).
79 changes: 79 additions & 0 deletions starlark-rust/starlark_js_example/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
<!doctype html>
<html lang="en-US">

<head>
<meta charset="utf-8" />
<title>Starlark evaluator</title>
<style type="text/css">
body {
font-family: sans-serif;
}

textarea {
width: calc(100% - 20px);
height: 20em;
box-sizing: border-box;
margin: 5px;
border-radius: 3px;
padding: 5px;
border-color: lightgray;
}
</style>
</head>

<body>
<script>
function run() {
WebAssembly.instantiateStreaming(fetch("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.allocation(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 = document.getElementById("input").value;
const offset = instance.exports.evaluate(writeString(content));
const ok = readU8(offset) != 0;
const result = readString(offset + 4);
const output = document.getElementById("output");
output.value = (ok ? "" : "ERROR\n") + result;
});
}

window.addEventListener("load", function () {
document.getElementById("input").addEventListener("input", run, false);
run()
})
</script>
<h1>Starlark evaluator</h1>
<p>
Using <a href="https://github.com/facebook/starlark-rust">starlark-rust</a> compiled to web assembly.
Change the input to see it update.
</p>
<textarea id="input">
def hello(name):
return "hello " + name

hello("friend")</textarea>
<textarea id="output" readonly="readonly" style="background-color: lightgray;">
</textarea>
</body>

</html>
64 changes: 64 additions & 0 deletions starlark-rust/starlark_js_example/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*
* Copyright 2019 The Starlark in Rust Authors.
* Copyright (c) Facebook, Inc. and its affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

use std::mem;
use std::slice;
use std::str;

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

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

#[no_mangle]
pub unsafe extern "C" fn evaluate(s: *const u8) -> *mut u8 {
let length = u32::from_le_bytes(*(s as *const [u8; 4])) as usize;
let input = slice::from_raw_parts(s.offset(4), length);
let output = evaluate_buffers(input);
mem::ManuallyDrop::new(output).as_mut_ptr()
}

fn evaluate_buffers(input: &[u8]) -> Vec<u8> {
let contents = str::from_utf8(input).unwrap();
let result = evaluate_starlark(contents);
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());
buffer
}

fn evaluate_starlark(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)?;
Ok(res.to_string())
}

0 comments on commit 1f720eb

Please sign in to comment.