Getting Started With Lua

API7.ai

September 23, 2022

OpenResty (NGINX + Lua)

After a general understanding of the basics of NGINX, we will learn Lua further. It is the programming language used in OpenResty, and it is necessary to master its basic syntax.

Lua is a small and subtle scripting language, born in a university lab in Brazil, whose name means "beautiful moon" in Portuguese. NGINX was born in Russia, Lua in Brazil, and OpenResty in China from the author's country. Interestingly, these three equally clever open source technologies came from BRICS countries, not Europe or America.

Lua was designed to position itself as a simple, lightweight, embeddable glue language that doesn't go the big and bold route. Although you may not write Lua code directly in your everyday work, Lua is widely used. Many online games, such as World of Warcraft, use Lua to write plugins; Redis, the key-value database, has Lua built-in to control the logic.

On the other hand, although Lua's library is relatively simple, it can easily be called a C programming language library, and many mature C programming language codes can be used for it. For example, in OpenResty, you will often need to call NGINX and OpenSSL C programming language functions, thanks to Lua and LuaJIT's ability to reach C libraries easily.

Here, I'll take you through a quick familiarization with Lua data types and syntax so you can learn OpenResty more smoothly later.

Environment and hello world

We don't need to specifically install a standard Lua 5.1 environment because OpenResty no longer supports standard Lua, only LuaJIT. Note that the Lua syntax I present here is also compatible with LuaJIT and is not based on the latest Lua 5.3.

You can find the LuaJIT directory and executable in the OpenResty installation directory. I'm on a Mac environment and used brew to install OpenResty, so your local path will likely differ from the following.

$ ll /usr/local/Cellar/openresty/1.13.6.2/luajit/bin/luajit
     lrwxr-xr-x  1 ming  admin    18B  4  2 14:54 /usr/local/Cellar/openresty/1.13.6.2/luajit/bin/luajit -> luajit-2.1.0-beta3

You can also find it in the system's executable file directory.

 $ which luajit
 /usr/local/bin/luajit

Check the version of LuaJIT.

$ luajit -v
 LuaJIT 2.1.0-beta2 -- Copyright (C) 2005-2017 Mike Pall. http://luajit.org/

After checking this information, you can create a new 1.lua file and use LuaJIT to run the hello world code.

$ cat 1.lua
print("hello world")
$ luajit 1.lua
 hello world

Of course, you can also use resty to run it directly, knowing that it is ultimately executed with LuaJIT as well.

$ resty -e 'print("hello world")'
 hello world

Both of these ways of running hello world are possible. I prefer the resty approach because a lot of OpenResty code is also run by resty later on.

Data Type

There are not many data types in Lua, and you can return the type of a value with the type function, such as the following.

$ resty -e 'print(type("hello world"))
 print(type(print))
 print(type(true))
 print(type(360.0))
 print(type({}))
 print(type(nil))
 '

The following information will be printed out.

 string
 function
 boolean
 number
 table
 nil

These are the basic data types in Lua. Let's briefly introduce them.

String

In Lua, the string is an immutable value. If you want to modify a string, you must create a new one. This approach has advantages and disadvantages: the advantage is that even if the exact string appears many times, there is only one copy in memory, but the drawback is also apparent: if you want to modify and splice strings, you create a lot of extra unnecessary strings.

Let's take an example to illustrate this disadvantage. In Lua, we use two dot marks to indicate the addition of strings. The following code stitches together the numbers 1 through 10 as strings.

$ resty -e 'local s  = ""
 for i = 1, 10 do
     s = s .. tostring(i)
 end
 print(s)'

Here we loop ten times, and only the last result is what we need; the nine new strings in between are useless. They not only take up extra space but also consume unnecessary CPU operations.

Of course, we will have a solution for this later in the performance optimization section.

Also, in Lua, you have three ways to express a string: single quotes, double quotes, and long parentheses ([[]]). The first two are relatively easy to understand and are generally used in other languages, so what is the use of long parentheses?

Let's look at a concrete example.

$ resty -e 'print([[string has \n and \r]])'
 string has \n and \r

You can see that the strings in the long brackets are not escaped in any way.

You may ask another question: What if the string above includes the long brackets? The answer is simple: add one or more = symbols in the middle of the long brackets.

$ resty -e 'print([=[ string has a [[]]. ]=])'
  string has a [[]].

Boolean

This is a simple one, true and false. In Lua, only nil and false are false; everything else is true, including 0 and the empty string. We can verify this with the following code.

$ resty -e 'local a = 0
 if a then
   print("true")
 end
 a = ""
 if a then
   print("true")
 end'

This type of judgment is inconsistent with many common development languages, so to avoid errors in such matters, you can explicitly write the object of comparison, such as the following.

$ resty -e 'local a = 0
 if a == false then
   print("true")
 end
 '

Number

Lua's number type is implemented as a double-precision floating-point number. It is worth mentioning that LuaJIT supports dual-number mode, which means that LuaJIT stores integers as integers and floating-point numbers as double-precision floating-point numbers, depending on the context.

In addition, LuaJIT supports long-long integers for large integers, such as the following example.

$ resty -e 'print(9223372036854775807LL - 1)'
9223372036854775806LL

Function

Functions are first-class citizens in Lua, and you can store a function in a variable or use it as an incoming and outgoing reference to another function.

For example, the following two function declarations are exactly equivalent.

function foo()
 end

and

foo = function ()
 end

Table

The Table is the only data structure in Lua and is naturally very important, so I will devote a special section to it later. We can start by looking at a simple example code.

$ resty -e 'local color = {first = "red"}
print(color["first"])'
 red

Null value

In Lua, a null value is nil. If you define a variable but do not assign a value, its default value is nil.

$ resty -e 'local a
 print(type(a))'
 nil

When you enter the OpenResty system, you will find many null values, such as ngx.null, and so on. We will talk more later.

Lua's data types, I will mainly introduce so much, first give you a foundation. We will continue to learn what you need to focus on mastering later in the article. Learning through practice and use is always the most convenient way to absorb new knowledge.

Common Standards Library

Often, learning a language is really about learning its standard libraries.

Lua is relatively small and does not have many standard libraries built in. Also, in the OpenResty environment, the Lua standard library is a very low priority. For the same function, I recommend using OpenResty API first, LuaJIT's library functions, and the normal Lua functions.

OpenResty's API > LuaJIT's library functions > standard Lua's functions is a priority that will be mentioned repeatedly in terms of usability and performance.

However, despite this, we will inevitably use some Lua libraries in our actual projects. Here, I have selected a few of the more commonly used standard libraries to introduce them, and if you want to know more, you can check the official Lua documentation.

String library

String manipulation is what we often use and where the pitfalls are the greatest.

One simple rule is that if regular expressions are involved, please be sure to use ngx.re.* provided by OpenResty to solve them, not Lua's string.* processing. This is because Lua's regular is unique and does not conform to the PCRE specification, and I believe most engineers will not be able to play with it.

One of the most commonly used string library functions is string.byte(s [, i [, j ]]), which returns the ASCII code corresponding to the characters s[i], s[i + 1], s[i + 2], ------, s[j]. The default value of i is 1, the first byte, and the default value of j is i.

Let's look at a sample code.

$ resty -e 'print(string.byte("abc", 1, 3))
 print(string.byte("abc", 3)) -- Missing third parameter, the third parameter is the same as the second by default, which is 3
 print(string.byte("abc"))    -- The second and third parameters are missing, both of which default to 1
 '

Its output is:

 979899
 99
 97

Table Library

In the context of OpenResty, I don't recommend using most of the table libraries that come with Lua, except for a few functions such as table.concat and table.sort. As for their details, we'll leave them to the LuaJIT chapter.

Here I will briefly mention table.concat. table.concat is generally used in string concatenation scenarios, such as the example below. It can avoid generating a lot of useless strings.

$ resty -e 'local a = {"A", "b", "C"}
 print(table.concat(a))'

Math Library

The Lua math library consists of a standard set of mathematical functions. The introduction of the math library both enriches the Lua programming language and makes it easier to write programs.

In OpenResty projects, we seldom use Lua to do mathematical operations. Still, two functions related to random numbers, math.random() and math.randomseed(), are commonly used, such as the following code, which can generate two random numbers in a specified range.

$ resty -e 'math.randomseed (os.time())
print(math.random())
print(math.random(100))'

Dummy Variables

After understanding these shared standard libraries, let's learn a new concept - dummy variables.

Imagine a scenario where a function returns multiple values, some of which we don't need, so how should we receive these values?

I don't know how you feel about this, but for me, at least, it's torturous to try to give meaningful names to these unused variables.

Luckily, Lua has a perfect solution for this, providing the concept of a dummy variable conventionally named with an underscore to discard unneeded values and serve as a placeholder.

Let's take the standard library function string.find as an example to see the use of dummy variables. This normal library function returns two values representing the start and ends subscripts, respectively.

If we only need to get the start subscript, it is simple to declare a variable to receive the return value of string.find as follows.

$ resty -e 'local start = string.find("hello", "he")
 print(start)'
 1

But if you only want to get the ending subscript, then you have to use the dummy variable

$ resty -e 'local  _, end_pos = string.find("hello", "he")
 print(end_pos)'
 2

In addition to being used in the return value, dummy variables are often used in loops, such as the following example.

$ resty -e 'for _, v in ipairs({4,5,6}) do
     print(v)
 end'
 4
 5
 6

And when there are multiple return values to ignore, you can reuse the same dummy variable. I won't give an example here. Can you try to write a sample code like this yourself? You are welcome to post the code in the comment section to share and exchange with me.

Summary

Today, we've taken a quick look at the data structures and syntax of standard Lua, and I'm sure you've gotten a first look at this simple and compact language. In the next lesson, I'll take you through the relationship between Lua and LuaJIT, with LuaJIT being the main focus of OpenResty and worth digging into.

Finally, I want to leave you with one more thought-provoking question.

Remember the code you learned in this post when we talked about the math library? It generates two random numbers in a specified range.

$ resty -e 'math.randomseed (os.time())
print(math.random())
 print(math.random(100))'

However, you may have noticed that the code is seeded with the current timestamp. Is there a problem with this approach? And how should we generate good seeds? Often, the random numbers we develop are not random and have excellent security risks.

Welcome to share your opinions with us, and also welcome to forward share this post to your colleagues and friends. Let's communicate and improve together.