Chapter 1. Running a Lua Script from C
In the first part of this book, the focus is on understanding the data and control flows between Lua and C. After those ideas have been established, we’ll be ready to tackle security in the second part, which begins with Chapter 5.
An Overview of the C/Lua Interface
Many programming languages are distributed in the form of a language-specific compiler or interpreter. Lua, on the other hand, is essentially distributed as a C API. Many users interact with Lua by using the official interpreter, but even this interpreter is built on top of the C API, without any special access to the language’s implementation. In other words, Lua is not only written in C; it is also written to be used from C.
Lua’s C API can create or destroy entire runtime environments, which are referred to as Lua states. A Lua state is a C pointer to an opaque data structure. This structure knows everything about a running Lua environment, including all global values, closures, coroutines, and loaded modules. Virtually every function in Lua’s C API accepts a Lua state as its first parameter.
Because Lua states are opaque structures, we need to use API functions to access the Lua values stored within a state. To this end, every Lua state has a Lua stack, which is a container for Lua values such as Lua strings or tables. You can think of the Lua stack as a window into the set of Lua values currently relevant to your C code. We’ll dive into all the relevant details of this stack and the interlanguage interface within the first three chapters of this book.
An Example: EatyGuy
Much of the code in this book builds an example game called EatyGuy. EatyGuy is a small yellow character stuck in a maze filled with dots that he can eat (see Figure 1-1). He is chased by several bad guys throughout this maze and can find satisfaction only by eating all of the dots in the maze, at which point he can proceed to the next level. (If this game sounds familiar, it’s probably because great minds think alike, and you just happened to have the same idea I did.)
This game has two interesting features. First, it is rendered entirely in text, given that providing the code for an interesting and cross-platform game is challenging enough without serious graphics. Second, it is a moddable game in the sense that we will be building an API by which anyone can write custom code to control some of the characters in the game.
In fact, the vast majority of the EatyGuy code is Lua, but it has a critical component of C at its core. If you’re feeling generous, you can think of the C core as a simple game engine that integrates with a game script by calling certain key functions. This interface will suffice to illustrate the basic interactions between C and Lua.
Getting the Lua Source
Let’s dive into some code! Because we’ll be linking Lua into a C binary, we need the header files for Lua’s C API as well as a library to link against. The best way to get this source depends on your operating system.
Although this book focuses on compiling C from the command line, everything in Lua’s C API is perfectly compatible with development environments like Visual Studio or Xcode. When using those tools, the only thing that you need to update are the build settings to ensure that the Lua library is properly linked with the binary being built.
If You’re Using macOS or Linux…
To build Lua’s source on macOS, you’ll need to have command-line developer tools like make installed. The easy way to install these is to open a terminal (such as the built-in Terminal app) and attempt to run a developer command such as gcc
. If you don’t have the developer tools installed, you’ll be prompted with a dialog box explaining that you need to install them. Click the Install button in that dialog box and follow through with the installation. If running gcc
doesn’t open a dialog box but rather causes gcc
to print a complaint about a lack of input files, these tools are already installed.
If you’re working on Linux, you’ll need to ensure that you have a C compiler as well as some flavor of make
. Here are shell commands to achieve this on Ubuntu, which happens to be the Linux distribution I’ve used the most:
# To install make and cc (an alias for gcc) on Ubuntu: $ sudo apt-get update $ sudo apt-get install build-essential
You can find the official source for Lua at https://www.lua.org/download.html. You can download and build the source by using the following shell commands:
curl -R -O http://www.lua.org/ftp/lua-5.3.3.tar.gz tar zxf lua-5.3.3.tar.gz
Those commands are for Lua 5.3.3; the preceding download URL is the best place to determine which version is current. If you’re running macOS, build from source like so:
cd lua-5.3.3 make macosx test
If you’re on Linux, replace macosx
with linux
in that last line.
The make command builds several key files in the src directory. One of these is the Lua interpreter, built as the binary file src/lua. You can run it and see Lua’s interpreter prompt, like this:
./src/lua Lua 5.3.3 Copyright (C) 1994-2016 Lua.org, PUC-Rio >
Press Ctrl-D to exit the Lua interpreter.
If you haven’t already installed Lua on your system, you could make it available in your path by typing, for example:
sudo ln -s $(pwd)/src/lua /usr/local/bin/lua
Another key file we just built is src/liblua.a, a library that you can statically link with your C code. The header files for Lua’s C API are also in the src directory.
If You’re Using Windows…
There are multiple ways to install Lua on Windows. I recommend using a tool called Cygwin, and throughout this book I’ll describe the details of using Lua via this particular tool. However, you might also consider the Lua for Windows environment or building Lua from source by using advice from the lua-users wiki.
Cygwin is an environment that is designed to act like a Linux distribution that sits on top of Windows. If you’re familiar with the bash shell or typical unix tools, you’ll find a lot of that functionality available from Cygwin. Figure 1-2 demonstrates that the actual interface is a console that acts like a bash terminal.
You can install Cygwin by downloading and running setup-x86_64.exe from the install page on cygwin.com. Because Cygwin’s installation procedure can be confusing, I’ll describe it here in detail. The setup-x86_64.exe file acts as both an initial installer as well as a package manager, similar to tools like apt-get on Linux, or Homebrew on macOS. In other words, if you ever want to add or subtract packages from your cygwin installation, simply run this setup executable again and choose different options.
When you run setup-x86_64.exe, I recommend choosing all the default options except for those on the Select Packages dialog box, which you can see depicted in Figure 1-3.
You can use this dialog box to choose particular packages to install or uninstall. If you know the name of the package you want to manage, you can type its name in the search box near the upper-left corner; don’t press Enter when you do—the search happens as soon as you type. For example, if you want to install git, in the search box, type git
. Then, expand the displayed categories to find the exact package you want. Continuing the git example, expand the Devel category to find a package described as “git: Distributed version control system.” Clicking the word to the right of the symbol cycles through various choices that indicate whether to install or uninstall the package. Typical options include Keep, Skip, or a version number such as, for example, 2.8.2-1. Leaving this choice set to the version number indicates that it will be installed when you click Next at the bottom of the window.
I recommend installing the packages listed in Table 1-1, which are sufficient to follow along with the rest of this book.
Package name | Category | Description |
---|---|---|
gcc-core | Devel | C compiler |
gcc-g++ | Devel | C++ compiler |
git | Devel | The git version control system |
lua | Interpreters | The Lua interpreter |
lua-devel | Interpreters | Lua’s C header files and library file |
make | Devel | A tool to help build binaries from source code |
If you happen to use a terminal-based text editor, such as vim or emacs, you also can install it from the Select Packages interface. Because Cygwin’s files are also visible on Windows, you’re free to use any Windows-based text editor you like, such as Sublime Text or Visual Code.
When all of these packages are installed, you’re ready to go!
This Book’s GitHub Repo
This book is essentially self-contained; it includes enough source code for you to follow along and re-create each example on your own system. However, because typing out code by hand is only one notch more exciting than watching grass grow, I’ve put together the following public code repo that includes all the source in this book:
https://github.com/tylerneylon/APIsWithLua
The repo is structured so that there’s a single lua directory in the base directory, which contains Lua’s source code (with both header files like lua.h as well as source files like loadlib.c). There is also one directory per chapter, with scintillating names like ch1, ch2, and even the rather saucy ch3.
If you’re using macOS or Linux and you choose not to clone this git repo, I recommend you use a similar file structure so that your code resides in a directory that’s a peer to a lua directory containing Lua’s source, like so:
APIsWithLua/ src/ an_example.lua an_example.c ... <all your example files> lua/ lua.h loadlib.c ... <all of Lua's source; see "Getting the Lua source">
I recommend this structure because it’s what I’ve assumed when suggesting compilation commands. If you’re using Windows, the directory structure is less important because Cygwin puts Lua’s header files and compiled library file in locations that the C compiler knows about. It’s also possible to similarly install Lua’s development files in compiler-known locations on macOS or Linux, but I opted to explain how to explicitly work with these files on those operating systems because it provides a deeper understanding of what’s going on behind the scenes.
From here on out, I’ll write as if you’re copying code from this book rather than GitHub, but feel free to clone the git repo, run make in the directory for your current chapter, and explore the code that way, instead. As of this writing, all of the source files in this written book and in the repo are identical.
Running a Lua Script from C
Let’s prepare a new directory for example code and copy over the files we’ll need:
cd .. mkdir APIsWithLua && cd APIsWithLua
If you’re on macOS or Linux, copy over the header files and the liblua.a library file:
# Windows users may skip these three commands. mkdir lua cp ../lua-*/src/{lua,luaconf,lauxlib,lualib}.h lua cp ../lua-*/src/liblua.a lua
Make a new directory that will hold all your source files:
mkdir src && cd src
Suppose that we have a minimalistic Lua script called script.lua with the following contents:
-- script.lua print('Hi from script.lua!')
We could run this script like so:
$ lua script.lua Hi from script.lua!
Now, let’s see how to run it from C. First, I’ll show you some code, explain how to compile and run it, and then discuss the key details of how it works. Here’s the code:
// doscript.c #include "lauxlib.h" #include "lua.h" #include "lualib.h" int main() { lua_State *L = luaL_newstate(); luaL_openlibs(L); luaL_dofile(L, "script.lua"); return 0; }
Let’s compile this! The exact invocation of your C compiler depends on your operating system. Here are the respective commands:
# On Windows: $ cc doscript.c -o doscript -llua # On macOS: $ cc doscript.c -o doscript -llua -L../lua -I../lua # On Linux: $ cc doscript.c -o doscript -llua -L../lua -I../lua -lm -ldl
You can compile all of the code examples in Chapter 1 through Chapter 3 by using a command line like those just presented. In these chapters, the flags remain the same, whereas the filenames might change. In Chapter 4, I introduce new compiler flags that enable linking against Lua libraries you build in C.
After the doscript.c file is compiled, you can run it by using the following:
$ ./doscript Hi from script.lua!
Let’s break down the command and flags used to compile the binary:
-
cc
is a C compiler. You could alternatively use gcc or clang if you prefer. -
The first parameter,
doscript.c
, is the file to compile. -
The
-o
option specifies the name of the output binary. -
The
-l
option specifies the name of a library with which to link. In general, the flag-l
<name>
will link with a file named lib<name>.a or with the prefix lib<name>.so, depending on which of these files the compiler can find. In our case, we want to link with the library file liblua.a. (Windows often uses .dll files as libraries, but we won’t see those, because we’re using the gcc compiler in Cygwin.) -
The
-L
<dir>
option tellscc
to include the directory<dir>
in its search path for library files. -
The
-I
<dir>
option tellscc
to include the directory<dir>
in its search path for#include
’d header files. -
Finally, the
-lm
and-ldl
flags are necessary on Linux in order to link with the math and dynamic linking libraries, respectively.
The C code in doscript.c performs several steps that are common to most Lua usage from C. First, I’ll give a summary of the Lua C API functions used, and then I’ll explain them in detail.
Table 1-2 takes a look at the Lua C API functions used.
C API function | Description |
---|---|
luaL_newstate() |
Create and return a new Lua state in the form of a lua_State pointer. |
luaL_openlibs(L) |
Load Lua’s standard library into the Lua state L . This includes modules like string , table , io , and so on. |
luaL_dofile(L, <filename> ) |
Load the file <filename> from disk and then parse and execute the Lua code it contains in the context of Lua state L . Any return values from the Lua script are made available to you by pushing them onto the top of the Lua stack. |
The file doscript.c creates a fresh Lua state by calling luaL_newstate()
. All use of the Lua’s C API involves a Lua state in the form of a lua_State
pointer. A lua_State
pointer is meant to be treated as a black-box object that contains everything about a running Lua state, including all global values, all defined functions, and possibly even any currently running threads if you’re using coroutines. We must create this object before we do anything else with Lua, and the resulting pointer is handed in as the first parameter to essentially every other function in Lua’s C API.
Next, doscript.c calls luaL_openlibs(L)
. Technically, because script.lua doesn’t use any modules, there was no requirement to load the standard library. However, it’s a good idea to habitually call luaL_openlibs()
immediately after any call to luaL_newstate()
to give future script writers access to the standard library. An unusual case for which you’d want to omit this function would be in running a user-written script that shouldn’t have access to the filesystem, in which case the io
module is best left unloaded. This use case is covered in more detail in Part II.
Finally, the function luaL_dofile()
is called, which loads a file from disk, parses and compiles it as a Lua string, executes that Lua string, and places any return values from the Lua code in a place where we can retrieve them if we’d like. In this particular C code, we’ve ignored any return values from the script.
This is a good opportunity to review best practices for writing a Lua module because it gives us a chance to look at more idiomatic code as well as to learn about receiving Lua return values in C.
Lua Modules and Loading Them from C
A common way to write a Lua module is to define a local table with the same name as the module, populate that table with functions, and then return that table from the module. (In Lua, a table is a key-value data structure that can have any non-nil type as either keys or values.) Lua modules are treated like the bodies of Lua functions so that they can both receive arguments and return values. These return values are visible within Lua as the return values of a require()
call and are visible within C as the items left on the Lua stack after a call to luaL_dofile()
or another function that executes a block of Lua code.
Note
In Lua jargon, a block of executable Lua code is called a chunk. It’s good to know this term when looking things up in Lua’s online reference manual.
Following is a simple Lua module written in this style:
-- mymodule.lua local mymodule = {} function mymodule.sayhi() print('Why hello from mymodule!') end return mymodule
Here’s a simple Lua script that uses mymodule
:
local mymodule = require 'mymodule' mymodule.sayhi() -- Prints out the hello message.
A modified version of our doscript.c program could also load this module:
// loadmodule.c // // Loads mymodule.lua but doesn't call any functions in it. // #include "lauxlib.h" #include "lua.h" #include "lualib.h" int main() { lua_State *L = luaL_newstate(); luaL_openlibs(L); luaL_dofile(L, "mymodule.lua"); lua_setglobal(L, "mymodule"); lua_settop(L, 0); return 0; }
This code introduces a couple of new functions from Lua’s C API. To understand them, you first need to get a better sense of how Lua’s stack receives return values.
Lua’s stack is more like an array than a strict stack because we can actually look at arbitrary values in it and insert or remove items from any location at any time. However, conceptually it does work like a call stack in that it tracks parameters and return values from function calls, and most common operations tend to either push new values onto the top of the stack or remove values from the top.
When we call a function in Lua’s C API that provides new Lua values, those values are placed on the top of the stack. In some cases, where return values are provided—including the return values from luaL_dofile()
—we might end up with an arbitrary number of values pushed onto the top of Lua’s stack. In our case, we expect a single table as a return value on top of the stack, and we want to assign that table value to the Lua global variable mymodule
. The lua_setglobal()
function is perfect for this job.
Table 1-3 describes both of the newly introduced C API functions. This table also introduces a [–m,+n] notation to help summarize function behavior. In this book, whenever you see [–m,+n] next to a function from Lua’s C API, it means that the function first pops m values from the top of the stack, and then pushes n values back onto the top. This same style of notation is used by Lua’s online reference manual, with an additional third value like so: [–2, +0, e], which indicates whether a Lua error might occur during the function call. This notation will be omitted when the values of m and n are not fixed values, as is the case for lua_settop()
.
Table 1-3 shows both of the newly introduced C API functions.
C API function | Description | Stack use |
---|---|---|
lua_setglobal(L, <string> ) |
Pop the top element E off the stack and set the Lua global variable named <string> equal to E. |
[–1, +0] |
lua_settop(L, <new_stack_size> ) |
Change the stack to contain <new_stack_size> values. If the stack shrinks, the top elements are discarded. If the stack grows, nil values are added to the top. |
In our case, we’re emptying the stack by setting the number of values to zero. Lua effectively provides each C function with a clean stack, so there’s no need to worry about accidentally removing values that Lua expected to continue existing on the stack. The only code relying on the state of the C-visible stack is yours.
So far, we’ve loaded a module, but we haven’t run anything yet. Let’s take a look at a more interesting Lua module and then learn one way to run it from C.
Drawing Mazes
There are a surprising number of fascinating algorithms that randomly generate mazes. In fact, some developers have spent a lot of time on algorithms that draw mazes when they could have been doing more productive things such as writing reports that teach people how to make solid APIs. In this section, we’ll consider a slight modification of a depth-first search that essentially drills paths through a grid of walls.
Let’s look at the eatyguy0.lua module that sets up a maze for Mr. EatyGuy, but does nothing more. The zero in the filename eatyguy0.lua is there to place it first in a sequence of increasingly fun Lua example files that build on one another throughout this book.
The module’s entry point is a public-facing function that I’ve decided to call eatyguy.init()
. This function generates a maze stored in the 2D grid
array. (I’m using the word “array” to mean a Lua table with integer keys, thought of as a list; it’s a Lua convention that these keys begin at 1.) If the element grid[x][y]
is falsy, it’s a wall; otherwise it’s an open space.
Note
The word falsy indicates any value that evaluates to false when converted to a boolean; truthy is anything that converts to true.
It’s convenient to use falsy values to represent walls, given that nonexistent Lua values are falsy, so we get a grid of walls without setting any data. The maze will be created by walking through adjacent (x, y) points and setting them to truthy values, which effectively drills paths through the grid. Note that both walls and open spaces are each represented by two adjacent characters—either '##'
or ' '
, respectively—because terminal characters tend to be twice as tall as they are wide; thus, a two-character block is close to square in aspect ratio.
Most of the work is initiated by a recursive function called drill_path_from(x, y)
, which executes a depth-first search through the grid, turning walls into open space as it goes. The search is randomized so that the paths take unpredictable turns, and a small percentage of locations can be visited more than once in order to add more cross-over between paths within the maze.
Here’s the complete module:
-- eatyguy0.lua local eatyguy = {} -- Globals. local percent_extra_paths = 15 local grid = nil -- grid[x][y]: falsy = wall. local grid_w, grid_h = nil, nil -- Internal functions. local function is_in_bounds(x, y) return (1 <= x and x <= grid_w and 1 <= y and y <= grid_h) end local function get_neighbor_directions(x, y, percent_extra) -- percent_extra is the percent chance of adding extra paths. percent_extra = percent_extra or 0 local neighbor_directions = {} local all_directions = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}} for _, direction in pairs(all_directions) do local nx, ny = x + 2 * direction[1], y + 2 * direction[2] local is_extra_ok = (math.random(100) <= percent_extra) -- Add `direction` if the neighbor is not yet in a path, or -- if we randomly got an extra ok using percent_extra. if is_in_bounds(nx, ny) and (not grid[nx][ny] or is_extra_ok) then table.insert(neighbor_directions, direction) end end return neighbor_directions end local function drill_path_from(x, y) grid[x][y] = 'open' local neighbor_directions = get_neighbor_directions(x, y) while #neighbor_directions > 0 do local direction = table.remove(neighbor_directions, math.random(#neighbor_directions)) grid[x + direction[1]][y + direction[2]] = 'open' drill_path_from(x + 2 * direction[1], y + 2 * direction[2]) neighbor_directions = get_neighbor_directions(x, y, percent_extra_paths) 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()) -- Build the maze. grid = {} for x = 1, grid_w do grid[x] = {} end drill_path_from(1, 1) -- Draw the maze. for y = 0, grid_h + 1 do for x = 0, grid_w + 1 do -- This line is like: chars = (grid[x][y] ? ' ' : '##'). local chars = (grid[x] and grid[x][y]) and ' ' or '##' io.write(chars) end io.write('\n') -- Move cursor to next row. end end return eatyguy
Note
Some languages have preferred naming conventions for variables, usually choosing between CamelCaseLikeThis
and snake_case_like_this
. The Lua community, thus far, does not have an overwhelming preference about the naming style of Lua variables. My impression is that it’s slightly more popular to use snake_case
for function and variable names, and CamelCase
for class names, so that is the convention I’ll use in this book (with the interesting exception of module and filenames, both of which avoid uppercase letters and underscores).
The eatyguy0.lua module is not written as a standalone script, so running lua eatyguy0.lua
won’t do anything. This design makes it easier to control from C, which we’ll expand on in Chapter 2. For now, you can use this shell command to run the current script as is:
$ lua -e 'require("eatyguy0").init()'
I used a slightly modified version of the module to capture snapshots of the maze, shown in Figure 1-4, illustrating how it evolves as the code runs.
Running the module directly results in a completed maze like the one depicted in Figure 1-5.
Not bad for such a small module, right?
One Way to Call a Lua Function from C
Now that we have the Lua code to draw mazes, let’s see how to write a C program that can run the Lua file. As examples of why this is useful, you could run a Lua script from Objective-C in an iOS app, or run a cross-platform Lua script from platform-specific C or C++ code.
The next program loads and runs the init()
function from the eatyguy module. It’s similar to loadmodule.c, so I’ve highlighted the new lines (in bold):
// runmodule.c // // Loads eatyguy0.lua and runs eatyguy.init(). // #include "lauxlib.h" #include "lua.h" #include "lualib.h" int main() { // Create a Lua state and load the module. lua_State *L = luaL_newstate(); luaL_openlibs(L); luaL_dofile(L, "eatyguy0.lua"); lua_setglobal(L, "eatyguy"); lua_settop(L, 0); // Run the init() function. lua_getglobal(L, "eatyguy"); lua_getfield(L, -1, "init"); // -1 means stack top. lua_call(L, 0, 0); // 0, 0 = #args, #retvals return 0; }
Table 1-4 lists the three new functions.
C API function | Description | Stack use |
---|---|---|
lua_getglobal(L, <string> ) |
Push the value of the Lua global variable with name <string> onto the top of the stack. |
[–0, +1] |
lua_getfield(L, <index> , <name> ) |
Push the value table[ <name> ] onto the top of the stack, where the table is expected to be at location <index> in the stack. Indexes are explained below. |
[–0, +1] |
lua_call(L, n_args, n_ret) |
Call the function f(arg1, arg2, ...) and push the first n_ret return values onto the top of the stack.The function arguments arg1 , etc., are the top n_args values on the stack, and the function itself is the stack value below those arguments. The function f and its arguments are popped before the return values are pushed. If f returns n < n_ret values, n_ret - n copies of the nil value are pushed onto the stack after the return values. |
[–(n_args +1), +n_ret ] |
As used in runmodule.c, these three functions invoke the Lua function eatyguy.init()
. Even though the C version of the call is more verbose, the atomic-step approach of the C API keeps the interface economical. It’s easier to master and customize than a larger API would have been.
Stack Indexes
The lua_getfield()
function introduces the idea of an index, which is an integer indicating a position in Lua’s stack. Suppose that the Lua stack currently contains these values, with the bottom of the stack on the left:
[3, nil, false, {key = 'value'}, 42]
Positive indexes indicate values starting from the bottom of the stack; thus, index 1 indicates value 3, and index 5 indicates the value 42. Negative indexes indicate values starting from the top of the stack; thus, –1 always refers to the top element on the stack. In our example, index –1 refers to the value 42, and index –3 refers to the value false
.
Valid versus acceptable stack indexes
The Lua reference manual assigns specific meanings to the terms valid index and acceptable index, both of which denote ranges of index values into the Lua stack. If you’re coding extensively with the C API, it can be helpful to understand the structure of the stack and the distinction between these terms, so I’ll explain them in this section.
At any given moment, the Lua stack has two size parameters: the top of the stack, meaning how many values are in the stack, and the allocated size of the stack, indicating the maximum possible number of values that the stack can hold without experiencing a stack overflow. The default allocated stack size is 20, which is large enough for many operations. You can allocate more space by calling lua_checkstack()
.
Figure 1-6 presents a diagram of an example stack, showing both used stack slots and allocated-but-unused slots.
In this example, there are four values on the stack, and the total allocated size is eight. You could successfully push four more values onto the stack, but if you attempted to push one more, you would find yourself in the eerie and treacherous zone of undefined behavior. In other words, Lua assumes you won’t do that, and things might break if you do.
As you might have guessed from the diagram, the term “valid index” refers to any index of a non-empty stack slot. In our example, the values –4 through –1 and 1 through 4 are all valid indexes; zero is excluded. The term “acceptable index” refers to any index of an allocated stack slot, including empty slots. All valid indexes are also acceptable indexes. In our example, the indexes 5 through 8 are also acceptable, although they’re not valid.
In Lua, the lack of a value is indicated by the special value nil. When you use the C API, stack slots could either hold the nil value or be empty, meaning that that the given stack slot is allocated but unused. In other words, there are two distinct ways to indicate the absence of a value on the Lua stack.
Giving Lua the Power of C
In this chapter, we saw how to compile and run an arbitrary Lua script from C at runtime. That was fun, but it’s not enough to build an API. To create a Lua interface that can do anything C can do, we need to give Lua the ability to effectively call C code. Chapter 2 takes a look at how to do that.
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.