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:
- Understand the structure of every libuv program (init, run, cleanup)
- Create and manage a timer handle (
uv_timer_t) - Register callbacks and understand when they’re invoked
- Explain why
uv_run()blocks and when it returns - Properly close handles to avoid resource leaks
- 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:
- Waits for I/O events or timer expirations
- Dispatches the appropriate callback when something happens
- 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:
- Initialized (e.g.,
uv_timer_init()) - Started with an operation (e.g.,
uv_timer_start()) - 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
- Initialize the default event loop
- Create and start a repeating timer (1-second interval)
- Print “Tick” each time the timer fires
- Stop the timer after 5 ticks
- Clean up resources and exit
3.3 Non-Functional Requirements
- No memory leaks (verified with Valgrind)
- Clean compilation with
-Wall -Wextra(no warnings) - 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:
- 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”
- It blocks until all handles are closed or
- 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”
- The loop has no more active handles, so
- 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:
- At what time does the 5th tick occur?
- When exactly does
uv_run()return? - 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
uv_default_loop()- Get the loopuv_timer_init(loop, &timer)- Initialize handleuv_timer_start(&timer, callback, timeout, repeat)- Start timeruv_run(loop, UV_RUN_DEFAULT)- Run until doneuv_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
- “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
- “What happens if a callback blocks for a long time?”
- The entire loop is blocked
- No other callbacks can run
- Timers will be delayed
- “How is libuv different from using
select()orpoll()?”- Cross-platform abstraction
- Higher-level handle/callback model
- Built-in thread pool for blocking operations
- “Why use an event loop instead of threads?”
- No synchronization needed
- No context switch overhead
- Simpler mental model for I/O-bound work
- “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
- What if timeout is 0? (Should fire immediately)
- What if repeat is 0? (Should fire once)
- 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
- Is libuv linked correctly? (
ldd ./timer) - Does
uv_timer_init()return 0? - Does
uv_timer_start()return 0? - Is the callback function signature correct?
- 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:
- Functional: Prints “Tick” 5 times at 1-second intervals
- Clean: No compiler warnings, no memory leaks
- Documented: Comments explaining key sections
- Understood: You can answer all interview questions from section 5.8
Bonus: Complete at least one extension from section 8.
Navigation
| Previous | Up | Next |
|---|---|---|
| - | README | P02: Async Cat Clone |