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,372 +1,9 @@
/////////////////////////////////////////////////////////
//
//
// GENERAL CONCEPT
//
// The original lua C API asks you to work with a stack machine. When you
// use the original API, you're manually pushing and popping in every
// line of code. I find it hard to remember what stack position contains
// what value. This wrapper is designed to alleviate that problem.
//
// However, the lua garbage collector demands that we keep all lua values
// on the lua stack. I can't change that. I can create a wrapper, but I
// still have to keep all lua values on the lua stack. So, here's how
// this wrapper works:
//
// At the beginning of any C++ lua function, we allocate enough space on
// the lua stack to hold all the arguments, all the return values, and all
// the temporary values that we need. We give each of the stack slots
// that we allocate a human-readable name. From that point forward,
// we don't push or pop anything on the lua stack. Instead, we do all our
// lua value manipulation using the stack slots that we allocated and named
// at the beginning of the function.
//
// LUASTACK: See the markdown document "Our In-House
// Lua API" for more information about what this is.
//
/////////////////////////////////////////////////////////
//
// LUAARG, LUARET, LUAVAR, and LUADEFSTACK
//
// The best way to explain this wrapper is with an example. This
// function, 'table_removevalue', takes a table and a 'removethis'
// value. It searches the table for any (key,val) pairs
// where val==removethis, and it removes those pairs.
//
// int table_removevalue(lua_State *L) {
// LuaArg table, removethis;
// LuaRet returnvalue;
// LuaVar key, val;
// LuaDefStack LS(L, table, removethis, returnvalue, key, val);
// LS.set(key, LuaNil);
// while (LS.next(table, key, val)) {
// if (LS.rawequal(val, removethis)) {
// LS.rawset(table, key, LuaNil);
// }
// }
// LS.set(returnvalue, table);
// return LS.result();
// }
//
// Now I'll explain this function. First, you have some declarations for
// LuaArg (lua function arguments), LuaRet (lua function return values), and
// LuaVar (lua local variables). These are small structs each containing
// an 'int' indicating an absolute position on the lua stack. These are all
// collectively called lua stack slots. Class LuaArg, LuaRet, and
// LuaVar all derive from a base class LuaSlot.
//
// At construction time, all the LuaSlots are initialized to zero, which
// means they aren't ready to use yet. They don't point to anywhere on the
// lua stack. Remember, lua numbers everything starting at 1, so zero is
// not a valid lua stack position.
//
// After the LuaSlot declarations, you have the LuaDefStack constructor.
// This takes the lua stack as a parameter, and then all the LuaSlots.
// This allocates space on the lua stack for all the LuaSlots. When it's
// done, the lua stack will contain exactly five values, one for each of the
// five LuaSlots. The LuaSlot structs will contain the stack positions of
// these values. The LuaRet and LuaVar stack slots will be initialized to nil.
// The LuaArg stack slots will be initialized with the function arguments.
// After the LuaDefStack constructor, you are ready to do lua calculations.
//
// Next, you have the loop that iterates over the table. To iterate over
// a table in lua, you initialize 'key' to nil, then you call the 'next'
// operator to get the next (key,val) pair. The 'next' operator will return
// false if there is no next pair. For each (key,val) pair in the table,
// we check if the val is equal to the thing we want to remove, and if so,
// we change the table entry to nil. After the loop, we set the returnvalue
// slot to the table that was passed in.
//
// The last line, return LS.result(), is a piece of boilerplate, every lua
// C function must end with this. This function removes everything but the
// return values from the stack, and returns the number of return values.
//
//
/////////////////////////////////////////////////////////
//
// CLASS LUAEXTSTACK.
//
// Class LuaDefStack, which I showed above, is meant to be used at the
// top level of a C++ lua function.
//
// Class LuaExtStack is meant to be used when you want to allocate
// some MORE stack slots halfway through a C++ lua function.
// Class LuaExtStack is particularly useful when you want to write
// a recursive implementation. Typically you would put LuaDefStack
// in the top-level function, and LuaExtStack in the recursive
// implementation.
//
// Class LuaExtStack differs in two ways: first of all, it doesn't allow
// LuaArg and LuaRet slots, only LuaVar. Second, it has a destructor that
// automatically puts the stack back the way it was when it was constructed.
//
// You might wonder why class LuaDefStack doesn't have a destructor that
// cleans up the lua stack. It's because of return values: it can't remove
// everything from the stack, because it has to leave the return values
// on the stack.
//
// I call these two classes the 'LuaStack' classes. When I say that
// the LuaStack classes do something, I mean that both LuaDefStack
// and LuaExtStack do that thing.
//
//
/////////////////////////////////////////////////////////
//
// THE LIBRARY OF BUILTIN OPERATORS
//
// The LuaStack classes provide a whole library of methods to operate
// on lua values. Roughly speaking, there is one function in LuaStack
// for every function in the raw lua API, and they are similarly named.
//
// However, the functions in the raw lua API push and pop values on the
// lua stack. The equivalent functions in LuaStack take inputs from
// LuaSlots, and store their outputs into other LuaSlots. Nothing is
// pushed or popped.
//
// To get the complete list, you will have to scroll down to class
// LuaCoreStack, below. All the prototypes are there, and they are all
// documented.
//
//
/////////////////////////////////////////////////////////
//
// AUTOMATIC TYPE CONVERSION
//
// In some cases, LuaStack can do automatic conversions of C++ values into
// lua values. This is supported in any function where it makes sense.
// One function that supports automatic conversion is 'LuaStack::set':
//
// LS.set(val1, val2);
//
// This is actually a copy operation that copies from one lua local variable to
// another. But using auto conversions, it can also be used to assign C++
// values to lua slots. These are all valid:
//
// LS.set(value, 1); // int and int64_t can be converted.
// LS.set(value, "banana"); // char*, string, and string_view can be converted.
// LS.set(value, true); // bool can be converted.
// LS.set(value, 2.39); // float and double can be converted.
// LS.set(value, LuaNil); // This special token stores nil.
//
// Automatic conversion is generally allowed in any context where it would
// make sense. For example, these are all valid:
//
// LS.rawget(value, mytable, 1);
// LS.rawget(value, mytable, "banana");
// LS.rawget(value, mytable, true);
// LS.rawget(value, mytable, 2.39);
// LS.rawget(value, mytable, LuaNil);
//
// There's also the 'LuaNewTable' constant. This is handled by the
// auto-conversion system, but it's not really a conversion:
//
// LS.set(value, LuaNewTable);
//
// This will cause a new table to be created, and stored in value.
//
//
/////////////////////////////////////////////////////////
//
// API INTEROPERABILITY
//
// This wrapper can intentionally be mixed with the standard, raw lua API.
// You still have an explicit handle to the lua stack, and you can get the
// stack addresses out of the LuaSlot variables using LuaSlot::index. So
// if there's some tricky thing you can't do with this wrapper, you can always
// fall back to the raw API for just the section of code that you need to.
//
// I also sometimes use the raw lua API for code that is doing truly
// unusual things, that aren't easy to do with this wrapper.
//
/////////////////////////////////////////////////////////
//
//
// ON RAWGET AND METAMETHODS
//
// The raw lua API provides functions like lua_gettab and lua_equal
// which automatically call metamethods. I do not think it is wise to
// habitually use functions like that, because some of the code we write
// is not executed in a protected context. That means that a metamethod
// that generates an error would bring down the whole system rather than
// just stopping a script thread. It also means that a metamethod could
// return an invalid piece of data, corrupting a system data structure
// rather than just a script data structure.
//
// Because there are so many contexts where it is just not safe to call
// metamethods, I've made it deliberately difficult to call metamethods.
// Our API includes 'rawget' which doesn't call metamethods,
// but it omits 'gettab' which does.
//
// If someday we actually need metamethod support, we can do that,
// but I'll have to write a safe wrapper for them. I know how to do that,
// but it's a lot of work and I'm not going to bother unless it's needed.
//
//
/////////////////////////////////////////////////////////
//
// LUA TABLE TYPES
//
// We have modified the lua interpreter to allow us to assign
// table subtypes to different tables. Most lua tables are
// marked as 'general' tables. If you create a table using the
// lua newtable operator, you'll get a general table.
//
// Aside from general tables, we have special table types for the
// lua registry, for lua global environment tables, for tangibles,
// for tangible metatables, and for classes. These tables get
// handled specially in various parts of the code.
//
//
/////////////////////////////////////////////////////////
//
// LIGHTUSERDATA AND CLASS LUATOKEN
//
// Before I tell you what tokens are, I'm going to tell you what problem
// I was trying to solve that led me to create tokens.
//
// I was trying to write a JSON to LUA converter. That's mostly
// straightforward. Json tables are very similar to lua tables.
// For example, consider this JSON:
//
// {
// "name": "John Smith",
// "alive": true,
// "address": {
// "city": "New York",
// "state": "NY",
// },
// "spouse": null
// }
//
// That converts very straightforwardly to a lua table:
//
// {
// name = "John Smith",
// alive = true,
// address = {
// city = "New York",
// state = "NY",
// },
// spouse = nil
// }
//
// There's only one problem here: that "spouse" line doesn't really work.
// Setting a key to nil in lua is the same as not setting that key at all.
// That's not correct. Instead of storing json null as lua nil,
// we could store json null as the string "null". But that would be make it
// impossible to parse and store the literal string "null". That's not correct
// either.
//
// Lua has a datatype called 'lightuserdata'. A lightuserdata holds an
// int64. That gives me an option: I can store json null as a
// lightuserdata. When we see this lightuserdata value, we would know
// we have a json null.
//
// So that finally brings me to what a "token" is. A token is a lightuserdata
// containing a short string encoded as a fixed-width base38 number. Tokens
// may only contain the characters a-z, 0-9, and underscore, and can be up to
// 12 characters long (since 38^12 fits in 64 bits). In effect, it's a short
// string, but it's
// a string that's distinguishable from a normal lua string. It doesn't have
// the same type as a lua string (it shows up as a lightuserdata).
// The purpose of tokens is to represent special unique values, like json null.
//
// To make working with tokens easy, I've created a C++ struct 'LuaToken'.
// It stores an int64. You can construct a LuaToken in two different ways:
//
// LuaToken(0x559D0F68151CB900)
// LuaToken("null")
//
// Those are equivalent. The second form is just as fast as the first,
// because of C++ constexpr magic. You can use our automatic type conversion
// system to automatically convert C++ LuaToken structs into lua lightuserdata
// values, like this:
//
// LS.set(value, LuaToken("null"))
//
// When our pretty-printer is printing out lua data structures, and it
// encounters a lightuserdata, it prints it out as a token, ie, as a short
// string, but using brackets instead of quotation marks.
//
/////////////////////////////////////////////////////////
//
// LUA CLASSES
//
// This module adds the concept of a 'class' to lua. The function
// LuaStack::makeclass creates a class. This function is exposed to lua.
// For example, you could create a lua class to manipulate deques:
//
// makeclass("deque")
//
// This creates a new table, which you will find in the global
// environment under the name 'deque'. The class is a table for
// storing functions related to deques. You can now add lua functions
// to the class:
//
// function deque.insertr(...)
// end
//
// If you call makeclass a second time with the same classname, this
// is a no-op. This is useful because if you have two sourcefiles that
// both add functions to class 'deque', they can both makeclass 'deque',
// and no conflict will occur.
//
// Class tables have a distinct LuaTableType: LUA_TT_CLASS. That
// way, it is easy to tell that the table is intended as a class.
// Class tables are treated uniquely by our engine in several ways.
//
// A class also has a field "__index" which points to itself.
// C.__index = C. That makes it possible to use the class as a
// metatable, and any attempt to look up a name in the table
// that fails will then attempt to look up that name in the class.
//
//
/////////////////////////////////////////////////////////
//
// LUADEFINE
//
// LuaDefine is a macro that defines a C function which is exposed to lua.
// In addition to actually defining the C function, it also creates a global
// registry of all such functions. The registry is used to actually
// know which functions to inject into lua.
//
// Here is an example of a typical LuaDefine:
//
// LuaDefine(table_removevalue, "table, removethis",
// "|This function removes a specified value from a table."
// "|",
// "|Iterates over all the (key, val) pairs in a table and"
// "|removes the ones where val == removethis."
// ) {
// LuaArg table, removethis;
// LuaRet returnvalue;
// LuaVar key, val;
// LuaDefStack LS(L, table, removethis, returnvalue, key, val);
// LS.set(key, LuaNil);
// while (LS.next(table, key, val)) {
// if (LS.rawequal(val, removethis)) {
// LS.rawset(table, key, LuaNil);
// }
// }
// LS.set(returnvalue, table);
// return LS.result();
// }
//
// So you might notice that this is the same example function from
// earlier, but this time with LuaDefine. If you omit the LuaDefine
// and write the function as it was shown earlier, you will get a
// function that can be called from C++, and which works fine when
// called from C++, but it won't be visible from lua.
//
// The example function above will show up in lua as 'table.removevalue'.
// This lua function name is derived straightforwardly from the C++
// function name.
//
// Note that both of the string parameters to LuaDefine are part of the
// function documentation, neither has any effect on how the lua function
// behaves. The function documentation goes into the registry and from
// there is accessible through the lua documentation system.
//
/////////////////////////////////////////////////////////
#ifndef LUASTACK_HPP
#define LUASTACK_HPP
@@ -1151,31 +788,13 @@ struct LuaCountArgs<LuaExtraArgs, Ts...> {
//
// LuaDefStack
//
// This version of LuaStack should only be used inside a LuaDefine. It can
// assign stack slots to LuaArg, LuaRet, LuaVar, and LuaExtraArgs. It
// arranges for the arguments to be in the LuaArg variables, and it arranges for
// the LuaRet variables to be returned. It also makes sure that the function
// has the correct number of arguments.
//
// At the end of the LuaDefine function, you're supposed to return LS.result().
// LS.result causes the allocated stack slots to be freed except for the LuaRet
// values, which have to stay on the stack in order to pass them back as return
// values. LS.result returns the number of LuaRet variables left on the stack.
//
// If you terminate a LuaDefine by calling lua_error or lua_yield, then
// obviously, you don't get a chance to call LS.result. That's not a problem.
// The lua interpreter will clean up after an error or yield.
//
// Implementation note: LuaDefStack doesn't have a destructor to deallocate
// stack slots. That's deliberate: you shouldn't expect this class to clean up
// its stack frame, because after all, it has to leave return values on the
// stack. It would be deceptive to put a destructor, which then doesn't
// actually clean up anyway. Better to just let it be known that this class
// doesn't clean up its stack frame.
// This version of LuaStack should only be used inside a
// LuaDefine. It handles the passing of arguments from lua
// to C++, and return values from C++ to lua. See the
// markdown for more information.
//
////////////////////////////////////////////////////////////////////
class LuaDefStack : public LuaCoreStack {
private:
int nret_;
@@ -1242,15 +861,7 @@ private:
// This version of LuaStack is meant to be used in any context where
// you want to assign stack slots to some LuaVars, and then you want
// to automatically deallocate those LuaVars when the LuaExtStack
// goes out of scope.
//
// Unlike LuaDefStack, this version of LuaStack is meant to fully
// deallocate its stack frame when it goes out of scope, so it does
// have a destructor to do that. There is a special case in the
// destructor: if lua is throwing an error, the destructor leaves
// the stack alone, in order to preserve the error message that's
// on the stack. After an error throw, the lua interpreter will
// clean up the stack.
// goes out of scope. See the markdown for more information.
//
////////////////////////////////////////////////////////////////////