You're looking at the documentation for the "main" branch, click here to view the documentation for the latest stable release.

Interacting with C

Sometimes you are in need of certain functionality for which no Inko library exists, but a library written in C providing the functionality does. Inko's foreign function interface allows you to use C libraries with almost no runtime overhead.

Before we dive into how to use the FFI, there are a few things to keep in mind.

First, when using the FFI, Inko's safety guarantees are thrown out of the window, as the compiler can't verify if C function calls are safe (e.g. they don't mutate global state).

Second, Inko won't manage memory allocated through C functions, meaning you need to manually release it yourself (e.g. using free()). This means you may run into memory leaks if you're not careful.

Third, the API comes with several limitations (further discussed below), which may complicate using certain C libraries.

In short: you should avoid using C code as much as you can. When you do need to use it, be careful as it's easy to make mistakes.

We'll say it again just to be clear: avoid using C code unless you have determined there's no other option.

With that out of the way, let's get started.

Foreign types

When interacting with C code, we need to work with types that are specific to C, such as pointers. Inko's FFI offers the following types, along with their C equivalents:

Inko typeSize (bits)C type
Int88int8_t
Int1616int16_t
Int3232int32_t
Int6464int64_t
UInt88uint8_t
UInt1616uint16_t
UInt3232uint32_t
UInt6464uint64_t
Float3232float
Float6464double
Pointer[T]641T*

Pointer-pointers don't have a dedicated type (i.e. there's no Pointer[Pointer[Int8]]), instead they are represented as just regular pointers (e.g. Pointer[Int8]).

There's no equivalent of C's size_t type, as Inko only supports 64-bits platforms, and thus you can just use UInt64 instead.

C types don't support methods and operators, and C structures aren't compatible with generic type parameters. This means you'll either need to cast the types (e.g. casting an Int32 to Int), or wrap the data in a custom type.

C types are treated as value types and are allocated on the stack, including structs (which we'll discuss later).

Type casting

These types can be passed to/from Inko types:

let a = 42 as Int32 # => Int32
let b = a as Int    # => Int

Inko doesn't perform implicit type casts, so passing an Int32 when a Int64 is expected will result in a compile-time error. Similarly, pointers of type Pointer[A] aren't implicitly compatible with pointers of type Pointer[B], and instead require an explicit type cast.

Importing libraries

Libraries to link against are specified using the import extern "NAME" syntax. For example, to import libm:

import extern "m"

The name should be the library name without any prefix (e.g. "lib"), file extension or file path. To illustrate, let's take this simple program and save it as test.inko:

import extern "m"

class async Main {
  fn async main {}
}

Then run the following:

inko build -o /tmp/test test.inko
ldd /tmp/test

On a GNU Linux system this outputs:

linux-vdso.so.1 (0x00007ffdcc7a6000)
libm.so.6 => /lib64/libm.so.6 (0x00007feb7eb59000)
libc.so.6 => /lib64/libc.so.6 (0x00007feb7e97b000)
/lib64/ld-linux-x86-64.so.2 (0x00007feb7ec48000)

Dynamic vs static linking

By default, C libraries are linked dynamically. This is done because while dynamic libraries are widely available, not all platforms provide static equivalents, or require extra steps. For example, on many Linux distributions installing the package "foo" only gives you a dynamic library, requiring an extra "foo-static" package to be installed to also get the static library.

If you happen to have all the necessary static libraries installed, you can instruct the compiler to link them statically using inko build --static. This flag applies to all libraries, meaning we either link all of them dynamically or statically. Inko doesn't support dynamically linking some libraries while statically linking others.

libc and libm are always dynamically linked (even with the --static flag), unless you are using a platform that defaults to static linking them, such as Alpine Linux.

Some platforms merge libc and libm together, such as macOS. In this case Inko only links against libc.

To illustrate static linking, we'll update our test.inko to import zlib instead:

import extern "z"

class async Main {
  fn async main {}
}

Then we build it and show what libraries the executable is linked against:

inko build -o /tmp/test test.inko
ldd /tmp/test
        linux-vdso.so.1 (0x00007ffd8eb8f000)
        libm.so.6 => /lib64/libm.so.6 (0x00007fb87d9c4000)
        libz.so.1 => /lib64/libz.so.1 (0x00007fb87d9aa000)
        libc.so.6 => /lib64/libc.so.6 (0x00007fb87d7cc000)
        /lib64/ld-linux-x86-64.so.2 (0x00007fb87dab3000)

On Fedora Linux, we can get a static version of zlib by running sudo dnf install zlib-static, after which we can use the --static flag to link statically against zlib:

inko build --static -o /tmp/test test.inko
ldd /tmp/test
        linux-vdso.so.1 (0x00007ffc06993000)
        libm.so.6 => /lib64/libm.so.6 (0x00007fbb76e50000)
        libc.so.6 => /lib64/libc.so.6 (0x00007fbb76c72000)
        /lib64/ld-linux-x86-64.so.2 (0x00007fbb76f3f000)

Functions

Importing libraries alone isn't useful, so let's define bindings for some functions from the libm library and use them. Defining the signatures of C functions is done using fn extern. For example, if we want to use the ceil() function from libm, we'd define the signature as follows:

fn extern ceil(value: Float64) -> Float64

If a signature is defined without a return type, the return type is inferred as the C type void (i.e. no value is returned).

C functions are called like regular Inko methods:

import std.stdio (Stdout)
import extern "m"

fn extern ceil(value: Float64) -> Float64

class async Main {
  fn async main {
    let out = Stdout.new

    # Float64 is a C type and we can't call methods on such types, so we must
    # explicitly cast the returned value to Float (Inko's floating point type).
    let val = ceil(1.123 as Float64) as Float

    out.print(val.to_string)
  }
}

When running this program, the output will be 2.0.

If a C function defines an argument of type Int, Inko treats this as Int64 and implicitly converts Int arguments to Int64 arguments. This is only true for Int arguments, and return types should be Int64 and Float64 instead of Int and Float respectively, as Int and Float have a different memory representation at the moment.

Don't use types such as ref T and mut T in signatures. While this is supported, it's used to interact with Inko's runtime library written in Rust, and shouldn't be used outside of the standard library.

Variadic functions are also supported, and are defined as follows:

fn extern printf(format: Pointer[UInt8], ...) -> Int32

class async Main {
  fn async main {
    printf("Hello %s\n".to_pointer, "Inko".to_pointer)
  }
}

This program prints "Hello Inko" to STDOUT.

When using variadic functions, the compiler doesn't type-check any of the additional arguments, because it doesn't know what the expected types are.

Structures

Inko supports defining signatures for C structures, similar to classes. This is done using the class extern syntax. For example, to define the timespec structure from the libc time.h header, we'd write the following:

class extern Timespec {
  let @tv_sec: Int64
  let @tv_nsec: Int64
}

Like classes, we can create instances of these structs:

class extern Timespec {
  let @tv_sec: Int64
  let @tv_nsec: Int64
}

class async Main {
  fn async main {
    Timespec(tv_sec: 123 as Int64, tv_nsec: 456 as Int64)
  }
}

When creating an instance of a struct we can leave out all the fields. This reserves the necessary stack space but doesn't initialize it. This is useful when dealing with large structs that are initialized by a C function:

class extern Timespec {
  let @tv_sec: Int64
  let @tv_nsec: Int64
}

class async Main {
  fn async main {
    let spec = Timespec()

    hypothetical_initialization_function_that_sets_the_fields(mut spec)
  }
}

Structures are allocated on the stack and are value types, meaning a move results in a copy (unless this is optimised away). Reading and writing of structure fields uses the same syntax as regular Inko classes:

class extern Timespec {
  let @tv_sec: Int64
  let @tv_nsec: Int64
}

class async Main {
  fn async main {
    let spec = Timespec(tv_sec: 123 as Int64, tv_nsec: 456 as Int64)

    spec.tv_sec = 1000 as Int64
  }
}

Inko doesn't run destructors for any types stored in a C structure, which may lead to memory leaks if you don't manually run these where necessary.

If a structure is stored in a field, referring to the field doesn't incur a copy, instead you get a pointer to the structure. This makes it easier to work with structures stored in Inko classes:

import std.stdio (Stdout)

class extern Timespec {
  let @tv_sec: Int64
  let @tv_nsec: Int64
}

class Box {
  let @time: Timespec
}

class async Main {
  fn async main {
    let out = Stdout.new
    let spec = Timespec(tv_sec: 123 as Int64, tv_nsec: 456 as Int64)
    let box = Box(time: spec)

    box.time.tv_sec = 400 as Int64
    out.print((box.time.tv_sec as Int).to_string) # => 400
  }
}

If you want a copy anyway, you can dereference the pointer (more on that in just a moment) to get a copy.

Pointers

Pointer types are defined using the syntax Pointer[T], where T is the type pointed to. For example, the type signature for a pointer to our Timespec is Pointer[Timespec].

Creating a pointer to a C value is done using the mut expr expression, where expr is an expression to create a pointer to. To illustrate this, we'll use the libc function clock_gettime(), which expects a timespec pointer as its second argument:

import std.stdio (Stdout)

let CLOCK_REALTIME = 0

class extern Timespec {
  let @tv_sec: Int64
  let @tv_nsec: Int64
}

fn extern clock_gettime(id: Int32, time: Pointer[Timespec]) -> Int32

class async Main {
  fn async main {
    let out = Stdout.new
    let spec = Timespec(tv_sec: 0 as Int64, tv_nsec: 0 as Int64)

    clock_gettime(CLOCK_REALTIME as Int32, mut spec)

    out.print((spec.tv_sec as Int).to_string)
  }
}

Here mut spec passes a pointer to our Timespec structure, allowing clock_gettime() to mutate it in-place.

Note that the pointer is created to the result of the expr expression. If expr is a variable, the pointer is created to whatever value is stored in the variable. This means that if you use mut object.field, and field returns e.g. a structure, you create a pointer to that newly copied structure, not the original structure stored in field.

Pointers to methods

If a module method is defined using the extern keyword, you can create a pointer to the method using mut method_name:

# This guarantees this method uses the C calling convention.
fn extern example(value: Int) -> Int {
  value
}

class async Main {
  fn async main {
    let pointer_to_method = mut example
  }
}

For this to work, you must leave out the arguments and parentheses in the mut expression, otherwise you get a reference or pointer to the result of calling the method. The use of the extern keyword for the method definition is required as this guarantees the use of the C calling convention, instead of Inko's calling convention (which may not necessarily be the same).

Dereferencing

Dereferencing a pointer is done by reading from and writing to the pseudo field 0. Doing so returns a copy of the value pointed to:

import std.stdio (Stdout)

class extern Timespec {
  let @tv_sec: Int64
  let @tv_nsec: Int64
}

class async Main {
  fn async main {
    let out = Stdout.new
    let spec = Timespec(tv_sec: 0 as Int64, tv_nsec: 0 as Int64)
    let ptr = mut spec

    # This writes an entirely new value to the pointer.
    ptr.0 = Timespec(tv_sec: 400 as Int64, tv_nsec: 0 as Int64)

    # This prints `tv_sec` of a _copy_ of the Timespec structure, because the
    # dereference using `ptr.0` returns a _copy_.
    out.print((ptr.0.tv_sec as Int).to_string) # => 400
  }
}

For pointers to structures, you can also read and write fields as with regular Inko objects. This avoids the need for intermediate copies, allowing you to mutate data pointed to in-place:

import std.stdio (Stdout)

class extern Timespec {
  let @tv_sec: Int64
  let @tv_nsec: Int64
}

class async Main {
  fn async main {
    let out = Stdout.new
    let spec = Timespec(tv_sec: 0 as Int64, tv_nsec: 0 as Int64)
    let ptr = mut spec

    # This modifies the data `ptr` points to, not a copy, resulting in `spec`
    # containing the new value.
    ptr.tv_sec = 400 as Int64

    out.print((ptr.tv_sec as Int).to_string)  # => 400
    out.print((spec.tv_sec as Int).to_string) # => 400
  }
}

Avoid using ptr.0.field_name when ptr points to a structure, and use ptr.field_name instead. This way you avoid unnecessary copies of the structure.

Pointer arithmetic

Inko doesn't support pointer arithmetic, meaning some_pointer + 16 is invalid. For cases where you need to compute pointer offsets, you'll have to cast the pointer to an Int, perform the computation, then cast the result back to a pointer. For example, here we mutate tv_nsec using such an approach:

import std.stdio (Stdout)

class extern Timespec {
  let @tv_sec: Int64
  let @tv_nsec: Int64
}

class async Main {
  fn async main {
    let out = Stdout.new
    let spec = Timespec(tv_sec: 0 as Int64, tv_nsec: 0 as Int64)
    let ptr = mut spec

    (ptr as Int + 8 as Pointer[Int64]).0 = 400 as Int64

    out.print((spec.tv_nsec as Int).to_string)
  }
}

Manually calculating pointer offsets can lead to bugs, such as reading invalid memory. You'll want to avoid this whenever possible.

Error handling

Many C functions return some sort of flag upon encountering an error, and set errno to an error code. Inko supports reading these values using std.io.Error.last_os_error:

import std.stdio (Stdout)
import std.io (Error)

let CLOCK_REALTIME = 0

class extern Timespec {
  let @tv_sec: Int64
  let @tv_nsec: Int64
}

fn extern clock_gettime(id: Int32, time: Pointer[Timespec]) -> Int32

class async Main {
  fn async main {
    let out = Stdout.new
    let spec = Timespec(tv_sec: 0 as Int64, tv_nsec: 0 as Int64)
    let res = clock_gettime(CLOCK_REALTIME as Int32, mut spec)

    if res as Int == -1 {
      let err = Error.last_os_error

      panic('clock_gettime() failed: ${err}')
    }

    out.print((spec.tv_sec as Int).to_string)
  }
}

When using Error.last_os_error, it's crucial that you call this method as soon as possible after calling the C function that may produce an error. If any code that may reschedule the process is run between the C function call and the Error.last_os_error call, the process may be rescheduled onto a different OS thread and read the wrong value. Further, Inko makes no attempt at clearing errno before C function calls, so you should only read it when the C function call indicated some sort of value (e.g. by returning -1 in the above example).

In other words, code such as this is incorrect:

let res = clock_gettime(CLOCK_REALTIME as Int32, mut spec)

do_something_else()

if res as Int == -1 {
  let err = Error.last_os_error

  panic('clock_gettime() failed: ${err}')
}

Instead, you'll want to do the following:

let res = clock_gettime(CLOCK_REALTIME as Int32, mut spec)

if res as Int == -1 {
  let err = Error.last_os_error

  panic('clock_gettime() failed: ${err}')
}

do_something_else()

Thread-local storage and the scheduler

The Inko scheduler is free to reschedule Inko processes on different OS threads. This means that if C libraries depend on thread-local state, the state observed may differ as a process is moved between threads. Some GUI libraries outright break when used on a thread different from the one the thread-local state is set up for.

The Main process on is guaranteed to always run on the main thread. As such, you should only use such libraries from the Main process. Pinning processes to arbitrary OS threads isn't supported.

Runtime performance

Calling C functions comes with the same cost as when writing code in C itself. Converting some Inko types to C types and the other way around may incur a slight cost, but in most cases this should be negligible.

Inko's scheduler doesn't detect slow/blocking C function calls, meaning it's possible for a C function call to block the current OS thread indefinitely.

This is a deliberate choice, as detecting blocking operations incurs a runtime cost likely too great for most cases where C libraries are necessary. In the future we may offer a way of explicitly marking an operation as blocking, allowing the scheduler to take care of blocking operations for you.

Limitations

The C FFI is a bit spartan, only offering what we believe is necessary for most of the C libraries out there. Most notably, the following isn't supported:

  • Using C globals, including thread-local globals. Relying on global state is going to cause trouble due to Inko's concurrent nature, so even if we did support this it wouldn't make your life easier.
  • Compiling C source code as part of the Inko build process.
  • Compile-time expressions such as sizeof() to automatically get type sizes.
  • Setting errno to a custom value.
  1. On 32-bit platforms this type would have a size of 32 bits, but Inko doesn't support 32-bit platforms, so in practise this value is always 64 bits.