Day Two

Ian Harris

2025-01-02

Setup and improvements

I’ve been meaning to make my blog look a little bit nicer, have some more features like an RSS feed, and improve the deployment process. It’s okay. There’s a bit more that I want to change, probably after I share some more interesting stuff.

Zigler

Zigler has been pretty fun to play with. I quite like Zig, and for NIFs, Zig is fantastic. Since Zig has really nice interop with C, using existing C libraries from Elixir is basically free. Here’s a basic setup for working with Raylib, for example:

defmodule Raylib do
  use Zig,
    otp_app: :raylib,
    c: [
      include_dirs: ["/usr/local/include"],
      link_lib: [{:system, "raylib"}]
    ],
    easy_c: "raylib.h",
    nifs: [
      InitWindow: [],
    ]
end

Now if we move over to iex:

$ iex -S mix
iex(1)> Raylib."InitWindow"(400, 400, "Hello!")
# a bunch of debug output from Raylib

And you’ll see this wonderful window:

hello window

Beautiful.

But is it beautiful?

No, not really. For one, that Zigler setup isn’t great. The next step would be to add all of the functions from Raylib there. This can basically be copied and pasted, so not too bad.

Another problem is function naming. Raylib uses the same casing as Elixir’s module casing, so we need to quote the function to call it. One way around this is to define aliases in Zigler. We’d change the nifs option to something like this:

defmodule Raylib do
  use Zig,
  # ...
  nifs: [
    InitWindow: [],
    init_window: [alias: :InitWindow]
  ]
end

This is.. okay. At least we can call Raylib.init_window(400, 400, "Hello!"), but it also means every function needs two entries. Still not too bad, but if you have a lot of functions, it’s a lot of noise.

The last main problem with this is it’s actually broken, subtly. To show this, let’s make an actual window loop. To start, we’ll need these functions:

defmodule Raylib do
  use Zig,
  # ...
  nifs: [
    InitWindow: [],
    init_window: [alias: :InitWindow],

    CloseWindow: [],
    close_window: [alias: :CloseWindow],

    WindowShouldClose: [],
    window_should_close: [alias: :WindowShouldClose],

    BeginDrawing: [],
    begin_drawing: [alias: :BeginDrawing],

    EndDrawing: [],
    end_drawing: [alias: :EndDrawing],

    ClearBackground: [],
    clear_background: [alias: :ClearBackground],

    DrawText: [],
    draw_text: [alias: :DrawText],

    SetTargetFPS: [],
    set_target_fps: [alias: :SetTargetFPS]
  ]
end

Next we’ll make our basic window example:

defmodule Window do
  def start do
    Raylib.init_window(400, 400, "Hello!")

    Raylib.set_target_fps(60)

    loop()

    Raylib.close_window()
  end

  defp loop(state \\ 0)
  defp loop(state) do
    Raylib.begin_drawing()
      Raylib.clear_background(r: 42, g: 42, b: 42, a: 255)
      Raylib.draw_text("frame ##{state}", 100, 100, 20, r: 200, g: 200, b: 200, a: 255)
    Raylib.end_drawing()

    if not Raylib.window_should_close() do
      loop(state + 1)
    end
  end
end

If you’re playing along at home, you may see something like this:

junk string in text

Those extra characters are just random from a non-null terminated string. They may be different for you. The point is, our string is running too long accidentally. One fix for this is to just append a null byte to the string from Elixir:

Raylib.draw_text("frame ##{state}\0", 100, 100, 20, r: 200, g: 200, b: 200, a: 255)

Which is not too bad. It would also be easy enought to wrap that call in another function and append that byte to whatever is passed. But I’d rather not have to deal with it at all - and we don’t have to!

Sentinel-terminated pointers

Really what’s happening here is that Zigler’s easy_c is generating a Zig file like this:

const easy_c = @cImport({
  @cInclude("raylib.h");
});

pub const InitWindow = easy_c.InitWindow;
pub const CloseWindow = easy_c.CloseWindow;
// ...

Which will treat a C type like const char * as Zig’s [*c]const u8. The [*c] type is kind of Zig’s looser pointer for C which does weird things like C’s weird pointers. It’s generally recommended to change this to a more specific type when dealing with C in Zig.

So what we have currently is basically this:

const easy_c = @cImport({
  @cInclude("raylib.h");
});

pub fn InitWindow(width: c_int, height: c_int, title: [*c]const u8) void {
  easy_c.InitWindow(width, height, title);
}

But if we change this to use a sentintel-terminated pointer with \0 as the sentinel, the string will be properly terminated. Unfortunately, we must now abandon easy_c.

Writing our own Zig bindings

Since we only have a few functions, this won’t be too bad. Let’s start with the Zig:

const raylib = @cImport({
  @cInclude("raylib.h");
});

pub fn init_window(width: c_int, height: c_int, title: [*:0]u8) void {
  raylib.InitWindow(width, height, title);
}

pub const close_window = raylib.CloseWindow;
pub const window_should_close = raylib.WindowShouldClose;
pub const begin_drawing = raylib.BeginDrawing;
pub const end_drawing = raylib.EndDrawing;
pub const clear_background = raylib.ClearBackground;
pub fn draw_text(text: [*:0]u8, pos_x: c_int, pos_y: c_int, font_size: c_int, color: raylib.Color) void {
  raylib.DrawText(text, pos_x, pos_y, font_size, color);
}
pub const set_target_fps = raylib.SetTargetFPS;

On the Elixir side, we can reduce things to the following:

defmodule Raylib do
  use Zig,
    otp_app: :raylib,
    c: [
      include_dirs: ["/usr/local/include"],
      link_lib: [{:system, "raylib"}]
    ],
    zig_code_path: "raylib.zig"
end

And now if we run things again..

fixed string