Writing extensions for Owl lisp
Owl lisp can be extended with any langauge that can compile alongside C code - this includes languages like C (ofc), Rust etc.
How
Owl lisps' vm can require
word prim_custom(int op, word a, word b, word c) which we
need to define in a low level language, and compile with the vm to be
able to call it from compiled lisp code at runtime.
Owl does "system calls" via a (sys-prim op a b c)
function, which is almost exactly like an assembly
syscall.
Warning: the additional extension code shown in this
post can be run only at runtime. Running it at
"compile"-time is possible, but out of scope of this post. If you're
interested in doing that, You can look at how raylib-owl handles
that - TL;DR: you need to compile a custom interpreter with custom
sys-prims built-in.
A "Hello World" example
Our custom-code C file will need to define prim_custom,
and dispatch functions using op - a number that tells as
what should we do.
ext.c:
#include <stdio.h>
#include "ovm.h"
word
prim_custom(int op, word a, word b, word c)
{
switch (op) {
case 100:
printf("Hello from C!\n");
return ITRUE;
}
return IFALSE;
}main.scm:
(λ (_)
(sys-prim 100 #f #f #f)
0)Then compile it like this:
$ ol -o main.c main.scm
$ cc -I/path/to/owl-source/c/ -o main -DPRIM_CUSTOM ext.c main.c
$ ./main
Hello from C!The lisp code should probably also wrap the raw sys-prim call into a function to ease the usage of the foreign function.
(define (hello-from-c)
(sys-prim 100 #f #f #f))
(λ (_)
(hello-from-c)
0)Passing values
ovm.h defines few helpers for moving stuff between c and
owl lisp. The current (05 june 2024) list is as follows:
| function or value | description |
|---|---|
INULL |
empty list - () |
IFALSE |
#f |
ITRUE |
#t |
W |
word size |
car(l) |
1st element of a list |
cdr(l) |
rest of a list |
mkstring(s) |
cstring s → lisp string |
mkbvec(vp, n) |
uint8 list → lisp bytevector |
mkport(fd) |
c file descriptor → lisp port |
mkint(x) |
c unsigned integer → lisp number |
onum(n, s) |
c integer → lisp integer (if s != 0 then signed, else unsigned) |
mkrat(p, q) |
create a rational number p/q |
mkfloat(f) |
c float → lisp rational |
mkseq(v, n, T) |
create a sequential thing with data of length n stored
in v and set the type to T |
mkpair(h, a, d) |
create a pair of a and d with header
h - for basic usage use cons |
BOOL(cval) |
c boolean → lisp boolean |
PTR(t) |
c pointer → c pointer that can be stored in lisp |
cnum(a) |
lisp number → c number |
cstr(s) |
lisp string → cstring ((c-string s)
must be called on the lisp side first) |
cptr(v) |
c pointer stored in lisp → c pointer |
cons(a, b) |
(a . b) |
llen(l) |
length of list l |
so as an example
ext.c:
#include <stdio.h>
#include <stdlib.h>
#include "ovm.h"
word
prim_custom(int op, word a, word b, word c)
{
switch (op) {
case 100: { /* a + (bx * by) - c where b is (x . y) and c is stored as a string */
int va = cnum(a),
bx = cnum(car(b)),
by = cnum(cdr(b)),
vc = atoi(cstr(c));
return onum(va + (bx * by) - vc, 1);
}
case 101: { /* sum numbers in a where sum(a) < MAX_INT */
int l = llen(a), sum = 0;
while (l)
sum += cnum(car(a)), a = cdr(a), l--;
return onum(sum, 1);
}
}
return IFALSE;
}main.scm:
(define (do-math a b c)
(sys-prim 100 a b (c-string c)))
(define (c-sum l)
(sys-prim 101 l #f #f))
(λ (_)
(print "1 = " (do-math 4 (cons 3 2) "9"))
(let ((l (iota 1 1 5)))
(print (sum l) " = " (c-sum l)))
0)$ ol -o main.c main.scm
$ cc -I/path/to/owl-source/c/ -o main -DPRIM_CUSTOM ext.c main.c
$ ./main
1 = 1
10 = 10to handle floating point numbers in C I use this macro - i am 70% sure it is correct, but the remaining 30% is keeping me from writing a pull request with it.
#define cfloat(x) \
(is_type(x,TRAT)?((float)cnum(x)/(float)cnum(x+W)) \
: ((is_type(x,TNUM)||is_type(x,TNUMN))?(cnum((x))) \
: ((float)(cnum(car(x)))/(float)cnum(cdr(x)))))It works well enough in raylib-owl :P
More examples
- See owl-bot's tls.{c,scm} to see some in-the-wild usage,
- See raylib-owl's raylib.c to see an ugly side of this approach,
- See ovm.c and ovm.h for the source code