2
\$\begingroup\$

I have defined a userdata in Lua (Lua version is 5.1) as shown below-

#include <lua.h>
#include <lauxlib.h>
#include <stdio.h>

typedef struct FloatArray {
  int size;
  float values[1];
} FloatArray;

static int new_array(lua_State *L) {
  int n = luaL_checkint(L, 1);
  size_t nbytes = sizeof(FloatArray) + (n - 1) * sizeof(float);
  FloatArray *a = (FloatArray *)lua_newuserdata(L, nbytes);
  a->size = n;
  return 1;
}

I can create a new array of given size by using following Lua code-

a = array.new(10)

Now, I am extending the above function for converting any given 1-d table to array. Please see the code snippet below-

static int new_array_from_table(lua_State *L) {
  luaL_argcheck(L, lua_type(L, -1) == LUA_TTABLE, 1, "'1-d array' expected");

  // get the length of input table
  int n = lua_objlen(L, -1);
  int index;
  float values[n];

  // fetch data and use it to fill the array
  for (index = 1; index <= n; index++) {
    lua_rawgeti(L, -1, index);
    values[index - 1] = (float)lua_tonumber(L, -1);
    lua_pop(L, 1);
  }

  // create new userdata
  size_t nbytes = sizeof(FloatArray) + (n - 1) * sizeof(float);
  FloatArray *a = (FloatArray *)lua_newuserdata(L, nbytes);
  a->size = n;

  // fill the userdata
  for (index = 0; index < n; index++)
    a->values[index] = values[index];

  return 1;
}

The above code works. However, as you can notice that fetching the table data and filling the userdata are done separately.

I wish to have a better implementation, in which we first create new userdata and then we fill the userdata from fetched data at one step.

In other words, I am looking for a way to do the following-

// get the length of input table
int n = lua_objlen(L, -1);

// create new userdata
size_t nbytes = sizeof(FloatArray) + (n - 1) * sizeof(float);
FloatArray *a = (FloatArray *)lua_newuserdata(L, nbytes);
a->size = n;

// fetch data and use it to fill the array
int index;
for (index = 1; index <= n; index++) {
  lua_rawgeti(L, -1, index);
  a->values[index] = (float)lua_tonumber(L, -1);
  lua_pop(L, 1);
}

I noticed that after creating new userdata, the data is being converted to 0. Any workaround, please?

\$\endgroup\$
2
\$\begingroup\$

You have a duplication of the allocation and init part in the 2 functions, instead you can create a helper that will create the usertable, return a pointer to it and leave it on top of the stack:

static FloatArray *create_array(lua_State *L, int n) {
  size_t nbytes = sizeof(FloatArray) + (n - 1) * sizeof(float);
  FloatArray *a = (FloatArray *)lua_newuserdata(L, nbytes);
  a->size = n;
  return a;
}

static int new_array(lua_State *L) {
  int n = luaL_checkint(L, 1);
  create_array(L, n);
  return 1;
}

When accessing parameters in the C api it's easier to always index them from the bottom of the stack. That wat you won't end up being mistaken about the state of the stack and try to use the wrong value by accident (like you did in your attempt).

static int new_array_from_table(lua_State *L) {
  luaL_argcheck(L, lua_type(L, 1) == LUA_TTABLE, 1, "'1-d array' expected");

  // get the length of input table
  int n = lua_objlen(L, 1);

  // create new userdata
  FloatArray *a = create_array(L, n); 
  //leaves the new table at the top of the stack

  // fetch data and use it to fill the array
  int index;
  for (index = 1; index <= n; index++) {
    lua_rawgeti(L, 1, index);
    a->values[index-1] = (float)lua_tonumber(L, -1);
    lua_pop(L, 1);
  }
  return 1;
}

With the creation bit in a single place you can now add a metatable to it so the script can access the values using the indexing syntax.

luaL_Reg[] floatArrayFuncs = { {"__index", get_array_value},
         {"__newindex", set_array_value},
         //...
         {NULL, NULL},};

//call on initialization
static void init_FloatArray(lua_State *L){
    luaL_newmetatable (L, "FloatArray");
    luaL_setfuncs(L, floatArrayFuncs , 0);
    lua.pop(1);
}

static FloatArray *create_array(lua_State *L, int n) {
  size_t nbytes = sizeof(FloatArray) + (n - 1) * sizeof(float);
  FloatArray *a = (FloatArray *)lua_newuserdata(L, nbytes);
  luaL_setmetatable (L, "FloatArray", -1); 
  a->size = n;
  return a;
}

The metatable also allows you an easy way to type check the userdata from lua using

FloatArray *a = (FloatArray *)luaL_testudata (L, arg, "FloatArray");

Also you can overload the creation function by doing

static int new_array(lua_State *L) {
  switch(lua_type(L, 1)){
  case LUA_TNUMBER:
      int n = luaL_checkint(L, 1);
      create_array(L, n);
      return 1;
  case LUA_TTABLE:
      return new_array_from_table(L);
  case LUA_TUSERDATA:
      return duplicate_array(L);  //exercise for the reader
  default:
      luaL_argcheck(L,0, 1, "expected number, 1D array or FloatArray");
  }
  return 0; //never reached
}
\$\endgroup\$
  • \$\begingroup\$ Thank you very much. I personally liked how beautifully you overloaded array creation by using switch/case statement. Other than this, I found following typos, can you please fix it? (1) The index should be index-1 in a->values[index] = (float)lua_tonumber(L, -1) (2) LUA_TTNUMBER should be LUA_TNUMBER: in case LUA_TTNUMBER:. I couldn't understand two things (1) Can you, please provide an explanation for doing luaL_setmetatable (L, "FloatArray") (2) when something is pushed to the stack, you won't end up accessing the wrong value \$\endgroup\$ – Ravi Joshi Jul 5 '18 at 14:41
  • \$\begingroup\$ In your attempt to merge the 2 loops you used lua_rawgeti(L, -1, index); however -1 pointed to the userdata instead of the table you wanted to get from. \$\endgroup\$ – ratchet freak Jul 5 '18 at 15:12

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.