Foreign Function Interface
Inko supports interacting with C through it's Foregin Function Interface (FFI), powered by libffi.
Note
Inko's FFI is quite basic and comes with various limitations. We aim to improve this over time.
Inko's FFI is provided using the module std::ffi
. For more information, refer
to the source code of this module.
Loading libraries
To load a library at runtime, use the type std::ffi::Library
:
import std::ffi::Library
class async Main {
fn async main {
let lib = Library
.new(['libc.so', 'libc.so.6', 'libSystem.dylib', 'msvcrt.dll'])
.unwrap
}
}
Library.new
tries to open any of the given library names or paths, returning a
Some(Library)
if the library is found.
When the Library
value is dropped, the handle to the C library is closed
automatically.
Loading functions
After creating a Library
you can load functions through it. For example,
malloc
and free
are loaded as follows:
import std::ffi::(Library, Type)
class async Main {
fn async main {
let lib = Library
.new(['libc.so', 'libc.so.6', 'libSystem.dylib', 'msvcrt.dll'])
.unwrap
let malloc = lib
.function('malloc', arguments: [Type.SizeT], returns: Type.Pointer)
.unwrap
let free = lib
.function('free', arguments: [Type.Pointer], returns: Type.Void)
.unwrap
}
}
Similar to Library.new
, Library.function
returns an Option
, with a None
signalling the function doesn't exist.
Loading variables
Similar to loading functions we can load variables:
import std::ffi::(Library, Type)
class async Main {
fn async main {
let lib = Library
.new(['libc.so', 'libc.so.6', 'libSystem.dylib', 'msvcrt.dll'])
.unwrap
let errno = lib.variable('errno').unwrap
}
}
Note that C libraries tend to implement (global) variables using macros, in
which case Library.variable
won't be able to find the variable. For example,
musl defines errno
as a macro, which calls a
function under the hoods.
Calling functions
Calling functions is done using methods such as Function.call0
,
Function.call1
, etc. The arguments of these methods are of type Any
, and
only the following values can be passed to these arguments:
String
Int
Float
ByteArray
- Any
Any
produced by C code (e.g. the return value ofmalloc
in the above example)
Inko performs no type-checking whatsoever on values of type Any
, so you have
to be careful when using such values. Incorrect use of Any
values may lead to
the program crashing, eating your laundry, or work just fine until Friday
afternoon just as you're about to leave for home, at which point everything
catches fire.
When using the return value of a C function, it's up to you to cast it to or
construct the appropriate type. Consider our malloc
example from earlier: it's
return type is a pointer (specified using returns: Type.Pointer
), for which
std::ffi
provides the type Pointer
. This means we can use the result like
so:
import std::ffi::(Library, Type, Pointer)
class async Main {
fn async main {
let lib = Library
.new(['libc.so', 'libc.so.6', 'libSystem.dylib', 'msvcrt.dll'])
.unwrap
let malloc = lib
.function('malloc', arguments: [Type.SizeT], returns: Type.Pointer)
.unwrap
let free = lib
.function('free', arguments: [Type.Pointer], returns: Type.Void)
.unwrap
let mem = Pointer.new(malloc.call1(32))
}
}
The Pointer
type is a regular Inko type, so we can use it and pass it around
like any other value. If we want to pass it back to C, we have to get the
underlying raw pointer using Pointer.raw
:
import std::ffi::(Library, Type, Pointer)
class async Main {
fn async main {
let lib = Library
.new(['libc.so', 'libc.so.6', 'libSystem.dylib', 'msvcrt.dll'])
.unwrap
let malloc = lib
.function('malloc', arguments: [Type.SizeT], returns: Type.Pointer)
.unwrap
let free = lib
.function('free', arguments: [Type.Pointer], returns: Type.Void)
.unwrap
let mem = Pointer.new(malloc.call1(32))
free.call1(mem.raw)
}
}
If the return type is a built-in type such as String
or Int
, we can cast the
Any
to the appropriate type:
import std::ffi::(Library, Type, Pointer)
class async Main {
fn async main {
let lib = Library
.new(['libc.so', 'libc.so.6', 'libSystem.dylib', 'msvcrt.dll'])
.unwrap
let atol = lib
.function('atol', arguments: [Type.String], returns: Type.I64)
.unwrap
atol.call1('123') as Int # => 123
}
}
If you cast an Any
to the wrong type, all hell breaks loose, so be careful!
Defining structures
The FFI module supports the means to build wrappers around structures, making it
easier to read and write their fields. This is done using the LayoutBuilder
and Struct
types. A LayoutBuilder
is used to construct the layout of a
struct, represented using the Layout
type. A Struct
wraps a pointer to a C
structure, and a corresponding Layout
.
For example, if you have a struct with two int
fields (Type.I32
), you'd
define the layout like so:
import std::ffi::(LayoutBuilder, Struct)
let builder = LayoutBuilder.new
builder.field('foo', Type.I32)
builder.field('bar', Type.I32)
let layout = builder.into_layout
let struct = Struct.new(pointer_to_the_c_struct, layout)
struct['foo']
struct['foo'] = 42
The struct padding/alignment is calculated automatically, and can be disabled
using LayoutBuilder.no_padding
. Memory is still managed manually, so don't
forgot to somehow free the structure when you're done with it.
Memory management
Inko's FFI doesn't automatically manage memory of C values, besides closing a
loaded library when dropping a Library
value. This means its your own
responsibility to release memory (e.g. by using C's free()
function) whenever
necessary.
Limitations
- C calling back into Inko isn't supported.
- Variadic functions aren't supported.
- The
Function
type only supports calling of functions with up to six arguments, at least for now. - Inko doesn't support pinning of processes to OS threads (this is by design), making it more difficult to interact with C code that uses thread-local storage. See this issue for more information.
- C code that requires to be run on the main thread should be called from the "Main" process, as this process is always run on the main thread. There's no support for forcing other processes to run on the main thread.