Lots of work on the LuaStack documentation

This commit is contained in:
2026-02-22 02:12:28 -05:00
parent 28604e7e17
commit ee94f9f3a1
2 changed files with 186 additions and 458 deletions

View File

@@ -1,11 +1,36 @@
### Our In-House Lua API
# Our In-House Lua API
Lua comes with a C API. In general, the functions in this
API use the following calling convention: push the arguments
onto the lua stack, call the function, then pop the return
values from the lua stack. I find this difficult to use: I
find it hard to remember what's on the stack at any given
time, and which stack position contains which value. It's
not how I'm used to writing code.
So instead, we created our own in-house lua API called
"LuaStack." It allows you to create C++ local variables that
contain lua values. It has most of the same operations as
the original lua API, but they use different calling
conventions: you don't push arguments, and you don't pop
return values. Instead, you pass arguments as readable
parameters, and you pass return values as writable
parameters.
There's a caveat: we don't actually store lua values in C++
local variables. That would make it difficult for the lua
garbage collector to trace values. Instead, we use a hidden
level of indirection: the C++ local variable contains the
index of a pre-allocated spot on the lua stack. The
pre-allocated spot on the lua stack actually contains the
lua data. We'll walk you through the details of exactly how
this works in the following sections.
In our codebase, you are expected to use LuaStack, not
the original API, whenever possible.
== An Example of our API
Lua comes with a C API, which makes it possible to write C
functions that manipulate Lua data. The standard API is
functional, but it has some readability issues and can be
difficult to work with. Therefore, we have created our own
replacement API, which we call "LuaStack." In our codebase,
you are expected to use LuaStack, not the standard Lua API.
This paper will explain LuaStack using the following example
function in Lua, which compares two tables to see if they
are exactly equal:
@@ -39,13 +64,18 @@ and table2 are the same table (ie, pointer equality). The
function above, by contrast, checks if two different tables
contain the same key-value pairs. The built-in function
*table.nkeys* is something that we added to lua, it returns
the number of key-value pairs in a table. Once you fully
understand the Lua code above, have a look at the following
C++ code, which does the same thing. Don't worry if you
don't understand everything yet:
the number of key-value pairs in a table, it works on any
table. Once you fully understand the Lua code above, have a
look at the following C++ code, which does the exact same
thing. Don't worry if you don't understand everything yet:
```cpp
LuaDefine(table_equal, "table1, table2", "Return true if two tables are equal") {
LuaDefine(table_equal, "table1, table2",
"|Return true if two tables are equal."
"|"
"|The values in the table are not deep-compared,"
"|they are compared using pointer comparison."
) {
LuaArg table1, table2;
LuaVar size1, size2, key, value1, value2;
LuaRet equalflag;
@@ -251,6 +281,13 @@ our codebase - the LuaDefStack is called *LS*. But unlike
*LuaDefStack LS* is a C++ style smart pointer, which has
methods.
When the function returns, the only thing
left on the stack is supposed to be the return values.
The LuaDefStack is responsible for making that happen.
In the function LS.return, the stack is rearranged
to put the return values on the stack and remove
everything else. More on that later.
## Methods of Class LuaDefStack
In our example function, you'll find this line of code:
@@ -453,21 +490,36 @@ function always consists of two steps:
## The LuaDefine Macro
Our sample function starts with this line of code:
Our sample function starts with this:
```cpp
LuaDefine(table_equal, "table1, table2", "Return true if two tables are equal")
LuaDefine(table_equal, "table1, table2",
"|Return true if two tables are equal."
"|"
"|The values in the table are not deep-compared,"
"|they are compared using pointer comparison."
)
{ ... function body here ...}
```
The two strings in this line of code are just documentation.
Neither has any effect on how the function works. They are
collected automatically by our documentation system, which
can print them out on demand.
LuaDefine is a C macro that takes three parameters: the
function name, the parameter list docstring, and the big
docstring. Note that in C++, when you put two strings right
next to each other, C++ automatically concatenates them into
one string, and we've used that in the big docstring above.
LuaDefine is a C preprocessor macro. Before I tell you what
it macro expands into, I'd like to tell you how Lua
functions are normally written using the standard Lua API.
When using the standard API, lua functions look like this:
The two strings passed to the LuaDefine are just
documentation. Neither has any effect on how the function
works. They are collected automatically by our documentation
system, which can print them out on demand.
The big docstring doesn't contain newlines. Instead, we use
the pipe character to designate where new lines should
start. I find it harder to screw up.
Before I tell you what LuaDefine macro expands into, I'd
like to tell you how you write C functions that are intended
to be inserted into Lua:
```cpp
int table_equal(lua_State *L) {
@@ -476,24 +528,23 @@ int table_equal(lua_State *L) {
```
The C prototype is always the same: the return value is
always *int,* and the only argument is *lua_State*L*, a
always *int,* and the only argument is *lua_State *L*, a
pointer to the lua stack. A C function must have this
prototype to be compatible with the lua runtime.
Next, I will show you the macro expansion of a LuaDefine to
help you understand what it does. Here is the LuaDefine that
I will macro expand:
help you understand what it does. Here is the macroexpansion
of the LuaDefine above:
```cpp
LuaDefine(table_equal, "table1, table2", "Return true if two tables are equal")
```
Here is the expansion:
```cpp
int lfn_table_equal(lua_State *L);
const char *lfnarg_table_equal = "table1, table2";
const char *lfndoc_table_equal = "Return true if two tables are equal";
const char *lfndoc_table_equal =
"|Return true if two tables are equal."
"|"
"|The values in the table are not deep-compared,"
"|they are compared using pointer comparison.";
LuaFunctionReg reg_table_equal(
"table_equal", lfnarg_table_equal, lfndoc_table_equal, false, lfn_table_equal);
int lfn_table_equal(lua_State *L)
@@ -504,34 +555,32 @@ function lfn_table_equal. As you can see, it returns *int*
and accepts *lua_State *L,* which makes it compatible with
the lua runtime. This is a forward declaration.
Next, there are the two documentation strings. I've given
these documentation strings two names: lfnarg_table_equal
and lfndoc_table_equal. That way, it's possible to access
these documentation strings in code.
Next, there are the two documentation strings.
Next, there is the "registry entry" reg_table_equal, of
class LuaFunctionReg. The constructor of reg_table_equal
runs at program initialization time. The constructor stores
its arguments: the function name, the documentation strings,
the flags, and the C function pointer. These are all stored
inside reg_table_equal. Then, the constructor links
reg_table_equal into a global linked list of LuaFunctionReg
objects. This linked list makes up the registry of all
LuaDefines.
Next, class LuaFunctionReg implements a registry of
LuaDefines. It is the means by which we can loop over all
LuaDefines and insert them into a lua environment. The
constructor of LuaFunctionReg runs at program initialization
time. The constructor stores its arguments: the function
name, the documentation strings, the flags, and the C
function pointer. These are all stored inside
reg_table_equal. Then, reg_table_equal is linked
into a global linked list of LuaFunctionReg objects. This
linked list makes up the registry of all LuaDefines.
Finally, we have a second copy of the prototype for
lfn_table_equal. This time, it's not a forward declaration:
there's no semicolon. This prototype is meant to be followed
by the function body.
by the function body. A LuaDefine must always be followed
by a function body.
The upshot of all this is that LuaDefine doesn't just
declare a function. It also links that function into a
global registry of all the LuaDefines. The registry contains
not only the function name and function pointer, but also
the function documentation. Remember also that in C++,
static objects like reg_table_equal are constructed before
*main*. So by the time *main* starts, the registry is
ready-to-use.
declare a function. It also adds that function to the
registry of all LuaDefines. The registry contains not only
the function name and function pointer, but also the
function documentation. Remember also that in C++, static
objects like reg_table_equal are constructed before *main*.
So by the time *main* starts, the registry is ready-to-use.
The registry of all LuaDefines has two primary uses:
@@ -549,29 +598,31 @@ The registry of all LuaDefines has two primary uses:
What if you want to write C++ code that manipulates Lua
data, but which *isn't* called from inside Lua? In that
case, LuaDefStack won't work, because LuaDefStack is
hardwired to assume that a caller *exists*:
hardwired to assume that a lua caller *exists*,
and that it wants to pass arguments and return values:
- It checks that the caller passed the right number of
arguments.
- It transfers the arguments from the caller to the LuaArg
slots.
- It transfers the LuaRet slots back to the caller at
function end.
For situations where there isn't any caller, we have created
an alternative to LuaDefStack called LuaExtStack. There are
several things that LuaExtStack does differently:
By contrast, class LuaExtStack is for a situation where
you're in the middle of doing some C++ processing, and
you suddenly decide you need to store some more lua
variables. Class LuaExtStack will allocate space for
the variables.
- LuaExtStack doesn't accept LuaArg or LuaRet parameters -
it only allows LuaVar parameters. That only makes sense,
since it should be used in contexts where there's no
caller.
it only allows LuaVar parameters.
- There is no code in LuaExtStack to check that the caller
passed the right number of arguments, since again, there
is no caller.
passed the right number of arguments, since again, it
is not meant to be used in that context.
- Class LuaExtStack doesn't have a method LS.result, since
there's nobody to return a result to.
@@ -715,3 +766,69 @@ is useful for copying a value from a LuaSlot to the top of
the stack. This is generally useful when you want to take a
value from a LuaSlot, and then manipulate it using the
standard API.
## Metamethod Functions missing from our API
Several API functions from the old Lua API will
automatically call metamethods. For example, consider the
function lua_less. This can be used to compare two
strings. But if you store the right metamethod, lua_less
can end up calling a closure and running lua code.
I feel strongly that this is a *very dangerous* design
decision. Imagine that you're writing code for the
difference transmitter. You *think* you're just comparing
two strings, but all of a sudden you're running untrusted
lua code. If that code generates a lua error, suddenly
you're throwing a C++ exception from inside the difference
transmitter.
Because of this, LuaStack is *strongly* discouraging the use of
lua API functions that use metamethods: they're not even
exposed through the LuaStack API. Here is a short list
of API functions that use metamethods and substitutes that
don't:
| Uses Metamethods | Safe Substitute |
|-------------------------|--------------------------|
| lua_gettable | LS.rawget |
| lua_settable | LS.rawset |
| lua_equal | LS.rawequal |
| lua_lessthan | LS.genlt |
| lua_len | LS.rawlen or LS.nkeys |
## Our Extensions to Lua
We have made many extensions to the lua interpreter:
* We have created a new lua datatype, the "token". This is
a string-like datatype, limited to holding short lua
identifiers. The entire string is actually packed in the
8 bytes of a LUA_LIGHTUSERDATA. See the markdown
document, "Tokens-a-new-Lua-Type." LuaStack is the
module that implements and supports tokens.
* We have patched the lua runtime in quite a few ways.
See the markdown "A-Summary-of-our-Lua-Patches." Some of
these patches have APIs which are accessible via LuaStack.
* Table Subtypes. One of our patches allows you to assign
table subtypes: tables can be general tables, classes,
tangibles, and several other specialized types. LuaStack
provides the enum for all the table types, and the
accessors to get and set the extended type of a table.
* Classes. We have added the concept of a "class" to lua.
The lua registry contains a table of all classes.
LuaStack implements the functions to create classes,
lookup classes, get class names, and the like.
Classes will be documented in another markdown document.
* Tangible basics. Tangibles are a complex data structure
which is in class World, which is *way* above LuaStack's
abstraction level. However, LuaStack does implement
one tiny part of the tangible infrastructure: it provides
the tangibles table in the lua registry, and functions
to look up tangibles. The rest of what tangibles are about
are implemented by the World module.