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}