Modules

Emilua has its own module system. It may look familiar, and indeed it is the intention. Given the fact that other libraries on the wild will have incompatible execution models, compatibility with existing lua libraries is not a concern (although it is most likely to just work for libraries w/o advanced needs).

The module system is highly inspired by the Rust packaging system. The two languages, however, are too different and these differences impact the module system as well. To import a module in dynamic languages such as lua, Python and JavaScript, it is to evaluate/execute source code. Rust doesn’t have this constraint and Rust gets just fine with a lot of static analysis. The two languages live in separate worlds. Finally, the module system is also inspired by what Python and NodeJS do.

A module system is meant to isolate pieces of code, symbols and names. One module should not interfere with each other. And a module can have dependencies on other modules to reuse code. So, there is the need for private members and exported members. Lua has all features we need — closures, nested scopes, environments, global scope as a table — to implement a module system easily.

Quick-start

The things you need to know to get started:

  • require() is a free function receiving a string with the module id and returning the module. Two imports to the same module will only evaluate it once. The result is cached per running VM instance.

  • Every file you write is a module.

  • Global names will be exported for modules that import your module.

  • Modules can also be directories. In this case, a file named init.lua will be searched and imported in that directory. init.lua can import any other module inside its directory.

  • Cyclic references are unsupported and will raise an error on import.

  • You can use the syntax require('../foobar') to import a sibling module named foobar.

  • If the module id doesn’t start with './' or '../' then it is assumed to refer to an external package and different rules apply (see section at the end).

Small example

File src/init.lua:

local server = require('./server')

local hostname = '127.0.0.1'
local port = 3000

local s = server.new(function(sock, req, res)
  res.headers = {
    ['content-type'] = 'text/plain'
  }
  res.body = 'Hello World\n'
  sock:write_response(res)
end)

s:listen(hostname, port)

File src/server.lua:

local ip = require('ip')
local http = require('http')

local mt = {}
mt.__index = mt

function new(handler)
  return setmetatable({ handler = handler }, mt)
end

function mt:listen(hostname, port)
  local acceptor = ip.tcp.acceptor.new()
  acceptor:open(ip.address.new(hostname))
  acceptor:bind(hostname, port)
  acceptor:listen()
  spawn(function()
    while true do
      local s = http.socket.new(acceptor:accept())
      spawn(function()
        local req = http.request.new()
        local res = http.response.new()

        while true do
          s:read_request(req)
          res.status = 200
          res.reason = 'OK'
          res.headers = nil
          res.body = nil
          res.trailers = nil
          self.handler(s, req, res)
        end
      end)
    end
  end):detach()
end

Big modules

A typical project structure may look as follows:

src
├── init.lua
├── my_module
│   ├── error.lua
│   ├── init.lua
│   ├── util.lua
│   └── worker.lua
└── util.lua

In this example, there is the project scope whose root begins at src/init.lua — the root module.

In the root module, it is forbidden to use require('../') statements as there is no parent module. Any name the src/init.lua file require()s will be searched on the src directory. For instance, if src/init.lua contains require('./util'), emilua will use the src/util.lua file to define the importing module.

But modules may grow and can be further split into files within a directory by itself. That was the case for my_module. The init.lua file in that directory will be searched for, and, once found, evaluated. If src/my_module/init.lua contains more require() calls whose arguments start with './', files within that directory (src/my_module) will be searched for.

For instance, if src/my_module/init.lua contains require('./worker'), the file src/my_module/worker.lua will be searched for. Any file (except for init.lua) within src/my_module can import other files from the same directory (i.e. their siblings) using the require('../') form (src/my_module/init.lua siblings live in the directory above, src). For instance, src/my_module/worker.lua and src/my_module/util.lua may both want to use the same error type (possibly private) to that module — src/my_module/error.lua. In this case, all they need to contain is the call require('../error'). And finally due to how they are defined by files (not directories by themselves), they don’t have children modules and can’t use the usual require('./') call (i.e. the call argument must start with ../).

Any number of super levels is allowed (e.g. require('../../../../foobar')).

External packages

If the module name to import doesn’t begin with './' nor '../' then it’ll be searched for outside of the project directory. The places Emilua will look for are:

  • Core modules (e.g. 'inbox').

  • External packages.

Emilua looks for external packages by examining the following locations (in that order):

  1. The EMILUA_PATH environment variable. That’s a colon-separated list[1] of directories.

  2. The installation-dependent default (usually $PREFIX/lib/emilua-$VERSION).

Misc

You might be interested in restricting the filenames of your modules to the set discovered by Boost developers over the years:


1. It’s semicolon-separated on Windows.