📜 ⬆️ ⬇️

Lua API ++

Hello colleagues.
I want to introduce you to my small project, which I hope can be useful to you.

I met Lua a few years ago when I was looking for a scripting language to be implemented that is distinguished by its modest size and high performance. Lua not only answers these requests, but also captivates with surprising simplicity and expressiveness.

I can not say that I am dissatisfied with the Lua API: it is an excellent set of functions, convenient and easy to use. The integration of the language into your application and the addition of your own extensions did not cause difficulties, there were no “pitfalls” either. But still, using this C-oriented API, I was not left with the idea that this process could be more comfortable. The first attempt to make a convenient object-oriented wrapper failed: with the available means I did not manage to create something worthy of existence, everything was too cumbersome and not obvious.
')
And then C ++ 11 appeared , which removed all the obstacles that hindered me (more precisely, added what was missing), and the puzzle gradually began to take shape. The second run was successful, and as a result I was able to create a fairly lightweight wrapper library with the natural syntax of most operations. This library, which I called Lua API ++ , is intended to serve as a convenient replacement for the Lua API. This article, based on my presentation at the Lua Workshop, will help you get acquainted with the basic concepts of the Lua API ++ and the features it provides.



The main actors


Familiarity should begin with the basic concepts used by the library, and the relationship between them. As expected, these concepts are reflected in the corresponding types.

State
State is the owner of the Lua state. This is a standalone type, practically unrelated to the rest of the library. In addition to controlling the creation and destruction of the state, it provides only the means to execute files, strings, and Lua-compatible functions. Errors that occur during their work are converted to exceptions.

LFunction
Everything else in the library happens inside LFunction , functions of a special format, compatible with Lua API ++. This is an analogue of Lua-compatible functions, which in turn was given the name CFunction . The special format of the function was needed mainly in order to let in our next character:

Context
Context is the context of the function, as well as the access center to all Lua features. With it, you can access global variables, function arguments, the registry, and upvalues . You can manage the garbage collector, signal errors, pass multiple return values, and create closures. Simply put, everything that is not directly related to operations on values ​​that are the prerogative of our closing is done through Context .

Value
Unlike previous concepts, which uniquely corresponded to the same class, the “value” in the Lua API ++ is somewhat vague (although the Value class, of course, exists). First of all, it is connected with the policy of “open borders”, which allows free migration of native values ​​in Lua and vice versa. Wherever Lua values ​​are expected, you can substitute the native values ​​of the supported types and they will automatically “move” to the Lua stack. Operators of implicit type conversion will help to move the values ​​in the opposite direction, and in case of incompatibility of the real and expected type, they will notify us with an exception.
In addition, the values ​​in Lua, depending on their origin, can be represented by different types that support a common interface. This interface implements all valid operations on values: explicit and implicit conversion to native types, function calls, indexing, arithmetic operations, comparison, type checking, writing and reading of metatables.

Valref
This is a reference to the value placed on the stack, or more precisely, not so much the value as the specific slot on the Lua stack. Valref does not Valref with placing or deleting values ​​on the stack, but focuses solely on value operations. In the Lua API ++ Valref serves as a model that follows an interface of other types representing values.

Temporal
With temporary values ​​that are the result of operations, somewhat more complicated. These are the values ​​that will be placed (or maybe not) on the stack as a result of the operation, used once , and then deleted. In addition, the arguments of the operation themselves may be the results of other operations, and even without guarantees of success. Yes, and the use is different: when indexing as a result of reading, a new value is created on the stack instead of a key, and as a result of writing, the key and the recorded value are removed from the stack. And what about the need to strictly observe the order of placement of arguments of operations? And what to do with unused objects?
Many probably already guessed what I'm getting at. Temporary values ​​are represented by proxy types. Invisible to the user, they are constructed using templates and reproduce the Valref interface. Using them is easy, simple and convenient, but make a mistake, and the compiler will “delight” you with a voluminous work full of angle brackets.

Anchors
Anchors are so named because they allow one or several values ​​to be “sticked” to the stack. Value is a universal “anchor” for one value, Table specialized for tables, and Valset stores several values.



Now that the main characters are presented to us, we can proceed to a more detailed analysis of what we can do with them.

State

State has a default constructor that performs all the actions necessary to initialize the context. An alternative constructor allows you to use a custom memory management function . You can query the “raw” pointer to the state object used in the Lua API by getRawState function.
Included are the runFile , runString and call functions that allow you to make the simplest interpreter:

The simplest interpreter
 #include <iostream> #include <luapp/lua.hpp> using namespace std; using namespace lua; void interpretLine(State& state, const string& line) { try { state.runString(line); //    } catch(std::exception& e) { //          cerr << e.what() << endl; } } void interpretStream(State& state, istream& in) { string currentLine; while(!in.eof()) { //        getline(in, currentLine); interpretLine(state, currentLine); } } int main() { State state; interpretStream(state, cin); } 




Error processing


The approach used by the library is to keep Lua from getting underfoot, therefore, those errors that are related to the work of the library itself are diagnosed, such as attempts to create a Table not from a table, or those that need to be (possibly) intercepted in user code , like type conversion errors. The library does not attempt to diagnose in advance those errors that may appear when calling the Lua API. Therefore, an attempt, for example, to use a function call on a value that is actually a number, will not cause an exception. It will be detected inside the lua_call call and will cause a Lua-style error (abort execution and return to the closest point of the secure call).



LFunction


In general, the library supports a “transparent” wrapper for functions that operate on supported types (and even member functions). Simply mention the name of the function where the Lua value is expected. But if we want to get access to all Lua conveniences provided by the Lua API ++, we need to write L-functions in accordance with the following prototype:
 Retval myFunc(Context& c); 

Everything is simple here: our function gets the Context , and Retval is a special type that helps to conveniently return an arbitrary number of values ​​through the function Context::ret .

The mkcf template allows mkcf to make from LFunction what Lua will make friends with:
 int (*myCfunc)(lua_State*) = mkcf<myFunc>; 

Thus, we can explicitly create wrappers for our function. The “transparent” wrapper will also work, but the overhead will be slightly higher. On the other hand, mkcf will create a separate wrapper function in each case.
One way or another, but in any case, the “wrapper” will create a Context object, pass it to our function, and upon completion of the work, return the values ​​returned via Retval to Lua. All exceptions exceeding the limits of the wrapped function will be intercepted and converted to a Lua error.
A function that returns itself? Give two!
 Retval retSelf(Context& c) { return c.ret(retSelf, mkcf<retSelf>); //    ,  - } 




Context


The function context is the central access point for Lua. Everything that is not directly related to working with values ​​is performed via Context . I will not reject hints of an obvious resemblance to the god object , but in this case, this decision is dictated by the architecture of the Lua API. Through Context you can manage the garbage collector, you can find out the version number and the number of values ​​placed on the stack. It is implicitly converted to lua_State* in case you need to conjure the Lua API directly. On the same case, a magic word (more precisely, a static constant of the signal type) is provided initializeExplicitly , which allows you to create a Context explicitly, outside of the LFunction .

Return Values
No matter how pleasant it is to simply indicate in the return statement the values ​​returned from the function, this is impossible. It was necessary to make a choice between two closest alternatives: a cunning "starter" with operator overloading with a comma or a function call. Defeated friendship feature. Therefore, LFunction requires you to return Retval , which can only be created by Retval the Context method with the modest name ret . This is a special function: after its call, work with the stack stops, so as not to throw off our values ​​from it, therefore, it should be used only directly in the return statement . In a ret call, you can list as many return values ​​as you need.
Comparison
 return ctx.ret(1, "two", three); 

Equivalent code:
 lua_pushinteger(ctx, 1); lua_pushstring(ctx, "two"); lua_pushvalue(ctx, three); return 3; 



Error reporting
Claiming that the only way to create a Retval is to access the ret function, I did not sin against the truth, but there is one nuance ... From a formal point of view, there is also an error function, which also returns this type. Only in fact, Retval does not reach the creation of the Retval , because no return occurs from this function. The maximum you can count on is to pass your message to the Lua error handling mechanism. The Lua API documentation recommends using the lua_error call in the return statement to indicate the fact that the execution of the function is interrupted during the call. The same approach is applied in the Lua API ++, that is why the error declared as returning Retval .
Lua-value with an error message is taken as an argument, and the concatenation here will be quite appropriate, especially since the originator can be the where function, which creates a string describing the current function. The same value is used if the message is not specified at all.
 if(verbose) return ctx.error(ctx.where() & " intentional error " & 42); else return ctx.error(); //   ,  return ctx.error(ctx.where()); 

Equivalent code
 if(verbose) { luaL_where(ctx, 0); lua_pushstring(ctx, " intentional error "); lua_pushinteger(ctx, 42); lua_concat(ctx, 3); return lua_error(ctx); } else { luaL_where(ctx, 0); return lua_error(ctx); } 


Access to the environment
Our Context is obviously the primary source of the values. In fact, where else would they come from?
We are given access to use objects that are designed as open members of the Context class, allowing us to reach various interesting places of the environment. All of them allow both reading and writing values.

First of all it is args , function arguments. Unlike other objects of access, for each of which a special type inaccessible to the user was created, here the usual constant Valset . Its constancy means only that we cannot change its size, but to rewrite the values ​​of the arguments for health. Since Valset was created as an STL-compatible container, the numbering of elements in it starts from 0. In other cases, the library follows the rules of Lua and implies that indexing starts from 1.
 if(ctx.args.size() > 1 && ctx.args[0].is<string>()) {...}; 

Equivalent code
  if(nArgs > 1 && lua_isstring(ctx, 1) ) {...}; 

In second place is access to global variables. The global object is indexed by strings.
 ctx.global["answer"] = 42; //      ,   

Equivalent code
 lua_pushinteger(ctx, 42); lua_setglobal(ctx, "answer"); 

If our LFunction concurrently is a closure, then we can access the values ​​stored in it through upvalues with an integer index (starting with 1, everything is correct). There is no way to find out the number of stored values: it is assumed that this is already known.

The registry Lua, accessible through the registry , is used in two ways. For string keys, metatables for user data are stored there. Integer keys are used when using the registry as a repository of values. The key is created by calling registry.store and is subsequently used to read and write to the registry , erasing the value and releasing the key occurs when writing nil .
 auto k = ctx.registry.store(ctx.upvalues[1]); // decltype(k) == int ctx.registry [k] = nil; //  k       store 

Equivalent code
 lua_pushvalue(ctx, lua_upvalueindex(1)); auto k = luaL_ref(ctx, LUA_REGISTRYINDEX); luaL_unref(ctx, LUA_REGISTRYINDEX, k); 

Functions
I just mentioned that Lua allows you to create closures. In the Context object, the closure function is used for this, which receives the CFunction and the values ​​that will be stored in the closure. The result is a temporary object, that is, a full Lua value.
Instead of CFunction we can specify LFunction at once, but this lightness has its price. In the resulting closure, the first upvalue will be reserved (the address of the function is stored there, since the wrapper is the same for any LFunction). The same function is used for transparent migration of LFunction with the same consequences. This is different from the mkcf template, which does not reserve anything, but it creates a separate wrapper function for each function.

And you can also create chunks: compiled Lua code. The text itself is compiled using the chunk method, and the contents of the file using load . For the cases of “done and forgotten” there is a runString and runFile , exactly the same as in State . In terms of using a chunk, this is a common function.

Closures can also be created from incompatible functions using the wrap method. It automatically creates a wrapper that takes the arguments from the Lua stack, converts them to the values ​​accepted by our function, makes the call and places the result on the Lua stack as the return value. By default, this works with all supported types, including user data. And if this is not enough (for example, we need to get something up with strings stored in a vector , then we can also specify the conversion to one side or the other using special macros.
That wrap works when implicit migration functions. The fraternal vwrap method does almost everything the same, only ignores the return value of the function being wrapped.



Value migration


The Lua API ++ supports the following native types:
Numeric
int
unsigned int
long long
unsigned long long
float
double
String
const char*
std::string
Functions
CFunction: int (*) (lua_State*)
LFunction: Retval (*) (Context&)
Arbitrary functions
Member functions
miscellanea
Nil
bool
LightUserData: void*
registered user types

The values ​​of the types listed in the table can migrate to the Lua stack and back (except, of course, Nil and “wrapped” functions, which remain pointers to wrappers).
The reverse migration is performed using the implicit conversion operators built into the Value-types and using the cast template function. If the Lua value contains data that cannot be converted to what we are trying to, an exception will be thrown. The optcast function instead of exceptions will return a “spare” value.
 int a = val; auto b = val.cast<int>(); auto c = val.optcast<int>(42); 

Equivalent code
 if(!lua_isnumber(ctx, valIdx)){ lua_pushstring(ctx, "Conversion error"); return lua_error(ctx); } int a = lua_tointeger(ctx, valIdx); if(!lua_isnumber(ctx, valIdx)){ lua_pushstring(ctx, "Conversion error"); return lua_error(ctx); } auto b = lua_tointeger(ctx, valIdx); auto c = lua_isnumber(ctx, valIdx) ? lua_tointeger(ctx, valIdx) : 42; 


It is possible to check compatibility with the desired type using the is function, and using type to find out the type of the stored value directly.
 if(val.is<double>()) ...; if(val.type() == ValueTypes::Number) ...; 




Single value operations


Assignment
If we have a Value, then in general it is possible to assign something to it, both other Value and native ones. But this does not apply to some temporary values, for example, to the result of a function call or length: when they are on the left side of the = sign, they will give a tricky error. But other temporary values, such as indexing or metatable, assignment is fully admissible. According to the meaning of the action performed, it is easy to guess what can be appropriated and what cannot.

Metatables
The mt method gives access to the metatable of a value that can be read and written.
 { Table mt = val.mt(); val.mt() = nil; } 

Equivalent code
 if(!lua_getmetatable(ctx, valIdx)){ lua_pushstring(ctx, "The value has no metatable"); return lua_error(ctx); } int mtIdx = lua_gettop(ctx); lua_pushnil(ctx); lua_setmetatable(ctx, valIdx); lua_pop(ctx, 1); 


Length
The operation of the len function differs in different versions of Lua: in compatibility mode with 5.1, it returns the native size_t , and in mode 5.2, a temporary value.

Indexing
Access to the elements of the table by key is carried out by indexing, the key can be of any supported type. But we must remember that when wrapping functions, new closures will be created:
 void myFunc(); Retval example(Context& c) { Table t(c); t[myFunc] = 42; //  myFunc   ... assert(t[myFunc].is<Nil>()); //   -  ,   . t[mkcf<example>] = 42.42; //     CFunction,   "" assert(t[mkcf<example>] == 42.42); } 

Equivalent code
 void myFunc(); int wrapped_void_void(lua_State* s) { if(!lua_islightuserdata(ctx, lua_upvalueindex(1))) { lua_pushstring(ctx, "Conversion error"); return lua_error(ctx); } void (*fptr) () = reinterpret_cast<void(*)()>(lua_touserdata(ctx, lua_upvalueindex(1))); fptr(); return 0; } int mkcf_myFunc(lua_State* s) { myFunc(); return 0; } int example(lua_State* ctx) { lua_createtable(ctx, 0, 0); int t = lua_gettop(ctx); lua_pushlightuserdata(ctx, reinterpret_cast<void*>(&myFunc)); lua_pushcclosure(ctx, &wrapped_void_void, 1); lua_pushinteger(ctx, 42); lua_settable(ctx, t); lua_pushlightuserdata(ctx, reinterpret_cast<void*>(&myFunc)); lua_pushcclosure(ctx, &wrapped_void_void, 1); lua_gettable(ctx, t); assert(lua_isnil(ctx, -1)); lua_pushcfunction(ctx, &mkcf_myFunc); lua_pushnumber(ctx, 42.42); lua_settable(ctx, t); lua_pushcfunction(ctx, &mkcf_myFunc); lua_gettable(ctx, t); lua_pushnumber(ctx, 42.42); int result = lua_compare(ctx, -1, -2, LUA_OPEQ); lua_pop(ctx, 2); assert(result == 1); } 


Function call
Lua- . call , , - . pcall .
 int x = fn(1); int y = fn.call(1); //    int z = fn.pcall(1); //     ,    

, . , ? Lua:
 function mrv() return 2, 3, 4; end 

.
-, , . .
 mrv(); 

 lua_pushvalue(ctx, mrvIdx); lua_call(ctx, 0, 0); 

-, Lua-. ( nil , ), .
 Value x = mrv(); // x == 2 

 lua_pushvalue(ctx, mrvIdx); lua_call(ctx, 0, 1); int x = lua_gettop(ctx); 

-, , (, ) : .
 print(1, mrv(), 5); //  1 2 3 4 5 

 lua_pushvalue(ctx, printIdx); int oldtop = lua_gettop(ctx); lua_pushinteger(ctx, 1); lua_pushvalue(ctx, mrvIdx); lua_call(ctx, 0, LUA_MULTRET); lua_pushinteger(ctx, 5); int nArgs = lua_gettop(ctx) - oldtop; lua_call(ctx, nArgs, 0); 

-, Valset , .
 Valset vs = mrv.pcall(); // vs.size() == 3, vs.success() == true 

 int vsBegin = lua_gettop(ctx) + 1; lua_pushvalue(ctx, mrvIdx); bool vsSuccess = lua_pcall(ctx, 0, LUA_MULTRET) == LUA_OK; int vsSize = lua_gettop(ctx) + 1 - vsBegin; 

Valset , ( ). . , Valset , .
 print(1, vs, 5); //  1 2 3 4 5 

 lua_pushvalue(ctx, printIdx); int oldTop = lua_gettop(ctx); lua_pushInteger(ctx, 1); lua_pushvalue(ctx, mrvIdx); for(auto i = 0; i < vsSize; ++i) lua_pushvalue(ctx, vsBegin + i); lua_pushinteger(ctx, 5); int nArgs = lua_gettop(ctx) - oldtop; lua_call(ctx, nArgs, 0); 

Valset . STL- STL, «» Valref . Valset , push_back pop_back . Valref , ( Valset ), . , .




, Value- , :
 string s = "The answer to question " & val & " is " & 42; 

 lua_pushstring(ctx, "The answer to question "); lua_pushvalue(ctx, valIdx); lua_pushstring(ctx, " is "); lua_pushinteger(ctx, 42); lua_concat(ctx, 4); string s = lua_tostring(ctx, -1); lua_pop(ctx, 1); 

& . , «» . , Valset .

Lua, .

5.2 , , «» ^ .




, Table , Valref . , , , . raw , , , iterate , for_each . , iterate ( , , ), -. Valref true , false , . :
 Table t = ctx.global["myTable"]; t.iterate([] (Valref k, Valref v) { cout << int(k) << int(v); }); 

 lua_getglobal(ctx, "myTable"); if(!lua_istable(ctx, -1)){ lua_pushstring(ctx, "Conversion error"); return lua_error(ctx); } int t = lua_gettop(ctx); lua_pushnil(ctx); size_t visited = 0; while(lua_next(ctx, t)) { ++ visited; if(!lua_isnumber(ctx, -2) || !lua_isnumber(ctx, -1)){ lua_pushstring(ctx, "Conversion error"); return lua_error(ctx); } cout << lua_tointeger(ctx, -2) << lua_tointeger(ctx, -1); lua_pop(ctx, 1); } 

iterate .

Tablearray records . .
 fn(Table::array(ctx, "one", 42, Table::array(ctx, 1, 2, 3))); //  ? ! 

 lua_pushvalue(ctx, fn); lua_createtable(ctx, 3, 0); lua_pushinteger(ctx, 1); lua_pushstring(ctx, "one"); lua_settable(ctx, -3); lua_pushinteger(ctx, 2); lua_pushinteger(ctx, 42); lua_settable(ctx, -3); lua_pushinteger(ctx, 3); lua_createtable(ctx, 3, 0); lua_pushinteger(ctx, 1); lua_pushinteger(ctx, 1); lua_settable(ctx, -3); lua_pushinteger(ctx, 2); lua_pushinteger(ctx, 2); lua_settable(ctx, -3); lua_pushinteger(ctx, 3); lua_pushinteger(ctx, 3); lua_settable(ctx, -3); lua_settable(ctx, -3); lua_call(ctx, 1, 0); 

, . , array , 1. , Valset .

records , -. .
 x.mt() = Table::records(ctx, "__index", xRead, "__newindex", xWrite, "__gc", xDestroy ); 

 lua_createtable(ctx, 0, 3); lua_pushstring(ctx, "__index"); lua_pushlightuserdata(ctx, reinterpret_cast<void*>(&xRead)); lua_pushcclosure(ctx, &wrapped_signature_1, 1); lua_settable(ctx, -3); lua_pushstring(ctx, "__newindex"); lua_pushlightuserdata(ctx, reinterpret_cast<void*>(&xWrite)); lua_pushcclosure(ctx, &wrapped_signature_2, 1); lua_settable(ctx, -3); lua_pushstring(ctx, "__gc"); lua_pushlightuserdata(ctx, reinterpret_cast<void*>(&xDestroy)); lua_pushcclosure(ctx, &wrapped_signature_3, 1); lua_settable(ctx, -3); lua_setmetatable(ctx, x); 




. - , : , cast , .
. LUAPP_USERDATA . , , . , registry -, :
 LUAPP_USERDATA(MyType, "MyType Lua ID") Retval setup(Context& ctx) { ctx.mt<MyType>() = Table::records(ctx); //   ,     ctx.registry["MyType Lua ID"] } 

Lua . , , «» , — .

Lua , Lua . Lua API++ placement new , , . POD-. , , .

. , -, , . , Lua , :
 #include <vector> using dvec = std::vector<double>; //      LUAPP_USERDATA(dvec, "Number array") //      dvec aCreate(size_t size) //    . { //  -       . return dvec(size); //  RVO        } void aDestroy(dvec& self) //  -        . { self.~dvec(); } void aWrite(dvec& self, size_t index, double val) //          __newindex { self.at(index) = val; //     at,     Lua } Retval setup(Context& c) { //   c.mt<dvec>() = Table::records(c, //     "__index", static_cast<double& (dvec::*)(size_t)> (&dvec::at), //         at //    (const  -const),      "__newindex", aWrite, "__len", dvec::size, //   size  vector ,    "__gc", aDestroy ); c.global["createArray"] = aCreate; //      return c.ret(); } 




Conclusion


, Lua . Lua API ++, . Lua Lua API (coroutine, string buffers, ).

, Lua API, Lua. inline , Lua API , , Link time code generation (LTO GCC). header-only. inline Lua.

, C++11 , Lua STL. Boost Unit Test Framework .

Lua 5.2 ( 5.3 ), 5.1, LuaJIT.

Lua API++ MIT — , Lua, . HTML, .

, - .

Source: https://habr.com/ru/post/201860/


All Articles