ffi - calling C with the types written down.
A shared object is opened with dlopen, and the dot on it gives a symbol. That symbol can be called straight away, which is quick and takes your word for the types; or it can be given a signature, which is what this module is for:
ffi = import("ffi")
libm = dlopen("libm.so.6")pow = ffi.Func(libm.pow, "double pow(double, double)")
println(pow(2.0, 10.0)) # 1024, a float and not a machine wordThe signature is a C declaration. The name of the function and the names of the arguments may be there or not, so a line copied out of a header works as it stands, and const, volatile and restrict are read and ignored:
snprintf = ffi.Func(libc.snprintf,
"int snprintf(char *s, size_t n, const char *fmt, double x)")The widths C leaves to the machine - char, short, int, long, size_t - are the widths of this machine. The exact width names of stdint.h work too, and so do the tau spellings of the same: uint64 and uint64_t, float64 and double.
ponytail: a signature is parsed with a scan and a table, not a grammar. It reads declarations, not C: a function pointer parameter is not understood, and neither is a struct passed by value.
¶Void = 0valuesource
The type codes, which are the other half of an agreement with the enum at the top of internal/obj/ffi.c: the numbers travel to cfunc and must line up.
¶Bool = 1valuesource
¶Int8 = 2valuesource
¶UInt8 = 3valuesource
¶Int16 = 4valuesource
¶UInt16 = 5valuesource
¶Int32 = 6valuesource
¶UInt32 = 7valuesource
¶Int64 = 8valuesource
¶UInt64 = 9valuesource
¶Float32 = 10valuesource
¶Float64 = 11valuesource
¶Pointer = 12valuesource
¶CString = 13valuesource
¶Sig = fn(text)source
Sig reads a signature and returns [result code, [argument codes], name], where the name is the one the declaration gave the function, or "".
It is the parse on its own, for building a call without going through text: what it returns is what Func hands to cfunc.
¶Func = fn(sym, signature)source
Func returns a callable for the symbol sym, with the types the signature says. sym comes from the dot on a shared object:
libm = dlopen("libm.so.6")
pow = ffi.Func(libm.pow, "double pow(double, double)")
¶Alloc = fn(n)source
Alloc returns n bytes of memory C owns. Unlike bytes(n), the collector knows nothing about it and will not free it: that is Free, and forgetting to call it leaks.
¶Free = fn(p)source
Free gives back what Alloc returned.
¶Write = fn(p, data)source
Write copies data, a string or bytes, into memory at p, which is what fills a buffer a C function handed back.
¶Read = fn(p, n)source
Read returns n bytes copied out of the pointer p, which is bytes(p, n) under another name, here so that the module reads as one thing.
¶String = fn(p)source
String returns the C string at p, read up to its NUL and copied into tau.
¶Export = fn(signature, f)source
Export turns a tau function into one C can call, which is what a library wants when it takes a handler, a comparator or a visitor:
cmp = ffi.Export("int compare(const void *a, const void *b)", fn(a, b) {
...
})
libc.qsort(buf, 5, 1, cmp)The signature is the one C will call it by, and the declaration comes first here because the function is usually written on the spot.
What comes back lives for as long as the program does: whoever was given it keeps it, and there is no moment at which tau can know they are finished with it. A function handed to C and then freed is a crash waiting for the next event.
ponytail: the call is answered by the VM of the thread that entered C, so a library that calls back from a thread of its own gets zero and nothing runs. Threads of that kind need a VM of their own to be created and registered, which is a change to the collector, not to this.
¶Lib = fn(name)source
Lib opens a shared library by the name it has on this system, so that a program that wants the maths library says "m" rather than the file name of one machine:
libm = ffi.Lib("m") # libm.so.6 here, libm.dylib elsewhere
libc = ffi.Lib("c")A name with a separator or an extension in it is a path and is opened as it stands. Everything else is tried in the shapes this system uses, and if none of them opens, the error says what was tried.
ponytail: the candidates are a list, not a query to the loader. glibc keeps the real library behind a version suffix and the unversioned name is a linker script that dlopen refuses, which is why libm.so.6 is in the list; a machine whose suffix is not there needs the path spelled out. Reading the ldconfig cache is the fix if that ever bites.
¶Bind = fn(lib, signatures)source
Bind returns an object with one function per signature, named the way the signature names it, which is the short way to take a library whose names are the ones you want to call:
m = ffi.Bind("libm.so.6", [
"double pow(double, double)",
"double sqrt(double)",
])
m.pow(2.0, 10.0)The first argument is a library: a handle from dlopen, or the name of one, which is opened here. Nothing else is done to it - a module that wants its own names, its own error handling or a wrapper around a call writes them out one by one with Func, which is what stdlib/math.tau does.