LibGodotConnector (Elixir)

This is a proof-of-concept showing how to interface Elixir with LibGodot using NIFs.

Architecture

  1. Elixir to Godot: The LibGodot module provides functions like start/0 and iteration/0 which call into the C++ NIF. It also provides send_message/2, which enqueues messages for Godot to consume via the ElixirBus singleton.
  2. Godot to Elixir: Godot can call ElixirBus.send_event(kind, payload) to push events back to the subscribed Elixir process. You can "subscribe" a process using LibGodot.subscribe(self()).

How to build

  1. Ensure you have built libgodot in the root directory.
  2. Build the NIF via Mix (recommended):
cd samples/elixir_sample
mix deps.get
mix compile

This uses elixir_make and the local Makefile to drive a CMake build.

Optional: use precompiled NIFs from GitHub Releases

If you publish precompiled artefacts (tarballs) to GitHub Releases for the current version, mix compile can download them instead of building locally.

cd samples/elixir_sample
mix compile

To override the default URL template, set LIBGODOT_PRECOMPILED_URL to a template containing @{artefact_filename}.

To force a local build (skip download attempts):

export LIBGODOT_FORCE_BUILD=1
mix compile

Using this as a dependency (local + Hex)

This project is set up to work like a typical Hex package that ships precompiled NIFs.

Key idea: end-users installing from Hex should not need C++ tooling. During mix compile, elixir_make will:

  1. Determine the current target triple (see LibGodotConnector.Precompiler.current_target/0).
  2. Compute the expected artefact filename (@{artefact_filename} in the URL template).
  3. Download and unpack the tarball into the dependency's priv/ directory.
  4. Optionally verify checksums using checksum-lib_godot_connector.exs if it is present in the Hex package.

If the precompiled artefact is missing/unavailable, elixir_make falls back to building locally (unless LIBGODOT_FORCE_BUILD=1 is set).

Test "install elsewhere" locally (without publishing)

  1. Create a new scratch Elixir project somewhere else:
mix new /tmp/test_libgodot_dep
cd /tmp/test_libgodot_dep
  1. Add a path dependency to this sample in /tmp/test_libgodot_dep/mix.exs:
defp deps do
    [
        {:lib_godot_connector, path: "/Users/dragosdaian/Documents/appsinacup/libgodot/samples/elixir_sample"}
    ]
end
  1. Run:
mix deps.get
mix compile

This uses the exact same compilation/precompile logic that Hex would.

Test the precompiled download flow locally

You can simulate GitHub Releases by hosting the precompiled tarballs over HTTP locally.

  1. In samples/elixir_sample/, build a precompiled archive for your current machine:
cd samples/elixir_sample
mix deps.get
mix elixir_make.precompile

The task prints the path of the generated archive (under your elixir_make cache dir).

  1. Serve the directory containing that archive:
cd <the-folder-with-the-generated-.tar.gz>
python3 -m http.server 8080
  1. In your scratch project, point the URL template to that server and compile:
export LIBGODOT_PRECOMPILED_URL='http://localhost:8080/@{artefact_filename}'
mix deps.clean --all
mix deps.get
mix compile

If everything matches, mix compile will download instead of building.

Publishing precompiled artefacts (Hex + GitHub Releases)

To publish this to Hex in a way that "just works" for users:

  1. Ensure mix.exs version (and release tag) match (e.g. v4.5.1).
  2. Build per-target/per-OTP (NIF version) tarballs on CI using mix elixir_make.precompile.
  3. Upload the .tar.gz artefacts and their checksum sidecar files (e.g. .sha256) to GitHub Releases.
  4. Generate and commit checksum-lib_godot_connector.exs so Hex installs can verify downloads:
mix elixir_make.checksum --all
  1. Make sure the checksum file is included in the Hex package (this project already includes it when present).

Note: artefacts are keyed by target triple and Erlang NIF version. If you want to support multiple OTP versions, you need to publish artefacts for each of those NIF versions.

  1. Alternatively, build the NIF with CMake directly:
cd samples/elixir_sample
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build

For a debug build:

cd samples/elixir_sample
cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug
cmake --build build
  1. Run the Elixir code:
mix compile
iex -S mix

To run it as a normal OTP application (starts LibGodot.Driver under supervision):

mix run --no-halt

Example Usage

# In IEx
LibGodot.subscribe(self())

{:ok, godot} =
    LibGodot.create([
        "godot",
        "--headless",
        "--quit"
    ])

# If you want to be explicit about which libgodot to load:
# {:ok, godot} = LibGodot.create("../../build/libgodot.dylib", ["godot", "--headless"])  # macOS
# {:ok, godot} = LibGodot.create("../../build/libgodot.so", ["godot", "--headless"])     # Linux

LibGodot.start(godot)

# Run a few frames (or drive this from a GenServer timer)
LibGodot.iteration(godot)
LibGodot.iteration(godot)

LibGodot.shutdown(godot)

# Wait for message
flush()
# Should see: {:godot_status, :started}

Driving iteration/1 from Elixir

For convenience, this sample includes a tiny GenServer that owns the Godot instance and calls iteration/1 on a timer:

iex -S mix

{:ok, _pid} =
    LibGodot.Driver.start_link(
        interval_ms: 16,
        notify_pid: self()
    )

# You should see messages like:
# {:godot_status, :started}
flush()

# Send a message into Godot (available via Engine.get_singleton("ElixirBus") in scripts)
:ok = LibGodot.Driver.send_message("hello from Elixir")

# Request/reply: sends a request into Godot and blocks waiting for a response.
# Godot must call ElixirBus.respond(request_id, response) for this to complete.
{:ok, resp} = LibGodot.Driver.request("ping", 1_000)
IO.inspect(resp)

# Receive events sent from Godot via ElixirBus.send_event/2
LibGodot.subscribe(self())
flush()

On macOS, embedding Godot inside the BEAM is only supported in headless mode. The driver defaults to --headless and the native layer disables AppKit initialization when it detects --headless. You can override by passing args: [...], but windowed mode may crash due to AppKit main-thread requirements.

Note: the sample project lives at samples/project/, so from samples/elixir_sample/ the path is ../project/.