Skip to content

Latest commit

 

History

History

ffi-examples

README.md

alcove FFI

alcove can call C functions from any shared library via libffi. The core builtin is (ffi-fn LIB FN-NAME RETURN-TYPE ARG-TYPES…), which returns a callable; you store it like any value and call it like any alcove function. (ffi-callback RET (ARG-TYPES…) FN) goes the other way — it hands an alcove function to C as a function pointer (see 05-callbacks.alc).

Security: FFI runs C code with the full privileges of the alcove process. A user script can (ffi-fn "libc.so.6" "system" "int" "string") and execute arbitrary shell commands. Only run scripts you trust — there is no sandbox.

Quick example

(= sqrt (ffi-fn "libm.so.6" "sqrt" "double" "double"))
(prn (sqrt 16.0))   ; → 4.000000

(= pow  (ffi-fn "libm.so.6" "pow"  "double" "double" "double"))
(prn (pow 2.0 10.0))   ; → 1024.000000

(= strlen (ffi-fn "libc.so.6" "strlen" "long" "string"))
(prn (strlen "alcove"))   ; → 6

Type names

The C ABI types alcove understands. Pass each as a string literal.

name C type alcove type
"void" void nil (return only)
"int" int32_t fixnum
"long", "int64" int64_t fixnum
"double" double float
"string", "char*" const char* string
"ptr", "void*" void* fixnum (the address)

(ffi-fn "lib" "name" "RETURN-TYPE" "ARG-TYPE-1" "ARG-TYPE-2" …) — omit arg types for zero-arg functions.

Library names

Pass the runtime .so (not the dev .so symlink). Common ones on Linux:

library typical SONAME
C standard libc.so.6
math libm.so.6
pthreads libpthread.so.0
dynamic loader libdl.so.2
readline libreadline.so.8

For your own libraries, use a path with a slash (./mylib.so) or install on LD_LIBRARY_PATH / standard search paths.

An empty library name ("") resolves symbols already linked into the running alcove process — libc, libm, and alcove's own exported functions — without naming a platform-specific shared object:

(= slen (ffi-fn "" "strlen" "long" "string"))   ; libc, portable
(prn (slen "hello"))                             ; → 5

This is how the test suite exercises FFI with no external .so. Use (ffi?) to check at runtime whether the build has FFI support.

Examples in this directory

  • 01-libm-math.alc — sqrt, sin, cos, pow, log, exp.
  • 02-libc-strings.alc — strlen, strcmp, toupper, tolower, getpid.
  • 03-time-syscalls.alc — time(2), usleep(3).
  • 04-custom-mylib.c / .alc — build your own .so and call into it (add, fib, strlen, stateful counter, returned C string).
  • 05-callbacks.alc — pass an alcove function to C as a function pointer via (ffi-callback …) (C calling back into alcove, incl. in a loop, with double and closures).
  • 06-structs.alc — by-value structs via (ffi-struct …) / (ffi-pack …) / (ffi-unpack …): struct args, struct returns, and both.
  • 07-varargs.alc — variadic C functions (printf-style) via (ffi-vfn …): a fixed-arg prefix plus a variadic tail whose types are inferred from the values.

Run them all (auto-builds libmylib.so):

make run

Custom libraries

Build with -shared -fPIC:

cc -shared -fPIC -O2 -o libmylib.so mylib.c

Then from alcove:

(= my-add (ffi-fn "./libmylib.so" "mylib_add" "long" "long" "long"))
(prn (my-add 3 4))   ; → 7

State across calls works fine — the library's static variables are preserved between alcove invocations because alcove keeps the dlopen handle for the lifetime of the process.

Limitations and gotchas

  • Max 8 args per FFI fn (compile-time constant, easy to bump).
  • Callbacks are supported via (ffi-callback RET (ARG-TYPES…) FN) — see 05-callbacks.alc. Pass the result where a ptr arg is expected. Callback ret/arg types: void int long double ptr (no string return).
  • Struct-by-value is supported via (ffi-struct …) + (ffi-pack …) / (ffi-unpack …) — see 06-structs.alc. Fields: int long double ptr, or a nested struct descriptor (struct-in-struct; unpack yields the inner struct as a blob). Works as ffi-fn arg and return types. (No string fields yet — use ptr.)
  • Variadic functions are supported via (ffi-vfn …) — see 07-varargs.alc. The fixed args are declared; the variadic tail is inferred per call (fixnum→long, float→double, char→int, string/nil→pointer). Works on Linux and macOS incl. Apple silicon (libffi 3.3+).
  • Returned char* is copied into a fresh alcove string. If your function returns a pointer the caller is supposed to free(), alcove leaks it (no free builtin yet — wrap it via FFI: (= cfree (ffi-fn "libc.so.6" "free" "void" "ptr"))).
  • Reserved names can't be shadowed at top level. alcove has a builtin time, so (= time (ffi-fn "libc.so.6" "time" …)) does bind time in the global env but lookup checks the reserved symbol table first and never finds your binding. Use a different name (e.g. ctime).
  • NULL pointer arg. Pass 0 (a fixnum) wherever the C function expects NULL for a pointer arg.
  • No bignum — return values bigger than 2^61 silently overflow alcove's tagged fixnum encoding.

Implementation

The FFI is libffi-backed (-lffi). Each (ffi-fn …) call:

  1. dlopens the library (cached per-name in a process-wide table).
  2. dlsyms the function.
  3. Builds an ffi_cif from the type signature.
  4. Wraps the fn pointer + cif in an EXP_FFI exp_t.

When invoked, alcove's evaluate detects EXP_FFI ahead of the islambda/isinternal paths, evaluates each arg, marshals to the right C type, calls ffi_call, and marshals the return back.

See alcove.c's alc_ffi_t struct, ffifncmd, and alc_ffi_call for the implementation (~150 lines).