A self-guided workshop for learning libuv, the C library that powers Node.js. Each exercise builds on the previous one, gradually introducing more advanced concepts around event loops, asynchronous I/O, networking, and threading.
All eight exercises have been completed successfully.
What it teaches: How to call basic libuv utility functions without an event loop. Shows how libuv follows a consistent error-checking convention (return values should always be checked) and how results are passed back via pointer arguments rather than return values.
Approach: Call uv_resident_set_memory to retrieve the RSS of the process, check the return value for errors, and report the result using log_report.
What it teaches: How to create and run a libuv event loop, and how to register an idle handler — a callback that libuv invokes whenever the loop has nothing else to do.
Approach: Initialize an idle handle with uv_idle_init, start it with uv_idle_start pointing at the provided callback, then create and run the default event loop with UV_RUN_DEFAULT.
What it teaches: The fundamentals of libuv's filesystem API. Introduces uv_fs_open, uv_buf_init, and uv_fs_read, and explains that passing NULL as the callback is what makes these operations execute synchronously.
Approach: Open the file synchronously, allocate and initialize a buffer with uv_buf_init, read from the file descriptor into the buffer, report the contents, then close the file and clean up all request objects.
What it teaches: How to introduce asynchronous behaviour into the filesystem workflow. Unlike the previous step where we passed NULL as the callback, we see here that callback-driven libuv calls help to execute actions asynchronously.
Approach: Open the file synchronously, set up the buffer, then call uv_fs_read with a real callback (read_cb). Inside the callback, report the data, close the file descriptor synchronously, and clean up all three request objects.
What it teaches: The data field pattern — a fundamental libuv technique for threading arbitrary state through asynchronous callbacks. Because automatic (stack-allocated) variables are freed when a function returns, heap allocation with malloc is required for objects that are referenced beyond the scope of the function where they are created and must outlive the function.
Approach: Define a context_t struct to hold the dynamically allocated request objects. Allocate the context and requests with malloc in the init function, attach the context to read_req->data, then recover it inside read_cb by casting read_req->data back to context_t*.
What it teaches: How to make the entire file operation pipeline fully asynchronous — open, read, and close all happen via callbacks. The context struct grows to track both open_req and read_req.
Approach: Chain the callbacks so that open_cb initiates the async read, read_cb reports data and initiates the async close, and close_cb frees all allocated memory. The context is heap-allocated in init and flows through all three callbacks via the data field.
What it teaches: How to build a TCP server in C using libuv's networking API. Covers TCP handle initialization, binding to an address, listening for connections, accepting clients, reading from a stream, writing back to it, and graceful shutdown. Also demonstrates the C struct-embedding ("inheritance") technique libuv uses so that a uv_tcp_t can be passed wherever a uv_stream_t is expected.
Approach: Initialize a uv_tcp_t handle, resolve the bind address with uv_ip4_addr, bind and then call uv_listen. In the connection callback, accept the client with uv_accept, start reading with uv_read_start, and in the read callback echo data back with uv_write. A write_req_t wrapper struct embeds uv_write_t alongside the buffer so the buffer can be recovered and freed in the write callback. Sending QUIT shuts the server down cleanly.
What it teaches: How libuv's thread pool and async handles let CPU-intensive work run on background threads without blocking the event loop. This prevents heavy computation from starving other tasks on the main thread. The exercise also shows how to safely dispatch progress updates from worker threads back to the main thread for UI rendering.
Approach: For each horse, call uv_async_init to create an async handle and uv_queue_work to dispatch the work function to a thread pool thread. Inside the work function (which runs on a background thread), call uv_async_send after each movement step to signal the main thread. The main thread's progress_cb then uses ncurses to redraw the horse at its updated position, avoiding cross-thread rendering issues. The finish_cb fires on the main thread when a horse's work is complete.
