Writing external functions#
Warning
Crochet’s FFI currently assumes trusted modules, and it’ll most likely evolve into an Alien-based layer that allows a more nuanced view of security and permits safe sandboxing at a lower cost.
The core idea of Crochet’s foreign interfaces is to be able to define functions in a separate language, and then execute those functions from Crochet code. Currently the only supported language is JavaScript.
Native modules#
Foreign functions are written in “native modules”, and are loaded by specifying them in the package configuration. A JavaScript native module has the following form:
exports.default = (ffi) => {
// native function definitions go here
}
Within that exported function, the native module can either specify
simple synchronous functions—through the ffi.defun
method—,
or complex asynchronous functions—through the ffi.defmachine
method.
A synchronous function here is simply one that just takes some arguments,
does something, and then returns a result.
For example, a native counter could be written as follows:
exports.default = (ffi) => {
class Counter {
#value;
constructor(value) {
this.#value = value;
}
next() {
return new Counter(this.#value + 1n);
}
value() {
return this.#value;
}
}
ffi.defun("make-counter", (initial_value0) => {
const initial_value = ffi.integer_to_bigint(initial_value0);
const counter = new Counter(initial_value);
return ffi.box(counter);
})
ffi.defun("next", (counter0) => {
const counter = ffi.unbox(counter0);
if (!(counter instanceof Counter)) {
throw ffi.panic("invalid-type", "expected Counter");
}
return ffi.box(counter.next());
})
ffi.defun("value", (counter0) => {
const counter = ffi.unbox(counter0);
if (!(counter instanceof Counter)) {
throw ffi.panic("invalid-type", "expected Counter");
}
return ffi.integer(counter.value());
})
}
It could then be used in Crochet as follows:
type counter(box);
command #counter with: (Value is integer) =
foreign make-counter(Value);
command counter next =
foreign next(self);
command counter value =
foreign value(self);
The names defind by the native module are only valid within
the package loading it, so there’s no chance of unexpected
collisions. However, it’s still possible to use dots (.
)
in the name—e.g.: counter.next
would be a valid foreign
function name. These dots don’t have any special meaning, but
it can make names easier to follow in packages that use a
significant amount of foreign functions.
defun
and defmachine
#
Functions can be defined with both defun
and defmachine
.
One could see defun
as a simplified form of defmachine
,
but defun
has different performance characteristics as well.
When using defun
, the function being called takes over the
execution for as long as it takes to return a value to Crochet,
and it isn’t allowed to execute any Crochet code. This lets
us not bother with having stack frames for native code, which
is significantly cheaper, however it does mean that the Crochet
VM is paused for the duration of the function being executed;
it’s only really suitable for functions that finish quickly.
When using defmachine
, we provide a JavaScript generator
to Crochet. This generator allows us to coordinate with
Crochet code and do asynchronous things, but it also means
that we need to allocate and deal with mixed Crochet and
native frames in the execution stack—this complexity has
a price that can’t be entirely removed.
One use case for defmachine
is to interact with JavaScript
promises, which are asynchronous. For example, a native
function that fetches data from the network could look
like the following:
async function fetch_text(url) {
const response = await fetch(url);
const text = await response.text();
return ffi.text(text);
}
ffi.defmachine("fetch", function* (url) {
const text = yield ffi.await(fetch_text(url));
return text;
})
The ffi.await
function produces a signal that lets the Crochet VM
resume the current generator once the promise settles. And we send this
signal to the VM by yield
-ing it. In this sense, the VM runs the
generator step-by-step. At each step, the generator can yield a signal
that causes the VM to perform some work. And it finishes by returning
a value.
Now, the important thing to remember here is that all intermediate
values have to be Crochet values—not JavaScript ones. That’s why we
abstract fecth
into a fetch_text
function, which ensures that
the promise will be resolved with a Crochet data structure.