Embedding hare-wren

Embedding hare-wren into your Hare programs is relatively straightforward and not much different from embedding Wren into a C program. Pair a reading of the upstream Wren documentation with a reading of the Hare API reference and the integration should be straightforward.

hare-wren also provides an optional, simple Wren-facing API and an event-driven runtime and fiber scheduler, which you can use to quickly get up and running with a more sophisticated embedded Wren runtime for your application. The Hare interface for the runtime is available at wren::api.

Getting started with wren::api

wren::api is based on the hare-ev event loop. Your application should provide an ev::loop instance to support the runtime, which you are free to use for other I/O and event management in your broader application.

In addition to preparing an event loop, you need a suitable configuration via wren::api::config, which is a superset of the Wren virtual machine configuration that adds fields for customizing the behavior of the runtime. Sensible defaults are provided via wren::api::default_config. In particular, the runtime provides a module loader to bind the Wren API to the virtual machine (and to load local modules).

Warning

The Wren modules available via wren::api offer direct, unmanaged access to the host filesystem and similar features. If you want to disable the built-in modules, or offer a subset of supported modules, you have to customize the module loader – see the next section.

A simple example of embedding the hare-wren runtime in your application might look like this:

use ev;
use os;
use wren;
use wren::api;

export fn main() void = {
     const loop = ev::newloop()!;
     const rt = api::new(&loop, [])!;
     defer api::destroy(rt);

     // Interpret the startup code. In this case it's a simple "Hello
     // world", but a more sophisticated use-case could set up timers,
     // schedule async I/O, etc.
     match (api::interpret(rt, "main", `System.print("Hello world!")`)) {
     case void => void;
     case let err: wren::error =>
             fmt::errorln(wren::strerror(err))!;
     };

     // Run the Wren virtual machine until all scheduled fibers are
     // completed.
     match (api::run(rt)) {
     case void => void;
     case let err: wren::error =>
             fmt::errorln(wren::strerror(err))!;
             os::exit(os::status::FAILURE);
     case let err: errors::error =>
             fmt::errorln(errors::strerror(err))!;
             os::exit(os::status::FAILURE);
     };
};
$ hare run -lc main.ha
Hello world!

Customizing the runtime

If you want to provide your own foreign methods and classes, or sandbox the runtime and/or provide a subset of the runtime API to Wren scripts, you will need to customize the module loader.

In order to do so, one should customize the appropriate fields of the config struct used to initialize the VM, in particular:

  • resolve_module

  • load_module

  • bind_foreign_method

  • bind_foreign_class

You can customize the behavior of each by either replacing them entirely, or providing a replacement which ultimately calls the runtime’s default implementation of the corresponding functionality as a fallback.

Customizing the module loader

One can customize the module loader by overriding the resolve_module and load_module functions and providing your own functionality. You can use this, for exmaple, to suppliment the available modules with additional modules providing new foreign classes and methods.

The default behavior is implemented by wren::api::load_module. Feel free to call this function if you want to load the default modules after your application-specific processing is complete.

Customizing the runtime API

The default implementations for bind_foreign_method and bind_foreign_class will bind foreign classes and methods for the entire runtime API as documented in the Wren API reference. It is often desirable to customize this behavior, either adding additional foreign classes and/or methods, or disabling the defaults to sandbox Wren scripts.

To add additional foreign methods and classes, provide your own implementations of bind_foreign_class_fn and/or bind_foreign_method_fn to bind your foreign members to the VM. If your implemenation is called with a class or method name that you do not recognize, you can call the runtime implementations to provide the standard API:

Do not call these functions if you want to sandbox the VM. If you want to provide limited support for the standard API, you can also bind on a module-by-module basis through functions, for instance using io_bind_class and io_bind_method to bind only the io module.

Extending the scheduler

The runtime’s Scheduler is used to suspend fibers while awaiting asynchronous operations, such as I/O. It is possible for users of the Hare API to implement new foreign classes and methods (see the previous section on adding these to the runtime) which provide their own asynchronous operations.

To implement a new asynchronous operation, one should utilize both the Wren and Hare APIs together. To illustrate, let’s create an example similar to the time.Timer.sleep method. On the Wren side, provide a convenience function and a foreign stub:

import "scheduler" for Scheduler

class Timer {
     static sleep(ms) {
         Scheduler.await { Time.sleep_(Fiber.current, ms) }
     }

     foreign static sleep_(fiber, ms)
}

The Scheduler will suspend the fiber after this function runs, and the foreign method should retain a reference to that fiber to resume with wren::api::scheduler_resume (or the related wren::api::scheduler_return (or and wren::api::scheduler_error (or functions). Implementing the foreign method and the request lifecycle looks something like this on the Hare side:

use ev;
use time;
use wren;
use wren::api;

// State for the asynchronous operation
type time_op = struct {
     vm: *wren::vm,
     timer: ev::timer,
     fiber: wren::handle,
};

// Bind me to time.Timer.sleep_(_,_) with a foreign method loader
fn time_sleep(vm: *wren::vm) void = {
     // Fetch the arguments
     const fiber = wren::get_handle(vm, 1);
     const ms = wren::get_f64(vm, 2): uint;

     // Execute the user's request and handle errors
     match (time_dosleep(vm, fiber, ms)) {
     case nomem =>
             wren::abort_fiber(vm, "Out of memory");
     case let err: errors::error =>
             wren::abort_fiber(vm, errors::strerror(err));
     case void => void;
     };
};

fn time_dosleep(
     vm: *wren::vm,
     fiber: wren::handle,
     ms: uint,
) (void | nomem | errors::error) = {
     let ok = false;
     const loop = api::getloop(vm);

     // Create a timer with the event loop for the operation
     const timer = ev::timer_init(loop,
             time::clock::MONOTONIC,
             &time_on_expired, null)?;
     defer if (!ok) ev::timer_finish(&timer);

     // Allocate state for the operation, storing the timer and the fiber
     // that needs to be resumed when the operation is complete
     const op = alloc(time_op {
             vm = vm,
             timer = timer,
             fiber = fiber,
     })?;
     ok = true;

     const delay = ms: time::duration * time::MILLISECOND;
     ev::req_setuser(&op.timer, op);
     ev::timer_configure(&op.timer, delay, 0);
};

fn time_on_expired(req: *ev::timer, user: nullable *opaque) void = {
     const op = user: *time_op;
     const fiber = op.fiber;
     const vm = op.vm;
     // Free state associated with the operation
     ev::timer_finish(&op.timer);
     free(op);

     // Resume the pending fiber. Note that this won't return until the fiber
     // finishes running.
     scheduler_resume(vm, op.fiber);
};