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.
- For common return values, return an
enum
instead of c.int
- Put them in another package so it can be namespaced:
sqlite.open(...)
is way
nicer than clobbering open
or coming up with your own naming convention.
References