使用 Lua 和 C++ 管理堆栈

2021-12-30 00:00:00 lua c++

我需要向 lua 脚本传递单个字符串(文件路径),并将 0 返回给多个字符串.

I need to pass a lua script a single string (path to file), and return 0 to many strings.

int error = 0;
lua_State *L = lua_open();
luaL_openlibs(L);

std::vector<string> list_strings;

用于在我加载和调用源文件之前将字符串压入堆栈

is used to push the string onto the stack, before I load and call the source file

if ((error = luaL_loadfile(L, "src/test.lua")) == 0)
{
    lua_pushstring(L, path.c_str());

    if ((error = lua_pcall(L, 1, LUA_MULTRET, 0)) == 0)
    {
        lua_gettable(L, LUA_GLOBALSINDEX);
        lua_pcall(L,1,1,0);

        if (lua_gettop(L) == 1 && lua_istable(L,1))
        {
            int len = lua_objlen(L,1);
            for (int i=1;i =< len; i++)
            {
                lua_pushinteger(L,i);
                lua_gettable(L,1);

                const char *s = lua_tostring(L,-1);
                if (s)
                {
                    list_strings.push_back(s);
                }

                lua_pop(L,1);
            }
        }
    }
}

就目前而言,我只是从示例中复制代码,所以我不确定我在做什么是我想做的......我想将路径推入堆栈,并调用lua 函数将该值从堆栈中取出,并将解析与该路径关联的文件.

As it stands, I've just been copying code from examples so I'm not really sure if what I'm doing is what I want to do... I want push the path onto the stack, and call a lua function which takes that value off of the stack, and will parse the file which associates to that path.

解析后,它应该返回一个包含其中的字符串的表(我想你可以把它想象成一个搜索特定字符串的函数)

After parsing, it should return a table containing the strings that are inside of it (you can just think of it as a function that searches for a specific string, I suppose)

更清楚.

任何建议/资源?这里有类似的问题吗?或任何有用的资源?

Any advice/resources? Any similar questions here? or any helpful resources?

推荐答案

在我们看到您似乎哪里出错之前,我想确保我了解您在做什么.您有一个 Lua 脚本文件.您要执行此脚本,向它传递一个字符串参数.它会做一些事情,然后返回零个或多个字符串作为返回值.并且您想在代码中获取这些值.

I want to make sure I understand what you're doing before we see where you seem to be going wrong. You have a Lua script file. You want to execute this script, passing it a single string argument. It will do some stuff, then return zero or more strings as return values. And you want to fetch these values in your code.

好的,让我们从顶部开始:

OK, let's start from the top:

if ((error = lua_pcall(L, 1, LUA_MULTRET, 0)) == 0)

通常,当您执行 lua_pcall 时,第三个参数会告诉 Lua 确切地预期有多少返回值.如果被调用的函数返回的值多于这个数字,则这些返回值将被丢弃.如果它返回的数量少于这个数字,则使用额外的 NIL 值来填充计数.

Normally, when you perform a lua_pcall, the third parameter tells Lua exactly how many return values are expected. If the function being called returns more than this number, those return values are discarded. If it returns fewer than this number, then additional NIL values are used to fill out the count.

LUA_MULTRET 告诉 Lua 不要这样做.使用时,所有结果都被压入堆栈.

LUA_MULTRET tells Lua not to do this. When this is used, all results are pushed onto the stack.

现在,由于您忽略了发布您的脚本,我不得不猜测您的脚本是什么样的.您正在返回多个字符串,但您从未说过这是如何发生的.Lua 作为一种语言,允许多个返回值:

Now, since you neglected to post your script, I have to make some guesses as to what your script looks like. You are returning multiple strings, but you never say how this happens. Lua, as a language, allows multiple return values:

return "string1", "string2";

这会导致 2 个字符串被压入堆栈.这不同于:

This results in 2 strings being pushed onto the stack. This is different from:

return {"string1", "string2"};

这会将一个对象放入堆栈:一张桌子.该表包含 2 个字符串.看到区别了吗?

This puts one object onto the stack: a table. The table contains 2 strings. See the difference?

查看您的代码,您似乎希望 Lua 脚本返回一个表字符串,而不是多个返回值.

Looking at your code, it seems like you expect the Lua script to return a table of strings, not multiple return values.

在这种情况下,你应该像这样调用你的 Lua 脚本:

In which case, you should call your Lua script like this:

if ((error = lua_pcall(L, 1, 1, 0)) == 0)

这告诉 Lua 你期望一个单一的返回值,如果用户不提供一个,Lua 会将一个 NIL 压入堆栈.

This tells Lua that you expect a single return value, and if the user doesn't provide one, Lua will push a NIL onto the stack.

现在让我们谈谈堆栈.在发出函数调用之前,堆栈的状态是这样的:

Now let's talk about the stack. The state of the stack was this before issuing the function call:

2- {string: path.c_str()}
1- {function: loaded from file "src/test.lua"}

这是从栈顶到栈底".如果您使用我给您的 lua_pcall,您将在堆栈中获得以下内容:

This is from the top of the stack to the "bottom". If you use the lua_pcall that I gave you, you will get the following on your stack:

1- {return value}

lua_pcall 将从堆栈中移除参数和函数.因此它将从堆栈中弹出 N + 1 个项目,其中 N 是 lua_pcall(第二个参数)指定的 Lua 函数的参数数量.因此,Lua 会从堆栈中弹出 2 个东西.然后它会将 1 个值压入堆栈:返回值(如果没有返回值,则为 NIL).

The lua_pcall will remove the argument(s) and function from the stack. So it will pop N + 1 items from the stack, where N is the number of arguments to the Lua function as specified by lua_pcall (the second parameter). Therefore, Lua will pop 2 things off the stack. It will then push exactly 1 value onto the stack: the return value (or NIL if there was no return value).

这样我们就可以通过函数调用了.如果一切顺利,我们现在期望堆栈包含:

So that gets us past the function call. If all has gone well, we now expect the stack to contain:

1- {table: returned from function}

然而,一切可能并不顺利.该脚本可能已返回 NIL.或者是其他东西;不能保证它是一张桌子.因此,下一步是验证返回值(注意:这是您的代码不再有意义的地方,所以这是全新的).

However, all may not have gone well. The script may have returned NIL. Or something else; there's no guarantee that it was a table. So, the next step is to verify the return value (note: this is where your code stops making sense, so this is all new).

if(lua_istable(L, -1))

lua_istable 顾名思义:确定给定的项目是否是表格.但是-1"是什么意思,为什么它不是您代码中的1"?

lua_istable does exactly what the name suggests: determine if the given item is a table. But what does that "-1" mean, and why isn't it the "1" you had in your code?

这个参数是对堆栈上一个位置的引用.Lua的栈也是Lua的寄存器文件.这意味着,与真实堆栈不同,您可以在堆栈中的任何元素处达到峰值.堆栈上的元素在堆栈上具有绝对位置.现在,这就是我们的堆栈的样子:

This argument is a reference to a location on the stack. Lua's stack is also Lua's register file. This means that, unlike a real stack, you are allowed to peak at any element on the stack. Elements on the stack have an absolute location on the stack. Now, here's what our stack looks like again:

1- {return value}

我写的那个1"是这个值在栈上的绝对位置.我可以推送值和弹出值,但除非我弹出这个值,否则它的位置将总是为1".

That "1" I wrote is the absolute location on the stack of this value. I can push values and pop values, but unless I pop this value, it's location will always be "1".

然而,它只是1",因为我们的堆栈开始时空.假设这一点有点粗鲁(因为如果堆栈不为空,它真的会咬你.Lua 文档确实有助于说明何时可以假设堆栈确实是空的,或者如果不是,堆栈上已经有什么了).因此,您可以使用相对位置.

However, it is only "1" because our stack started out empty. It's somewhat rude to assume this (as it can really bite you if the stack isn't empty. The Lua docs do helpfully state when you can assume the stack really is empty, or if its not, what is already on the stack). Therefore, you can use relative locations.

这就是-1"的含义:它是从堆栈的顶部开始的第一个堆栈索引.我们上面定义的 lua_pcall 函数将从堆栈中弹出 2 个项目(参数和函数),并压入 1 个项目(返回值或 NIL).因此,-1"将总是引用我们的返回值.

And that's what "-1" is: it is the first stack index from the top of the stack. Our lua_pcall function as defined above will pop 2 items from the stack (the argument and the function), and push 1 item (the return value or NIL). Therefore, "-1" will always refer to our return value.

因此,我们检查堆栈索引-1"(堆栈顶部)是否为表.如果不是,则失败.如果是,那么我们可以解析我们的列表.

Thus, we check to see if the stack index "-1" (the top of the stack) is a table. If it isn't, then fail. If it is, then we can parse our list.

这就是我们要进行列表解析的地方.第一步是获取列表中的项数:

And this is where we get to list parsing. The first step is to get the number of items in the list:

int len = lua_objlen(L, -1);
list_strings.reserve(len);

第二个只是一个细节,这样你就不会分配很多时间.您确切地知道该列表中将包含多少个字符串,因此您最好提前告知该列表,对吗?

The second one is just a nicety, so that you're not allocating a bunch of times. You know exactly how many strings are going to be in that list, so you may as well let the list know ahead of time, right?

lua_objlen 获取表中数组元素的个数.请注意,这可以返回零,但我们的循环会处理这种情况.

lua_objlen gets the number of array elements in the table. Note that this can return zero, but our loop will handle that case.

接下来,我们走桌子,拉出绳子.

Next, we walk the table, pulling out the strings.

for (int i=0; i < len; i++) {
    //Stuff from below.
}

记住 Lua 使用 1-base 索引.我个人更喜欢在 C/C++ 代码中使用 0 基索引,甚至是与 Lua 接口的代码.所以我尽可能晚地做翻译.但你不必这样做.

Remember that Lua uses 1-base indices. I personally prefer using 0-base indices while in C/C++ code, even code that interfaces with Lua. So I do the translation as late as possible. But you don't have to.

现在,对于循环的内容.第一步是从表中获取表条目.为此,我们需要给 Lua 一个索引并告诉 Lua 从表中获取该索引:

Now, for the contents of the loop. The first step is to get the table entry from the table. To do that, we need to give Lua an index and tell Lua to get that index from the table:

lua_pushinteger(L, i + 1);
lua_gettable(L, -2);

现在,第一个函数将索引压入堆栈.之后,我们的堆栈看起来像这样:

Now, the first function pushes the index onto the stack. After that, our stack looks like this:

2- {integer: i + 1}
1- {table: returned from function}

lua_gettable 函数值得更多解释.它需要一个键(记住:Lua 中的表键不必是整数)和一个表,并返回与该表中该键关联的值.或者 NIL,如果那里没有关联的值.但它的工作方式有点奇怪.

The lua_gettable function deserves more explanation. It takes a key (remember: table keys in Lua do not have to be integers) and a table, and returns the value associated with that key in that table. Or NIL, if no value is associated there. But the way it works is a bit odd.

它假设栈顶是键.所以它采用的参数是键将索引到的表的堆栈位置.我们使用-2"是因为,看堆栈.由于我们推送了一个整数,因此该表从顶部算起 2;因此我们使用-2".

It assumes that the top of the stack is the key. So the parameter it takes is the stack location of the table that the key will index into. We use "-2" because, well, look at the stack. The table is 2 from the top since we pushed an integer; therefore we use "-2".

之后,我们的堆栈看起来像这样:

After this, our stack looks like this:

2- {value: from table[i + 1]}
1- {table: returned from function}

既然得到了一个值,就必须验证它是一个字符串,然后才能得到它的值.

Now that we have gotten a value, we must verify that it is a string, and then get its value.

size_t strLen = 0;
const char *theString = lua_tolstring(L, -1, &strLen);

这个函数一次性完成所有这些.如果我们从表中得到的值不是字符串(或数字,因为 Lua 会自动将数字转换为字符串),那么 theString 将为 NULL.否则,theString 将有一个指向字符串的 Lua 拥有的指针(不要删除).strLen 也有字符串的长度.

This function does all of these at once. If the value we got from the table is not a string (or a number, since Lua will auto-convert numbers to strings), then theString will be NULL. Otherwise, theString will have a Lua-owned pointer (do not delete) to the string. strLen will also have the length of the string.

顺便说一句:Lua 字符串是以 NULL 结尾的,但它们也可以在内部包含 NULL 字符.C 字符串不允许这样做,但 C++ std::strings are.这就是为什么我不像你那样使用 lua_tostring ;C++ 字符串可以完全按原样存储 Lua 字符串.

Quick aside: Lua strings are NULL-terminated, but they also can internally contain NULL characters. C-strings aren't allowed to do this, but C++ std::strings are. That's why I don't use lua_tostring the way you did; C++ strings can store Lua strings exactly as they are.

现在我们有了来自 Lua 的字符串数据,我们需要将它放入我们的列表中.为了避免不必要的副本,我更喜欢这种语法:

Now that we have the string data from Lua, we need to put it into our list. To avoid unnecessary copies, I prefer this syntax:

list_strings.push_back();
list_strings.back().assign(theString, strLen);

如果我使用支持 C++11 的标准库和编译器,我只会使用 list_strings.emplace_back(theString, strLen);,依赖于 emplace_back 函数就地构造 std::string.这巧妙地避免了不必要的字符串副本.

If I were using a C++11-enabled standard library and compiler, I would have just used list_strings.emplace_back(theString, strLen);, relying on the emplace_back function to construc the std::string in-place. This neatly avoids making more copies of the string than necessary.

我们需要做最后一点清理工作.我们的堆栈上仍然有两个值:字符串和表.我们已经完成了字符串,所以我们需要摆脱它.这是通过从 Lua 堆栈中弹出一个条目来完成的:

There is one final bit of cleanup we need to do. Our stack still has two values on it: the string and the table. We're done with the string, so we need to get rid of it. This is done by poping one entry from the Lua stack:

lua_pop(L, 1);

这里,1"是要弹出的条目数,而不是堆栈位置.

Here, the "1" is the number of entries to pop, not the stack location.

您现在了解 Lua 中堆栈管理的工作原理了吗?

Do you understand how stack management works in Lua now?

1) 在调用之前查看堆栈的状态... luaL_loadfile 将一个函数压入堆栈?还是lua_pcall?

1) Looking at the state of the stack before the call... luaL_loadfile pushes a function to the stack? Or does lua_pcall?

假设你除了创建 Lua 状态之外没有做任何事情,那么在 luaL_loadfile 之前 stack 是空的.是的,luaL_loadfile 将一个函数压入堆栈.此函数表示已加载的文件.

Assuming you haven't done anything with the Lua state besides create it, then stack is empty before luaL_loadfile. And yes, luaL_loadfile pushes a function onto the stack. This function represents the file that was loaded.

3) 如果函数调用后返回错误值,堆栈的结果是什么?

3) What would the result of the stack be, if after making the function call it returned an error value?

正是文档所说的内容. 现在您了解了堆栈的工作原理,您应该通读文档.还推荐使用 Lua 编程一书.5.0 版可在线免费获得,但 5.1 版书要花钱.5.0 书籍仍然是一个有用的起点.

Exactly what the documentation says. Now that you understand how the stack works, you should read through the docs. The Programming in Lua book is also recommended. Version 5.0 is available online for free, but the 5.1 book costs money. The 5.0 book is still a useful starting point.

4) list_strings.reserve(len);至于这个......这个lua脚本实际上嵌入在一个通过代码库递归的小型C程序中,并将收集lua脚本从所有文件中返回的所有字符串......我不知道具体如何保留有效,但我要说的是我将使用许多表将字符串添加到此列表中...在这种情况下应该不使用保留吗?或者还在用...

4) list_strings.reserve(len); As for this... This lua script is actually embedded in a small C program that recurses through a code base and will collect ALL of the strings that the lua script returns from ALL of the files... I don't know exactly how reserve works, but What I'm saying is that I will be using many tables to add strings to this list... Should reserve just be not used in that case? or still used...

std::vector::reserve 确保 std::vector 将至少包含足够的空间用于 X 元素,其中 X 是您传递给它的值.我这样做是因为 Lua 会告诉你表中有多少元素,所以没有必要让 std::vector 自行扩展.你可以让它为所有事情做一次内存分配,而不是让 std::vector::push_back 函数根据需要分配更多内存.

std::vector::reserve ensures that the std::vector will contain at least enough space for X elements, where X is the value you pass it. I did this because Lua tells you how many elements are in the table, so there is no need to let the std::vector expand on its own. You can make it do one memory allocation for everything, rather than letting the std::vector::push_back function allocate more memory as needed.

只要您调用 Lua 脚本一次,这就会很有用.也就是说,它从 Lua 获取单个返回值.无论返回的表有多大,这都会起作用.如果您多次调用 Lua 脚本(来自 C++),则无法提前知道要保留多少内存.您可以为返回的每个表保留空间,但 std::vector 的默认分配方案有可能在大型数据集的分配数量上胜过您.所以在那种情况下,我不会打扰 reserve.

This is useful so long as you call your Lua script once. That is, it gets a single return value from Lua. No matter how large the table returned is, this will work. If you call your Lua script (from C++) multiple times, then there's no way to know ahead of time how much memory to reserve. You could reserve space for each table you get back, but it's possible for the std::vector's default allocation scheme to beat you in number of allocations for large datasets. So in that case, I wouldn't bother with reserve.

然而,作为一种默认情况,从健康规模的 reserve 开始并不是不明智的.选择一个您认为足够大"的数字,并预留足够的空间.

However, it wouldn't be unwise to start off with a healthy-sized reserve, as kind of a default case. Pick a number that you think would be "big enough", and reserve that much space.

相关文章