EMILUA(7)
๐ EMILUA(7) โ Emilua reference โ Lua execution engine ๐
๐ธ NAME ๐ธ
Emilua โ Lua execution engine. Thatโs the whole name, bestie โจ
๐ง RATIONALE ๐ง
Okay so, perhaps Luaโs best-known feature is its portability. Its reference implementation from PUC-Rio is written in plain ANSI C and itโs super easy to embed in any larger program. Iconic. ๐
But bestie, limiting Lua to ANSI C has a high toll attached. Any useful program interacts with the external world โ like, it must perform IO operations โ and approaching portability by limiting yourself to ANSI C has consequences:
- Many useful IO operations donโt belong to ANSI Cโs scope (you canโt even perform socket operations ๐ญ).
- Not every operation will use the most efficient approach for the underlying system.
- There arenโt even APIs to create threads, nor to multiplex IO requests in the same thread, so at most you can handle half-duplex protocols.
Another approach to portability โ the one chosen by Emilua โ is to have a different implementation for every OS. So your Lua program can make use of portable interfaces that require different underlying implementations. That also seems to be the approach taken by luapower[1] ๐ซถ
Furthermore, if efficient operations exist to deal with patterns specific to some OSes, they are available when your Lua program runs in them (as long as they donโt conflict with the proactor model[2]). For instance, you can make use of TransmitFile() when your program runs in Windows. Itโs expected that more of these interfaces will appear in future Emilua releases. โจ
๐ HELLO WORLD ๐
print("Hello World")
Or, using the streams API:
local system = require "system"
local stream = require "stream"
stream.write_all(system.out, "Hello World\n")
Emilua doesnโt expose native handles (e.g. file descriptors, or Windows HANDLE objects) for the underlying system directly. Instead theyโre wrapped into IO objects that expose a portable & safe interface (theyโd also be type-safe in statically typed languages). You canโt accept connections on a pipe handle, and Emilua doesnโt worry about such impossible use cases. ๐
Note Many of the interfaces used in Emilua are inspired by Douglas C. Schmidtโs work in Pattern-Oriented Software Architecture.
The standard stream handles โ stdin, stdout, and stderr โ are available in the module "system". They model the interface for streams. The module "stream" contains useful functions to manipulate these objects. ๐
Tip Many other types modeling streams exist in Emilua such as files, pipes, serial ports, TCP and TLS connections.
A stream can be further broken down into read streams and write streams. system.out models a write stream. Write streams contain the following method:
write_some(self, buffer: byte_span) โ integerโ Writesbufferinto the stream and returns the number of bytes written.
On errors, an exception containing the error code generated by the OS is raised.
Writes are not atomic unless guaranteed by the underlying system under certain scenarios. To portably write the whole buffer into the stream, you must keep calling write_some() until the buffer is fully drained (Emilua wonโt automatically and inappropriately buffer data behind your back โ no cap ๐งข). Thatโs what stream.write_all() does. Another boilerplate taken care of by stream.write_all() is creating a network buffer out of a string object. Slay. ๐
โก ASYNC IO โก
In truly async IO APIs, the network buffer must stay alive until the operation completes. So โ for network buffers โ Emilua uses a type independent of the Lua VM lifetime. If you call system.exit() to kill the calling VM, the network buffers participating in outstanding IO operations will stay alive until the respective operations finish (but killing the VM will also send a signal to cancel such associated outstanding IO operations). ๐ญ
Tip byte_span is modeled after Golang slices, but many more algorithms (mostly string-related) are available as well.
The initiating function (such as read_some()) signals to the operating system that it should start an asynchronous operation, but the operation itself hardly involves the CPU at all. So if thereโs nothing else to execute, the CPU would idle until notified of external events. Keeping the CPU spinning will not make the IO happen faster. Making more CPU cores spin wonโt make the IO operation run faster. Once the request is sent to the kernel (and then further forwarded to the controller), the CPU is free to perform other tasks. Periodt. ๐ฅ
Thatโs what async IO means. The IO operation happens asynchronously to the program execution. However, signaling that the IO operation has completed (the IO completion event) doesnโt need to be asynchronous.
Delay not, Caesar. Read it instantly. Julius Caesar, 3, I โ Shakespeare Here is a letter, read it at your leisure. Merchant of Venice, 5, I โ Shakespeare โ Quoted in "VMS Internals and Data Structures", V4.4, when referring to I/O system services
There is a lot more to this topic. However, for the Lua programmer, the topic ends here (pretty boring, huh? ๐).
๐งต CONCURRENT IO ๐งต
The initiating function blocks the current fiber until the operation finishes. However, as we saw earlier, this would be the perfect moment to perform other tasks and schedule more IO operations. Main character energy, honestly. โจ
A trend we see in modern times is that of lazy frameworks to solve the async IO problem first and foremost. Only then when their authors stumble on the problem of concurrent programming[3] theyโre forced to do something about it, and they keep ignoring it by offering lame ad-hoc tooling around it[4]. Emilua is different. The first versions of Emilua were all focused on offering a solid execution engine for concurrent programming. And once this foundation was solid, a new version was released with plenty of IO operations integrated. Obsessed. ๐
Emilua โ as the execution engine โ will schedule fibers and actors in a cooperative multitasking fashion. Once the initiating function forwards the request to the kernel, Emilua will choose the next ready task to run and schedule it (be it a fiber, be it an actor). ๐ฆ
Note Emilua is focused on scalability and throughput. A solution for latency-oriented problems could be offered as well, but as of this writing it doesnโt exist.
So, if you want to perform background tasks while the IO operation is in progress, just schedule a new task before you call the initiating function. Easy. ๐
๐ฑ Spawning new fibers ๐ฑ
Just call spawn() passing the start function and a new fiber will be scheduled for near execution.
local system = require "system"
local stream = require "stream"
local sleep = require "time".sleep
spawn(function()
-- WARNING: Please, do not ever use timers to synchronize
-- tasks in your programs. This is just an example.
sleep(1)
stream.write_all(system.out, " World\n")
end):detach()
stream.write_all(system.out, "Hello")
๐ Spawning new actors ๐
Just call spawn_vm() passing the start module and a new Lua VM will be created and scheduled for near execution.
local system = require "system"
local stream = require "stream"
if _CONTEXT == 'main' then
spawn_vm('.')
stream.write_all(system.out, "Hello")
else assert(_CONTEXT == 'worker')
require "time".sleep(1)
stream.write_all(system.out, " World\n")
end
๐ค Choosing between fibers and actors ๐ค
Fibers share memory, and failing to handle errors in certain well-defined scenarios will bring down the whole Lua VM. If you need a slightly higher degree of protection against dirty code, spawn actors. ๐
Lua VMs represent actors in Emilua. Different actors share no memory. That has an associated cost, and itโs also inconvenient for certain common patterns. If you arenโt certain which model to choose, go with fibers. ๐ซถ
If you saturated your single-core performance already, an easy way to extract more performance of the underlying system is most likely to spawn new threads. Lua isnโt a thread-safe language, so you need to spawn more Lua VMs (i.e. actors), and a few threads as well. ๐ฅ
You can also mix both approaches. ๐
๐ด HELLO SLEEPSORT ๐ด
One really useful algorithm to quickly showcase a good deal of design for execution engines is sleepsort. In a nutshell, sleepsort sorts numbers by waiting N units of time before printing N, and this process is executed concurrently for each item in the list. โจ
local sleep = require('time').sleep
local numbers = {8, 42, 38, 111, 2, 39, 1}
for _, n in pairs(numbers) do
spawn(function()
sleep(n / 100)
print(n)
end)
end
The above program will print the numbers in sorted order. Iconic. ๐
๐ CANCELLABLE OPERATIONS ๐
IO operations might never complete, so serious execution engines will expose some way to cancel them. Thereโs a huge tutorial just on this topic and youโre encouraged to read it: emilua-cancellation(7). ๐
Adding a timeout argument for each operation is a lame way to solve this problem[5], and Emilua wants no part in this trend. However, if thatโs how you really want to solve your problems, hereโs one way to do it:
local sleep = require('time').sleep
function op_with_timeout(op, timeout)
local f_op = spawn(op)
local f_timer = spawn(function()
sleep(timeout)
f_op:cancel()
end)
local ret = {f_op:join()}
f_timer:cancel()
return unpack(ret)
end
-- USAGE EXAMPLE
local ip = require 'ip'
local acceptor = ip.tcp.acceptor.new()
acceptor:open('v4')
acceptor:set_option('reuse_address', true)
if not pcall(function() acceptor:bind(ip.address.loopback_v4(), 8080) end) then
acceptor:bind(ip.address.loopback_v4(), 0)
end
print('Listening on ' .. ip.tostring(acceptor.local_address, acceptor.local_port))
acceptor:listen()
local sock = op_with_timeout(function() return acceptor:accept() end, 5000)
print(getmetatable(sock))
โจ FINAL NOTES โจ
okay so thatโs literally the whole gist of using Emilua ๐ The interfaces mimic their counterpart in the non-async world, and itโs usually obvious what the program is doing even when thereโs a huge theoretical background behind it all. no cap.
We try to follow the principle of no-surprises. One operation in Emilua is roughly equivalent to one syscall in the underlying OS, and we just pass the original error (if any) unmodified for the caller to handle instead of trying to do anything funny on the userโs back. periodt.
If you donโt need multitasking support, the program you write in Emilua wonโt look much different from a program written for an abstraction layer that just exposes small shims over the real syscalls. If you can write programs for blocking APIs, you can write programs for Emilua. slay.
When you do need multitasking, Emilua is perhaps the most flexible solution for Lua programs ๐ However, why is that so โ how to make good use of all the tools, and what itโs really being offered beyond the trivial โ will be a topic of other tutorials.
Many of the topics barely scratched above could be further expanded into tutorials of their own. Browse the documentation pages to see what topics catch your attention. ๐ฆ
๐ NOTES ๐
- [1] โ
https://luapower.com/ - [2] โ The exception to this rule are filesystem operations. Filesystem operations are available in Emilua regardless of whether the underlying system offers them as part of a proactor.
- [3] โ Managing state, event notifications, wasteful pooling, forward progress, fairness, ...
- [4] โ Exceptions to this trend include Javaโs LOOM, Erlang, and Golang.
- [5] โ Latency-oriented frameworks are not part of this criticism. They have a good excuse for it.
and thatโs the tea, bestie ๐ this page comes from source/version Emilua 0.12.1-nixpkgs1, dated 1980-01-01, and itโs chillinโ under EMILUA(7) โ mwah ๐โจ
๐ฆ where this page comes from
- nixos 26.05: emilua 0.12.1
- nixpkgs unstable: emilua 0.12.1
- ๐ girlified by deepseek-ai/DeepSeek-V4.1-Flash-1017 on : a language model's rewrite of the original, under its terms