A Very Simple Class Implementation in Lua

2022/11/30


Lua is a beautiful language.

Most scripting languages will make you feel dirty at some point. Packaging a project in Python is a hellscape littered with broken glass, plus it’s about three thousand source files between the C stuff and the Python stuff. V8 is roughly two million lines of source code, and the ecosystem around it is fractured and generally gorged to exploding with transpilers and polyfills and bundlers other various flavors of bullshit.

This is fine, in the same way that traffic in my city (Atlanta) is fine. It’s terrible, and it makes you want to stay away from your car, but nobody’s really to blame. Institutional problems, sure, and the general shit-rolls-downhill quality of the actions of large groups, but it’s really just this thing that Sucks and that you can’t do anything about.

Lua is a dozen files of plain C. It’s something like 35,000 lines. Mike Pall wrote a JIT compiler for it that’s real fast and real cool. There’s only one complex datatype in the whole language. Lua rules. This isn’t to say that Lua wouldn’t be on paragraph one’s naughty list if it had ten or a hundred or a thousand times as many users like those languages do. If I had to bet, I’d bet that it would be terrible too. Still – it doesn’t, so it rules.

But that’s not what this page is about. I’m making a game. Think of a visual novel, except extremely reactive, like Mass Effect or Disco Elysium. Plus, it plays more like a point and click adventure game, where you can poke around your environment. I’ve written the engine myself, and my tools of choice are C and Lua.

When I was learning what it meant to write an engine, I found some articles by Elias Daler1. He makes 2D games, sometimes using his own engines, often using Lua. In hindsight, he was just another kid figuring out to make games. I wouldn’t use the code from the linked article today. But then, you won’t use my code in five years when you’ve nailed down your own style. It’s not an insult in the slightest. I loved watching his games progress, the code snippets, the gifs – watching someone do precisely what you want to do with some degree of success. It filled me with determination, and I hope that anyone reading this gets the same feeling.

Here’s the point: It’s really easy to make a basic class system in Lua. There’s only something like fifty lines of code. You can take it and stick it into your engine, or your modeling tool, or your flight simulator, and poke at it until it’s something useful to you. Here goes!


Goal

In my engine, I just wanted a class system that let me define methods on objects. That’s ninety percent of what we need. You could get away with defining methods C-style (compare C-style (collider_is_point_inside(collider, point) to method-style collider.is_point_inside(point)), especially in a scripting language where the un-bound methods could be called by anything if I was sloppy.

In short, I want to write this:

-- Define class with one line
local Vec2 = define_class('Vec2')

-- Assign it methods
function Vec2:init(x, y)
  self.x = x
  self.y = y
end

function Vec2:add(other)
  return Vec2(self.x + other.x, self.y + other.y)
end

-- Create instances with new
local a = Vec2.new(1, 2)
local b = Vec2.new(5, 7)

-- Use Lua's method syntax
local c = a:add(b)
print(c.x, c.y)

I wanted a couple smaller things, too. 1. I’d like to be able to store class-static stuff. 2. I want a basic inheritance (I’ve heard this called a mixin, and while I call it that, I think that word may have a more precise meaning). 3. I also want another important feature: When I reload a script containing a class definition, I want all currently instantiated objects to pull the new method without doing any manual intervention. I want to save a file and have every object in my game point to the updated code. In the context of a game engine, this is extremely useful. Forcing any kind of retooling existing objects defeats the point of a scripting language. We’re not gonna do it!


A brief philosophick aside

One reason Lua pleases me is its insistence on using primitives for everything. Take, for example, the C API. The C API is generally just a set of functions to manipulate an internal stack inside Lua. For example, my game has a function in Lua that iterates over the game’s entities and updates them. Calling it from C looks like this:

// Push the error handler
lua_pushcfunction(raw_state, &traceback);
defer { lua_pop(raw_state, 1); };

// Push the global engine table onto the stack
lua_getglobal(raw_state, "tdengine");
defer { lua_pop(raw_state, 1); };

// Get the update function from said table
lua_pushstring(raw_state, "update_entities");
lua_gettable(raw_state, -2);

// Push arguments
lua_pushnumber(raw_state, dt);

// Call
auto result = lua_pcall(raw_state, 1, LUA_MULTRET, -4);

A few things to note here. 1. This is really complicated for the users of Lua. No language in common parlance uses a stack like this natively. It doesn’t make any sense, and even something as simple as calling a function can irreparably fubar your interpreter. 2. This is really simple for the authors of Lua. Everything is unified. You can implement whatever you need without special casing function calls versus table access versus whatever else.

That’s the essence of why Lua is such a lovely language. The API looks old. The API is old. It can be inconvenient. But in return for your inconvenience, you get a language that is understandable as a piece of code, rather than an abstract ecosystem. This is good. You should savor this inconvenience.


Metamethods, __index, and __newindex

The stack is one fundamental abstraction of Lua. Tables are another one. A third, also related to tables, is metamethods. A metamethod is simply a hook into certain situations that happen to a table while your code is running. These situations are completely ordinary things that happen in an interpreted language – defining relations with less than, equal and greater to, for instance. Or defining how a table should be converted to a string. (You can read about metamethods in Lua’s excellent manual2).

Two metamethods in particular are extremely powerful. The first, __index, is called when you look up a key in a table, but it’s not there. The second, __newindex, is called when you stick a new key-value-pair into a table. Here’s an example:

local t = {}

-- 't' doesn't have the key 'x'. This will trigger __index
local x = t.x

-- We're adding a new entry, so this triggers __newindex
t.y = 15 

That’s it. With these two primitives, you can make a class system in Lua. Ours will be simple, but you can pretty much make an arbitrarily complex class system still using just these two primitives.


Code

The full code for this article can be found here, in a single-file script that can be debugged from within the source code.

Tables are the only thing in Lua. Our goal is to trick certain tables into doing our bidding. In a sick twist of fate, our deceit can only be run through other tables, so we’ve set up some kind of strange cannibalistic betrayal. Here is how said betrayal will work.

We want to route the access and assignments such that we’re grabbing methods from the class table, member fields from the instance, etc. In specific, these are the cases we care about.

local vec2 = define_class('vec2')
-- Case 1: Adding a method or field to the class type
function vec2:print()
  print(self.x, self.y)
end

-- Case 2: Accessing the very special methods on the class type
local a = vec2.new()

-- Case 3: Accessing members on the instance that aren't part of the class
print(a.x)

-- Case 4: Accessing members on the instance that are part of the class
a:print()

Our class will just be a table. Because, yeah, everything is just a table. The class-table is going to have two special places: 1. The first special place will hold a few special functions. For our purposes, it holds new(). We’re going to call it __internal. 2. The second special place will hold anything added onto the class. Methods, static fields, whatever. We’re going to call it __members.

In addition, there will be a third place. It will not be special. This place is the object itself, the instance. It will have fields like any other table would, and they’ll be accessed without any trickery.


__members and __instances

This code will route accesses to the table for the class itself. One note: This implementation sticks the class in the global namespace (_G). This isn’t the common way of storing dependencies. Usually, your project is divided up into files (modules), which return what they export as a table. You can keep dependencies seperate in this way. I prefer not to do it like this, but you should know that it’s not normal.

function make_class(name)
  -- *whispers* a class is just a table
  local class = {}
  class.name = name
  class.__internal = {}
  class.__members = {}
  
  local metatable = {
    -- We looked up a field on the class itself, but we couldn't find it. __index tells us
    -- to check at our Special Methods (read: new, but can add other useful things).
    --
    -- This would be invoked when we do, for instance, vec2.new()
    __index = function(t, k)
      print(string.format('class(%s) is looking for %s in __internal', name, k))
      return class.__internal[k]
    end,
    -- We added a field to the class itself. Put it in its special place. This would
    -- be invoked when we do function vec2:print(). `print` is a new field for the
    -- vec2 class' table, so this method is invoked
    __newindex = function(class, k, v)
      print(string.format('class(%s) is adding %s to __members', name, k))
      class.__members[k] = v
    end
  }
  setmetatable(class, metatable)

  -- Stick the class table in the global namespace. You don't have to do this. You can put
  -- it in a table for your project's library calls. You can e-mail me for credentials to
  -- a public S3 bucket and upload the class there. Anywhere.
  _G[name] = class

  return class
end

new

This code will route accesses to instances of the class.

-- Put the new code adjacent to the previous code. Same function
function make_class(name)
  -- For this implementation, __internal just holds new. This function will forward the varargs
  -- to the class' init function.
  class.__internal = {}
  class.__internal.new = function(...)
    -- Allocate a table for the instance itself. This is the object returned to the caller.
	local instance = {}

    -- __index is run whenever a key is not found. This is the metatable for the instance.
    -- When a field isn't found on the instance, we want to make sure that it's not a method
    -- or static field we defined on the class.
    local metatable = {}
    metatable.__index = function(t, key)
      print(string.format('instance(%s) is looking for %s in __members', name, key))

      -- We have access to the class table directly above. Why are we getting the class using
      -- _G[name] instead of just using what's right there? Well, you have an extremely discerning
      -- eye, my reader. I am just so proud of you.
      --
      -- The reason: hot reloading methods. Goal number three. When we reload a script, we'll
      -- redefine the class. That includes creating a new table for the class definition.
      -- If we keep a hard reference to the class table as it was when this instance was created,
      -- we won't get the updated methods.
      return _G[name].__members[key]
    end
    setmetatable(instance, metatable)

    -- Finally, call whatever init the class defined and give the caller the result.
    if instance.init then instance:init(...) end
    return instance
  end
end

vec2

Now, we can write our vec2 class. It’s so easy!

local vec2 = class('vec2')
function vec2:init(x, y)
  self.x = x
  self.y = y
end

function vec2:add(other)
  return vec2.new(self.x + other.x, self.y + other.y)
end

main()

-- Code!
local a = vec2.new(1, 2)
local b = vec2.new(5, 7)
local c = a:add(b)
print(c.x, c.y)

And another thing!

That’s something like forty lines of code. The class system isn’t powerful, but give it forty more lines of code and you can get something seriously useful. More importantly, you built this. You understand how it works. There are no double dispatch mechanisms inside the language, no vtables generated by the compiler, no super() nonsense. If you need anything like that, add it.

There are some good implementations of classes floating around. Middleclass is an excellent reference for a simple implementation of a class system3.

Here are some things which you can do in the aforementioned next-forty-lines: 1. Add simple inheritance, in which you can type vec2:include(some_table_defining_methods_and_data) 2. Expand __internal to add common serialization and deserialization. Since everything is a table, simple serde is an absolute joy in Lua. 3. Instead of storing class types in _G, define vec2 in another file and include it in the main Lua script.


  1. Nothing advanced goes on in the games or code he shows, but he’s still one of my favorite people writing about games on the net. His art has an Undertale charm and his code has an earnest enthusiasm to it. https://twitter.com/eliasdaler
  2. Lua was written by three people. Roberto Ierusalimschy, whose name I did have to refer to the copy of PiL on my desk to spell, seems by all accounts to be the kind of kind BDFL encouraged and strengthened by success but unspoiled by the veritable hell of maintaining extremely popular open source software. Here’s a link to PiL, which talks about metamethods and an implementation of a class system and everything else. Buy a copy!
  3. Unfortunately, it registers at a whopping 193 lines of source code! Software bloat is truly a scourge upon the porcelain-fine sensibilities of a programmer.