τau /

ffi

module
ffi = import("ffi")

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 word

The 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.