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-prim
s 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(int op, word a, word b, word c)
prim_custom{
switch (op) {
case 100:
("Hello from C!\n");
printfreturn ITRUE;
}
return IFALSE;
}
main.scm:
(λ (_)100 #f #f #f)
(sys-prim 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)100 #f #f #f))
(sys-prim
(λ (_)
(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(int op, word a, word b, word c)
prim_custom{
switch (op) {
case 100: { /* a + (bx * by) - c where b is (x . y) and c is stored as a string */
int va = cnum(a),
= cnum(car(b)),
bx = cnum(cdr(b)),
by = atoi(cstr(c));
vc
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)
+= cnum(car(a)), a = cdr(a), l--;
sum
return onum(sum, 1);
}
}
return IFALSE;
}
main.scm:
define (do-math a b c)
(100 a b (c-string c)))
(sys-prim
define (c-sum l)
(101 l #f #f))
(sys-prim
_)
(λ ("1 = " (do-math 4 (cons 3 2) "9"))
(print let ((l (iota 1 1 5)))
(" = " (c-sum l)))
(print (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 = 10
to 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