Chapter 4. Making Your API Classy
Often, the functions in an API can be broken down into a few categories. Suppose, for instance, that a library allows you to manage both image files and movie files. In cases like this, it’s natural to use a class to represent each conceptual object or task being performed by a subset of the interface—for example, we could have an Image
class and a Movie
class. This chapter covers common methods of defining classes in Lua; implementing inheritance; and defining custom, class-like userdata types in C.
EatyGuy Version 6: Writing a Class in Lua
Most of Lua’s design is simple and transparent, including the building blocks of Lua’s class system. Nonetheless, Lua does ask for some depth of thought when it’s time to implement classes and class inheritance. This is due to the fact that classes in Lua are not directly supported by the language; rather, they are enabled by giving coders the low-level pieces needed to be assembled into class mechanics. Because of this, I’ll cover Lua classes in more detail than I’ve previously covered other language features.
In this chapter, we add enemies to EatyGuy. Let’s call them baddies because it sounds more fun. This is a great opportunity to implement a class–subclass relationship: the player and baddies can share a common superclass that we’ll call Character
, whereas the baddy-specific behavior will live in subclass called Baddy
. As is common in some languages, we’ll capitalize the names of classes and class-like userdata. All other names we’ll keep in lowercase. Let’s also use the word method to refer to functions that are defined as part of a class.
A Common Code Pattern for Lua Classes
Because Lua provides only low-level devices from which coders build classes, different class implementations are possible. Let’s use a code pattern that is simple yet works well with single inheritance. Here’s a bare-bones example:
-- MyClass.lua MyClass = {} function MyClass:new(obj) obj = obj or {} -- Line 1. self.__index = self -- Line 2. return setmetatable(obj, self) -- Line 3. end function MyClass:method() print('Hi from ' .. self.name) end return MyClass
You could use this class in another Lua file by running the following code:
local MyClass = require 'MyClass' local obj = MyClass:new({name = 'Winnifred'}) obj:method() --> Prints out 'Hi from Winnifred'.
Because you’re an astute reader, you probably noticed that these functions are both defined with, and called with, a colon before the function name rather than a period. This colon notation is syntactic sugar for receiving or sending in a special first parameter called self
. I’ll explain this notation and then explain MyClass
in more detail.
Suppose that you define a function as a member of a table like so:
function atable:amethod(a, b, c) -- (function body here) end
The colon indicates that the function accepts an implicit first parameter called self
, followed by any explicitly listed parameters. The preceding example code has the same effect as this more verbose code:
function atable.amethod(self, a, b, c) -- (function body here) end
Now, instead of defining a function, suppose that you’re calling a function on a table:
anothertable:amethod(10, 20)
In this case, the colon syntax means that the value just before the colon will be sent in as an inserted first parameter to the function. So, the preceding method call is the same as this longer version:
anothertable.amethod(anothertable, 10 20)
These two mechanics work together nicely, allowing class methods to access the class instance on which they’re being operated.
Now that you understand colon syntax, let’s see what else is going on in the MyClass
example. The most interesting piece is the constructor—that is, the new()
method. Let’s understand the actions it takes by stepping through this call:
local obj = MyClass:new({name = 'Winnifred'})
The new()
method’s first explicit parameter is obj
, and the first line of new()
is this:
obj = obj or {} -- Line 1.
If the constructor were called with no parameters, obj
would have a nil value, which is falsy. This first line ensures that obj
is an empty table in that case. In general, we’d like obj
to be some kind of table, and it will gain status as an instance of MyClass
over the next two lines. Here’s the second line of the constructor:
self.__index = self -- Line 2.
Because new()
was called with MyClass
immediately before the colon, the value of self is MyClass
, so that the line effectively takes this step:
MyClass.__index = MyClass
Here’s the third line:
return setmetatable(obj, self) -- Line 3.
This makes MyClass
the metatable of obj
. Any key lookups on obj
will fall back to key lookups on the table found in obj
’s metatable’s __index
value. Temporarily ignoring the case of falsy values, we could write this behavior like so:
x = obj.key or getmetatable(obj).__index.key
In Lua, this expression is essentially executed for you when you simply write obj.key
; and Lua is smart enough to evaluate the expression to false if that happens to be the direct value of obj.key
. As a result, this method call
obj:method()
works as desired because it’s the same as this one:
fn = obj.method or MyClass.method fn(obj) -- And this is the same as MyClass.method(obj).
The main mechanism is that failed key lookups result in a secondary lookup if the table has a metatable with an __index
value. In fact, you can chain this delegation process through a series of metatables. We’ll see this chaining device later in this chapter when we discuss class inheritance.
The Character Class Example
Now that we’ve seen the basics of implementing a class in Lua, let’s add one to our blossoming game engine. A character will be any entity that takes up one grid space, such as the player or a baddy, and that may move around the screen at regular time intervals.
Following is the code for Character.lua, but I won’t explain it in detail because it’s all extracted from the previous version of our EatyGuy Lua file. At first, it might feel as if I’m simply moving code around rather than improving things. The improvement comes with the reuse of this code that we’ll see when baddies are added later in this chapter. Here’s Character.lua:
-- Character.lua -- -- A class to capture behavior of general -- characters. -- Character = {} function Character:new(c) c = c or {} self.__index = self return setmetatable(c, self) end function Character:can_move_in_dir(dir, grid) local p = self.pos local gx, gy = p[1] + dir[1], p[2] + dir[2] return (grid[gx] and grid[gx][gy]), {gx, gy} end function Character:move_if_possible(grid) -- Try to change direction; if we can't, next_dir will take -- effect at a corner where we can turn in that direction. if self:can_move_in_dir(self.next_dir, grid) then self.dir = self.next_dir end -- Move in direction self.dir if possible. local can_move, new_pos = self:can_move_in_dir(self.dir, grid) if can_move then self.old_pos = self.pos -- Save the old position. self.pos = new_pos end end function Character:draw(grid) -- Draw the character. local color = self.color or 3 -- Default to yellow. set_color('b', color) set_color('f', 0) -- Black foreground. local x = 2 * self.pos[1] local y = self.pos[2] set_pos(x, y) io.write(self.chars) io.flush() -- Erase the old character pos if appropriate. if self.old_pos then local op = self.old_pos local x = 2 * op[1] local y = op[2] set_pos(x, y) set_color('f', 7) -- White foreground. set_color('b', 0) -- Black background. io.write(grid[op[1]][op[2]]) io.flush() self.old_pos = nil end end return Character
To make this class available to the Lua game code, the two lines that follow are added to eatyguy6.c immediately after util.lua is loaded. The file eatyguy6.c is otherwise a copy of eatyguy5.c:
luaL_dofile(L, "Character.lua"); lua_setglobal(L, "Character");
In pulling out the Character
functionality, we can create the file eatyguy6.lua with a few changes. The player
variable will become an instance of Character
. I’ve omitted several sections of code, which are identical to the previous version, and used a bold font to highlight the changes to EatyGuy’s Lua code:
-- Changed parts of eatyguy6.lua ... -- Globals. local percent_extra_paths = 15 local grid = nil -- grid[x][y]: falsy = wall. local grid_w, grid_h = nil, nil local player = Character:new({pos = {1, 1}, dir = {1, 0}, next_dir = {1, 0}}) ... -- The can_move_in_dir() function is removed. ... local function update(state) ... if state.clock < next_move_time then return end next_move_time = next_move_time + move_delta -- It's been at least move_delta seconds since the last -- time things moved, so let's move them now! player:move_if_possible(grid) end local function draw(clock) ... -- framekey switches between 1 & 2; basic sprite animation. local framekey = math.floor(clock / anim_timestep) % 2 + 1 player.chars = draw_data[dirkey][framekey] -- Draw the player and baddies. player:draw(grid) end ...
For the most part, the extraction either replaces code with a method call, or removes a function altogether in the case of can_move_in_dir()
.
The end result is a surprisingly small game file—eatyguy6.lua is only 127 lines long—for a pseudographical, procedurally generated interactive game, albeit a simple one.
EatyGuy Version 7: Class Inheritance
At long last, we’re ready to give EatyGuy a reason for all that running around: Let’s add some baddies!
In this section, I’ll cover the mechanics of inheritance while implementing an example subclass of Character
called Baddy
. There are two common ways that subclassing can appear in an API. You, the API writer, might want to subclass your own objects in order to offer related but slightly different interfaces. For example, suppose that you’ve implemented an Image
class. You might also choose to implement an AnimatedImage
subclass that adds functionality for working with animated images.
In the case of EatyGuy, there is already some separation between what can be considered a text-based game engine that is providing an API—with functions like set_color()
and set_pos()
—and game-specific code that uses this API. The Character
class is in a gray area that could fall on either side of the game/engine divide, but I’ll consider it to be part of the engine. By making Baddy
a subclass of Character
, we’ll see an example of a user-written subclass of an API-provided superclass.
Subclasses in Lua
Before diving into the Baddy
class, let’s take a look at a more generic subclass of the earlier MyClass
example. In the simplest case, your subclass won’t require any special per-instance initialization, and will only either add or replace existing methods. Here’s a simple subclass that overrides method()
:
-- Subclass.lua -- -- An example subclass of the earlier MyClass example. -- local MyClass = require 'MyClass' local Subclass = MyClass:new() function Subclass:method() print('Hi from subclass instance ' .. self.name) end return Subclass
The interesting thing here is that Subclass
begins life as an instance of MyClass
. (In other languages, it’s common for classes to simply define new types in the language, rather than to be values [such as class instances] themselves.) I’ll explain how this works in a moment. Here’s an example usage of Subclass
:
-- subclass_usage.lua local Subclass = require 'Subclass' local s = Subclass:new({name = 'Gwendolyn'}) s:method() --> Prints 'Hi from subclass instance Gwendolyn'.
In using the subclass, we give it similar treatment to the superclass, calling its new()
method first, followed by a method()
call on the resulting instance.
The preceding example works because of the following two connections:
-
Because
Subclass
is an instance ofMyClass
, any method ofMyClass
—includingnew()
—is also a method ofSubclass
. So, callingSubclass:new()
is the same as callingMyClass.new(Subclass)
. -
The
new()
method onMyClass
was written specifically to support this usage pattern. In particular, a call toMyClass.new(Subclass)
will ensure thatSubclass.__index = Subclass
, and that the new instanceobj
hasSubclass
as its metatable. So, methods defined onSubclass
will be callable on all instances ofSubclass
, and methods defined onMyClass
that are not overridden bySubclass
will also be callable onSubclass
instances.
The relationship between an instance of Subclass
called obj
, and the Subclass
and MyClass
tables is shown in Figure 4-1.
This simple example didn’t cover two common workflows. First, if you’d like to call a superclass’s method that you’ve overridden from a subclass’s method, you can do so by using this syntax:
Superclass.method(self, <other parameters>)
The other common workflow is the case in which your subclass needs its own new()
method. The Baddy
class will provide an example of this. You also can wrap calls to the superclass’s new()
method, but you must do so explicitly because overridden methods in Lua, even constructors, don’t automatically call the versions they override. Such a new()
method might look like this:
function Subclass:new(obj) obj = Superclass.new(obj) -- Subclass-specific setup. self.__index = self return setmetatable(obj, self) end
The Baddy Class
EatyGuy will contain baddies that move around at random. The current function that controls character movement is Character:move_if_possible()
.
We now have a design decision to make: we could either put the movement code for baddies in eatyguy7.lua, or we could put it in a new Lua file devoted to a subclass of Character
. One advantage of keeping the code in eatyguy7.lua is that doing so reduces the total number of files in our game, which is good if each file is not too long. On the other hand, it’s useful to decouple chunks of code that conceptually work on different units or perform separate operations. If code is decoupled nicely, it can be easier to maintain when future changes fit well with the separation you’ve chosen, in which case each piece of separated code will most likely be smaller and simpler to work with.
I’ve chosen to create a new file for the new code because this approach minimizes the impact on eatyguy7.lua, and because this makes it easier to focus on the new code for learning purposes. Here are the necessary changes to eatyguy7.lua, compared to the previous version, eatyguy6.lua:
-- eatyguy7.lua local eatyguy = {} -- Require modules. local Baddy = require 'Baddy' -- Globals. ... local baddies = {} -- Internal functions. ... local function update(state) ... player:move_if_possible(grid) for _, baddy in pairs(baddies) do baddy:move_if_possible(grid) end end local function draw(clock) ... player:draw(grid) for _, baddy in pairs(baddies) do baddy:draw(grid) end end -- Public functions. function eatyguy.init() -- Set up the grid size and pseudorandom number generation. grid_w, grid_h = 39, 21 math.randomseed(os.time()) -- Set up the baddies. local baddy_info = { {color = 1, chars = 'oo', pos = {1, 1}}, {color = 2, chars = '@@', pos = {1, 0}}, {color = 5, chars = '^^', pos = {0, 1}} } for _, info in pairs(baddy_info) do info.home = {(grid_w - 1) * info.pos[1] + 1, (grid_h - 1) * info.pos[2] + 1} table.insert(baddies, Baddy:new(info)) end -- Build the maze. ... end
In eatyguy.init()
, each baddy is assigned a pair of characters, such as @@
, which we use for their eyeballs. They are also assigned home positions, beginning in the three corners of the maze that are far from EatyGuy’s initial position at location (1, 1). I consider this to be a small change to the primary Lua file, particularly considering the level of functionality being added.
Next, here’s the code for Baddy.lua, which I’ll explain momentarily:
-- Baddy.lua -- -- A subclass of Character to capture behavior -- specific to baddies. -- local Character = require 'Character' -- Set a new direction and simultaneously update baddy.next_dir -- to be a right or left turn from new_dir. -- This function cannot be seen outside the Baddy module. local function set_new_dir(baddy, new_dir) baddy.dir = new_dir local sign = math.random(2) * 2 - 3 -- Either -1 or +1. baddy.next_dir = {sign * baddy.dir[2], -sign * baddy.dir[1]} end Baddy = Character:new() -- Set up a new baddy. -- This expects a table with keys {home, color, chars}. function Baddy:new(b) assert(b) -- Require a table of initial values. b.pos = b.home b.dir = {-1, 0} b.next_dir = {-1, 0} self.__index = self return setmetatable(b, self) end function Baddy:move_if_possible(grid) local deltas = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}} -- Try to change direction; otherwise the next_dir will take -- effect when we're at a turn in that direction. if self:can_move_in_dir(self.next_dir, grid) then set_new_dir(self, self.next_dir) end -- Try to move in self.dir; if that doesn't work, randomly -- change directions till we can move again. local can_move, new_pos = self:can_move_in_dir(self.dir, grid) while not can_move do set_new_dir(self, deltas[math.random(4)]) can_move, new_pos = self:can_move_in_dir(self.dir, grid) end self.old_pos = self.pos -- Save the old position. self.pos = new_pos end return Baddy
Instances of Baddy’s
superclass, Character
, simply halt as soon as they hit a wall. When a Baddy
instance hits a wall, however, it will continue moving in a random new direction. This behavior is different because the Baddy
class overrides the move_if_possible()
method.
The file Baddy.lua also contains a local function called set_new_dir()
that sets self.dir
to a given direction and also sets self.next_dir
to either a right or left turn away from self.dir
. This results in baddies that are capable of turning at any corner while also avoiding 180° turns.
The set_new_dir()
function is also an example of a function that is effectively private to a file. Because it’s a local function that isn’t accessible as a value from the Baddy
table, Lua code outside of this file has no access to it without resorting to the debug module. If you’d like to have private values or functions that are visible only within a single file, you can declare them as globals with local scope just like set_new_dir()
.
Figure 4-2 shows us what the game looks like at this point.
That’s a bit more exciting!
So far, we’ve talked about writing classes and subclasses in Lua, but we haven’t done much in C in this chapter. Let’s change that by talking about the userdata type, which offers C coders unique powers to combine C-based data structures, including arbitrary pointer values, with Lua-facing functionality.
EatyGuy Version 8: Introducing the userdata Type
Almost every Lua value is equally transparent to both C and Lua code. For example, anyone running either C or Lua can see all the keys and values in a table and look up that table’s metatable. This symmetric transparency, however, does not apply to the userdata type, which is specifically tailored to wrap C-side data in opaque, Lua-facing values.
Technically, a userdata instance has no intrinsic Lua-visible data. How can such an opaque object be useful? Because userdata instances can have metatables, and through these metatables they can effectively have methods and fields, just like tables, as well as support operator overloading. (If you want a refresher on metatables, flip back to the section “The Primary Table Functions” in Chapter 3.) In other words, from the Lua side, a userdata can effectively look and feel just like a table, although all that functionality has to come directly from a metatable instead of being built-in.
From C’s perspective, there are actually two types of userdata objects: full and light. A full userdata instance—the default type—corresponds to a block of dynamically allocated memory that’s managed by Lua’s garbage collector. This gives you the power, for example, to wrap an arbitrary C struct
in a Lua value. This struct
can be passed around to C functions that know nothing of Lua.
A light userdata, on the other hand, simply wraps a single pointer. Light userdata are seen by Lua as direct values, similar to the number type. You are completely responsible for managing the memory pointed to by a light userdatum. In addition, light userdata instances can’t have metatables, making them unsuitable as classes. The example C code that follows will work with the full userdata type.
Let’s see why it’s useful to be able to opaquely wrap C data. Consider the io.open()
method that’s part of Lua’s standard library. The following code reveals the type returned by this function:
> f = io.open('myfile.txt') > print(type(f)) --> Prints out 'userdata'.
Here are two potential follow-up statements that result in errors:
> setmetatable(f, {}) -- Error! > read = getmetatable(f).read -- Ok. > read(f) -- Ok, reads file. > read(42) -- Error!
We can’t change the metatable of f
, and we can’t call read()
on arbitrary values. To be more precise, we can try to do either of those things, but both result in type errors.
What’s going on behind the scenes is that the variable f
, which represents a file handle, is internally stored as a FILE
pointer in C. Imagine a parallel universe in which, rather than returning a userdata from io.open()
, Lua returned a number from io.fopen()
and accepted that number as an input to io.fread()
. These functions are meant to be analogous to fopen()
and fread()
in C, so that the return value of io.fopen()
is the Lua number with the same numeric value as the corresponding FILE
pointer in C. Suddenly, we can make mistakes like this:
> io.fread(1729) -- Ruh-roh, that's not good.
In this case, the number given is treated as a pointer so that C code can dereference arbitrary memory, resulting in a segfault or a bus error. At a higher level, this violates Lua’s safety. Lua is designed to be safe in the sense that any error, no matter how egregious, will at worst result in a Lua error, and not something worse such as crashing the hosting C app. In other words, if you were to wrap your Lua script in a call via pcall()
, it would be guaranteed to not crash. When you write C code that is executed by Lua, it’s your responsibility to uphold this safety by making sure that any error case is surfaced to Lua as a Lua error, rather than crashing the entire program.
The userdata type makes it easier to ensure this safety when working with values in C that need to be tightly controlled in order to remain valid. In the io.open()
example, this safety is provided because Lua users can neither change the value of the underlying FILE
pointer nor can they create new userdata values without going through the officially supported methods.
Next, I’m going to take a brief detour to explain how Lua loads modules written in C, and then we’ll bring these ideas together in an example module that implements (x, y) coordinate pairs in C, exposing them to Lua as a userdata-based class.
Writing a Lua-Loadable Module in C
So far in this book, the Lua interfaces we’ve created have either been written in Lua or have been explicitly loaded via C code, such as by using the luaL_dofile()
function. But there’s another way: you can write a Lua module entirely in C. Lua can use your platform’s dynamic linking library to find a compiled object file and to call a specially named function in that file that loads your C module. On Windows, these object files have the filename extension .dll (dynamic link library), whereas on Linux and macOS, they have the extension .so (shared object).
Suppose that your C module is written in the file cmodule.c. In that case, Lua expects it to contain the function luaopen_cmodule()
; this function is called when the following Lua statement is executed:
> cmodule = require 'cmodule'
Here’s a small, valid C module that can be loaded from Lua, but simply returns an empty table:
// cmodule.c #include "lua.h" #include <stdio.h> int luaopen_cmodule(lua_State *L) { printf("luaopen_cmodule() was called!\n"); lua_newtable(L); return 1; // Return the empty table. }
The module can be compiled like so, depending on your operating system:
On Windows: $ cc -c cmodule.c -o cmodule.o $ cc -shared cmodule.o -o cmodule.dll -llua On macOS: $ cc cmodule.c -o cmodule.so -undefined dynamic_lookup -I../lua On Linux: $ cc cmodule.c -o cmodule.so -I../lua -shared -fpic
The preceding macOS and Linux commands assume that your Lua header files are in the ../lua directory; if that’s not accurate, change the values of the -I
flags to the directory containing your Lua header files. You can find more details on this build process at the lua-users.org page on building modules.
After the .so or .dll file is built, you can load it like so:
$ lua Lua 5.3.3 Copyright (C) 1994-2016 Lua.org, PUC-Rio > require 'cmodule' luaopen_cmodule() was called! table: 0x7fe0b9e00f60
The response from the require()
call indicates two things. First, that your custom C function was effectively called despite the Lua interpreter not even knowing that it existed when the interpreter was loaded; second, that the require()
call did indeed return a table. If you inspected the contents of this table, you’d find it to be empty.
You might be wondering how the Lua interpreter found your module, given that you didn’t provide any information about it before require()
was called. Lua looks for module files in a fixed number of locations. Similar to a shell’s PATH
environment variable, Lua keeps this list of locations in a string called package.cpath
. Here’s the value of package.cpath
on my Mac:
$ lua -e 'print(package.cpath)' /usr/local/lib/lua/5.3/?.so;/usr/local/lib/lua/5.3/loadall.so; ./?.so
This is a list separated by semicolons. Each path in the list can contain zero or more question marks. When require(
<module_name>
)
is called, those question marks are replaced by the <module_name>
string. As an example, a call to require 'cmodule'
on my Mac performs the search for the module in this order:
-
It looks for the file /usr/local/lib/lua/5.3/cmodule.so. If that file exists, it attempts to run the function
luaopen_cmodule()
within it. If that succeeds, the search ends. -
If step 1 failed, it looks for the file /usr/local/lib/lua/5.3/loadall.so. If that file is found, Lua attempts to run the function
luaopen_cmodule()
within it. If that succeeds, the search ends. -
If steps 1 and 2 both failed, Lua looks for the file ./cmodule.so. If that file is found, Lua attempts to run the function
luaopen_cmodule()
within it.
In fact, this is just a subset of the complete search because the require()
function also looks for <module_name>.lua files, and keeps track of a cache of previously loaded modules, skipping the search if the module name is found in the cache. If none of the search steps succeed, a Lua error is thrown.
The cmodule.c example is a bit underwhelming in that it doesn’t actually do anything. Let’s take a look at a more interesting example that does do something.
Implementing a Class-Like userdata Type
We’re now ready to implement our own class-like userdata type. The EatyGuy code often uses (x, y) coordinate pairs to indicate either grid positions or directions. It would be convenient if we could perform standard arithmetic operations on these objects as if they were vectors. So, let’s implement this by using Lua’s __add()
and __mul()
metamethods.
In a burst of unbridled creativity, I’ve decided to use the name Pair for this example class. We could actually implement the Pair
class entirely in Lua because it doesn’t strictly require any of the extra power that C offers. However, I’m going to write it in C because I’m using it to illustrate how you can create userdata-based classes in C. In general, when you’re building a class, you have a choice of using a table or a userdata as the type. The need to store C data—especially pointers—is a good reason to base your class on the userdata type rather than on a table. Compared to building a class on the table type, building on the userdata type makes it much more practical to store and safely encapsulate the C values behind your class’s instances. If you don’t need to store C data, basing your class on a table might be less work for you, and it might provide more transparency to coders using your class.
Let’s begin by looking at the setup of Pair.c and the luaopen_Pair()
function:
// Pair.c // // When this module is loaded, it sets up two tables: // * Pair, which is the class itself, and. // * Pair_mt, the metatable of Pair. // Pair_mt contains metamethods such as __index. // Pair contains the new() method so the user can create new // Pairs by using the familiar pattern: // // local p = Pair:new{12, 34}. // #include "lauxlib.h" // -- Macros and types -- #define Pair_metatable "Pair" typedef struct { lua_Number x; lua_Number y; } Pair; ... // The function Pair_new is defined. // The functions Pair_mt_{index,add_eq,mul} are defined. // The basics of this omitted code are explained later in this // chapter; the full source is in this book's github repo. ... int luaopen_Pair(lua_State *L) { // The user may pass in values here, // but we'll ignore those values. lua_settop(L, 0); // stack = [] // If this metatable already exists, the library is already // loaded. if (luaL_newmetatable(L, Pair_metatable)) { // stack = [mt] static struct luaL_Reg metamethods[] = { {"__index", Pair_mt_index}, {"__add", Pair_mt_add}, {"__eq", Pair_mt_eq}, {"__mul", Pair_mt_mul}, {NULL, NULL} }; luaL_setfuncs(L, metamethods, 0); lua_pop(L, 1); // The table is saved in the Lua's registry. // stack = [] } static struct luaL_Reg fns[] = { {"new", Pair_new}, {NULL, NULL} }; luaL_newlib(L, fns); // Push a new table with fns key/vals. // stack = [Pair = {new = new}] return 1; // Return the top item, the Pair table. }
The first new C API function here is luaL_newmetatable()
. To describe this function and its companion, luaL_checkudata()
, I’d like to first describe the Lua registry. The Lua registry is a special table that is always available to C code but is hidden by default from Lua code. Aside from its hidden nature, it’s a completely normal Lua table. It exists as a place to store a variant of global values that are usable from C but not from Lua.
Table 4-1 describes the two main helper functions for working with metatables on C-defined classes.
C API function | Description | Stack use |
---|---|---|
luaL_newmetatable(L, <tname> ) |
Create a new table and push it onto the stack. At the same time, set this new table as the value of the key <tname> in the registry. |
[–0, +1] |
luaL_checkudata(L, <arg> , <tname> ) |
Check that the value at index <arg> in the stack is a userdata, and that this userdata’s metatable is the same as the metatable stored with key <tname> in the registry. If these conditions are all true, then the C pointer value of the userdata is returned; otherwise an error is thrown with an error message about a bad argument type. |
[–0, +0] |
Earlier in this chapter, we saw how the behavior of a Lua class is determined by its metatable. Lua userdata instances are similar—if they have a metatable, that metatable essentially determines to which class the instance belongs. Hence, when C code sets up a new Lua class, it needs to create a new table to act as the metatable for all instances, and it needs a way to refer back to that metatable later on. The creation and registration of this table is done by the luaL_newmetatable()
function. In a moment, we’ll see how the companion function luaL_checkudata()
can work with metatables created in this manner.
For now, let’s take a look at Table 4-2 and the other two C API functions just introduced.
C API function | Description | Stack use |
---|---|---|
luaL_setfuncs(L, <fns> , <nup> ) |
Pop <nup> values from the stack, set each function in <fns> as a key/value pair in the table now at the top of the stack. The functions in <fns> are expected to be given as string/C-function pairs using the luaL_Reg struct, and this array is expected to end with a {NULL, NULL} instance. |
[–<nup> , +0] |
luaL_newlib(L, <fns> ) |
Create a new table and populate it with the key/value pairs from <fns> . The functions in <fns> are expected to be in the same format as those given to luaL_setfuncs() . |
[–0, +1] |
Note
This book won’t dive into the details of upvalues, but I’ll mention that the number <nup>
handed to luaL_setfuncs()
indicates a number of upvalues to be shared by the functions being set. Upvalues are values visible from a closure—that is, values that are visible to a function but that are defined outside that function and are not globals. You can read more about upvalues in the appropriate section of Roberto Ierusalimschy’s book Programming in Lua.
The two C API functions in Table 4-2 are extremely useful when it comes to defining classes. The luaL_newlib()
function is perfect for setting up a new table with function values, which is exactly the kind of table you’d want to return when a module is set up via a require()
call. When you’re setting up a metatable, however, you’ll want to use luaL_setfuncs()
because it can work with tables created by other means, such as via the luaL_newmetatable()
function.
Our example luaopen_Pair()
function has the following effects:
-
A new table is set up with key/value pairs for the
__index()
,__add()
,__eq()
, and__mul()
metamethods. This new table is a value in the registry with the string key"Pair"
. -
Another table is set up to include the key
"new"
and whose value is the C functionPair_new()
. -
The table just created—the one that includes the
"new"
key—becomes the return value of the call torequire()
.
This is enough initialization to understand how the following two lines would work, assuming that your Lua interpreter is using the same version of Lua as the Lua version with which you’ve compiled Pair.c:
> Pair = require 'Pair' > p = Pair:new{1, 2}
Note
If you happen to call require 'Pair'
from a Lua interpreter whose version is different from the Lua version in your lua.h, the interpreter might display a version mismatch
error. The fix is to change either your interpreter version or your Lua source code version so that they match. (Type lua -v
to see your interpreter’s version; the source’s version is near the top of the lua.h file.)
The call to Pair:new()
uses curly braces because we’re sending in a single table as the only argument (ignoring the implicit self
value). In other words, the call Pair:new{1, 2}
is identical to the call Pair:new({1, 2})
, but we’re using the shorter syntax. Such a call would result in the C function Pair_new()
being called with the stack set to [Pair, {1, 2}]
, as those are the two arguments sent into the function.
Let’s take a closer look at Pair’s constructor, Pair:new()
, as written in C:
// Pair:new(p) int Pair_new(lua_State *L) { // Extract the x, y data from the stack. luaL_checktype(L, 2, LUA_TTABLE); // stack = [self, p] lua_rawgeti(L, 2, 1); // 2, 1 = idx in stack, idx in table // stack = [self, p, p[1]] lua_rawgeti(L, 2, 2); // stack = [self, p, p[1], p[2]] lua_Number x = lua_tonumber(L, -2); // p[1] lua_Number y = lua_tonumber(L, -1); // p[2] lua_settop(L, 0); // stack = [] // Create a Pair instance and set its metatable. Pair *pair = (Pair *)lua_newuserdata(L, sizeof(Pair)); // stack = [pair] luaL_getmetatable(L, Pair_metatable); // stack = [pair, mt] lua_setmetatable(L, 1); // stack = [pair] // Set up the C data. pair->x = x; pair->y = y; return 1; }
This function performs two operations. It begins by extracting the x, y values from its arguments and then creates a new userdata instance to hold those values. The C API functions shown in Table 4-3 are used to help extract the x, y values.
C API function | Description | Stack use |
---|---|---|
luaL_checktype(L, <idx> , <type> ) |
Check that the Lua type of the value at stack index <idx> matches the type <type> , which must be one of the C constants beginning with LUA_T and ending with the type name in all-caps (e.g., LUA_TTABLE ). If the check fails, an error message is thrown indicating that the value at <arg> was of the wrong type. |
[–0, +0] |
lua_rawgeti(L, <idx> , <n> ) |
For the table at stack index <idx> , get the value of integer key <n> , ignoring any __index() metamethod. Push the received value onto the top of the stack. |
[–0, +1] |
lua_rawseti(L, <idx> , <n> ) |
Pop the value off the top of the stack and set it as the value of integer key <n> in the table at stack index <idx> ; this ignores any __newindex() metamethod. |
[–1, +0] |
In our case, the functions in Table 4-3 make it easy to check the validity of the incoming argument, and to extract the values of keys 1 and 2 in the given table.
Next, the Pair:new()
constructor needs to set up a new userdata instance and give it Pair
’s metatable. The key function is lua_newuserdata()
, which allocates memory similar to malloc()
, except that Lua’s garbage collector will manage the actual memory for you. Also similar to malloc()
, the return value from lua_newuserdata()
can be cast to a pointer of your preferred type and henceforth treated as a pointer to whichever C type you’d like to work with. In the case of the Pair
class, the C-side type is a struct with fields x and y.
Let’s see how this nascent userdata obtains the proper metatable. Table 4-4 lists the key functions, including a quick review of lua_newuserdata()
.
C API function | Description | Stack use |
---|---|---|
lua_newuserdata(L, <size> ) |
Create a new userdata object tied to a chunk of newly allocated C memory of size <size> , in bytes. The new memory is managed by Lua and will be deallocated after the corresponding Lua value is no longer referenced. This function returns a pointer to the newly allocated memory. This pointer and its Lua value will be associated for their lifetime, like a pair of mated albatross. |
[–0, +1] |
luaL_getmetatable(L, <tname> ) |
Retrieve the table stored in the registry with key <tname> and push it onto the top of the stack. |
[–0, +1] |
lua_setmetatable(L, <idx> ) |
Pop the table from the top of the stack and set it as the metatable of the value found at stack index <idx> . |
[–1, +0] |
Note that this last function, lua_setmetatable()
, can set metatables for more than just tables. In particular, it can set metatables for userdata, which is exactly what Pair:new()
does.
The return value of Pair:new()
is a new userdata instance that wraps a C struct of type Pair
. The x, y values of this C struct are based on the arguments to Pair:new()
, and the userdata’s metatable is Pair_mt
, the table set up when luaopen_Pair()
made its call to luaL_newmetatable()
. Thus, the return value of Pair:new()
will have all the metamethods (__add()
, __eq()
, __mul()
, and __index()
) set up when the Pair
module was loaded.
Rather than cover the complete Pair
module in detail, let’s simply take a look at one more function in the Pair.c file. The C function Pair_mt_index()
, which corresponds with the __index
metamethod, enables Pair
users to dereference a single Pair
instance in multiple ways, like so:
> Pair = require 'Pair' > p = Pair:new{100, 200} > p.x -- p[1] is a synonym for this value. 100 > p[2] -- p.y is a synonym for this value. 200
Here’s the code for Pair_mt_index()
:
// Pair_mt:index(key) // This is used to enable p.x and p.y dereferencing on Pair p. // This also supports p[1] and p[2] lookups. int Pair_mt_index(lua_State *L) { // Expected: stack = [self, key] Pair *pair = (Pair *)luaL_checkudata(L, 1, Pair_metatable); // Find the key value as either 1 (x) or 2 (y). int key = 0; int t = lua_type(L, 2); if (t == LUA_TNUMBER) { int ok; key = (int)lua_tointegerx(L, 2, &ok); if (!ok || (key != 1 && key != 2)) key = 0; } else if (t == LUA_TSTRING) { const char *str_key = lua_tostring(L, 2); if (strcmp(str_key, "x") == 0) key = 1; if (strcmp(str_key, "y") == 0) key = 2; } // Push the value. if (key) { lua_pushnumber(L, key == 1 ? pair->x : pair->y); } else { lua_pushnil(L); } return 1; }
At last, we’ve used the long-anticipated luaL_checkudata()
function to simultaneously check that a given argument was of the expected type and convert that value into a C pointer. This code also uses the two C API functions described in Table 4-5.
C API function | Description | Stack use |
---|---|---|
lua_type(L, <idx> ) |
Return the type of the value at stack index <idx> . The returned value is a C int that can be compared with the same constants described for the function luaL_checktype() earlier in this chapter (e.g., LUA_TTABLE ). |
[–0, +0] |
lua_tointegerx(L, <idx> , <isnum> ) |
Attempt to convert the value at stack index <idx> into a lua_Integer type. If the conversion is successful, the value pointed to by <isnum> is true, and the return value is the converted integer; otherwise, the value pointed to by <isnum> is false. |
[–0, +0] |
Integrating a C Module into Your API
If you’d like a C module to be loaded only upon a user request, the dynamic loading process described earlier will suffice. You also have the option of loading your C module explicitly. For example, these two statements would load the Pair
class from C:
// C code similar to the Lua statement "Pair = require 'Pair'". luaopen_Pair(L); lua_setglobal(L, "Pair");
To make that code work, you’d need to set up a header file (e.g., Pair.h) that declared the luaopen_Pair()
function, and compile Pair.c into a typical object file—that is, Pair.o—as opposed to the shared object file used for dynamic loading. Here’s a typical command line to compile Pair.o:
cc -std=c99 -I../lua -c Pair.c -o Pair.o
All subsequent versions of EatyGuy will be linked with Pair.o. Linking Pair.o into eatyguy8 is as simple as adding the parameter Pair.o
to the command line you use to build eatyguy8. As an example, here is a compilation command that works on macOS:
cc -std=c99 -llua -L../lua -I../lua eatyguy8.c Pair.o -o eatyguy8
As an example of how the Pair
class can clarify EatyGuy’s code, consider this extract from the previous version of EatyGuy’s Lua file, eatyguy7.lua, with the portions we’re about to change shown in bold:
-- Part of eatyguy7.lua, the previous verison local function drill_path_from(x, y) grid[x][y] = '. ' local nbor_dirs = get_nbor_dirs(x, y) while #nbor_dirs > 0 do local dir = table.remove(nbor_dirs, math.random(#nbor_dirs)) grid[x + dir[1]][y + dir[2]] = '. ' drill_path_from(x + 2 * dir[1], y + 2 * dir[2]) nbor_dirs = get_nbor_dirs(x, y, percent_extra_paths) end end
You can change this function to accept a single Pair
value call pt
, and you can clarify several of its expressions by using Pair
values instead of tables. Here is the same function in eatyguy8.lua, with changes in bold:
-- Part of eatyguy8.lua local function drill_path_from(pt) grid[pt.x][pt.y] = '. ' local nbor_dirs = get_nbor_dirs(pt) while #nbor_dirs > 0 do local dir = table.remove(nbor_dirs, math.random(#nbor_dirs)) grid[pt.x + dir.x][pt.y + dir.y] = '. ' drill_path_from(pt + dir * 2) nbor_dirs = get_nbor_dirs(pt, percent_extra_paths) end end
Other changes of a similar nature can be made throughout eatyguy8.lua; the full example file is available in this book’s online repository (including compilation commands in the corresponding makefile). I won’t list all the changes here, because the purpose of this section was to explain C-based classes rather than to focus on changes in EatyGuy.
Making Your Code Debug-Friendly
This chapter taught you how to build classes in either Lua or C. We saw how classes are built with metatables and how inheritance fits into that structure. We also explored the utility of the userdata type, along with several C API functions and code patterns that can maintain Lua’s code safety while arbitrarily extending its functionality.
Albeit a rare and embarrassing event, most coders occasionally find themselves facing a bug that they must understand and defeat. In the long run, there are a number of practices you can learn that will help you both before and after any particular debugging battle. In Chapter 5 (which begins the section on Script Safety), we take a look at some techniques that will help you avoid bugs before they can happen. We also explore other approaches that can assist you in your quest to identify the source of any issues that do come up.
Get Creating Solid APIs with Lua now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.