Lua tricks

NOTE: This is not a Lua tutorial or reference, just a place to record something interesting when I read Lua source code, which is version 5.4.3

Lua global stuff

  • print will first call __tostring metamethod in value’s metatable if it exists.
local t = {}
local mt = {
    __tostring = function()
        return "willmafh's table"
    end
}
setmetatable(t, mt)

print(t)  -- will print `willmafh's table`
  • number, string, boolean, and nil will print their values as normal.
  • for other type values, it will try to get __name first, if it’s not a string, then Lua default type name will be used.
local t = {}
local mt = {
    __name = "willmafh's table"
}
setmetatable(t, mt)

print(t)  -- `willmafh's table: 0x5f3167d3acf0`
local t = {}
local mt = {
    -- here __name should be a string, otherwise it's meaningless
    __name = function() return "willmafh's table" end
}
setmetatable(t, mt)

print(t)  -- `table: 0x5f3167d3acf0`

Lua global table

-- _G is Lua's global table
print(_G)

-- following can recursively get the global table
print(_G["_G"])
print(_G._G)

Lua version

print(_G._VERSION)

warn function

  • should use control message "@on" to turn on warning message emitting first
  • warn(msg1, msg2, ...) function can concat multiple string message
  • then should use control message "@off" to turn off warn function
  • there are only two control messages, "@on"/"@off", other messages start with ‘@’ are normal messages
warn("@on")
warn("hello world")  -- Lua warning: hello world
warn("hello", " from willmafh")  -- Lua warning: hello from willmafh
warn("@off")
warn("hello world")  -- won't log this warning message

setmetatable function

  • setmetatable will return its first argument, that is the table itself, it won’t create a new table
local t = {}
local mt = {}
local rt = setmetatable(t, mt)
print(t, rt)  -- here t and rt are the same value
  • if the object’s metatable has __metatable field, it means the metatable is protected, and we can’t use setmetatable again to change it
local mt = {
    __metatable = "the metatable is protected",
    __tostring = function()
        return "willmafh's table"
    end
}
local t = setmetatable({}, mt)
setmetatable(t, {})  -- error throw here
  • nil metatable argument will clear table’s original metatable if there is any
local mt = {
    __tostring = function()
        return "willmafh's table from __tostring"
    end
}
local t = setmetatable({}, mt)
print(getmetatable(t))  -- table: 0x603284328ef0

setmetatable(t, nil)
print(getmetatable(t))  -- nil

getmetatable function

  • if the object’s metatable has __metatable, then returns its value, it can be any type. otherwise returns the metatable itself
local mt = {
    __metatable = "the metatable is protected",  -- can be any type value
    __tostring = function()
        return "willmafh's table"
    end
}
local t = setmetatable({}, mt)
print(getmetatable(t))  -- __metatable value is printed

require function

  • require function will first search registry["_LOADED"] table, and if there is a required module, then it return directly
  • package table is the upvalue of require function
local registry = debug.getregistry()

--[[
string  table
table: 0x5efa18719710
table: 0x5efa18719710
--]]
local name, upvalue = debug.getupvalue(require, 1)
print(type(name), type(upvalue))
print(upvalue)
print(_G["package"])

registry table

  • the first element in registry table array is the main Lua thread, and the second element in registry table array is the global table _G
local registry = debug.getregistry()

-- thread: 0x5886909e72a8
print(registry[1])  -- main thread

--[[
table: 0x5886909e7c50
table: 0x5886909e7c50
--]]
print(registry[2]) -- global table _G
print(_G)
  • all require loaded modules, including lua file module and c libs module, are kept in registry["_LOADED"] table.
require "lfs"
require "cjson.safe"

local registry = debug.getregistry()

for k, v in pairs(registry["_LOADED"]) do
    print(k, v)
end

--[[
_G      table: 0x6461ef84bc50
os      table: 0x5786711edbb0
...
lfs     table: 0x6461ef8526f0
cjson.safe      table: 0x6461ef855a80
...
--]]
  • all loaded c libs will be kept in registry["_CLIBS"] table and each lib will be kept in two ways, one is key value way, the other is an array element
local lfs = require "lfs"
local cjson = require "cjson.safe"

local registry = debug.getregistry()

local clibs = registry["_CLIBS"]

--[[
1       userdata: 0x567153cf7fe0
2       userdata: 0x567153cfa700
/usr/local/lib/lua/5.4/lfs.so   userdata: 0x567153cf7fe0
/usr/local/lib/lua/5.4/cjson.so userdata: 0x567153cfa700
--]]
for k, v in pairs(clibs) do
    print(k, v)
end

--[[
1       userdata: 0x567153cf7fe0
2       userdata: 0x567153cfa700
--]]
for i, v in ipairs(clibs) do
    print(i, v)
end
  • registry["_PRELOAD"] is normally an empty table, but it can be used to store loaders for lua modules, and when require function load a new module, it will first search this table, so you can use it to do something interesting
package.preload["devtools"] = function()
    return { editor = "vim", os = "linux" }
end

local devtools = require "devtools"
print(devtools.editor, devtools.os)

environment variables

LUA_NOENV

  • if lua is used with -E option, then LUA_NOENV will be set true in registry
#!/usr/bin/lua5.4 -E

local registry = debug.getregistry()
print(registry["LUA_NOENV"])  -- true, since -E is used

LUA_PATH_5_4/LUA_PATH, LUA_CPATH_5_4/LUA_CPATH

  • ;; in envs LUA_PATH_5_4/LUA_PATH means to insert default value from macro LUA_PATH_DEFAULT to package.path‘s final value
  • ;; in envs LUA_CPATH_5_4/LUA_CPATH means to insert default value from macro LUA_CPATH_DEFAULT to package.cpath‘s final value
  • when lua is initialized, function luaopen_package will handle all these details
--[[ /usr/local/share/lua/5.4/?.lua;/usr/local/share/lua/5.4/?/init.lua;/usr/local/lib/lua/5.4/?.lua;/usr/local/lib/lua/5.4/?/init.lua;./?.lua;./?/init.lua
--]]
print(package.path)

-- /usr/local/lib/lua/5.4/?.so;/usr/local/lib/lua/5.4/loadall.so;./?.so
print(package.cpath)

package table

package.config

  • a string describing some compile-time configurations for packages, please refer to lua manual for details
print(package.config)

package.loaded

  • package.loaded is a reference to registry["_LOADED"]
local registry = debug.getregistry()

--[[
table: 0x5c638d059a60
table: 0x5c638d059a60
--]]
print(registry["_LOADED"])
print(package.loaded)

package.preload

  • package.preload is a reference to registry["_PRELOAD"]
local registry = debug.getregistry()

--[[
table: 0x64a359eb7be0
table: 0x64a359eb7be0
--]]
print(registry["_PRELOAD"])
print(package.preload)

package.searchers

  • a table contains four searching functions (currently), which is used by require method to search modules, and they are searched in the following order
    • preload table
    • lua module, searching package.path one by one
    • c module, searching package.cpath one by one
    • c root module, still searching package.cpath, but build name by stripping components after the first dot ‘.’
  • using require "cjson.safe" as an example to illustrate the differences between searching c and searching c root
-- searching c: '/usr/local/lib/lua/5.4/`cjson/safe.so`', will try to find safe.so under cjson directory
-- searching c root: '/usr/local/lib/lua/5.4/`cjson.so`', will try to find cjson.so
local cjson = require "cjson.safe"
  • both of searching c and c root will finally look for function beginning with prefix luaopen_, such as ‘luaopen_cjson’ or ‘luaopen_cjson_safe’

package.searchpath

  • searching name in the given path, which is a template like package.path
    • dot ‘.’ in name will be replaced by ‘/‘
    • ‘?’ in path will be replaced by dot replaced name
local name = "foo.a"
local path = "./?.lua;./?.lc;/usr/local/?/init.lua"

-- so the search order will be: './foo/a.lua', './foo/a.lc', '/usr/local/foo/a/init.lua'
package.searchpath(name, path)

package.loadlib

  • it does not perform any path searching and does not automatically adds extensions like require, so libname arg must be the complete path
  • if funcname is “*” , then only loads and exports all symbols in the lib. otherwise looks for funcname and return it as a lua c function
  • libs loaded by this function will be added to registry["_CLIBS"] table
local cjson_path = "/usr/local/lib/lua/5.4/cjson.so"

print(package.loadlib(cjson_path, "*"))  -- true

-- error here, since "json_encode" is not exported by cjson.so, but hey this is just an example
local json_encode = package.loadlib(cjson_path, "json_encode")
-- using function following...

This article was updated on November 26, 2025