-
Notifications
You must be signed in to change notification settings - Fork 3
Home
Welcome to the runic wiki! I hope you will enjoy your time here :-).
-
Rune Configuration Documentation
Configuration file which controls everything that runic does. -
Runestone Documentation
Intermediate Language file that stores information about the contents of a library. -
Runecross
Generate bindings for multiple platforms properly -
Extern System
Refer to types that come from other libraries. -
Wrapper
Create a wrapper for code that is either not exported into the library or not compatible with the C-ABI. -
How system include files are handled
runic tries to be as host independent as possible. This of course also includes ignoring system headers (e.g.:/usr/include/
) and such from the host system. -
Overwrite System
Manually overwrite certain types or symbols (or aspects of them) in case runic just won't generate it as you like it.
Runic allows you to generate bindings that respect differences on multiple platforms. This is achieved through the Runecross system which derives its name from crossing multiple runestones. You can use this system by specifying some platforms in the platforms
entry of the rune file.
This would, as one would expect, generate bindings for Linux, Windows and Macos on the x86_64 architecture.
version: 0
platforms:
- Linux x86_64
- Windows x86_64
- Macos x86_64
from:
static.linux: lib/linux/libsea.a
static.windows: lib/linux/sea.lib
static.macos: lib/macos/libsea.a
You can declare different values for different platforms by utilising multi platform values.
If nothing is specified in the plaforms entry the platform of the host is used. These platform values are then used to generate a runestone per platform for the from
language. How these multiple runestones are then used on the to
side depends on the language how it is implemented. When generating bindings to odin it is handled like this:
// First a build tag is created that ensures that the code is only compilable on the specified platforms.
// One can disable this build tag by using the `to.no_build_tag` entry.
#+build linux amd64, windows amd64, darwin amd64
package sea
// Here is the Any Any code if some parts of the runestones are the same over all specified platforms.
fish :: struct {
size: fish_int,
hunger: fish_float,
}
@(default_calling_convention="c")
foreign runic_sea {
@(link_name="sea_fish_swim")
fish_swim :: proc (f: ^fish) ---
}
// Then using the when statements platform-specific parts are separated. The most prominent part are usually the libraries.
when (ODIN_OS == .Linux) || (ODIN_OS == .Macos) {
fish_int :: i64
fish_float :: f64
}
when (ODIN_OS == .Linux) {
foreign import runic_sea "lib/linux/libsea.a"
@(default_calling_convention="c")
foreign runic_sea {
@(link_name="sea_fish_render_on_linux")
fish_render_on_linux :: proc(f: ^fish, delta_time: fish_float)
}
}
when (ODIN_OS == .Windows) {
foreign import runic_sea "lib/windows/sea.lib"
fish_int :: i32
fish_float :: f32
@(default_calling_convention="c")
foreign runic_sea {
@(link_name="sea_fish_render_on_windows")
fish_render_on_macos :: proc(f: ^fish, delta_time: fish_float)
}
}
when (ODIN_OS == .Macos) {
foreign import runic_sea "lib/macos/libsea.a"
@(default_calling_convention="c")
foreign runic_sea {
@(link_name="sea_fish_render_on_macos")
fish_render_on_macos :: proc(f: ^fish, delta_time: fish_float)
}
}
You could also generate the when statements a little bit differently by utilising the to.use_when_else
entry.
The extern system can be used to properly reference types that are defined outside of the library from which the runestone is generated. Consider this example:
draw.h
#include <cairo.h>
extern void draw_stuff(cairo_t* cr);
Here the cairo rendering library is used. Ideally we would not want cairo_t
to be added as part of our library. Therefore you can make runic detect that this type comes from an external source, like this:
rune.yml
from:
language: c
shared: libdraw.so
headers: draw.h
extern:
- "path_to_cairo/cairo.h"
Now runic detects cairo_t as coming from an external source and it will be added to the runestone like this:
[extern]
cairo_t = "path_to_cairo/cairo.h" #Opaque
[symbols]
func.draw_stuff = #Untyped cr #Extern cairo_t #Attr Ptr 1 #AttrEnd
In this case cairo_t
is an opaque type, but the complete type will be added to the runestone just in the case no source is defined in from.extern.sources
. So now we come to the from
side. How do you now deal with extern types when generating bindings. If you generate bindings for odin this works as follows:
rune.yml
to:
language: odin
out: draw.odin
extern:
sources:
"path_to_cairo/cairo.h": "vendor:cairo"
On the from
side you can now assign a odin specific import path to the source. vendor:cairo
does not actually exist, but you can enter any import path and when generating the bindings the package will be imported and the correct prefix (cairo
in this case) will be prefixed everytime a type from the package is referenced. This is how the generated odin bindings would look like:
draw.odin
package draw
import "vendor:cairo"
foreign import runic_draw "system:draw"
@(default_calling_convention = "c")
foreign runic_draw {
@(link_name = "draw_stuff")
draw_stuff :: proc "c" (cr: ^cairo.cairo_t) ---
}
But there is an issue. In the theoretical package vendor:cairo
the type cairo_t
is actually called context_t
. So how do you handle this. You just need to make a small addition to the rune file:
rune.yml
to:
language: odin
package draw
out: draw.odin
extern:
sources:
"path_to_cairo/cairo.h": "vendor:cairo"
remaps:
"cairo_t": "context_t"
You need to assign a remap to cairo_t
and then the bindings will look like this and compile perfectly:
draw.odin
package draw
import "vendor:cairo"
foreign import runic_draw "system:draw"
@(default_calling_convention = "c")
foreign runic_draw {
@(link_name = "draw_stuff")
draw_stuff :: proc "c" (cr: ^cairo.context_t) ---
}
Using the wrapper
entry in the rune file you can generate a wrapper that wraps every function that is not visible in the library or not compatible with the C-ABI in a wrapper function that is visible in the library. For example if you have C code that looks like this:
calc.h
static int multiply(int a, int b) {
return a * b;
}
inline int divide(int a, int b) {
return a / b;
}
extern int plus(int a, int b);
When parsing this code only the plus
function would appear in the bindings because the other ones are not visible symbols of the library. Now you can use the wrapper
entry and solve this:
rune.yml
version: 0
wrapper:
language: c
in_headers: calc.h
out_header: calc-wrapper.h
out_source: calc-wrapper.c
The resulting wrapper will look like this:
calc-wrapper.h
#include "calc.h"
extern int multiply_wrapper(int a, int b);
extern int divide_wrapper(int a, int b);
calc-wrapper.c
#include "calc-wrapper.h"
int multiply_wrapper(int a, int b) {
return multiply(a, b);
}
int divide_wrapper(int a, int b) {
return divide(a, b);
}
Using the default settings the wrapper.out_header
is automatically added to from.headers
. You can disable this behaviour by setting wrapper.add_header_to_from
to false
. In case you have differences depending on the platform you can set wrapper.multi_platform
to true
. The following example shows how this would look like.
calc-multi-plat.h
#ifdef __linux__
inline int divide_linux(int a, int b) {
// linux specific stuff
}
#elif defined(__WIN32)
inline int divide_windows(int a, int b) {
// windows specific stuff
}
#else
inline int divide_macos(int a, int b) {
// macos specific stuff
}
#endif
rune.yml
version: 0
platforms:
- Linux x86_64
- Windows x86_64
- Macos x86_64
wrapper:
language: c
in_headers: calc-multi-platform.h
out_header: calc-wrapper.h
out_source: calc-wrapper.c
multi_plaform: true
A separate file will be created for every platform. The files will be named in the following convention:
calc-wrapper-Linux_x86_64.h
#include "calc-multi-plaform.h"
extern int divide_linux_wrapper(int a, int b);
calc-wrapper-Linux_x86_64.c
#include "calc-multi-platform.h"
int divide_linux_wrapper(int a, int b) {
return divide_linux(a, b);
}
On the to
side you can then trim the _wrapper
suffix:
rune.yml
version: 0
platforms:
- Linux x86_64
- Windows x86_64
- Macos x86_64
wrapper:
language: c
in_headers: calc-multi-platform.h
out_header: calc-wrapper.h
out_source: calc-wrapper.c
multi_plaform: true
from:
language: c
headers: calc-multi-platform.h
to:
language: odin
out: calc.odin
trim_suffix:
functions: _wrapper
If you then build the wrapper:
cc -c -o calc-wrapper-Linux_x86_64.o calc-wrapper-Linux_x86_64.c
ar rs calc-wrapper-Linux_x86_64.o libcalc-wrapper-Linux_x86_64.a
You can add the resulting static library file to the additional libraries when generating bindings for odin:
rune.yml
to:
language: odin
out: calc.odin
add_libs.linux.x86_64: ./libcalc-wrapper-Linux_x86_64.a
add_libs.windows.x86_64: ./calc-wrapper-Windows_x86_64.lib
add_libs.macos.x86_64: ./libcalc-wrapper-Macos_x86_64.a
Runic tries to maximize host independence especially since it wants to generate bindings for all kinds of platforms no matter what the host system is. Of course this then implies that when generating a runestone from c the system headers must be ignored. But there is a system in place to handle system headers (e.g. libc headers) that are required by the library. In case you want system headers to not be ignored you can set from.enable_host_includes
to true
.
- Generating empty stubs of all system headers
By default runic generates a directory in the temp (/tmp/
on linux) folder filled with empty header files that are named the same as libc, unistd headers and so on. This directory is then automatically added to the from.includedirs
. Wether this directory will be generated can be configured through the from.disable_system_include_gen
entry in the rune file. The empty files are necessary for libclang to stop complaining about missing system include files and it will actually output which types are not found.
- Adding macro definitions for stdint types
By default runic will define types like uint8_t
, size_t
, ptrdiff_t
and so on as macros through the -D
flag. This behaviour can be disabled by the from.disable_stdint_macros
entry in the rune file.
- Adding stubs for system types
This is not something that runic does automatically, but you can do this in case libclang complains about missing types. For example if you have the following code:
#include <sys/types.h>
extern void kill_process(pid_t id);
libclang will complain that pid_t
can not be found. To solve this you can create a stub of sys/types.h
in your project directory (e.g. stdinc/sys/types.h
) and implement the type pid_t
inside of it:
stdinc/sys/types.h
typedef int pid_t;
The specific definition of type does not really matter. It only matters that the type is defined, because if it is not libclang will think it is an integer and wont tell anyone that it is actually a type called pid_t
.
Then you can use the extern system to properly reference the type:
rune.yml
from:
extern:
- 'stdinc/*'
- 'stdinc/sys/*'
to:
language: odin
extern:
sources:
"stdinc/sys/types.h": "core:sys/posix"
remaps:
"pid_t": "Pid"
In this case the type pid_t
is called Pid
in the core:sys/posix
odin package, therefore we need a remap. Using this approach libclang can parse your code and the bindings will properly reference the type.
In the case that runic does not parse your code in the way that you want. You can manually overwrite it. Using the from.overwrite
entry you can overwrite types, functions, variables and constants and also just parts of them. When overwriting a type you need to specify the new type using the runestone types syntax. But you need to be careful, there are no checks in place that check whether the resulting bindings are correct. Here are some examples of overwrites:
rune.yml
version: 0
from:
overwrite:
types:
fish_int: #SInt64
fish_float: #Float64
overwrite.windows:
types:
fish_int: #SInt32
fish_float: #Float32
Here entire types have been overwritten and the types even differ per platform.
fish.h
struct fish {
fish_int size;
fish_float hunger;
};
rune.yml
version: 0
from:
overwrite:
types:
fish.member.0.name: width
fish.member.1.type: '#Float32'
Here only the first member's name and the second member's type is overwritten. This same paradigm can also be used for functions and function pointers.
fish.h
void fish_render(fish* f, fish_float delta_time);
rune.yml
version: 0
from:
overwrite:
functions:
fish_render.param.0.name: fishy
fish_render.param.1.type: '#Float32'
fish_render.return: '#SInt32'
These overwrites change the first parameters name to fishy
, the second parameters type to float
and the return type of the function to int32_t
. Of course in this case the bindings would break, because the actual return type is void
.
Here is a more complex example that shows how you can use the overwrite system for multiple platforms. This has been taken directly from odin-gtk.
odin-gtk/glib/rune.yml
from:
overwrite:
functions: &any_function_overwrites
g_assertion_message_cmpint.param.8.type: 'gchar'
g_assertion_message_cmpnum.param.8.type: 'gchar'
types: &any_type_overwrites
# This makes sure that the integer types of glib have the correct size on all platforms
gint8: '#SInt8'
guint8: '#UInt8'
gint16: '#SInt16'
guint16: '#UInt16'
gint32: '#SInt32'
guint32: '#UInt32'
gint64: '#SInt64'
guint64: '#UInt64'
gboolean: '#Bool32'
_GDoubleIEEE754: 'gdouble'
_GFloatIEEE754: 'gfloat'
# Here the type is overwritten with an array because it uses specific bit widths
_GDate: '#UInt8 #Attr Arr 8 #AttrEnd'
# These types had to be specifically set to #RawPtr, because they are using bit widths
GMainContextPusher: '#RawPtr'
GMutexLocker: '#RawPtr'
GRecMutexLocker: '#RawPtr'
GRWLockWriterLocker: '#RawPtr'
GRWLockReaderLocker: '#RawPtr'
GRefString: 'gchar'
constants: &any_constant_overwrites
'FALSE': '0 #Untyped'
'TRUE': '1 #Untyped'
'SOURCE_REMOVE': '0 #Untyped'
'SOURCE_CONTINUE': '1 #Untyped'
'G_MAXSIZE': '"USIZE_MAX" #Untyped'
'G_MINSSIZE': '"SSIZE_MIN" #Untyped'
'G_MAXSSIZE': '"SSIZE_MAX" #Untyped'
'G_DATE_BAD_JULIAN': '0 #Untyped'
'G_DATE_BAD_DAY': '0 #Untyped'
'G_DATE_BAD_YEAR': '0 #Untyped'
overwrite.linux.x86_64: &overwrites_64
functions:
<<: *any_function_overwrites
types:
# This ensures that all 64-Bit platforms have 64-Bit types
gssize: '#SInt64'
gsize: '#UInt64'
goffset: 'gint64'
gintptr: '#SInt64'
guintptr: '#UInt64'
<<: *any_type_overwrites
constants:
<<: *any_constant_overwrites
overwrite.linux.arm64:
<<: *overwrites_64
overwrite.windows.x86_64:
<<: *overwrites_64
overwrite.windows.arm64:
<<: *overwrites_64
overwrite.linux.x86: &overwrites_32
functions:
<<: *any_function_overwrites
types:
gssize: '#SInt32'
gsize: '#UInt32'
goffset: 'gint32'
gintptr: '#SInt32'
guintptr: '#UInt32'
<<: *any_type_overwrites
constants:
<<: *any_constant_overwrites
overwrite.linux.arm32:
<<: *overwrites_32
overwrite.windows.x86:
<<: *overwrites_32
overwrite.windows.arm32:
<<: *overwrites_32