Glenn Yonemitsu - software engineer. yonemitsu@gmail.com

Embedding C in Odin

Odin offers good C interoperability but it's docs can be a bit confusing for someone new to Odin.

This post compiles some notes to help get started. However I'm not very well versed in the world of C and library linking. Topics such as specifying the dynamic library search path will be left up to the reader to figure out.

These examples will use SQLite as the C library to use in Odin. The autoconf amalgamation package makes it very easy to compile both a static and dynamic library in Linux.

Compiling SQLite

Just a simple ./configure and make was enough to get things built but there are plenty of compile time options available.

There should be two files available: libsqlite3.a and libsqlite3.so. Copy these into your Odin source directory.

Static linking example

First we tell Odin we are bringing in a static library.

foreign import sqlite "libsqlite3.a"

Not much happens here. Our next task is to translate SQLite's functions and typedefs into our namespace.

C libraries can be fairly large and SQLite is as well. For this tutorial we will bring in just enough functions to know we did this right. In general this can be a valid strategy in your project as well.

We are going to bring in two functions: sqlite3_open and sqlite3_exec and the goal is to open and create a new sqlite3 database with a table in it.

Looking at the sqlite3_open docs, we see it uses a sqlite3 pointer. If we are not too concerned with the internals of this struct then we can just mark that in Odin as an opaque rawptr.

sqlite3 :: distinct rawptr

Then we can define the C functions in Odin. We want to try and best match the C spec to Odin. So if the return value is int in C, we return c.int, use cstring for char arrays, etc.

@(default_calling_convention="c")
foreign sqlite {
    sqlite3_open :: proc(filename: cstring, handle: ^sqlite3) -> c.int ---
    sqlite3_close :: proc(handle: ^sqlite3) -> c.int ---
    sqlite3_exec :: proc(
        handle: sqlite3,
        sql: cstring,
        callback: rawptr,
        callback_arg: rawptr,
        errmsg: ^cstring
    ) -> c.int ---
}

The callback parameters are just rawptr types and will be nil for this part. This is considered a valid parameter as detailed in the SQLite docs.

Good C libraries typically use a prefix in all their names. We can take advantage in the fact SQLite does the same with the sqlite3_ prefix.

@(default_calling_convention="c", link_prefix="sqlite3_")
foreign sqlite {
    open :: proc(filename: cstring, handle: ^sqlite3) -> c.int ---
    close :: proc(handle: ^sqlite) -> c.int ---
    exec :: proc(
        handle: sqlite3,
        sql: cstring,
        callback: rawptr,
        callback_arg: rawptr,
        errmsg: ^cstring
    ) -> c.int ---
}

And altogether it looks like this.

foreign import sqlite "libsqlite3.a"

@(default_calling_convention="c", link_prefix="sqlite3_")
foreign sqlite {
    open :: proc(filename: cstring, handle: ^sqlite3) -> c.int ---
    close :: proc(handle: ^sqlite) -> c.int ---
    exec :: proc(
        handle: sqlite3,
        sql: cstring,
        callback: rawptr,
        callback_arg: rawptr,
        errmsg: ^cstring
    ) -> c.int ---
}

sqlite3 :: distinct rawptr

Now let's try to call these C functions.

package sqlitetut

import "core:c"

foreign import sqlite "libsqlite3.a"

@(default_calling_convention = "c", link_prefix="sqlite3_")
foreign sqlite {
    open :: proc (filename: cstring, handle: ^sqlite3) -> c.int ---
    close :: proc(handle: ^sqlite) -> c.int ---
    exec :: proc(
        handle: sqlite3,
        sql: cstring,
        callback: rawptr,
        callback_arg: rawptr,
        errmsg: ^cstring
    ) -> c.int ---
}

sqlite3 :: distinct rawptr

main :: proc() {
    db := sqlite3{}
    res := open("mydb.db", &db)
    defer close(&db)
    if res != 0 {
        fmt.printf("error opening sqlite db\n")
        return
    }

    exec(db, "create table person(id, name)", nil, nil, nil)
}
$ odin run .
$ file mydb.db
mydb.db: SQLite 3.x database, last written using SQLite version 3049001, ...

Dynamic linking example

To "verify" it's using the system library first let's use our compiled one and check it's version string.

package sqlitetut

import "core:c"
import "core:fmt"

foreign import sqlite "libsqlite3.so"

@(default_calling_convention="c", link_prefix="sqlite3_")
foreign sqlite {
    libversion :: proc() -> cstring ---
}

main :: proc() {
    fmt.printf("SQLite version is %s\n", libversion())
}

Note the foreign import line now specifies .so, or shared object. Let's see what our library version is on.

$ odin run .
SQLite version is 3.49.1

At this stage the libsqlite3.so file can be changed. But what if we want to use our distro's library? We make a slight adjustment to the foreign import line:

foreign import sqlite "system:libsqlite3.so"

Now we can verify a couple of ways:

$ odin run .
SQLite version is 3.40.1
$ ldd sqlite
        ...
        libsqlite3.so.0 => /lib/x86_64-linux-gnu/libsqlite3.so.0 (0x00007f2efa095000)
        ...

Other topics when translating C into Odin

This tutorial placed everything into the main proc for simplicity but there are some things that can make it nicer.

References