Skip to content
PucklaJ edited this page Dec 29, 2024 · 13 revisions

Runic Wiki

Welcome to the runic wiki! I hope you will enjoy your time here :-).

Table of Contents

  1. Rune Configuration Documentation
    Configuration file which controls everything that runic does.
  2. Runestone Documentation
    Intermediate Language file that stores information about the contents of a library.
  3. Runecross
    Generate bindings for multiple platforms properly
  4. Extern System
    Refer to types that come from other libraries.
  5. Wrapper
    Create a wrapper for code that is either not exported into the library or not compatible with the C-ABI.
  6. 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.
  7. Overwrite System
    Manually overwrite certain types or symbols (or aspects of them) in case runic just won't generate it as you like it.

Runecross

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.

Extern System

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) ---
}

Wrapper

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

How system include files are handled

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.

  1. 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.

  1. 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.

  1. 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.

Overwrite System

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
Clone this wiki locally