Project 1: The Event Loop “Hello, World”

Build the simplest possible libuv program to understand the fundamental concepts of event loops, handles, and callbacks.

Quick Reference

Attribute Value
Difficulty Beginner
Time Estimate Few hours
Language C
Prerequisites Basic C (pointers, functions)
Key Topics Event loop, timers, handles, callbacks

1. Learning Objectives

By completing this project, you will:

  1. Understand the structure of every libuv program (init, run, cleanup)
  2. Create and manage a timer handle (uv_timer_t)
  3. Register callbacks and understand when they’re invoked
  4. Explain why uv_run() blocks and when it returns
  5. Properly close handles to avoid resource leaks
  6. Debug basic libuv programs using print statements and error checks

2. Theoretical Foundation

2.1 Core Concepts

The Event Loop

The event loop is the heart of libuv and all async I/O systems. It’s a single-threaded mechanism that:

  1. Waits for I/O events or timer expirations
  2. Dispatches the appropriate callback when something happens
  3. Repeats until there are no more pending operations
┌────────────────────────────────────────────────────────────────┐
│                     Event Loop Iteration                        │
├────────────────────────────────────────────────────────────────┤
│                                                                 │
│   ┌─────────────┐    Are there active     ┌──────────────┐    │
│   │   Start     │──────handles or ──────────│  Stop Loop   │    │
│   │ Iteration   │       requests?   NO     │   (return)   │    │
│   └──────┬──────┘                          └──────────────┘    │
│          │ YES                                                  │
│          ▼                                                      │
│   ┌─────────────┐                                              │
│   │ Run timers  │ ◄── Execute callbacks for expired timers    │
│   └──────┬──────┘                                              │
│          │                                                      │
│          ▼                                                      │
│   ┌─────────────┐                                              │
│   │ Run pending │ ◄── Execute deferred callbacks               │
│   │  callbacks  │                                              │
│   └──────┬──────┘                                              │
│          │                                                      │
│          ▼                                                      │
│   ┌─────────────┐                                              │
│   │  Poll for   │ ◄── Block waiting for I/O or timeout        │
│   │     I/O     │                                              │
│   └──────┬──────┘                                              │
│          │                                                      │
│          ▼                                                      │
│   ┌─────────────┐                                              │
│   │ Run check   │ ◄── setImmediate callbacks in Node.js       │
│   │  callbacks  │                                              │
│   └──────┬──────┘                                              │
│          │                                                      │
│          ▼                                                      │
│   ┌─────────────┐                                              │
│   │ Run close   │ ◄── Handle close callbacks                   │
│   │  callbacks  │                                              │
│   └──────┬──────┘                                              │
│          │                                                      │
│          └──────────────► Next Iteration                        │
│                                                                 │
└────────────────────────────────────────────────────────────────┘

Handles

A handle is a long-lived object that represents a resource:

  • Timers (uv_timer_t)
  • TCP sockets (uv_tcp_t)
  • Pipes (uv_pipe_t)
  • TTY terminals (uv_tty_t)

Every handle must be:

  1. Initialized (e.g., uv_timer_init())
  2. Started with an operation (e.g., uv_timer_start())
  3. Closed when done (uv_close())

Callbacks

A callback is a function pointer you provide to libuv. When an event occurs (timer fires, data arrives), libuv calls your function:

// Callback type for timers
typedef void (*uv_timer_cb)(uv_timer_t* handle);

// Your callback implementation
void my_timer_callback(uv_timer_t* handle) {
    printf("Timer fired!\n");
}

2.2 Why This Matters

The event loop pattern is foundational to modern async programming:

Technology Uses Event Loop
Node.js libuv event loop directly
Python asyncio Based on event loop concept
JavaScript browsers DOM event loop
Rust tokio Similar design
Go runtime Similar goroutine scheduling

Understanding libuv’s event loop helps you understand all async systems.

2.3 Historical Context

  • 1999: Dan Kegel publishes “The C10K Problem” - how to handle 10,000 concurrent connections
  • 2002: Linux adds epoll, a scalable I/O event notification mechanism
  • 2009: Ryan Dahl creates Node.js, needs cross-platform async I/O
  • 2011: libuv extracted from Node.js as a standalone library
  • Today: libuv handles millions of concurrent connections in production

2.4 Common Misconceptions

Misconception Reality
“Event loops use multiple threads” The loop itself is single-threaded; only blocking operations may use a thread pool
“Callbacks run in parallel” Callbacks run one at a time, sequentially
“The loop polls continuously” It efficiently blocks, waiting for events
“You can call sleep() in callbacks” Blocking in a callback blocks the entire loop

3. Project Specification

3.1 What You Will Build

A timer that prints “Tick” every second for 5 seconds, then gracefully exits.

3.2 Functional Requirements

  1. Initialize the default event loop
  2. Create and start a repeating timer (1-second interval)
  3. Print “Tick” each time the timer fires
  4. Stop the timer after 5 ticks
  5. Clean up resources and exit

3.3 Non-Functional Requirements

  1. No memory leaks (verified with Valgrind)
  2. Clean compilation with -Wall -Wextra (no warnings)
  3. Graceful exit (program terminates, doesn’t hang)

3.4 Example Usage / Output

$ ./timer
Timer started...
Tick
Tick
Tick
Tick
Tick
Program exiting.

Timing: Each “Tick” should appear approximately 1 second apart.

3.5 Real World Outcome

A working libuv program that demonstrates you understand:

  • Event loop lifecycle
  • Handle management
  • Callback registration
  • Clean shutdown

This is the foundation for every libuv program you’ll ever write.


4. Solution Architecture

4.1 High-Level Design

┌──────────────────────────────────────────────────────────────┐
│                          main()                              │
├──────────────────────────────────────────────────────────────┤
│                                                              │
│   1. uv_default_loop()     ──► Get event loop reference      │
│                                                              │
│   2. uv_timer_init()       ──► Initialize timer handle       │
│                                                              │
│   3. uv_timer_start()      ──► Start timer, register callback│
│                                                              │
│   4. uv_run()              ──► Run loop (BLOCKS)             │
│                                        │                     │
│                                        ▼                     │
│                            ┌─────────────────────┐           │
│                            │  timer_callback()   │           │
│                            │  - Print "Tick"     │           │
│                            │  - Increment counter│           │
│                            │  - If count >= 5:   │           │
│                            │    uv_timer_stop()  │           │
│                            └─────────────────────┘           │
│                                        │                     │
│                                        ▼                     │
│   5. uv_close()            ◄── Loop exits (no active handles)│
│                                                              │
│   6. return 0              ──► Clean exit                    │
│                                                              │
└──────────────────────────────────────────────────────────────┘

4.2 Key Components

Component Type Purpose
loop uv_loop_t* Reference to the event loop
timer_handle uv_timer_t The timer resource
counter int64_t Tracks number of ticks
timer_callback Function Called on each tick

4.3 Data Structures

// Timer handle structure (simplified view)
struct uv_timer_s {
    // Base handle fields
    void* data;           // User-defined data pointer
    uv_loop_t* loop;      // Loop this handle belongs to
    uv_handle_type type;  // UV_TIMER

    // Timer-specific fields
    uv_timer_cb timer_cb; // Your callback function
    uint64_t timeout;     // When to first fire
    uint64_t repeat;      // Repeat interval (0 = once)
    // ... internal fields ...
};

4.4 Algorithm Overview

ALGORITHM: Timer Tick Program

INPUT: None
OUTPUT: "Tick" printed 5 times, program exits

1. INITIALIZE
   - Get default loop
   - Create timer handle
   - Start timer (delay=1000ms, repeat=1000ms)

2. RUN LOOP
   - Loop blocks waiting for timer
   - Timer fires every 1000ms
   - Execute callback

3. CALLBACK LOGIC
   - Increment counter
   - Print "Tick"
   - IF counter >= 5:
       Stop timer
       (Loop will exit on next iteration)

4. CLEANUP
   - Close timer handle
   - Print exit message
   - Return success

5. Implementation Guide

5.1 Development Environment Setup

# Create project directory
mkdir timer-project && cd timer-project

# Create source file
touch main.c

# Create Makefile
cat > Makefile << 'EOF'
CC = gcc
CFLAGS = -Wall -Wextra -g $(shell pkg-config --cflags libuv)
LDFLAGS = $(shell pkg-config --libs libuv)

timer: main.c
	$(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS)

clean:
	rm -f timer

.PHONY: clean
EOF

5.2 Project Structure

timer-project/
├── Makefile
└── main.c

5.3 The Core Question You’re Answering

How does an event-driven program maintain control flow without explicit loops or blocking waits?

The answer: The event loop manages the waiting, and your callbacks define the behavior.

5.4 Concepts You Must Understand First

Before coding, answer these questions:

  1. What happens when you call uv_run()?
    • It blocks until all handles are closed or uv_stop() is called
    • Reference: libuv docs, “Running the loop”
  2. Why does stopping the timer cause the program to exit?
    • The loop has no more active handles, so uv_run() returns
    • Reference: libuv docs, “Reference counting”
  3. What’s the difference between a handle and a request?
    • Handles are long-lived; requests are for single operations
    • Reference: libuv docs, “Design overview”

5.5 Questions to Guide Your Design

Initialization:

  • How do you get a reference to the default loop?
  • What happens if uv_timer_init() fails?

Callback Design:

  • How will the callback know how many times it has been called?
  • What happens if the callback takes longer than 1 second?

Cleanup:

  • What happens if you forget to call uv_close()?
  • Why does uv_close() take a callback parameter?

5.6 Thinking Exercise

Before writing any code, trace through this scenario:

Time 0ms:    main() starts
             uv_timer_init() called
             uv_timer_start(timeout=1000, repeat=1000) called
             uv_run() called - BLOCKS

Time 1000ms: Timer expires
             timer_callback() invoked
             Prints "Tick", counter = 1
             Callback returns
             Loop continues

Time 2000ms: Timer expires again
             timer_callback() invoked
             Prints "Tick", counter = 2
             Callback returns

...continue tracing until exit...

Questions:

  1. At what time does the 5th tick occur?
  2. When exactly does uv_run() return?
  3. What would happen if you called uv_close() inside the callback?

5.7 Hints in Layers

Hint 1: Starting Point

You need these headers:

#include <stdio.h>
#include <uv.h>

Your callback function signature must match uv_timer_cb:

void timer_callback(uv_timer_t *handle);

Hint 2: Key Function Calls

  1. uv_default_loop() - Get the loop
  2. uv_timer_init(loop, &timer) - Initialize handle
  3. uv_timer_start(&timer, callback, timeout, repeat) - Start timer
  4. uv_run(loop, UV_RUN_DEFAULT) - Run until done
  5. uv_timer_stop(&timer) - Stop when counter reaches 5

Hint 3: Counter Strategy

Use a global variable for simplicity:

static int64_t counter = 0;

Or use the handle’s data field for encapsulation:

timer_handle.data = &counter;
// In callback:
int64_t *count = (int64_t*)handle->data;

Hint 4: Debugging Techniques

Add error checking:

int r = uv_timer_init(loop, &timer);
if (r != 0) {
    fprintf(stderr, "Init error: %s\n", uv_strerror(r));
    return 1;
}

Use Valgrind to check for leaks:

valgrind --leak-check=full ./timer

5.8 The Interview Questions They’ll Ask

  1. “Explain how the libuv event loop works.”
    • Single-threaded loop that waits for I/O/timers
    • Dispatches callbacks when events occur
    • Exits when no active handles remain
  2. “What happens if a callback blocks for a long time?”
    • The entire loop is blocked
    • No other callbacks can run
    • Timers will be delayed
  3. “How is libuv different from using select() or poll()?”
    • Cross-platform abstraction
    • Higher-level handle/callback model
    • Built-in thread pool for blocking operations
  4. “Why use an event loop instead of threads?”
    • No synchronization needed
    • No context switch overhead
    • Simpler mental model for I/O-bound work
  5. “What’s the relationship between libuv and Node.js?”
    • Node.js is built on libuv
    • JavaScript’s async/await compiles to libuv callbacks
    • libuv provides the cross-platform I/O layer

5.9 Books That Will Help

Topic Book Chapter
libuv fundamentals An Introduction to libuv Chapters 1-3
Event-driven programming Advanced Programming in UNIX Chapter 14 (Signal handling)
C programming C Programming: A Modern Approach Chapters 17, 22 (Pointers, I/O)

5.10 Implementation Phases

Phase 1: Compile and Run (30 minutes)

Goal: Get a minimal program that compiles and runs with libuv.

#include <stdio.h>
#include <uv.h>

int main() {
    uv_loop_t *loop = uv_default_loop();
    printf("Loop created\n");
    uv_run(loop, UV_RUN_DEFAULT);
    printf("Loop finished\n");
    return 0;
}

This should compile and immediately exit (no active handles).

Phase 2: Add Timer (1 hour)

Goal: Timer fires once.

Add:

  • Timer handle declaration
  • Timer initialization
  • Timer start with callback
  • Simple callback that prints

Phase 3: Repeat and Count (30 minutes)

Goal: Timer fires 5 times.

Add:

  • Counter variable
  • Logic to stop after 5 ticks

Phase 4: Clean Shutdown (30 minutes)

Goal: Proper cleanup.

Add:

  • uv_close() call
  • Verify no memory leaks with Valgrind

5.11 Key Implementation Decisions

Decision Options Recommendation
Loop type Default vs custom Default (simpler for now)
Counter storage Global vs handle->data Global (simpler for beginners)
Timer stop Inside callback vs after Inside callback (immediate)
Close callback NULL vs function NULL (no cleanup needed)

6. Testing Strategy

Manual Testing

# Compile
make

# Run and observe output
./timer

# Verify timing (should take ~5 seconds)
time ./timer

# Check for memory leaks
valgrind --leak-check=full ./timer

Expected Valgrind Output

==12345== HEAP SUMMARY:
==12345==     in use at exit: 0 bytes in 0 blocks
==12345==   total heap usage: X allocs, X frees, Y bytes allocated
==12345==
==12345== All heap blocks were freed -- no leaks are possible

Edge Cases to Test

  1. What if timeout is 0? (Should fire immediately)
  2. What if repeat is 0? (Should fire once)
  3. What if you never call uv_timer_stop()? (Loop runs forever)

7. Common Pitfalls & Debugging

Problem Symptom Root Cause Fix
Program hangs Never exits Timer not stopped Call uv_timer_stop()
Segfault Crash Handle not initialized Call uv_timer_init() first
Memory leak Valgrind reports Handle not closed Call uv_close()
No output Nothing prints Callback not registered Check uv_timer_start() call
Wrong timing Ticks too fast/slow Wrong timeout value Timeout is in milliseconds

Debugging Checklist

  1. Is libuv linked correctly? (ldd ./timer)
  2. Does uv_timer_init() return 0?
  3. Does uv_timer_start() return 0?
  4. Is the callback function signature correct?
  5. Is the loop actually running? (Add debug prints)

8. Extensions & Challenges

Extension 1: Multiple Timers

Create two timers with different intervals (500ms and 1000ms).

Challenge: Which timer fires first? How do they interleave?

Extension 2: User Data

Store the counter in handle->data instead of a global variable.

Challenge: How do you safely cast the data pointer?

Extension 3: Graceful SIGINT

Handle Ctrl+C to stop the program gracefully using uv_signal_t.

Challenge: How do you stop the timer from the signal handler?

Extension 4: Custom Loop

Create your own loop with uv_loop_init() instead of using the default.

Challenge: What cleanup is required for a custom loop?


9. Real-World Connections

How Node.js Uses This

// JavaScript
setTimeout(() => console.log("Tick"), 1000);

// Under the hood (simplified)
// 1. V8 calls into Node.js binding
// 2. Node.js calls uv_timer_start()
// 3. libuv handles the timing
// 4. libuv calls the C callback
// 5. C callback triggers JavaScript callback

Production Timer Patterns

Use Case Pattern
Health checks Timer that pings services every 30s
Token refresh Timer that refreshes auth tokens before expiry
Batch processing Timer that flushes write buffer every 100ms
Watchdog Timer that kills hung operations

10. Resources

Documentation

Code Examples

Video Resources

  • “Understanding the Node.js Event Loop” (YouTube)
  • “libuv internals” (YouTube)

11. Self-Assessment Checklist

Before moving to Project 2, verify:

  • Your program compiles without warnings
  • Output matches expected format
  • Program exits after 5 ticks (doesn’t hang)
  • Valgrind shows no memory leaks
  • You can explain why uv_run() blocks
  • You can explain why stopping the timer causes exit
  • You understand the callback signature
  • You know the difference between timeout and repeat parameters

12. Submission / Completion Criteria

Your project is complete when:

  1. Functional: Prints “Tick” 5 times at 1-second intervals
  2. Clean: No compiler warnings, no memory leaks
  3. Documented: Comments explaining key sections
  4. Understood: You can answer all interview questions from section 5.8

Bonus: Complete at least one extension from section 8.


Previous Up Next
- README P02: Async Cat Clone