18.1 event — Event System

**Drive your game loop with a high-performance async event system.**

NanoLang Mascot

The event module wraps libevent, a portable asynchronous event notification library. In NanoLang games it serves as the heartbeat of the application: you create an event base, register a timer that fires every frame, and dispatch the event loop. When you're ready to quit, you break out of the loop and clean up.

Quick Start


from "modules/event/event.nano" import nl_event_base_new, nl_event_base_dispatch,
                                        nl_event_base_free, nl_evtimer_new,
                                        nl_evtimer_add_timeout, nl_event_free,
                                        nl_event_get_version

fn main() -> int {
    # Check the libevent version at startup
    let ver: string = (nl_event_get_version)
    (println (+ "libevent version: " ver))

    # Create the event loop base
    let base: int = (nl_event_base_new)

    # Create a one-shot timer, fire in 1 second
    let timer: int = (nl_evtimer_new base)
    (nl_evtimer_add_timeout timer 1)

    # Block until all events fire (or loopbreak/loopexit is called)
    (nl_event_base_dispatch base)

    # Cleanup
    (nl_event_free timer)
    (nl_event_base_free base)

    return 0
}

shadow main { assert true }

Concepts

The Event Base

The event base is the core object — it holds the state of the event loop and keeps track of all registered events. All other functions take an int handle that refers to this base.


let base: int = (nl_event_base_new)   # allocate
# ... register events, dispatch ...
(nl_event_base_free base)             # always free when done

The base is backed by the best available I/O notification mechanism on the current platform (kqueue on macOS, epoll on Linux, etc.). You can query the method in use:


let method: string = (nl_event_base_get_method base)
(println (+ "Using backend: " method))

Timer Events

Timer events fire after a delay expressed in whole seconds. They are the most common event type in games because they give you the regular tick you need to advance your simulation.


let timer: int = (nl_evtimer_new base)   # create timer attached to base
(nl_evtimer_add_timeout timer 0)         # arm it: fire after 0 seconds
# ... timer fires, your callback runs ...
(nl_event_del timer)                     # disarm without freeing
(nl_event_free timer)                    # free the event object

> **Note on granularity:** nl_evtimer_add_timeout takes whole seconds. For sub-second game ticks you typically call nl_event_base_dispatch in a tight loop and use nl_event_sleep for fractional delays, or integrate with your OS's high-resolution timer.

Dispatching the Loop

Two functions drive the loop:

FunctionBehaviour
nl_event_base_dispatch(base)Run until no more events are registered. Blocks.
nl_event_base_loop(base, flags)Run with flags (e.g. EVLOOP_ONCE = 1 for a single pass).

For a game that runs until the player quits, nl_event_base_dispatch is the right choice:


(nl_event_base_dispatch base)   # blocks here until the loop exits

Exiting the Loop

From inside a callback (or from any thread) you can stop the loop two ways:


# Exit after a timeout (in seconds); 0 means "exit now at next opportunity"
(nl_event_base_loopexit base 0)

# Break immediately, even if events are pending
(nl_event_base_loopbreak base)

loopbreak is the right choice inside a game-quit handler because it stops the loop without waiting for the current dispatch cycle to finish.

Sleeping Inside the Loop

For simple timing without a dedicated timer event:


(nl_event_sleep base 1)   # sleep 1 second, keeping the base alive

This is useful for rate-limiting a polling loop.

API Reference

Event Base Management


nl_event_base_new() -> int

Allocate and return a new event base. Returns a handle (positive int) on success.


nl_event_base_free(base: int) -> void

Free all resources associated with the event base. Call this after dispatch returns.


nl_event_base_dispatch(base: int) -> int

Enter the event loop and block until there are no more pending events, or until loopbreak/loopexit is called. Returns 0 on normal exit, -1 on error.


nl_event_base_loop(base: int, flags: int) -> int

Like dispatch, but accepts flags. Pass 1 for EVLOOP_ONCE (process one batch then return) or 2 for EVLOOP_NONBLOCK (process only already-pending events).


nl_event_base_loopexit(base: int, timeout_secs: int) -> int

Schedule the event loop to exit after timeout_secs seconds. Pass 0 to exit as soon as the current callback returns.


nl_event_base_loopbreak(base: int) -> int

Break out of the event loop immediately, even if events are waiting. Returns 0 on success.

Event Base Information


nl_event_base_get_method(base: int) -> string

Return the name of the I/O notification method in use (e.g. "kqueue", "epoll").


nl_event_base_get_num_events(base: int) -> int

Return the number of events currently registered with the base.


nl_event_get_version() -> string

Return the libevent version string (e.g. "2.1.12-stable").


nl_event_get_version_number() -> int

Return the libevent version as a packed integer for programmatic comparison.

Timer Events


nl_evtimer_new(base: int) -> int

Create a new timer event attached to base. Returns a timer handle.


nl_evtimer_add_timeout(event: int, timeout_secs: int) -> int

Arm the timer to fire after timeout_secs seconds. Call again inside the callback to create a repeating timer. Returns 0 on success.


nl_event_del(event: int) -> int

Disarm the event (stop it from firing) without freeing it.


nl_event_free(event: int) -> void

Free the event object. Always call this after you are done with a timer.

Utilities


nl_event_enable_debug_mode() -> void

Enable libevent's internal debug mode. Call before creating any event base. Useful for diagnosing event lifecycle issues.


nl_event_sleep(base: int, seconds: int) -> int

Suspend execution for seconds seconds while keeping the event base alive.

Examples

Repeating Timer (Fixed-Rate Game Loop)

The key pattern for a fixed-rate game loop is to re-arm the timer at the end of each callback:


from "modules/event/event.nano" import nl_event_base_new, nl_event_base_dispatch,
                                        nl_event_base_free, nl_event_base_loopbreak,
                                        nl_evtimer_new, nl_evtimer_add_timeout,
                                        nl_event_free

let mut tick_count: int = 0
let mut game_timer: int = 0
let mut game_base: int = 0

fn on_tick() -> void {
    set tick_count (+ tick_count 1)
    (println (+ "tick " (int_to_string tick_count)))

    if (>= tick_count 5) {
        (nl_event_base_loopbreak game_base)
    } else {
        # Re-arm the timer for the next tick (1 second intervals)
        (nl_evtimer_add_timeout game_timer 1)
    }
}

shadow on_tick {
    set tick_count 0
    (on_tick)
    assert (== tick_count 1)
}

fn main() -> int {
    set game_base (nl_event_base_new)
    set game_timer (nl_evtimer_new game_base)
    (nl_evtimer_add_timeout game_timer 1)

    (nl_event_base_dispatch game_base)

    (nl_event_free game_timer)
    (nl_event_base_free game_base)
    return 0
}

shadow main { assert true }

Checking Available Events


from "modules/event/event.nano" import nl_event_base_new, nl_event_base_get_num_events,
                                        nl_evtimer_new, nl_event_base_free, nl_event_free,
                                        nl_event_base_get_method

fn show_event_info() -> void {
    let base: int = (nl_event_base_new)
    let method: string = (nl_event_base_get_method base)
    (println (+ "Backend: " method))

    let t: int = (nl_evtimer_new base)
    let n: int = (nl_event_base_get_num_events base)
    (println (+ "Registered events: " (int_to_string n)))

    (nl_event_free t)
    (nl_event_base_free base)
}

shadow show_event_info {
    (show_event_info)
}

---

**Previous:** Chapter 18 Overview

**Next:** 18.2 vector2d