I am trying to write a lua library providing 2d arrays with the C api. I have followed the tutorial available on the lua website where it is demonstrated how to implement 1d arrays. To add support for 2d arrays I have simply added a third argument to the get method, but it doesn't seems to be the right way to do it, it seems the third parameter is never sent to the get function.
Here's the C code:
#include <lauxlib.h>
#include <lua.h>
typedef struct Array2d {
size_t height, width;
int data[];
} Array2d;
static int array2d_get(lua_State *L) {
Array2d *arr = lua_touserdata(L, 1);
size_t x = luaL_checkinteger(L, 2) - 1;
size_t y = luaL_checkinteger(L, 3) - 1;
luaL_argcheck(L, arr != NULL, 1, "'array' expected");
luaL_argcheck(L, x >= 0 && x < arr->width, 2, "x is not in [1,width]");
luaL_argcheck(L, y >= 0 && y < arr->height, 3, "y is not in [1,height]");
lua_pushinteger(L, arr->data[x + y * arr->width]);
return 1;
}
static int array2d_size(lua_State *L) {
Array2d *arr = lua_touserdata(L, 1);
luaL_argcheck(L, arr != NULL, 1, "'array' expected");
lua_pushinteger(L, arr->width * arr->height);
return 1;
}
static int array2d_new(lua_State *L) {
size_t width = luaL_checkinteger(L, 1);
size_t height = luaL_checkinteger(L, 2);
size_t nbytes = sizeof(Array2d) + width * height * sizeof(int);
Array2d *arr = lua_newuserdata(L, nbytes);
arr->width = width;
arr->height = height;
luaL_getmetatable(L, "lib.arr");
lua_setmetatable(L, -2);
for (int i = 0; i < width * height; i++) {
arr->data[i] = 0;
}
return 1;
}
static const struct luaL_Reg libarr_f[] = {{"new", array2d_new}, {NULL, NULL}};
static const struct luaL_Reg libarr_m[] = {
{"__index", array2d_get}, {"__len", array2d_size}, {NULL, NULL}};
int luaopen_libarr(lua_State *L) {
luaL_newmetatable(L, "lib.arr");
luaL_setfuncs(L, libarr_m, 0);
luaL_newlib(L, libarr_f);
return 1;
}
And here is the lua code:
local libarr = require "libarr"
local width=3
local height=3
arr = libarr.new(width,height)
for i=1,width do
for j=1,height do
print(arr[i][j])
end
end
And when I run the code I get following message, which shows that the get function never received the third argument:
lua: main.lua:7: bad argument #3 to 'index' (number expected, got no value)
stack traceback:
[C]: in metamethod 'index'
main.lua:7: in main chunk
[C]: in ?
Does someone knows what I am doing wrong? Any clues on how to make it works?
Does someone knows what I am doing wrong?
arr[i][j]
does not invoke getmetatable(arr).__index(arr, i, j)
as you seem to be expecting it to.
Rather, it invokes getmetatable(arr).__index(arr, i)[j]
. So getmetatable(arr).__index
, which resolves to array2d_get
, is called with arr, i
, not arr, i, j
, which throws the error you're seeing.
If this didn't throw an error, the return value of this would be indexed with j
.
Any clues on how to make it works?
You might be tempted to try something like arr[i, j]
, but unlike some other languages, indexing in Lua only allows exactly a single value; this is a syntax error.
I see 4 options to fix this:
Write a "get
" "method" rather than "overloading" []
. That "method" can take parameters i, j
then: arr:get(i, j)
desugars to getmetatable(arr)["get"](arr, i, j)
. You just need to set the metatable up such that getmetatable(arr)["get"]
resolves to your array2d_get
function (for example by setting __index = {get = array2d_get}
).
I would also argue that this is probably cleaner as you add methods to your 2d arrays. Otherwise, something like arr:push(42)
will always first have to check whether "push"
is a valid index, then resolve to the method if it isn't. This is also not great for performance (of method calls).
Move from your "2d array" userdata back to "1d array of 1d arrays", then everything will work naturally, albeit with somewhat more memory overhead (and negative implications e.g. for cache locality and GC pressure, so this also comes at a runtime cost).
You could make arr[y]
return a "row" userdata object for the y-th row (effectively just a fancy pointer) which, when indexed, would give you the x-th element in the row.
Creating such "row" objects for every indexing operation might be a problem performance-wise, however.
For the sake of completeness, you could also use a table (or a userdata object, or a string, or a closure...) to implement something like arr[{x, y}]
or arr[Index2D(x, y)]
. If you need performance, this is probably a bad idea as each indexing operation requires creating a garbage table then.