Lecture 19 | How to embed scripting language?

Since 2005, it has become increasingly popular to use C/C++ language combined with scripting languages (Lua, Python, Ruby, etc.) to write games. This is because the traditional way of writing games in C/C++ has too much hard code, and games written using hard code are very difficult to update unless the program is recompiled.

As a result, some people began to use configuration files to do activity logic. For example, fill in the configuration table, what is the player level, what is the attack power, how much damage is equal to it, etc. These contents are read into the code from the beginning and calculated in real time in the game.

But this method is actually not convenient. Games a long time ago generally loaded the WAV format due to hardware resource limitations. Loading MP3 requires the machine to decompress the music file before playing it. If the machine hardware has poor computing power, the running efficiency of the entire game will decrease due to decompression.

The same is true for scripting languages. If the machine hardware capabilities are not good, the virtual machine of the scripting language will have to interpret the program, resulting in a decrease in game running efficiency. With the improvement of computer hardware, it has become possible for us to load MP3 music files in the game, and of course it is also possible to load scripting languages in the game for logic writing.

World of Warcraft is written using the Lua scripting language. Large-scale games such as “GTA” all have their own scripting language and system. The purpose of using scripting language is to be able to write hard code while also writing logical code easily without recompiling. In fact, many large-scale games now use this method to write code, and even some game engines themselves support separate writing of script languages and languages provided by the engine itself. For example, the engine is written in C++ language and the scripting language is written in Lua.

Why use Lua scripts to embed C/C++ hard code?

Today I will teach you how to use Lua script to embed C/C++ hard code. Why should I choose Lua scripting language to write code?

BecauseLua scripts are lightweight enough and have almost no redundant code. The execution efficiency of the Lua virtual machine is almost comparable to that of C/C++. If you choose common scripting languages such as Python and Ruby for embedding, it is not impossible, but you have to pay the price of execution efficiency. Because the execution efficiency of Python and Ruby is far inferior to Lua.

If you don’t have a lot of coding experience, you may ask, why are the execution efficiency of Python and Ruby far inferior to Lua? This issue can probably be fully explained in the length of a book. Here I will only briefly mention the reasons.

Lua’s virtual machine is very simple, and the instructions are designed to be streamlined. Lua itself is a register-based virtual machine implementation, while other scripting languages such as Python are stack-based virtual machines, and the register-based virtual machine bytecode is simpler and more efficient. Because bytecode generally contains instructions, operands, operation targets, etc. at the same time.

On the other hand, the reason why Python and Ruby are widely used is that they have a large number of mature libraries and frameworks, while Lua is just a very pure scripting language. Because Lua does not have too many third-party libraries, it only provides the most basic I/O processing, mathematical operation processing, string processing, etc. Others are closely related to the operating system, such as network, multi-threading, audio and video processing, etc. None are provided.

In Lecture 6, we have already explained in great detail how to compile Lua scripts into static libraries. If you don’t remember, you can go back and review it. After compiling the static library liblua.a, we can use it in programming.

You can also choose to use the make command to compile directly in the decompressed directory. The compilation will generate the executable files lua.exe and luac.exe of the Lua virtual machine. Of course, this requires a complete set of MinGW environment support.

To start, we still use MinGW Development Studio to create a project. Since this is just an example, the name can be arbitrary. I gave a project name called lua_test and set the project to Win32 Console Application. You can see this example diagram.

After creating the project, we create a new test.c file. This file is located in the lua source code path. We also put the liblua.a file in the same directory to facilitate subsequent link calls.

Before including the Lua header file, we need to write the header file under a certain .hpp file so that it can be included at once. Our code can be written like this.

#ifdef __CPLUSPLUS
extern "C" {
#endif
#include "src/lua.h"
#include "src/lualib.h"
#include "src/lauxlib.h"
#ifdef __CPLUSPLUS
}
#endi

As you can see, there are three codes included. These three codes come from the src directory, and the last one, lauxlib.h, contains a large number of C language interfaces and extended interfaces. Defining extern “C” means to use C for linking. The prerequisite is that your language is C++ language (ifdef __CPLUSPLUS).

After defining this hpp file, we can include it in C or C++ language.

#include “lua.hpp”

You need to know three details of Lua language

After writing the definition, we can start a series of binding operations on Lua. Before programming, I will first describe the details of the Lua language in some language you can understand. There are three points that need to be remembered.

First of all, Lua’s subscripts all use 1 as the initial value (of course you can use -1 as the subscript in reverse), not the 0 we are familiar with. There is a rumor that it was caused by a calculation error when the author wrote the original version of Lua, so it has been used ever since. Although this statement cannot be tested, it can be regarded as an explanation.

Secondly, in the practice of embedding Lua in C/C++, Lua has two methods of reading scripts.

One way is to run it directly after reading, and the function called is luaL_dofile. Using this function, the script will be run directly after reading. Of course, if an error occurs, you don’t know the specific location of the error, so debugging is not very convenient.

The second way is to push the script code to the top of the stack, and then use the pcall operation to run the script. This function is called luaL_loadfile. In fact, the first method also uses this method and calls the pcall operation directly. You can understand the code of the first method at a glance.

#define luaL_dofile(L, fn) \
      (luaL_loadfile(L, fn) || lua_pcall(L, 0, LUA_MULTRET, 0))

This line of code can be found in lauxlib.h. This code is very cleverly written. It means that if loadfile is successful, then the pcall function is run, and the || (or) in the middle has directly determined whether loadfile is successful. Because the loadfile function returns 0 if the operation is successful, otherwise it returns 1.

Under the logical judgment of “or”, as long as it is 0, the judgment will continue; as long as it is 1, it will directly return the condition to be true. Therefore, in this line of code, as long as it is 1, the operation of the dofile macro will be interrupted; as long as it is 0, the pcall operation will be performed.

Finally, I want to talk about Lua’s stack. Once you understand the counting method of the stack, you can easily understand the counting method in the code I will explain later. Lua’s stack can be seen from this picture. Going up from the bottom of the stack can be represented by 1, 2, 3, 4, 5, while going down from the top of the stack can be represented by -1, -2, -3, -4, – 5.

How to use Lua and liblua.a to perform binding operations with C language?

We now start using Lua and liblua.a to perform binding operations with the C language.

First, we need to include the lua.hpp header file we defined before, and then we start defining some variables at the main entry function.

#include "lua.hpp"
int main(int argc, char ** argv)
{
      int r;
      const char* err;
      lua_State* ls;
       ….
}

Here, we define three variables, among which r is used to receive the return value; err is a constant string used to receive the error string and print it out; and lua_State* ls is the pointer of the Lua virtual machine.

Let’s look at the next code again.

ls = luaL_newstate();
luaL_openlibs(ls); 

In these two lines of code, a virtual machine is first initialized (in Lua 5.1, the function used is lua_open to create a new virtual machine), and the virtual machine address is assigned to the ls pointer. Then, after we get this pointer, we “open” the various libraries needed by Lua in the subsequent code. We use luaL_openlibs. I am just giving you a demonstration now. You can open each library individually.

We created a new virtual machine and opened the Lua class library. Let’s continue looking at the code below.

r = luaL_loadfile(ls, argv[1]);
if(r)
{
err = lua_tostring(ls, -1);
if(err)
printf("err1: %s\\
", err);
return 1;
}
r = lua_pcall(ls, 0, 0, 0);
if(r)
{
err = lua_tostring(ls, -1);
if(err)
     printf("err2: %s\\
", err);
return 1;
}
lua_close(ls); 

Let’s explain it in detail. In this code, argv[1] is the first content entered on the command line. For example, if our program is called lua_test, then we enter lua_test a.lua in the Windows command line, then a.lua is the content of argv[1].

luaL_loadfile As we introduced before, it loads the file but does not run it. Of course during this period, it will check the basic syntax. If you have one less bracket or one more quotation mark, you will be given an error message at this time. This error message is determined using the r variable. If the return value of r is not equal to 0, something went wrong. When an error occurs, Lua will push the error information to the top of the stack, and the top of the stack is expressed starting from -1, so we need to take out the error information lua_tostring(ls, -1); on the top of the stack, and assign it to err, and finally use err is printed.

Once you think there are no mistakes, you have passed this level. In the second level, we need to use the lua_pcall function to call the Lua script file. The first parameter is the virtual machine pointer, the second parameter is the number of parameters passed to Lua, the third parameter is the value returned by this script, and the fourth parameter It is an error handling function, which can be 0, which means there is no handling function.

The same is true for the return value of pcall. If it is not 0, something went wrong. Different from the previous luaL_loadfile, this time it is usually a runtime error, such as a runtime type error, etc. Similarly, pcall will also push the error message to the top of the stack. We can directly convert the content on the top of the stack into a string and print it out. Finally, we close the Lua virtual machine through lua_close.

According to common sense, we can run the effect now. You can wait first. Let’s write a wrong Lua code first and see what happens when it is executed.

print "test running")

We deliberately write one less bracket, and then name the source code a.lua. Let’s run it and see. An error message like this will appear:

After discovering a syntax error, the program will report an error. In addition, if you enter a file that does not exist at all, such as we run it like this, test_lua xxx.lua, an error will occur during loadfile.

Summary

That’s it for today. Next time I will present you with further details of Lua script embedding. Let’s wrap up today’s content.

Because Lua scripts are lightweight enough, there is almost no redundant code. The execution efficiency of the Lua virtual machine is almost comparable to that of C/C++. So we chose to use Lua scripts to embed the C/C++ hard code.

Lua scripts are embedded in C/C++ language and need to declare a virtual machine and assign it to a pointer.

Lua scripts need to first loadfile and then pcall to call the script file. Loadfile will check the most basic script file content, such as whether the file exists, such as whether the script code has errors, and pcall will push the error to the top of the stack when an error occurs during runtime.

Lua errors will suppress the error on the top of the stack. If we want to take it out, we need to use the -1 subscript to take out the content on the top of the stack and convert it to a string for printing.