07 Quickly Getting Started With Lua

07 Quickly Getting Started with Lua #

Hello, I’m Wen Ming.

After having a basic understanding of NGINX, we are now going to further study Lua. Lua is a programming language used in OpenResty, and it is necessary to master its basic syntax.

Lua is a compact and elegant scripting language that originated from a university laboratory in Brazil. The word “Lua” means “moon” in Portuguese. Looking at the countries where the authors come from, NGINX was born in Russia, Lua was born in Brazil, and OpenResty was born in China. These three equally sophisticated open-source technologies come from the BRICS countries, rather than Europe or America, which is quite interesting.

Back to the Lua language. In fact, Lua was designed from the beginning as a simple, lightweight, and embeddable glue language, without going down the path of being all-encompassing. Although you may not directly write Lua code in your day-to-day work, Lua is actually widely used. Many online games, such as World of Warcraft, use Lua to write plugins, and the key-value database Redis has built-in Lua to control the logic.

On the other hand, although Lua’s own library is relatively simple, it can easily call C libraries. A large number of mature C code can be used for Lua. For example, in OpenResty, you often need to call NGINX and OpenSSL’s C functions, thanks to Lua and LuaJIT’s ability to easily call C libraries.

Next, I will guide you through a quick familiarization with Lua’s data types and syntax, so that you can learn OpenResty more smoothly later.

Environment and hello world #

We don’t need to specifically install an environment like standard Lua 5.1 because OpenResty no longer supports standard Lua and only supports LuaJIT. The Lua syntax I’m introducing here is also compatible with LuaJIT, not based on the latest Lua 5.3. Please pay special attention to this.

In the OpenResty installation directory, you can find the directory and executable file for LuaJIT. In my case, I’m using a Mac environment and installed OpenResty with brew, so your local path may be different 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

And check the version of LuaJIT:

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

After confirming this information, you can create a 1.lua file and run the hello world code using luajit:

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

$ luajit 1.lua
 hello world

Of course, you can also use resty to run it directly. Keep in mind that it ultimately uses LuaJIT for execution:

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

Both of the above methods for running hello world are viable. Personally, I prefer the resty approach because many OpenResty codes later on are also executed using resty.

Data Types #

Lua has few data types. You can use the type function to return the type of a value. Here is an example:

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

This will print:

 string
 function
 boolean
 number
 table
 nil

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

Strings #

In Lua, strings are immutable values. If you want to modify a string, you need to create a new one. This approach has both advantages and disadvantages. The advantage is that even if the same string appears many times, there is only one instance in memory. However, the drawback is that if you want to modify or concatenate strings, it will create many unnecessary strings.

Let’s take an example to demonstrate this drawback. The following code concatenates the numbers from 1 to 10 as strings. In Lua, we use two period characters to represent string concatenation:

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

In this code, we iterate 10 times, but only the last iteration gives us the desired result. The 9 intermediate strings created are unnecessary. They not only occupy additional space but also consume unnecessary CPU computation.

Of course, in the later chapters on performance optimization, we will provide methods to solve this problem.

In Lua, there are three ways to express a string: single quotes, double quotes, and long brackets ([[]]). The first two are easy to understand and are commonly used in other languages. So, what is the purpose of long brackets?

Let’s look at a specific example:

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

You can see that the string inside the long brackets is not subject to any escape processing.

You may ask another question: What if the long bracket string contains long brackets themselves? The answer is simple, you just need to add one or more = symbols inside the long brackets:

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

Booleans #

This is very simple. The values true and false are booleans. However, in Lua, only nil and false are considered false, while all other values are considered true, including 0 and an 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 comparison is not consistent with many common programming languages. Therefore, to avoid errors in this case, you can explicitly compare the objects, as shown below:

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

Numbers #

Lua’s number type is implemented using double-precision floating-point numbers. It is worth mentioning that LuaJIT supports the dual-number mode, which means that LuaJIT will use integers to store integers and double-precision floating-point numbers to store floating-point numbers based on the context.

In addition, LuaJIT also supports long long integers, as shown in the following example:

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

Functions #

In Lua, functions are first-class citizens. You can store functions in variables and use them as parameters and return values of other functions.

For example, the following two function declarations are equivalent:

function foo()
 end

and

foo = function ()
 end

Tables #

Tables are the only data structure in Lua and are therefore very important. So I will introduce them in a dedicated section later. Let’s look at a simple example:

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

Nil #

In Lua, nil is used to represent a nil value. If you define a variable but don’t assign a value to it, its default value is nil:

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

When you really dive into the OpenResty ecosystem, you will find many other types of nil, such as ngx.null, which we will discuss later.

These are the data types in Lua that I mainly introduced to give you a foundation. We will continue to explore them in future articles. Learning by practicing and using is always the most efficient way to absorb new knowledge.

Common Standard Libraries #

Many times, when we learn a language, we are actually learning its standard library.

Lua is relatively small and doesn’t have many built-in standard libraries. Moreover, in the context of OpenResty, the Lua standard library has a low priority. For the same functionality, I recommend using OpenResty’s API first, followed by LuaJIT’s library functions, and finally Lua’s standard functions.

The priority of OpenResty API > LuaJIT library functions > Lua standard functions will be repeatedly mentioned later. This not only affects usability, but also has a significant impact on performance.

However, despite this, in practical project development, we still inevitably need to use some Lua libraries. Here, I have selected a few commonly used standard libraries for introduction. If you want to learn more, you can refer to Lua’s official documentation.

string library #

String manipulation is the most commonly used and error-prone aspect. There is a simple rule: if regular expressions are involved, please use ngx.re.* provided by OpenResty to solve the problem instead of using Lua’s string.* functions. This is because Lua’s regular expression implementation is unique and does not comply with PCRE specifications, which I believe most engineers cannot handle.

One commonly used function in the string library is string.byte(s [, i [, j ]]), which returns the ASCII codes corresponding to the characters s[i], s[i + 1], s[i + 2], …, s[j]. The default value of i is 1, i.e., the first byte, and the default value of j is the same as i.

Let’s take a look at an example code snippet:

$ resty -e 'print(string.byte("abc", 1, 3))
 print(string.byte("abc", 3)) -- lacks the third parameter, which defaults to the same value as the second parameter, which is 3 in this case
 print(string.byte("abc"))    -- lacks both the second and the third parameters, which both default to 1 in this case
 '

Its output is:

 979899
 99
 97

table library #

In the context of OpenResty, for the Lua built-in table library, I do not recommend using most functions besides a few such as table.concat and table.sort. The details of these functions will be discussed in the LuaJIT chapter.

Here, I briefly mention table.concat. table.concat is usually used in scenarios where we need to concatenate strings, such as the example below. It can avoid generating many unnecessary strings.

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

math library #

The Lua math library consists of a set of standard mathematical functions. The introduction of the math library not only enriches the functionality of the Lua programming language, but also facilitates the programming process.

In practical projects in OpenResty, we rarely perform mathematical operations using Lua. However, the math.random() and math.randomseed() functions related to random numbers are relatively commonly used. For example, the following code generates two random numbers within a specified range:

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

Dummy Variables #

After understanding these common standard libraries, let’s learn about a new concept - dummy variables.

Imagine a scenario where a function returns multiple values, but we don’t need some of these values. In such cases, how do we receive these values?

I’m not sure about your thoughts, but at least for me, giving meaningful names to these unused variables seems like a troublesome task.

Fortunately, Lua can perfectly solve this issue. Lua provides the concept of a dummy variable, which is conventionally named with an underscore and serves as a placeholder to discard unwanted values.

Now let’s take the string.find standard library function as an example to see how dummy variables are used. This function returns two values, representing the starting and ending indexes.

If we only need to get the starting index, it’s simple. We can just declare a variable to receive the return value of string.find:

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

But if you only want to obtain the ending index, then you must use a dummy variable:

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

In addition to being used in return values, dummy variables are often used in loops, as shown in the following example:

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

When you need to ignore multiple return values, you can reuse the same dummy variable. I won’t provide an example here, but could you try writing such an example code yourself? Feel free to share and discuss it in the comments section.

Conclusion #

Today, we quickly learned about the data structures and syntax of standard Lua. I believe you now have a basic understanding of this simple and elegant language. In the next class, I will introduce the relationship between Lua and LuaJIT. LuaJIT is a major feature of OpenResty, which is worth exploring in depth.

Finally, I would like to leave you with a question to consider.

Do you remember the code we learned when discussing the math library in this class? It can generate two random numbers within a specified range.

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

However, you may have noticed that this code uses the current timestamp as the seed. Is this method problematic? And how can we generate good seeds? It is worth noting that many times the random numbers we generate are not actually random and can pose significant security risks.

Feel free to share your thoughts in the comments section, and please also consider forwarding this article to your colleagues and friends. Let’s exchange ideas and progress together.