# luv and the case for reaching libuv from Lua without writing C

> luv is a thin C binding that puts libuv behind a Lua API, one uv_loop per lua_State, with handles you must close by hand. It supports Lua 5.1 through 5.4 and LuaJIT, and the build system offers module, static and shared shapes.

**luvit/luv** — Bare libuv bindings for lua

- Repository: https://github.com/luvit/luv
- Stars: 948 · Forks: 202
- Language: C
- License: Apache-2.0
- Published: 2026-10-20 · Updated: 2026-10-20 · Language: en
- Canonical page: https://hysenlabs.com/projects/luvit-luv

## One uv_loop per lua_State, and the rule against sharing handles

luv makes libuv available to Lua scripts. The README describes it as bare libuv bindings for luajit and for lua 5.1, 5.2, 5.3 and 5.4, and it says plainly that the library was made for the luvit project but should be usable from nearly any Lua project.

The most important line in the README is about concurrency: the library can be used by multiple threads at once, but each thread is assumed to load the library from a different lua_State, and luv creates a unique uv_loop_t for each state. You cannot share uv handles between states or loops.

That is a hard boundary rather than a caveat, and it shapes how a threaded Lua program has to be written. The natural model of one shared event loop with several workers pushing work into it does not apply here, because the loop belongs to the state. A program that wants concurrency has to give each thread its own state and its own loop, and communicate between them by whatever means the Lua side provides, not by passing handles.

The API is close to libuv's own, which is the point of the binding. A handle is created with a constructor such as uv.new_timer() or uv.new_tcp(), and then the same handle types, start, close and read_start methods appear in Lua. The documentation lives at docs/docs.md, with the libuv documentation at docs.libuv.org as the reference underneath.

The primary language is C. The licence field on the repository says Apache-2.0, the repository is not archived, and the last push was on 2026-10-15.

## Handles must be closed by hand, because the collector cannot see libuv

There is one rule in the README that catches out every new user: you must always close your uv handles or you leak memory, and you cannot depend on the garbage collector since it does not know enough about libuv.

That is a real design constraint of the binding, not a style preference. A Lua object that goes out of scope and is collected still owns a libuv handle, and libuv will not release it, because the collector has no idea that handle exists. The consequence is that luv scripts need explicit lifecycle discipline that ordinary Lua code does not: every timer started, every socket opened, every file handle opened needs a matching close on every path, including error paths, because a Lua error thrown between start and close leaves the handle open.

The same discipline shows up in the TCP echo example in the README. The server handle is closed on the client side, the client is closed when the stream ends, and the accept callback asserts that there was no problem with the listen setup before it proceeds. Error handling in libuv is callback-based, so the pattern is: check the error argument first, then act on the data.

Two habits follow from this. First, prefer the library's own error checks over asserts in production code, since an assert in a callback kills the process. Second, remember that a handle closed inside a callback and a handle closed at shutdown are not the same thing, because the loop only returns when there are no longer any active handles, and a forgotten handle will keep uv.run() blocked forever. That failure looks like a hang, and it is the most common way luv-based scripts get stuck.

## Installing through Luarocks, and the uv.lua name shim

The quick path is Luarocks, where the module is published under the creationix user.

```bash
luarocks install luv
```

There is a naming wrinkle worth catching early. The README notes that to require the module as `require 'uv'`, which keeps compatibility with how luv is required in luvi, you should create a uv.lua file with a single line in it.

The file contains one line, `return require 'luv'`, and from then on `require 'uv'` resolves the same library.

So there are two valid names for the same library. `require 'luv'` is the direct form, and `require 'uv'` works once you add the shim file. If you are porting code from the luvit ecosystem, this is the line that decides whether the port works on the first try.

For a first real use, the smallest complete program is a timer. You create a timer handle, start it with a delay and a repeat count of zero, and pass a callback that runs after the delay. The callback closes the handle. Then you call uv.run(), which blocks and waits for all events to run, and it returns when there are no longer any active handles. Print a line before the call so you can see the ordering: the program prints its message, blocks in the loop, then the callback fires later and prints again.

The repository has a larger set of examples to read, including a TCP echo server and client, a cqueues pair with a main and a slave, timers, a REPL, a cluster example, a DNS stress example, a child-process example and a directory called uvbook that follows the libuv book.

## Building from source: three submodules, CMake and a Makefile wrapper

The source build is documented step by step, and the prerequisites are spelled out rather than assumed. On Linux that means gcc and make, with the build-essential package suggested on Ubuntu. On macOS the recommendation is XCode, which brings clang and make. On Windows the free Visual Studio Express works, and the README warns that the 2013 edition needs the Windows Desktop edition because the plain Windows edition has no working C compiler. CMake and git are also required, and on Windows cmake has to be added to the command prompt path.

The clone pulls the dependencies as submodules.

```bash
git clone https://github.com/luvit/luv.git --recursive
cd luv
```

The recursive flag matters because the Makefile has explicit rules that initialise the submodules for libuv, luajit and lua-compat-5.3, and those rules are what feed the CMake configuration step.

On unix systems the test and build entry point is the Makefile. The README shows it at a shell prompt.

```bash
~/Code/luv> make test
```

What that target does is worth knowing, because it is more than one command. The Makefile sets BUILD_MODULE to ON, BUILD_SHARED_LIBS and BUILD_STATIC_LIBS to OFF, WITH_SHARED_LIBUV to OFF, WITH_LUA_ENGINE to LuaJIT, LUA_BUILD_TYPE to Static and BUILD_TYPE to RelWithDebInfo, then translates those into CMake options and runs cmake -H. -B$(BUILD_DIR). The test target then runs the Lua test suite through the built interpreter, runs the CTest suite in the build directory, and finally executes a manual test for an external loop. The default build produces luv as a module library, which the README explains are plugins that are not linked into other targets.

On Windows the README points to a batch file written for that platform.

```bash
C:\Code\luv> msvcbuild.bat
C:\Code\luv> luajit tests\run.lua
```

The batch file runs the correct CMake commands and copies the output files for easy access, and the second line runs the Lua test suite with LuaJIT directly.

## Six build shapes, chosen by make variables

The build matrix is where this project is more flexible than the bare description suggests, and the variables are ordinary make variables you set on the command line.

To link against PUC Lua 5.4 instead of the default LuaJIT, the README gives one variable.

```bash
~/Code/luv> WITH_LUA_ENGINE=Lua make
```

To produce a static library instead of a loadable module, two variables are needed, and the result is libluv_a.a.

```bash
~/Code/luv> BUILD_MODULE=OFF BUILD_STATIC_LIBS=ON make
```

To produce a shared library, the shape is mirrored, and the result is libluv.so.

```bash
~/Code/luv> BUILD_MODULE=OFF BUILD_SHARED_LIBS=ON make
```

By default the build uses the supplied dependencies, which are libuv and LuaJIT or Lua. If the target system already has those installed, luv can link against them instead. Linking with a shared libuv takes one variable, because the default shared library name for libuv is libuv.

```bash
~/Code/luv> WITH_SHARED_LIBUV=ON make
```

Linking with a shared LuaJIT uses LUA_BUILD_TYPE=System, since the default shared library name for LuaJIT is libluajit-5.1.

```bash
~/Code/luv> LUA_BUILD_TYPE=System make
```

And linking with a shared Lua 5.x combines the two, because the default shared library name for Lua 5.x is liblua5.x.

```bash
~/Code/luv> LUA_BUILD_TYPE=System WITH_LUA_ENGINE=Lua make
```

The practical reading is that luv is usable as a loadable module, as a static archive for embedding in a host application, or as a shared object for a plugin system, and the host Lua runtime can be either LuaJIT or PUC Lua, either bundled or taken from the system. The Makefile also supports WITHOUT_AMALG, which passes -DWITH_AMALG=OFF, and the file libluv.pc.in suggests a pkg-config file is generated for the shared build.

## The test target runs a Lua suite, CTest and an external loop test

The test story in the Makefile is short enough to quote, and it is worth understanding what a green run covers.

```bash
${LUABIN} tests/run.lua
$(MAKE) -C $(BUILD_DIR) test
$(BUILD_DIR)/test tests/manual-test-external-loop.lua
```

Three things happen. The Lua test suite runs under the interpreter that was just built, which is the bundled luajit or lua binary in the build directory, selected by the WITH_LUA_ENGINE variable. The CTest suite in the build directory runs next, which covers the C side of the binding. Finally a compiled test binary runs tests/manual-test-external-loop.lua, which exercises the case where the script drives a loop that lives outside the library.

That last step is the one that catches integration mistakes, and the README's own test and examples directories are the reference for how the API is meant to be driven.

The CI configuration lives in a .ci directory and the repository also carries appveyor.yml for Windows and a CMakeLists.txt with a cmake directory, so the build is driven by CMake with the Makefile acting as a thin, documented wrapper that translates friendly variables into CMake options.

One more piece of the layout is worth noting for packaging work: luv-scm-0.rockspec and a rockspecs directory exist at the top level, and the module is published under the creationix user on Luarocks, so the rock definition and the source build are kept in the same repository.

## Release names track libuv, and the last push was on 2026-10-15

The release tags follow the libuv version with an extra luv revision suffix, and the recent list shows 1.53.0-0 released on 2026-10-01, 1.52.1-0 on 2026-03-16 and 1.51.0-2 on 2026-01-18. The Makefile also derives a tag from git describe --tags, so the version is meant to be stamped from the repository state rather than typed by hand.

The gap between the 2026-03-16 and 2026-10-01 releases is six months, which tells you the cadence is tied to libuv releases rather than to a schedule of the project's own. That is a sensible design for a binding whose entire job is to track an upstream C library, and it also means your upgrade decisions are libuv's decisions.

The licence is Apache-2.0, recorded in LICENSE.txt. For a C library linking into a host application, that is a permissive choice with no copyleft obligation, and it is the least complicated part of adopting luv.

The repository is not archived and the last push was on 2026-10-15, shortly after the 1.53.0-0 tag. Maintenance therefore looks tied to upstream, which suits a thin binding and means the risk of luv-specific bugs is low, while the risk of a libuv API change landing in your build is the one to watch. The topics attached to the repository, including hacktoberfest and luajit, match a project whose contributors arrive through the Lua and luvit communities rather than through a large standalone team.

## Conclusion

luv fits Lua and LuaJIT projects that need an event loop with sockets, timers, files and DNS without writing a C extension from scratch, and it fits teams that already deploy luvit and want the same bindings in a plain Lua process. It does not fit projects that want garbage collection to clean up sockets, because libuv handles are outside the collector's view, and it does not fit Lua 5.4 users who expect the default LuaJIT build to cover them, since PUC Lua needs an explicit engine choice at build time. Verify three things before you commit: that your Lua version is one of the supported ones, that the module name your code expects matches the uv.lua shim, and that every handle you allocate is closed on every path.

## FAQ

### How do I install luv?

Through Luarocks, with `luarocks install luv`. The module is published on Luarocks under the creationix user, and it is also built from source with CMake through the repository Makefile.

### How do I require luv as uv in Lua?

Create a uv.lua file containing `return require 'luv'`. The README gives this as the way to keep compatibility with how luv is required in luvi.

### Can I share libuv handles between Lua threads?

No. Each thread is assumed to load the library from a different lua_State, luv creates a unique uv_loop_t for each state, and handles cannot be shared between states or loops.

### How do I build luv against PUC Lua 5.4 instead of LuaJIT?

Run make with the engine variable set, so `WITH_LUA_ENGINE=Lua make`. By default luv is linked with LuaJIT 2.1.0-beta3, and LUA_BUILD_TYPE=System additionally links against an installed shared liblua5.x.

### Why does my luv script hang after the work is finished?

uv.run() only returns when there are no longer any active handles, and you must always close your uv handles because the garbage collector does not know enough about libuv to release them. A handle that is never closed keeps the loop blocked.

## Sources

- [Issues](https://github.com/luvit/luv/issues)
- [License: Apache-2.0](https://github.com/luvit/luv/blob/master/LICENSE)
- [luvit/luv on GitHub](https://github.com/luvit/luv)
- [README](https://github.com/luvit/luv/blob/master/README.md)
- [Releases](https://github.com/luvit/luv/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/luvit-luv
