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
:ref:`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 :ref:`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 :ref:`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:
.. code-block:: hare
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);
};
};
.. code-block::
$ 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 :ref:`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:
* `bind_foreign_class `_
* `bind_foreign_method `_
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 :ref:`io module `.
Extending the scheduler
-----------------------
The runtime's :ref:`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
:ref:`time.Timer.sleep ` method. On the Wren side,
provide a convenience function and a foreign stub:
.. code-block:: wren
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:
.. code-block:: hare
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);
};