Chapter 3. Using Lua Values in C

This chapter covers intermediate techniques of API construction in Lua. We’ll see how to pass function arguments from C to Lua, how to work with Lua tables, and how to implement API functions in Lua instead of C.

EatyGuy Version 3: Passing Data to a Lua Function

In Lua, you can pass as many arguments to a function as you like. Even if the function is defined to accept three parameters, for example, you can pass in one or four, or even no arguments when you call it.

You can achieve this same flexibility from C at runtime. Chapter 2 briefly introduces lua_call(), and because this is such a useful function, it’s worthwhile to take a more detailed look at it here. Following is the general technique for calling a Lua function from C, written with an example call to print(1, 2, ’three’):

// 1. Push the Lua function onto the stack.

lua_getglobal(L, "print");

// 2. Push the arguments, in order, onto the stack.

lua_pushnumber(L, 1);
lua_pushnumber(L, 2);
lua_pushstring(L, "three");

// 3. Call the function.
//    You must also indicate how many return values you'd like.

lua_call(L, 3, 0);  // 3 = #input values; 0 = #output values.

// The result on stdout is:
// 1    2    three

When I play games, I love having the ability to influence the outcome of the game by pressing buttons. Let’s add this feature to EatyGuy. In particular, we can use C’s getchar() function to get key codes, and pass those into the eatyguy.loop() function.

Receiving Arrow Key Presses

Terminal-based C programs receive input via the stdin (standard input) stream. From the code’s perspective, the stream is a sequence of bytes. Letters and numbers from the keyboard arrive in the form of their ASCII codes—for example, pressing the A key results in a byte with value 65 appearing on stdin. In C, the getchar() function returns an int value indicating which byte last appeared on the stdin stream. If you’re like me, you might be curious why the return value is an int when an unsigned char is a better fit for byte values. The answer is that getchar() returns negative values on error conditions, or when the end-of-file is encountered, which can happen when the user pipes something into your program’s stdin stream.

Two difficulties arise in writing a game that reads arrow keys from stdin. The first is that, by default, reads from stdin are blocked until data is received, and bytes from stdin are typically buffered until a newline is pressed. As you can imagine, it would be a catastrophe if a game froze every time the player hesitated before pressing a key. Luckily, it’s possible to make this process nonblocking by using C’s fcntl() (file control) function, which provides control over how file descriptors are handled by system calls. These mysterious lines of code are baked into our C code’s getkey() function in Chapter 2; hopefully, they will make more sense now:

// Make reading from stdin nonblocking.
int flags = fcntl(STDIN_FILENO, F_GETFL);
fcntl(STDIN_FILENO, F_SETFL, flags | O_NONBLOCK);

After stdin has been set as nonblocking, any call to getchar() returns immediately. If a byte is available, that byte value is returned; otherwise, the value EOF, reliably set to –1, is returned.

The second difficulty in reading arrow key presses is that, unlike most keys, arrow keys are not presented as single bytes on stdin. Rather, they are presented as short sequences of bytes known as escape sequences because they typically begin with the escape byte with numerical value 27. In this book, I use hardcoded values that work for Windows, Linux, and macOS. These escape sequences are three bytes long and begin with the byte value 27 followed by 91. The third byte indicates which arrow key was pressed. The C function below returns either the byte pressed or the third byte in an escape sequence, along with a flag called is_end_of_seq to indicate that an escape sequence was encountered:

// Part of eatyguy3.c.

int getkey(int *is_end_of_seq) {

  // Make reading from stdin nonblocking.
  int flags = fcntl(STDIN_FILENO, F_GETFL);
  fcntl(STDIN_FILENO, F_SETFL, flags | O_NONBLOCK);

  // We care about two cases:
  // Case 1: A sequence of the form 27, 91, X; return X.
  // Case 2: For any other sequence, return each int separately.

  *is_end_of_seq = 0;
  int ch = getchar();
  if (ch == 27) {
    int next = getchar();
    if (next == 91) {
      *is_end_of_seq = 1;
      ch = getchar();
      goto end;
    }
    // If we get here, then we're not in a 27, 91, X sequence.
    ungetc(next, stdin);
  }

end:

  // Turn off nonblocking I/O. On some systems, leaving stdin 
  // nonblocking will also leave stdout nonblocking, 
  // which can cause printing errors.
  fcntl(STDIN_FILENO, F_SETFL, flags);
  return ch;
}

This function resides in the file eatyguy3.c, which is the same as eatyguy2.c from Chapter 2 except for the changes explicitly described here.

To send useful key values into eatyguy.loop(), let’s translate the arrow keys from numerical values to the strings 'up', 'down', 'left', and 'right'. This approach has two benefits in terms of API design:

  • It encapsulates an implementation detail in C code that Lua can safely ignore. Specifically, Lua coders don’t need to know how to translate seemingly arbitrary numeric byte values into arrow keys.

  • It encourages readable Lua code. Rather than a conditional such as key == 65, the variant key == ’left’ is promoted, which makes the meaning of the string-based variant more obvious.

The following function pushes the key value onto the Lua stack, translating numeric values to the appropriate string for arrow keys:

// Part of eatyguy3.c.

void push_keypress(lua_State *L, int key, int is_end_of_seq) {
  if (is_end_of_seq && 65 <= key && key <= 68) {
    // up, down, right, left = 65, 66, 67, 68
    static const char *arrow_names[] = {"up", "down",
                                        "right", "left"};
    lua_pushstring(L, arrow_names[key - 65]);
  } else {
    lua_pushnumber(L, key);
  }
}

With this code in place, sending keypress data to Lua’s event loop takes place in main(), replacing the contents of the while(1) loop with the following:

// Part of eatyguy3.c

int is_end_of_seq;
int key = getkey(&is_end_of_seq);
if (key == ESC_KEY || key == 'q' || key == 'Q') done();

// Call eatyguy.loop(<key>).
lua_getfield(L, -1, "loop");
push_keypress(L, key, is_end_of_seq);
lua_call(L, 1, 0);

You can find the full code for eatyguy3.c in this chapter’s directory in the code repository. The source code presented in this chapter provides all of this file except for an additional function called sleephires(), which allows the process to sleep (essentially not using the CPU) for time intervals of subsecond resolution. Its only use within EatyGuy is to avoid using all of your machine’s CPU. Because it doesn’t affect the behavior of EatyGuy and doesn’t teach us much about Lua or creating APIs, I’m excluding the sleephires() code from this book.

On the Lua side, the loop function becomes this:

-- Part of eatyguy3.lua

function eatyguy.loop(key)
  update(key)
  draw()
end

The update() function is modified so that EatyGuy changes direction when an arrow key is pressed. In this code snippet, some unmodified lines have been omitted:

-- Part of eatyguy3.lua.
local function update(key)
  ...

  -- Update the next direction if an arrow key was pressed.
  local direction_of_key = {left = {-1, 0}, right = {1, 0},
                            up   = {0, -1}, down  = {0, 1}}
  local new_dir = direction_of_key[key]
  if new_dir then player.next_dir = new_dir end

  -- Only move every move_delta seconds.
  ... (This section is the same as in eatyguy2.lua.) ...

  -- Try to change direction; if we can't, next_dir will take
  -- effect at a corner where we can turn in that direction.
  if can_move_in_dir(player, player.next_dir) then
    player.dir = player.next_dir
  end

  -- Move in direction player.dir if possible.
  local can_move, new_pos = can_move_in_dir(player, player.dir)
  if can_move then
    player.old_pos = player.pos  -- Save the old position.
    player.pos = new_pos
  end
end

EatyGuy is now a game that allows you to move the hero by using the arrow keys! Our fledgling game has taken a real step forward in entertainment value. But wait—there’s more! Next I’ll be talking about how to implement API functions within Lua itself.

EatyGuy Version 4: Implementing API Functions Within Lua

So far, the simple game engine we’ve built supports the functions timestamp(), set_pos(), and set_color(), all implemented in C, and all callable from Lua. There’s great value in the ability to pass C functions into Lua like this—this is the only way that your API can do things that Lua can’t. So, you might be wondering why we’d still want to write some API functions in Lua.

The answer is that Lua makes certain things much easier to do than C does. The most obvious example is the use of Lua’s table structure, which offers fast storage and lookup of arbitrary key/value pairs. You might be able to use a C library to achieve similar functionality, but why add another library when we’re already working with the power of Lua?

In this section, we’ll fix a performance bottleneck in EatyGuy by rewriting set_pos() and set_color() in Lua. I’ll do this in a new file called util.lua because I want to maintain a clear separation between the game engine part of the code (which thus far consists of our C file and which will soon also include util.lua) and the game-specific part of the code (EatyGuy’s Lua file).

Fixing the tput Bottleneck

If you run the eatyguy3 binary based on the code in the previous section of this chapter, you might notice that it takes several seconds to draw the initial maze. This is surprising because all of the basic operations are conceptually simple, and there aren’t that many of them. The bottleneck turns out to be the fact that, in C, system() is called many times for every line of the maze that’s drawn. This call does more work than we need. Specifically:

  • The system() function begins by creating a new process.

  • It then executes a new shell in that process.

  • That shell is instructed to run the given string as a command.

Conceptually, this is a bit like building a physical library every time you want to read a book. In reality, many of the calls to tput will be identical, and tput is a deterministic command. EatyGuy would be much more efficient if it memorized the output values for a given tput command; we’ll add some Lua code to do just that. (In computer science, memorization tricks like this are often referred to as memoization.)

The code in the new util.lua file will use Lua’s io.popen() command to execute tput in a shell just like system() did. The io.popen() function pipes the output of the shell through a Lua file handle so that we can read from it; the name “popen” indicates opening a command whose input and output are piped with the creating process. By capturing the output of each tput command, those values can be cached for use the next time an identical command string is executed. For example, EatyGuy often changes the background color to blue. Caching the corresponding tput string means tput is called only a single time to change the background to blue—all future calls to set_color() doing the same thing can simply print out the saved string.

Here’s the util.lua code:

-- util.lua
--
-- Define the functions set_color and set_pos so that they cache
-- the strings returned from tput in order to reduce the number
-- of tput calls.


-- Local functions like memoized_cmd won't be globally visible
-- after this script completes.

local memoized_strs = {}  -- Maps cmd -> str.

local function memoized_cmd(shell_cmd)
  if not memoized_strs[shell_cmd] then
    local pipe = io.popen(shell_cmd)
    memoized_strs[shell_cmd] = pipe:read()
    pipe:close()
  end
  io.write(memoized_strs[shell_cmd])
end


-- Global functions will remain visible after this script
-- completes.

function set_color(b_or_f, color)
  assert(b_or_f == 'b' or b_or_f == 'f')
  memoized_cmd('tput seta' .. b_or_f .. ' ' .. color)
end

function set_pos(x, y)
  memoized_cmd(('tput cup %d %d'):format(y, x))
end

It’s now safe to delete the C definitions of set_color() and set_pos(). In their stead, we can expose the Lua versions of these functions by adding a call to luaL_dofile(L, “util.lua”) before loading the script that depends on them. In this section, I’ll work with the files eatyguy4.c and eatyguy4.lua, which are modified versions of eatyguy3.c and eatyguy3.lua from the last section (eatyguy3.lua and eatyguy4.lua are identical except for the names of the files). The changes in the C file are highlighted here:

// eatyguy4.c
//

...

// The functions set_color() and set_pos() are no longer defined.

...

int main() {

  start();

  // Create a Lua state and load the module.
  lua_State *L = luaL_newstate();
  luaL_openlibs(L);

  // Set up API functions written in C.
  lua_register(L, "timestamp", timestamp);
  // We no longer need to register set_color() or set_pos().

  // Set up API functions written in Lua.
  luaL_dofile(L, "util.lua");

  // Load eatyguy4.lua and call the init(), loop() functions.
  ...
}

On my system, this new version takes 0.037 seconds to create and draw the initial maze, whereas the previous version takes 3.69 seconds. This change made the drawing procedure 100 times faster! It’s worth noting that we’ve sped up a program by replacing some C code with Lua—and the replacement has not really increased our line count! All in all, a solid improvement.

Up next, we’ll get an introduction to working with Lua tables from C.

EatyGuy Version 5: Working with Lua Tables from C

Code that works with Lua tables is generally shorter in Lua than it is in C. That’s why our first API functions that used tables, which we wrote in util.lua in the last section, were written in Lua despite the fact that everything you can do in Lua is also possible in C. Although Lua code is often shorter, there are times when you might prefer to work with tables in C. For example, you might want to implement a speed-critical function entirely in C, but allow that function to accept or return tables. This section introduces the table functions in Lua’s C API.

Where Tables Live

From C’s point of view, Lua tables can reside only on the stack. To clarify what this means, consider an alternate universe in which the Lua implementers chose to give C code a different kind of access to Lua tables: What if there were a LuaTable type? If we were programming in C++, LuaTable might be a class, and we could use operator overloading to write lines like these to set up a table:

// This is a fabricated, non-working C++ example.
LuaTable *myTable = new LuaTable();
myTable["key"] = "value";

The first reason this won’t work is that Lua is quite decisively built in and for C, not C++. The C alternative would need to be a bit more verbose; for example:

// Warning: This code won't work!
// This is an example of an alternative to the actual C API.
LuaTable *my_table = lua_newtable();
lua_pushstring(L, "key");
lua_pushstring(L, "value");
   
// Set my_table["key"] = "value". 
lua_setkeyvalue(L, my_table, -2, -1);  

Now we get to a trickier problem: if tables were implemented like this, how would Lua manage its memory? If Lua deleted some internal representation of the table while you still held the my_table pointer, your code could crash the next time you used that pointer. Addressing that issue would require you to manage Lua objects just as you have to manage memory with malloc() and free(). Because Lua already has a garbage collector, this is redundant work that erodes Lua’s memory efficiency. Thus, the Lua designers decided to keep Lua tables on the stack, and the C API always works with tables in terms of their stack indexes.

The Primary Table Functions

The five functions listed in Table 3-1 provide most of the functionality for working with tables. With the exception of lua_newtable(), note that the first parameter after L is consistently the index of the table on the stack. Tables have key/value pairs, so I’ll use the letters k and v to stand for key and value, respectively. For these pairs, the value is consistently either pushed onto or popped from the top of the stack.

Table 3-1. The fundamental C API functions for working with Lua tables in C
C API function Description Stack use
lua_newtable(L) Create a new empty table and push it onto the top of the stack. [–0, +1]
lua_setfield(L, <index>, <key>) Set t[k] = v, where t is the table at <index>, k is the string <key>, and the value v is whatever is on top of the stack. [–1, +0]
lua_getfield(L, <index>, <key>) Push the value t[k] onto the top of the stack, where t is the table at <index> and k is the string <key>. Similar to Lua, nil is pushed onto the stack if <key> is not a key of table t. [–0, +1]
lua_settable(L, <index>) Set t[k] = v, where t is the table at <index>, k is at stack position –2, and v is at stack position –1. [–2, +0]
lua_gettable(L, <index>) Pop the value k from the top of the stack and then push the value t[k] onto the top of the stack, where t is the table at <index>. Similar to Lua, nil is pushed onto the stack if k is not a key of table t. [–1, +1]

The function lua_settable() can do everything that lua_setfield() does. The difference is that lua_setfield() accepts a string as a direct parameter, whereas lua_settable() accepts a stack index to the key, instead, which allows it to work with either string or nonstring keys. The lua_setfield() function exists because it’s convenient to write shorter code in the common case that your key is a string. The relationship between lua_gettable() and lua_getfield() is analogous.

As in Lua, you can delete a table by removing all references to it. The lua_pop() function described in Table 3-2 pops as many elements as you like from the top of the stack.

Table 3-2. The C API function lua_pop()
C API function Description Stack use
lua_pop(L, n) Remove (pop) n elements from the top of the stack. [–n, +0]

If the table you want to delete is on the top of the stack, you can delete it via C like so:

lua_pop(L, 1); // Pop the table from the stack. (╯°□°)╯︵ ┻━┻

A (hopefully short) span of time might pass before the garbage collector actually frees the associated memory.

You can attach special functions to Lua tables that effectively override setting and getting of new keys. These functions, called metamethods, are attached by being assigned as values in a table’s metatable, and by having a key that’s a language-recognized special name, such as __index. You’ll see metamethods in action in Chapter 4.

When you use a new key to perform a table lookup or set a value in a table with an __index() (for lookups) or __newindex() (for setting values) metamethod, that metamethod is called instead of Lua directly looking up or setting the value. These lookup/setting metamethods also work from C when using the C API functions that we just learned about. You can circumvent these metamethods by calling the functions lua_rawget() and lua_rawset(), or, specifically for integer keys, lua_rawgeti() and law_rawseti(). For the sake of brevity (the soul of wit), these last two functions are not covered in further detail in this book.

Using a Table to Send Game State to Lua

We’re now ready to enhance EatyGuy by sending a table called state to eatyguy.loop() instead of just the keypress value in key.

The code in eatyguy4.lua calls the function timestamp() several times. Because this function is likely to return a different value with every call, the code will probably receive different values, and bugs can arise based on the expectation that time only moves forward between event loop cycles. In addition, every call to timestamp() after the first call is redundant because the information is theoretically already available to the code.

To address this, we’ll replace the old loop parameter key with a new table parameter called state that has the following two key/value pairs:

state.key
The key that was pressed.
state.clock
A timestamp measured in seconds.

This section works with the files eatyguy5.c and eatyguy5.lua, which are slight modifications of the files eatyguy4.c and eatyguy4.lua from the previous section. The state table is created and pushed onto the Lua stack with the following C function in eatyguy5.c:

// Part of eatyguy5.c (anywhere after gettime() is defined)

void push_state_table(lua_State *L,
                      int key,
                      int is_end_of_seq) {

  // The state of the Lua stack is given in indented comments in
  // this function to clarify the actions of each function call.

  lua_newtable(L);

    // stack = [.., {}]

  push_keypress(L, key, is_end_of_seq);

    // stack = [.., {}, key]

  lua_setfield(L, -2, "key");

    // stack = [.., {key = key}]

  lua_pushnumber(L, gettime());

    // stack = [.., {key = key}, clock]

  lua_setfield(L, -2, "clock");

    // stack = [.., {key = key, clock = clock}]
}

Here’s the C code that passes this table into eatyguy.loop(), with changes in bold:

lua_getglobal(L, "eatyguy");
while (1) {
  int is_end_of_seq;
  int key = getkey(&is_end_of_seq);
  if (key == ESC_KEY || key == 'q' || key == 'Q') done();

  // Call eatyguy.loop(state).
  lua_getfield(L, -1, "loop");
  // This replaces the previous call to push_keypress().
  push_state_table(L, key, is_end_of_seq);
  lua_call(L, 1, 0);
}

The Lua code changes by receiving the state table and using the value of state.clock in place of the old calls to timestamp(). The updated loop() function passes the full state to the update() function, but only the clock value to the draw() function. The idea here is to emphasize that all user input is meant to be processed within the update() function, not in draw(). Here’s the loop() function:

-- Part of eatyguy5.lua

function eatyguy.loop(state)
  update(state)
  draw(state.clock)
end

And here’s the new update() function, with changes in bold:

local function update(state)

  ...

  -- Update the next direction if an arrow key was pressed.
  local dir_of_key = {left = {-1, 0}, right = {1, 0},
                      up   = {0, -1}, down  = {0, 1}}
  local new_dir = dir_of_key[state.key]
  if new_dir then player.next_dir = new_dir end

  -- Only move every move_delta seconds.
  if next_move_time == nil then
    next_move_time = state.clock + move_delta
  end
  if state.clock < next_move_time then return end

  ...
end

Finally, here’s the new draw() function, again with changes in bold:

local function draw(clock)

  -- Choose the sprite to draw.
  ...
  -- We now use `clock` instead of calling timestamp().
  local framekey = math.floor(clock / anim_timestep) % 2 + 1
  local chars    = draw_data[dirkey][framekey]
  ...

As you can see, the Lua-side changes are minimal. You now can run eatyguy5 to see the same behavior based on a cleaner design—in particular, we now have a more consistent clock value, and the code has reduced the number of Lua-based timestamp() calls from three down to zero. Not bad!

Adding Some Class to Your API

This chapter taught you the fine art of passing values from C into Lua functions, be they tables or primitive types like strings or numbers. We also witnessed the utility of building API functions by using Lua itself.

Coding with a great API can sometimes feel a bit like speaking in your own native language. You naturally think and speak in terms of nouns tied together by related verbs. So far, this book has focused on the verb part of APIs by teaching how to create standalone functions. In Chapter 4, we take an in-depth look at the noun part of APIs by exploring Lua-visible class interfaces that you can write in either Lua or C.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.144.41.229