Here is an attempt at making some Rust Wren bindings in a more Rust style. It acts at times pretty transparently (you do have to deal with the slot / foreign API), but should be
- More typesafe than using the C API directly
- Shouldn't get in the way of quick execution
- Should be relatively simple to get started
Just add:
ruwren = "0.4"
to your Cargo.toml.
To create a VM, use the VMConfig struct. To create a basic VM that logs it's output to the console, simply call
let vm = VMConfig::new().build();
You can run code by using interpret directly:
vm.interpret("main", r##"System.print("Cool beans!")"##).unwrap();
Which returns a Ok(())
on successful execution, and Err(e)
on failure (see VMError for more details).
You can also call code defined in Wren using a FunctionHandle like so:
vm.interpret("main", r##"
class GameEngine {
static update(delta) {
System.print(delta)
}
}
"##).unwrap();
let handle = vm.make_call_handle(FunctionSignature::new_function("update", 1));
vm.execute(|vm| {
vm.ensure_slots(2);
vm.get_variable("main", "GameEngine", 0);
vm.set_slot_double(1, 0.016);
});
vm.call_handle(&handle);
or more directly:
vm.interpret("main", r##"
class GameEngine {
static update(delta) {
System.print(delta)
}
}
"##).unwrap();
vm.execute(|vm| {
vm.ensure_slots(2);
vm.get_variable("main", "GameEngine", 0);
vm.set_slot_double(1, 0.016);
});
vm.call(FunctionSignature::new_function("update", 1));
Here's a short example of how you can embed your Rust data into Wren:
use ruwren::{Class, VM, VMConfig, ModuleLibrary, get_slot_checked, create_module};
struct Foo {
bar: f64,
}
impl Class for Foo {
fn initialize(vm: &VM) -> Self {
let bar = get_slot_checked!(vm => num 1);
Foo { bar }
}
}
impl Foo {
fn instance(&self, vm: &VM) {
vm.set_slot_double(0, self.bar);
}
fn static_fn(vm: &VM) {
let num = get_slot_checked!(vm => num 1);
vm.set_slot_double(0, num + 5.0)
}
}
create_module! {
class("Foo") crate::Foo => foo {
instance(fn "instance", 0) instance,
static(fn "static_fn", 1) static_fn
}
module => foobar
}
fn main() {
let mut lib = ModuleLibrary::new();
foobar::publish_module(&mut lib);
let vm = VMConfig::new().library(&lib).build();
vm.interpret("foobar", r##"
foreign class Foo {
construct new(bar) {}
foreign instance()
foreign static static_fn(num)
}
"##).unwrap();
// You could now write Wren code like:
vm.interpret("main", r##"
import "foobar" for Foo
var f = Foo.new(4)
System.print(Foo.static_fn(f.instance()))
"##).unwrap();
// This should print "9".
}
V2 foreigns emulate Wren's class system on top of the original foreign API, so the above example would be:
use ruwren::{wren_impl, wren_module, ModuleLibrary, VMConfig, WrenObject};
#[derive(WrenObject, Default)]
struct Foo {
bar: f64,
}
#[wren_impl]
impl Foo {
/*
you can also write out an allocator, if you
don't want the base struct itself to implement Default
#[wren_impl(allocator)]
fn allocator() -> FooClass {
FooClass {}
}
*/
#[wren_impl(constructor)]
fn constructor(&self, bar: f64) -> FooInstance {
FooInstance { bar }
}
#[wren_impl(instance)]
fn instance(&self) -> f64 {
self.bar
}
fn static_fn(&self, num: f64) -> f64 {
num + 5.0
}
}
wren_module! {
mod foobar {
pub crate::Foo;
}
}
fn main() {
let mut lib = ModuleLibrary::new();
foobar::publish_module(&mut lib);
let vm = VMConfig::new().library(&lib).build();
vm.interpret("foobar", r##"
foreign class Foo {
construct new(bar) {}
foreign instance()
foreign static static_fn(num)
}
"##).unwrap();
// You could now write Wren code like:
vm.interpret("main", r##"
import "foobar" for Foo
var f = Foo.new(4)
System.print(Foo.static_fn(f.instance()))
"##).unwrap();
// This should print "9".
}
It technically works as long as you target WASI, and have a WASI SDK setup somewhere. look at the justfile or example-wasi.nu for the environment variables to set to get it running. There is one big caveat tho:
WASM (even with WASI) is a panic=abort platform, so catch_unwind does nothing, and panics are unhandleable.
This means that some idioms of the v1 foreign API (namely get_slot_checked!
) are not very good on the platform. Basically, anything that panics doesn't work well.
With a minimal change the the v2 foreign API (namely, having the constructor be fallible) means that v2 should work relatively unchanged on web, and v1 is usable, it just shouldn't trigger a panic, or the wasm runtime will flip the table.