τau / ffi /

ffi.tau

source
/Users/niconex/Documents/tau/stdlib/ffi/ffi.tau
1# ffi - calling C with the types written down.2#3# A shared object is opened with dlopen, and the dot on it gives a symbol.4# That symbol can be called straight away, which is quick and takes your word5# for the types; or it can be given a signature, which is what this module is6# for:7#8#	ffi = import("ffi")9#	libm = dlopen("libm.so.6")10#11#	pow = ffi.Func(libm.pow, "double pow(double, double)")12#	println(pow(2.0, 10.0))     # 1024, a float and not a machine word13#14# The signature is a C declaration. The name of the function and the names of15# the arguments may be there or not, so a line copied out of a header works as16# it stands, and const, volatile and restrict are read and ignored:17#18#	snprintf = ffi.Func(libc.snprintf,19#		"int snprintf(char *s, size_t n, const char *fmt, double x)")20#21# The widths C leaves to the machine - char, short, int, long, size_t - are22# the widths of this machine. The exact width names of stdint.h work too, and23# so do the tau spellings of the same: uint64 and uint64_t, float64 and24# double.25#26# ponytail: a signature is parsed with a scan and a table, not a grammar. It27# reads declarations, not C: a function pointer parameter is not understood,28# and neither is a struct passed by value.2930runtime = import("runtime")31strings = import("strings")3233# The type codes, which are the other half of an agreement with the enum at34# the top of internal/obj/ffi.c: the numbers travel to cfunc and must line up.35Void = 036Bool = 137Int8 = 238UInt8 = 339Int16 = 440UInt16 = 541Int32 = 642UInt32 = 743Int64 = 844UInt64 = 945Float32 = 1046Float64 = 1147Pointer = 1248CString = 134950# The widths of this machine, which is what makes "long" mean what it means51# here rather than what it meant where the header was written.52ptrBits = if runtime.Arch() == "386" || runtime.Arch() == "arm" { 32 } else { 64 }5354# Windows keeps long at 32 bits even at 64 bits of address, everyone else55# grows it with the pointer.56longBits = if runtime.OS() == "windows" { 32 } else { ptrBits }5758signed = fn(bits) {59	if bits == 8 { return Int8 }60	if bits == 16 { return Int16 }61	if bits == 32 { return Int32 }62	return Int6463}6465unsigned = fn(bits) {66	if bits == 8 { return UInt8 }67	if bits == 16 { return UInt16 }68	if bits == 32 { return UInt32 }69	return UInt6470}7172# The names a signature may be written with. The ones C leaves to the machine73# are resolved above; the rest are the width they say.74names = {75	"void": Void,76	"bool": Bool,77	"_Bool": Bool,78	"char": signed(8),79	"signed char": signed(8),80	"unsigned char": unsigned(8),81	"short": signed(16),82	"short int": signed(16),83	"unsigned short": unsigned(16),84	"unsigned short int": unsigned(16),85	"int": signed(32),86	"signed": signed(32),87	"signed int": signed(32),88	"unsigned": unsigned(32),89	"unsigned int": unsigned(32),90	"long": signed(longBits),91	"long int": signed(longBits),92	"unsigned long": unsigned(longBits),93	"unsigned long int": unsigned(longBits),94	"long long": Int64,95	"long long int": Int64,96	"unsigned long long": UInt64,97	"unsigned long long int": UInt64,98	"size_t": unsigned(ptrBits),99	"ssize_t": signed(ptrBits),100	"intptr_t": signed(ptrBits),101	"uintptr_t": unsigned(ptrBits),102	"ptrdiff_t": signed(ptrBits),103	"int8_t": Int8,104	"int8": Int8,105	"uint8_t": UInt8,106	"uint8": UInt8,107	"int16_t": Int16,108	"int16": Int16,109	"uint16_t": UInt16,110	"uint16": UInt16,111	"int32_t": Int32,112	"int32": Int32,113	"uint32_t": UInt32,114	"uint32": UInt32,115	"int64_t": Int64,116	"int64": Int64,117	"uint64_t": UInt64,118	"uint64": UInt64,119	"float": Float32,120	"float32": Float32,121	"double": Float64,122	"float64": Float64,123	"string": CString,124	"pointer": Pointer125}126127# The words of a declaration that say nothing about the type.128qualifiers = {129	"const": true,130	"volatile": true,131	"restrict": true,132	"__restrict": true,133	"extern": true,134	"static": true,135	"inline": true136}137138isWordByte = fn(c) {139	return (c >= "a" && c <= "z") || (c >= "A" && c <= "Z") || (c >= "0" && c <= "9") || c == "_"140}141142# decl reads one declaration - a result type, or an argument, with or without143# the name that follows it - and returns [code, name], where the name is the144# word that was dropped, or "".145#146# A star anywhere makes it a pointer, and a char * is the one pointer that147# travels as a tau string.148decl = fn(text) {149	words = []150	stars = 0151152	i = 0153	for i < len(text) {154		c = text[i]155156		if c == " " || c == "\t" || c == "\n" || c == "\r" {157			++i158			continue159		}160		if c == "*" {161			++stars162			++i163			continue164		}165166		# An array parameter is a pointer, and what is inside the brackets167		# says nothing about the call.168		if c == "[" {169			++stars170			for i < len(text) && text[i] != "]" {171				++i172			}173			++i174			continue175		}176177		if !isWordByte(c) {178			return error("\"{c}\" has no place in a type")179		}180181		start = i182		for i < len(text) && isWordByte(text[i]) {183			++i184		}185186		word = slice(text, start, i)187		if qualifiers[word] == null {188			words = append(words, word)189		}190	}191192	if len(words) == 0 {193		return error("there is no type here")194	}195196	# The last word is the name of what is being declared unless it is part of197	# the type: "unsigned long" keeps both words, "unsigned long n" drops the198	# third.199	name = ""200	used = len(words)201	for {202		joined = strings.Join(slice(words, 0, used), " ")203		code = names[joined]204205		if code != null {206			if stars > 0 {207				# char * is a string, every other pointer is an address.208				if stars == 1 && (joined == "char" || joined == "signed char") {209					return [CString, name]210				}211				return [Pointer, name]212			}213			return [code, name]214		}215216		if used == 1 {217			return error("\"{joined}\" is not a type this understands")218		}219		name = words[used - 1]220		--used221	}222}223224# Sig reads a signature and returns [result code, [argument codes], name],225# where the name is the one the declaration gave the function, or "".226#227# It is the parse on its own, for building a call without going through text:228# what it returns is what Func hands to cfunc.229Sig = fn(text) {230	text = strings.TrimSpace(string(text))231232	open = strings.Index(text, "(")233	if open < 0 || len(text) == 0 || text[len(text) - 1] != ")" {234		return error("ffi: a signature is a C declaration, \"double(double, double)\", got \"{text}\"")235	}236	if strings.Contains(text, "...") {237		return error("ffi: \"{text}\" is variadic, say the types of the arguments this call passes instead")238	}239240	if failed(ret = decl(slice(text, 0, open))) {241		return error("ffi: in the result of \"{text}\": {ret}")242	}243244	args = []245	inside = strings.TrimSpace(slice(text, open + 1, len(text) - 1))246247	# f() and f(void) are both no arguments at all.248	if inside != "" {249		parts = strings.Split(inside, ",")250		for i = 0; i < len(parts); ++i {251			if failed(a = decl(parts[i])) {252				return error("ffi: in argument {i + 1} of \"{text}\": {a}")253			}254255			if a[0] == Void {256				if len(parts) == 1 {257					break258				}259				return error("ffi: argument {i + 1} of \"{text}\" cannot be void")260			}261			args = append(args, a[0])262		}263	}264265	return [ret[0], args, ret[1]]266}267268# Func returns a callable for the symbol sym, with the types the signature269# says. sym comes from the dot on a shared object:270#271#	libm = dlopen("libm.so.6")272#	pow = ffi.Func(libm.pow, "double pow(double, double)")273Func = fn(sym, signature) {274	if failed(s = Sig(signature)) {275		return s276	}277	return cfunc(sym, s[0], s[1])278}279280# The program itself, which is where the C library it was linked against281# lives: malloc, free and memcpy are ordinary C functions, so the memory a C282# call needs is not a thing the language has to grow.283self = dlopen(null)284285mallocFn = if failed(self) { self } else { Func(self.malloc, "void *malloc(size_t n)") }286freeFn = if failed(self) { self } else { Func(self.free, "void free(void *p)") }287memcpyFn = if failed(self) { self } else { Func(self.memcpy, "void *memcpy(void *dst, const void *src, size_t n)") }288strlenFn = if failed(self) { self } else { Func(self.strlen, "size_t strlen(const char *s)") }289290# Alloc returns n bytes of memory C owns. Unlike bytes(n), the collector knows291# nothing about it and will not free it: that is Free, and forgetting to call292# it leaks.293Alloc = fn(n) {294	if failed(mallocFn) {295		return error("ffi: no C library here: {mallocFn}")296	}297	if n <= 0 {298		return error("ffi: Alloc wants a size, got {n}")299	}300301	p = mallocFn(n)302	if p == null {303		return error("ffi: out of memory asking for {n} bytes")304	}305	return p306}307308# Free gives back what Alloc returned.309Free = fn(p) {310	if failed(freeFn) {311		return error("ffi: no C library here: {freeFn}")312	}313	freeFn(p)314	return null315}316317# Write copies data, a string or bytes, into memory at p, which is what fills318# a buffer a C function handed back.319Write = fn(p, data) {320	if failed(memcpyFn) {321		return error("ffi: no C library here: {memcpyFn}")322	}323	if p == null {324		return error("ffi: Write to a null pointer")325	}326327	if type(data) != "bytes" {328		if failed(data = bytes(string(data))) {329			return data330		}331	}332	memcpyFn(p, data, len(data))333	return len(data)334}335336# Read returns n bytes copied out of the pointer p, which is bytes(p, n) under337# another name, here so that the module reads as one thing.338Read = fn(p, n) { bytes(p, n) }339340# String returns the C string at p, read up to its NUL and copied into tau.341String = fn(p) {342	if failed(strlenFn) {343		return error("ffi: no C library here: {strlenFn}")344	}345	if p == null {346		return null347	}348	return string(bytes(p, strlenFn(p)))349}350351# Export turns a tau function into one C can call, which is what a library352# wants when it takes a handler, a comparator or a visitor:353#354#	cmp = ffi.Export("int compare(const void *a, const void *b)", fn(a, b) {355#		...356#	})357#	libc.qsort(buf, 5, 1, cmp)358#359# The signature is the one C will call it by, and the declaration comes first360# here because the function is usually written on the spot.361#362# What comes back lives for as long as the program does: whoever was given it363# keeps it, and there is no moment at which tau can know they are finished364# with it. A function handed to C and then freed is a crash waiting for the365# next event.366#367# ponytail: the call is answered by the VM of the thread that entered C, so a368# library that calls back from a thread of its own gets zero and nothing runs.369# Threads of that kind need a VM of their own to be created and registered,370# which is a change to the collector, not to this.371Export = fn(signature, f) {372	if failed(s = Sig(signature)) {373		return s374	}375	return cexport(f, s[0], s[1])376}377378# Lib opens a shared library by the name it has on this system, so that a379# program that wants the maths library says "m" rather than the file name of380# one machine:381#382#	libm = ffi.Lib("m")           # libm.so.6 here, libm.dylib elsewhere383#	libc = ffi.Lib("c")384#385# A name with a separator or an extension in it is a path and is opened as it386# stands. Everything else is tried in the shapes this system uses, and if none387# of them opens, the error says what was tried.388#389# ponytail: the candidates are a list, not a query to the loader. glibc keeps390# the real library behind a version suffix and the unversioned name is a391# linker script that dlopen refuses, which is why libm.so.6 is in the list; a392# machine whose suffix is not there needs the path spelled out. Reading the393# ldconfig cache is the fix if that ever bites.394Lib = fn(name) {395	name = string(name)396397	if isPath(name) {398		return dlopen(name)399	}400401	tried = candidates(name)402	for i = 0; i < len(tried); ++i {403		h = dlopen(tried[i])404405		if !failed(h) {406			return h407		}408	}409	names = strings.Join(tried, ", ")410	return error("ffi: no library named \"{name}\" here, tried {names}")411}412413# isPath reports whether a name is already the name of a file rather than the414# stem of one.415isPath = fn(name) {416	if strings.Contains(name, "/") {417		return true418	}419	if strings.Contains(name, ".so") {420		return true421	}422	if strings.Contains(name, ".dylib") {423		return true424	}425	return strings.Contains(name, ".dll")426}427428# candidates returns the file names a bare library name may have on this429# system, in the order to try them.430candidates = fn(name) {431	os = runtime.OS()432433	if os == "windows" {434		return [name + ".dll", "lib" + name + ".dll"]435	}436	if os == "darwin" {437		return ["lib" + name + ".dylib", name + ".dylib"]438	}439440	# The versioned names first: on glibc the unversioned one is a linker441	# script, which dlopen will not open.442	return [443		"lib" + name + ".so.6",444		"lib" + name + ".so.1",445		"lib" + name + ".so",446		name + ".so"447	]448}449450# Bind returns an object with one function per signature, named the way the451# signature names it, which is the short way to take a library whose names are452# the ones you want to call:453#454#	m = ffi.Bind("libm.so.6", [455#		"double pow(double, double)",456#		"double sqrt(double)",457#	])458#	m.pow(2.0, 10.0)459#460# The first argument is a library: a handle from dlopen, or the name of one,461# which is opened here. Nothing else is done to it - a module that wants its462# own names, its own error handling or a wrapper around a call writes them out463# one by one with Func, which is what stdlib/math.tau does.464Bind = fn(lib, signatures) {465	# A name rather than a handle: open it the way Lib does, so that the usual466	# case is one call and not two.467	if type(lib) == "string" {468		if failed(lib = Lib(lib)) {469			return lib470		}471	}472	if type(lib) != "native" {473		return error("ffi: Bind wants a library or its name, got {type(lib)}")474	}475476	out = new()477478	for i = 0; i < len(signatures); ++i {479		if failed(s = Sig(signatures[i])) {480			return s481		}482		if s[2] == "" {483			return error("ffi: \"{signatures[i]}\" has no name to bind it to")484		}485486		if failed(f = cfunc(lib[s[2]], s[0], s[1])) {487			return error("ffi: {s[2]}: {f}")488		}489		out[s[2]] = f490	}491492	return out493}