NodeMCU firmware is an IoT project ("the Project") which implements a Lua-based runtime for SoC modules based on the Espressif ESP8266 and ESP32 architectures. This NodeMCU Reference Manual (NRM) specifically addresses how the NodeMCU Lua implementation relates to standard Lua as described in the two versions of the Lua language that we currently support:
- The Lua 5.1 Reference Manual and
- The Lua 5.3 Reference Manual (LRM)
Developers using the NodeMCU environment should familiarise themselves with the 5.3 LRM.
The Project provides a wide range of standard library modules written in both C and Lua to support many ESP hardware modules and chips, and these are documented in separate sections in our online documentation.
The NRM supplements LRM content and module documentation by focusing on a complete description of the differences between NodeMCU Lua and standard Lua 5.3 in use. It adopts the same structure and style as the LRM. As NodeMCU provides a full implementation of the Lua language there is little content herein relating to Lua itself. However, what NodeMCU does is to offer a number of enhancements that enable resources to be allocated in constant program memory — resources in standard Lua that are allocated in RAM; where this does impact is in the coding of C library modules and the APIs used to do this. Hence the bulk of the differences relate to these APIs.
One of our goals in introducing Lua 5.3 support was to maintain the continuity of our existing C modules by ensuring that they can be successfully compiled and executed in both the Lua 5.1 and 5.3 environments. This goal was achieved by a combination of:
- enabling relevant compatibility options for standard Lua libraries;
- back porting some Lua 5.3 API enhancements back into our Lua 5.1 implementation, and
- making some small changes to the module source to ensure that incompatible API use is avoided.
Further details are given in the Lua compatibility section below. Notwithstanding this, the Project has now deprecated Lua 5.1 and will soon be moving this version into frozen support.
As well as providing the ability to building runtime firmware environments for ESP chipsets, the Project also offers a luac.cross
cross compiler that can be built for common platforms such as Windows 10 and Linux, and this enables developers to compile source modules into a binary file format for download to ESP targets and loading from those targets.
The NodeNCU runtime offers a full implementation of the LRM §2 core concepts, with the following adjustments:
- The firmware is compiled to use 32-bit integers and single-precision (32-bit) floats by default. Address pointers are also 32-bit, and this allows all Lua variables to be encoded in RAM as an 8-byte "
TValue
" (compared to the 12-byteTValue
used in Lua 5.1). - C modules can statically declare read-only variant of the
Table
structure type known as a "ROTable
". There are some limitations to the types for ROTable keys and value, in order to ensure that these are consistent with static declaration. ROTables are stored in code space (in flash memory on the ESPs), and hence do not take up RAM resources. However these are still represented by the Lua type table and a Lua application can treat them during execution the same as any other read-only table. Any attempt to write to aROTable
or to set its metatable will throw an error. - NodeMCU also introduces a concept known as Lua Flash Store (LFS). This enables Lua code (and any string constants used in this code) to be compiled and stored in code space, and hence without using RAM resources. Such LFS functions are still represented by the type function and can be executed just like any other Lua function.
These are implemented as per the LRM. Note that the Lua 5.1 and Lua 5.3 language implementations are different and can introduce breaking incompatibilities when moving between versions, but this is a Lua issue rather than a NodeMCU one.
All LFS functions, any string constants used in these functions, and any ROTables are stored in static code space. These are ignored by the Lua Garbage collector (LGC). The LGC only scans RAM-based resources and recovers unused ones. The NodeMCU LGC has slightly modified configuration settings that increase its aggressiveness as heap usage approaches RAM capacity.
The firmware includes the full coroutine implementation, but note that there are some slight differences between the standard Lua 5.1 and Lua 5.3 C API implementations. (See Feature breaks below.)
The NodeNCU runtime offers a full implementation of the Lua language as defined in LRM §3 and its subsections.
LRM §4 describes the C API for Lua. This is used by NodeMCU modules written in the C source language to interface to the the Lua runtime. The header file lua.h
is used to define all API functions, together with their related types and constants. This section 4 forms the primary reference, though the NodeMCU makes minor changes as detailed below to support LFS and ROTable resources.
LRM §4.6 describes how errors are handled within the runtime.
The normal practice within the runtime and C modules is to throw any detected errors -- that is to unroll the call stack until the error is acquired by a routine that has declared an error handler. Such an environment can be established by lua_pcall and related API functions within C and by the Lua function pcall; this is known as a protected environment. Errors which occur outside any protected environment are not caught by the Lua application and by default trigger a "panic". By default NodeMCU captures the error traceback and posts a new SDK task to print the error before restarting the processor.
The NodeMCU runtime implements a non-blocking threaded model that is similar to that of node.js
, and hence most Lua execution is initiated from event-triggered callback (CB) routines within C library modules. NodeMCU enables full recovery of error diagnostics from otherwise unprotected Lua execution by adding an additional auxiliary library function luaL_pcallx
. All event-driven Lua callbacks within our library modules use luaL_pcallx
instead of lua_call
. This has the same behaviour if no uncaught error occurs. However, in the case of an error that would have previously resulted in a panic, a new SDK task is posted with the error traceback as an upvalue to process this error information.
The default action is to print a full traceback and then trigger processor restart, that is a similar outcome as before but with recovery of the full error traceback. However the node.onerror()
library function is available to override this default action; for example developers might wish to print the error without restarting the processor, so that the circumstances which triggered the error can be investigated.
These are available for developers coding new library modules in C. Note that if you compare the standard and NodeMCU versions of lua.h
you will find a small number of entries not listed below. This is because the Lua 5.1 and Lua 5.3 variants are incompatible owing to architectural differences. However, laubxlib.h
includes equivalent wrapper version-compatible functions that may be used safely for both versions.
Extra structure types used in LROT
macros to declare static RO tables. See detailed section below.
void (lua_createrotable) (lua_State *L, ROTable *t, const ROTable_entry *e, ROTable *mt);
[-0, +1, -]
Create a RAM based ROTable
pointing to the ROTable_entry
vector e
, and metatable mt
.
void (lua_debugbreak)(void);
lua_debugbreak()
and ASSERT(condition)
are available for development debugging. If DEVELOPMENT_USE_GDB
is defined then these will respectively trigger a debugger break and evaluate a conditional assert prologue on the same. If not, then these are effectively ignored and generate no executable code.
int lua_dump (lua_State *L, lua_Writer writer, void *data, int strip);
[-0, +0, –]
Dumps function at the top of the stack function as a binary chunk as per LRM. However the last argument strip
is now an integer is in the range -1..2, rather a boolean as per standard Lua:
- -1, use the current default strip level (which can be set by
lua_stripdebug
) - 0, keep all debug info
- 1, discard Local and Upvalue debug info; keep line number info
- 2, discard Local, Upvalue and line number debug info
The internal NodeMCU Proto
encoding of debug line number information is typically 15× more compact than in standard Lua; the intermediate strip=1
argument allows the removal of most of the debug information whilst retaining the ability to produce a proper line number traceback on error.
int lua_freeheap (void);
[-0, +0, –]
returns the amount of free heap available to the Lua memory allocator.
int lua_gc (lua_State *L, int what, int data);
[-0, +0, m]
provides an option for
what
:LUA_GCSETMEMLIMIT
sets the available heap threshold (in bytes) at which aggressive sweeping starts.
void lua_getlfsconfig (lua_State *L, int *conf);
[-0, +0, -]
if conf
is not NULL
, then this returns an int[5] summary of the LFS configuration. The first 3 items are the mapped and flash address of the LFS region, and its allocated size. If the LFS is loaded then the 4th is the current size used and the 5th (for Lua 5.3 only) the date-timestamp of loaded LFS image.
lua_State * lua_getstate();
[-0, +0, -]
returns the main thread lua_State
record. Used in CBs to initialise L
for subsequent API call use.
void lua_pushrotable (lua_State *L, ROTable *p);
[-0, +1, -]
Pushes a ROTable onto the stack.
int lua_stripdebug (lua_State *L, int level);
[-1, +0, e]
This function has two modes. A value is popped off the stack.
- If this value is
nil
, then the default strip level is set to givenlevel
if this is in the range 0 to 2. Returns the current default level. - If this value is a Lua function (in RAM rather than in LFS), then the prototype hierarchy within the function is stripped of debug information to the specified level. Returns an approximate estimate of the heap freed (this can be a lot higher if some strings can be garbage collected).
void lua_writestring(const char *s, size_t l); /* macro */
Writes a string s
of length l
to stdout
. Note that any output redirection will be applied to the string.
void lua_writestringerror(const char *s, void *p). /* macro */
Writes an error with CString format specifier s
and parameter p
to stderr
. Note on the ESP devices this error will always be sent to UART0
; output redirection will not be applied.
int lua_pushstringsarray (lua_State *L, int opt);
[-0, +1, m]
Pushes an array onto the stack containing all strings in the specified strings table. If opt
= 0 then the RAM table is used, else if opt
= 1 and LFS is loaded then the LFS table is used, else nil
is pushed onto the stack.
Returns a status boolean; true if a table has been pushed.
Note that the LRM defines an auxiliary library which contains a set of functions that assist in coding convenience and economy. These are strictly built on top of the basic API, and are defined in lauxlib.h
. By convention all auxiliary functions have the prefix luaL_
so module code should only contain lua_XXXX()
and luaL_XXXX()
data declarations and functions. And since the lauxlib.h
itself incudes lua.h
, all C modules should only need to #include "lauxlib.h"
in their include preambles.
NodeMCU adds some extra auxiliary functions above those defined in the LRM.
int lua_lfsreload (lua_State *L);
[-1, +1, -]
This function pops the LFS image name from the stack, and if it exists and contains the correct image header then it reloads LFS with the specified image file, and immediately restarts with the new LFS image loaded, so control it not returned to the calling function. If the image is missing or the header is invalid then an error message is pushed onto the stack and control is returned. Note that if the image has a valid header but its contents are invalid then the result is undetermined.
int luaL_pcallx (lua_State *L, int narg, int nresults);
[-(nargs + 1), +(nresults|1), –]
Calls a function in protected mode and providing a full traceback on error.
Both nargs
and nresults
have the same meaning as in lua_call. If there are no errors during the call, then luaL_pcallx
behaves exactly like lua_call
. However, if there is any error, lua_pcallx
has already established an traceback error handler for the call that catches the error. It cleans up the stack and returns the negative error code.
Any caught error is posted to a separate NodeMCU task which which calls the error reporter as defined in the registry entry onerror
with the traceback text as its argument. The default action is to print the error and then set a 1 sec one-shot timer to restart the CPU. (One second is enough time to allow the error to be sent over the network if redirection to a telnet session is in place.) If the onerror
entry is set to print
for example, then the error is simply printed without restarting the CPU.
Note that the Lua runtime does not call the error handler if the error is an out-of memory one, so in this case the out-of-memory error is posted to the error reporter without a traceback.
int luaL_posttask (lua_State* L, int prio);
[-1, +0, e]
Posts a task to execute the function popped from the stack at the specified user task priority
prio
one of:LUA_TASK_LOW
LUA_TASK_MEDIUM
LUA_TASK_HIGH
Note that the function is invoked with the priority as its parameter.
int luaL_pushlfsmodule ((lua_State *L);
[-1, +1, -]
This function pops a module name from the stack. If this is a string, LFS is loaded and it contains the named module then its closure is pushed onto the stack as a function value, otherwise nil
is pushed.
Returns the type of the pushed value.
int luaL_pushlfsmodules (lua_State *L);
[-0, +1, m]
If LFS is loaded then an array of the names of all of the modules in LFS is pushed onto the stack as a function value, otherwise nil
is pushed.
Returns the type of the pushed value.
int luaL_pushlfsdts (lua_State *L);
[-0, +1, m-]
If LFS is loaded then the Unix-style date-timestamp for the compile time of the image is pushed onto the stack as a integer value, otherwise nil
is pushed. Note that the primary use of this stamp is to act as a unique identifier for the image version.
Returns the type of the pushed value.
void (luaL_reref) (lua_State *L, int t, int *ref);
[-1, +0, m]
Variant of luaL_ref. If *ref
is a valid reference in the table at index t, then this is replaced by the object at the top of the stack (and pops the object), otherwise it creates and returns a new reference using the luaL_ref
algorithm.
int (luaL_rometatable) (lua_State *L, const char* tname, const ROTable *p);
[-0, +1, e]
Equivalent to luaL_newmetatable()
for ROTable metatables. Adds key / ROTable entry to the registry [tname] = p
, rather than using a new RAM table.
luaL_unref2(l,t,r)
This macro executes luaL_unref(L, t, r)
and then assigns r = LUA_NOREF
.
bool luaL_totoggle(lua_State *L, int idx)
There are several ways of indicating a configuration toggle value:
- The modern Lua way, with a boolean (
true
/false
) - The "classic" Lua way, with
1
/nil
- The "C" way, with
1
/0
When implementing C modules for NodeMCU and needing to indicate an on/off setting, the preference is to do it as a boolean. In the interest of ease of use on the other hand, it is however nice to also support the other styles. The luaL_totoggle
function provides just that.
All NodeMCU C library modules should include the standard header "module.h
". This internally includes lnodemcu.h
and these together provide the macros to enable declaration of NodeMCU modules and ROTables within them. All ROtable support macros are either prefixed by LRO_
(Lua Read Only) or in the case of table entries LROT_
.
NODEMCU_MODULE(sectionname, libraryname, map, initfunc)
This macro enables the module to be statically declared and linked in the ROM
ROTable. The global environment's metafield __index
is set to the ROTable ROM
hence any entries in the ROM table are resolved as read-only entries in the global environment.
sectionname
. This is the linker section for the module and by convention this is the uppercased library name (e.g.FILE
). Behind the scenes_module_selected
is appended to this section name if corresponding "use module" macro (e.g.LUA_USE_MODULES_FILE
) is defined in the configuration. Only the modules sections*_module_selected
are linked into the firmware image and those not selected are ignored.libraryname
. This is the name of the module (e.g.file
) and is the key for the entry in theROM
ROTable.map
. This is the ROTable defining the functions and constants for the module, and this is the corresponding value for the entry in theROM
ROTable.initfunc
. If this is not NULL, it should be a valid C function and is call during Lua initialisation to carry out one-time initialisation of the module.
LROT_BEGIN(rt,mt,flags)
LROT_END(rt,mt,flags)
These macros start and end a ROTable definition. The three parameters must be the same in both declarations.
rt
. ROTable name.mt
. ROTable's metatable. This should be of the formLROT_TABLEREF(tablename)
if the metatable is used andNULL
otherwise.flags
. The Lua VM table access routines use aflags
field to short-circuit where the access needs to honour metamethods during access. In the case of a static ROTable this flag bit mask must be declared statically during compile rather than cached dynamically at runtime. Hence if the table is a metatable and it includes the metamethods__index
,__newindex
,__gc
,__mode
,__len
or__eq
, then the mask field should or-in the corresponding mask for example if__index
is used then the flags should includeLROT_MASK_INDEX
. Note that index and GC are a very common combination, soLROT_MASK_GC_INDEX
is also defined to be(LROT_MASK_GC | LROT_MASK_INDEX)
.
ROTables only support static declaration of string keys and value types: C function, Lightweight userdata, Numeric, ROTable. These are entries are declared by means of the LROT_FUNCENTRY
, LROT_LUDENTRY
, LROT_NUMENTRY
, LROT_INTENTRY
, LROT_FLOATENTRY
and LROT_TABENTRY
macros. All take two parameters: the name of the key and the value. For Lua 5.1 builds LROT_NUMENTRY
and LROT_INTENTRY
both generate a numeric TValue
, but in the case of Lua 5.3 these are separate numeric subtypes so these macros generate the appropriate subtype.
Note that ROTable entries can be declared in any order except that keys starting with "_
" must be declared at the head of the list. This is a pragmatic constraint for runtime efficiency. A lookaside cache is used to optimise key searches and results in a direct table probe in over 95% of ROTable accesses. A table miss (that is the key doesn't exist) still requires a full scan of the list, and the main source of table misses are scans for metafield values. Forcing these to be at the head of the ROTable allows the scan to abort on reading the first non-"_
" key.
ROTables can still support other key and value types by using an index metamethod to point at an C index access function. For example this technique is used in the utf8
library to return utf8.charpattern
LROT_BEGIN(utf8_meta, NULL, LROT_MASK_INDEX)
LROT_FUNCENTRY( __index, utf8_lookup )
LROT_END(utf8_meta, NULL, LROT_MASK_INDEX)
LROT_BEGIN(utf8, LROT_TABLEREF(utf8_meta, 0)
LROT_FUNCENTRY( offset, byteoffset )
LROT_FUNCENTRY( codepoint, codepoint )
LROT_FUNCENTRY( char, utfchar )
LROT_FUNCENTRY( len, utflen )
LROT_FUNCENTRY( codes, iter_codes )
LROT_END(utf8, LROT_TABLEREF(utf8_meta), 0)
Any reference to utf8.charpattern
will call the __index
method function (utf8_lookup()
); this returns the UTF8 character pattern if the name equals "charpattern"
and nil
otherwise. Hence utf8
works as standard even though ROTables don't natively support a string value type.
- Basic Lua functions, coroutine support, Lua module support, string and table manipulation are as per the standard Lua implementation. However, note that there are some breaking changes in the standard Lua string implementation as discussed in the LRM, e.g. the
\z
end-of-line separator; no string functions exhibit a CString behaviour (that is treat"\0"
as a special character). - The modulus operator is implemented for string data types so
str % var
is a synonym forstring.format(str, var)
andstr % tbl
is a synonym forstring.format(str, table.unpack(tbl))
. This python-like formatting functionality is a very common extension to the string library, but is awkward to implement withstring
being aROTable
. - The
string.dump()
strip
parameter can take integer values 1,2,3 (thelua_stripdebug
strip parameter + 1).false
is synonymous to1
,true
to3
and omitted takes the default strip level. - The
string
library does not offer locale support. - The 5.3
math
library is expanded compared to the 5.1 one, and specifically:- Included:
abs
,acos
,asin
,atan
,ceil
,cos
,deg
,exp
,tointeger
,floor
,fmod
,ult
,log
,max
,min
,modf
,rad
,random
,randomseed
,sin
,sqrt
,tan
andtype
- Not implemented:
atan2
,cosh
,sinh
,tanh
,pow
,frexp
,ldexp
andlog10
- Included:
- Input, output OS Facilities (the
io
andos
libraries) are not implement for firmware builds because of the minimal OS supported offered by the embedded run-time. The separately documentedfile
andnode
libraries provide functionally similar analogues. The host execution environment implemented byluac.cross
does support theio
andos
libraries. - The full
debug
library is implemented less thedebug.debug()
function.- An extra function
debug.getstrings(type)
has been added;type
is one of'ROM'
, or'RAM'
(the default). Returns a sorted array of the strings returned from thelua_getstrings
function.
- An extra function
Standard Lua has a number of breaking incompatibilities that require conditional code to enable modules using these features to be compiled against both Lua 5.1 and Lua 5.3. See Lua 5.2 §8 and Lua 5.3 §8 for incompatibilities with Lua 5.1.
A key strategy in our NodeMCU migration to Lua 5.3 is that all NodeMCU application modules must be compilable and work under both Lua versions. This has been achieved by three mechanisms
- The standard Lua build has conditionals to enable improved compatibility with earlier versions. In general the Lua 5.1 compatibilities have been enabled in the Lua 5.3 builds.
- Regressing new Lua 5.3 features into the Lua 5.1 API.
- For a limited number of features the Project accepts that the two versions APIs are incompatible and hence modules should either avoid their use or use
#if LUA_VERSION_NUM == 501
conditional compilation.
The following subsections detail how NodeMCU Lua versions deviate from standard Lua in order to achieve these objectives.
The following Compatibility modes are enabled:
LUA_COMPAT_APIINTCASTS
. Theseìnt
cast are still used within NodeMCU modules.LUA_COMPAT_UNPACK
. This retainsROM.unpack
as a global synonym fortable.unpack
LUA_COMPAT_LOADERS
. This keepspackage.loaders
as a synonym forpackage.seachers
LUA_COMPAT_LOADSTRING
. This keepsloadstring(s)
as a synonym forload(s)
.
- Table access routines in Lua 5.3 are now type
int
rather thanvoid
and return the type of the value pushed onto the stack. The 5.1 routineslua_getfield
,lua_getglobal
,lua_geti
,lua_gettable
,lua_rawget
,lua_rawgeti
,lua_rawgetp
andluaL_getmetatable
have been updated to mirror this behaviour and return the type of the value pushed onto the stack. - There is a general numeric comparison API function
lua_compare()
with macros forlua_equal()
andlua_lessthan()
whereas 5.1 only support the==
and<
tests through separate API calls. 5.1 has been update to mirror the 5.3. implementation. - Lua 5.3 includes a
lua_absindex(L, idx)
which converts ToS relative (e.g.-1
) indices to stack base relative and hence independent of further push/pop operations. This makes using down-stack indexes a lot simpler. 5.1 has been updated to mirror this 5.3 function.
-
\0
is now a valid pattern character in search patterns, and the%z
pattern is no longer supported. We suggest that modules either limit searching to non-null strings or accept that the source will require version variants. -
The stack pseudo-index
LUA_GLOBALSINDEX
has been removed. Modules must either get the global environment from the registry entryLUA_RIDX_GLOBALS
or use thelua_getglobal
API call. All current global references in the NodeMCU library modules now uselua_getglobal
where necessary. -
Shared userdata and table upvalues. Lua 5.3 now supports the sharing of GCObjects such as userdata and tables as upvalue between C functions using
lua_getupvalue
andlua_setupvalue
. This has changed from 5.1 and we suggest that modules either avoid using these API calls or accept that the source will require version variants. -
The environment support has changed from Lua 5.1 to 5.3. We suggest that modules either avoid using these API calls or accept that the source will require version variants.
-
The coroutine yield / resume API support has changed in Lua 5.3 to support yield and resume across C function calls. We suggest that modules either avoid using these API calls or accept that the source will require version variants.