FFI

From SlateWiki

Table of contents

Overview

Slate's Foreign-Function Interface (FFI) provides several convenience types:

  • ExternalLibrary which allows you to open a dynamic library and call its functions.
  • ExternalMethod wraps a single C function and performs basic type translations while acting like a normal Slate block or method.
  • ExternalInterface provides a facade for both of these and holds persistent meta-information about the API so that the interface may be refreshed easily.
  • CObject takes a C type definition and provides a Slate object that can be used to directly read and manipulate the C objects of that type, using Slate idioms.

Slate's foreign-function interface provides an object called ExternalLibrary which allows you to open a dynamic library and call its functions.

Getting what's needed

First of all, you need to load some files (with the AutoLoader, just type ExternalMethod, and this will happen transparently):

load: 'src/mobius/c/types.slate'.
load: 'src/lib/extlib.slate'.

The main facade

ExternalInterface (post-0.3.4) manages all of the connections for you, and persistently holds the information needed to tear down and setup the interface repeatedly.

This facade manages ExternalLibrary objects, by also setting up and holding their ExternalMethods and specs to re-generate them. The leader is a prefix for all the function names, but not the generated method names.

The primitives specifications should be a collection of three-element sequences. The elements:

  • The first element specifies the desired name of the primitive as well as the function name in the library minus the leader.
  • The second element should be the return type of the primitive as a symbol naming one of the types under External ArgumentFormat.
  • The third element should be a collection specifying the argument types as symbols naming elements of ExternalMethod ArgumentFormat; this can be an empty array ({}) if the function takes no arguments.

Call ExternalInterface newForLibrary: libName primitives: specs &leader: prefix to create a new facade. The "leader" is a prefix for function names from the library which is used to make the C name but thrown out on the Slate primitive side. The enable and disable set up and tear down the actual interface. ExternalMethods are installed on a namespace attached to the facade by name primitives.

Opening a library

Then you create your library by calling d@(ExternalLibrary traits) newNamed: libName where libName is the name of the library you want to open. libName should be the name of the .so file without the extension and without the path ('/usr/X11R6/lib/libX11.so' becomes 'libX11'). For example:

addSlot: #libX11 valued: (ExternalLibrary newNamed: 'libX11').

From now on, #libX11 will be the place to access this library. It may be necesary to set the right LD_LIBRARY_PATH to achieve this, specially if you are loading libraries located in Slate's root (like the plug ins). For that you need to run (in bash):

export LD_LIBRARY_PATH=./:$LD_LIBRARY_PATH

Calling a function

To call a function you must first find it in the lib. There's a method to do it: d@(ExternalLibrary traits) functionNamed: symname where symname is the name of the function without parameters and without return value. For example, for 'unsigned int sleep(unsigned int seconds)' it would be 'sleep'. This method returns a ExternalMethod which is the object that you are going to use to call the function, so, you have to save in a way similar to this:

addSlot: #XOpenDisplay valued: (libX11 functionNamed: 'XOpenDisplay').

The types of the parameters that the function will recieve should be listed, as an array, in the slot #argumentsFormat. The possible parameter types are: Void, Int, Float, Pointer, Bytes. Void, Int, Float and Pointer are like the C types with the C name. A Pointer is represented as a 4 bytes byte array in Slate. Bytes is a special type to represent all the other types, like for example, if you need to pass a char with certain content inside. Let's see some examples (for some bogus functions):

bFun1 argumentsFormat: {}.
bFun2 argumentsFormat: {ExternalMethod ArgumentFormat Int}.
bFun3 argumentsFormat: {ExternalMethod ArgumentFormat Int. ExternalMethod ArgumentFormat Float. ExternalMethod ArgumentFormat Float}.
bFun4 argumentsFormat: {ExternalMethod Arguments Pointer}.
bFun5 argumentsFormat: {ExternalMethod Arguments Bytes}.

In that functions, bFun1 receives no argument (bFun1(void)), bFun2 an Int (bFun2(Int)), bFun3 an Int and two Floats (bFun3(int, float, float), bFun4 a pointer of any kind (bFun4(char☆) or bFun4(void☆)) and bFun5 also a pointer, except that with a ByteArray, we can control what is the piece of memory where that pointer points to (in the case of bFun4, we can only manipulate where the pointer points to). To explain it further, we'll use something like bFun4 when we aren't really going to access what the pointer is pointing to, a typical case is another function returning us a pointer that we need to pass; the case of bFun5 would allow us to pass, for example, a char pointing to "Slate rulez" and we won't care about its address in memory. Before we can really call the function, we also have to define the return value, which is not an array because it is only one and it is called resultFormat. For funtions that return void, int, pointer or a float we would do the following:

bFun1 resultFormat: ExternalMethod ArgumentFormat Void.
bFun2 resultFormat: ExternalMethod ArgumentFormat Int.
bFun2 resultFormat: ExternalMethod ArgumentFormat Float.
bFun4 resultFormat: ExternalMethod Arguments Pointer.
bFun5 resultFormat: ExternalMethod Arguments Float.

After those definitions, it is time to call the function. For bFun1 that takes no arguments and returns no values we could simple do:

bFun1 do.

When there's one argument, like the case of bFun2, you can call it this way:

bFun2 applyWith: 15.

but since it returns something, you probably want to save it, so, you may call it this way:

addSlot: #bFun2Return valued: (bFun2 applyWith: 15).

and you'll have the return value in #bFun2Return. When the function takes more than one argument, we have to use the following method:

bFun3 applyTo: {1024. 15.7. 98.3}.

where we pass each argument as an item in an array. Knowing this, one could have as well written:

bFun2 applyTo: {15}.
bFun1 applyTo: {}.

and they are perfectly valid. Pointers, in Slate, are stored as 4-items ByteArrays, so, a call to bFun4 would look like this:

bFun4 applyWith: (ByteArray newSize: 4)

and bFun4 will be getting a NULL pointer, since all bytes in the ByteArray would be 0. One could create a ByteArray and store some values to make it point to somewhere, but I really doubt the use of this since you have to ensure that the memory in that section is holding what you want. Passing pointers around is especially useful when a function returns a pointer and you have to pass it to another function, in those cases, we don't care what's in the memory, we care about only the pointer itself.

When you have to pass a pointer and you really do care about what is in the memory, you use Bytes. In the case of bFun5, we'll pass a string that the C funtion will get as a char and we do it this way:

bFun5 applyWith: 'test\0'.

The first thing that might suprise you is the '\0'. Well, C string must be null terminated or else they can be dangerous, that's the NULL character, at the end.

But something you should know is that the parameter has to be a ByteArray. In this case 'test\0' is automatically converted into a ByteArray, but if it wasn't, we could have done something like
bFun5 applyWith: ('test\0' as: ByteArray).

For your case, you might have to build the ByteArray yourself, depending of how complex is what you are passing.