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 = 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