33 releases

0.4.10 Dec 15, 2023
0.4.4 Sep 3, 2023
0.4.2 Dec 31, 2021
0.4.1 Apr 10, 2021
0.3.1 Jun 7, 2020

#53 in Programming languages


Used in game_kernel

MIT/Apache

405KB
10K SLoC

C 7K SLoC // 0.2% comments Rust 2K SLoC // 0.0% comments Bitbake 615 SLoC Nushell 14 SLoC // 0.1% comments

Ruwren: Wren bindings for Rust Crates.io docs.rs

Build status: Travis CI

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

Including

Just add:

ruwren = "0.4"

to your Cargo.toml.

Main API

Creating a VM

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));

Embedding Rust code in Wren

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 Foreign

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".
}

About WASM Compilation

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.

Dependencies

~0.3–3MB
~64K SLoC