# Lua

Lua 是一种轻量小巧的脚本语言,用标准 C 语言编写并以源代码形式开放,其设计的目的是为了嵌入应用程序中,从而为应用程序提供灵活的扩展定制功能。

应用:

  • 热更新

Lua 不需要编译,可以在 Java、C# 等编译好后的代码中扩展功能,直接编写。

# 基础

# 游戏选择 Lua 的主要原因

所以需要将部分代码作为资源进行管理,随时可以修改更新。更新资源不需要发布新版本,也不需要苹果爸爸审核,用 lua 的优势就体现出来了。

无论是需要及时调整数值或是修 bug,这样的发布流程都是灾难。某些运营的新版本比如说春节大版本,所有的开发人员都在家过年了,没人来发版本。

所以需要将部分代码作为资源进行管理,随时可以修改更新。更新资源不需要发布新版本,也不需要苹果爸爸审核,用 lua 的优势几句体现出来了。

在手游时代,脚本就是刚需。因为客户端发一个安装包很麻烦,在 ios 上每次更新都需要提交给苹果爸爸审核,审核通过以后才可以发出来给用户下载。

不需要重新编译工程,直接写 lua 源代码就可以执行,实现热更新。

lua 本身表现好也是它被选择的必然原因,它轻量级、高效并且简单。只增加几百 k;lua 的执行速度大概是 C++ 的几十分之一,比绝大部分脚本性能都要高得多;lua 很简单,它是真的只需要半天都能学会的语言。

lua 驱动的 GUI。

# Lua 和 C# 的区别

需要重新下载安装包,才能更新。

C# 不能在移动平台上编译,而 lua 可以直接在操作系统运行(类似 javaScript 可以直接在浏览器运行一样)。

可以直接下载 lua 到移动平台上,只要操作系统本身支持编译的环境,具有 ISO(ANSI)C 编译器,就可以编译执行 Lua,开箱即用。不用像其他语言 C# 等要编译,lua 是解释型语言。

# lua 特性

  • 轻量级
  • 可扩展
  • 其他特性
    • 支持面向过程和函数式编程
    • 自动内存管理:只提供了一种通用类型的表(table),用它可以实现数组、哈希表、集合、对象。
  • 兼容大部分可以编译 C 语言的系统

# Lua 环境安装

# Mac OS X 系统上安装

这里下载的是编译后的二进制文件,如果直接下载源码的话,还要用 C 编译器编译成 lua 程序。

curl -R -O http://www.lua.org/ftp/lua-5.3.0.tar.gz
tar zxf lua-5.3.0.tar.gz
cd lua-5.3.0
make macosx test
sudo make install # 环境变量
1
2
3
4
5

进入 lua 环境,输入 print("Hello World!") 命令。

$ lua
Lua 5.3.5  Copyright (C) 1994-2018 Lua.org, PUC-Rio
> print("Hello World!")
Hello World!
>
1
2
3
4
5

直接通过 lua 解释器执行 HelloWorld.lua 文件:

$ lua HelloWorld.lua
Hello World!
1
2

从上面看到的结果,跟安装完 node 后的环境很像,node 是 v8 引擎下执行 js,node 是 C++编写。而 lua 则是 C 编写,安装编译后的 lua 环境,即可解释执行 lua (编译执行一起)的文件程序。

可以直接输入 lua 进入 lua 运行环境:

# lua 编程

# 交互式编程

Lua 提供了交互式编程模式,我们可以在命令行中输入程序并立即查看效果。

Lua 交互式编程可以通过命令 lua -ilua 来启用。

$ lua
Lua 5.3.5  Copyright (C) 1994-2018 Lua.org, PUC-Rio
>
1
2
3

在命令行中输入:

> print("Hello World!")
Hello World!
>
1
2
3

# 脚本式编程

我们可以将 Lua 程序代码保存到一个以 lua 结尾的文件,并执行,该模式称为脚本式编程。

$ lua hello.lua
1

# Lua 基本语法

end 表示一个代码块的结束

co2 = coroutine.create(
    function()
        for i=1,10 do
            print(i)
            if i == 3 then
                print(coroutine.status(co2))  --running
                print(coroutine.running()) --thread:XXXXXX
            end
            coroutine.yield()
        end
    end
)
1
2
3
4
5
6
7
8
9
10
11
12

# 注释

print("Hello World")
-- 单行注释
--[[ 多行注释
--]]
1
2
3
4

# 标志符

# 关键词

and	break	do	else
elseif	end	false	for
function	if	in	local
nil	not	or	repeat
return	then	true	until
while	goto
1
2
3
4
5
6

# 全局变量

在默认情况下,变量总是认为是全局的。

全局变量不需要声明,给一个变量赋值即创建了这个全局变量,访问一个没有初始化的变量也不会出错,只不过得到的结果是:nil。

$ lua
> b = 10
>
> print(b)
10
> b
10
>
> b = nil
> print(b)
nil
>
1
2
3
4
5
6
7
8
9
10
11
12

#

Lua 是动态类型语言,变量不需要类型定义,只需要为变量赋值。值可以存储在变量中,作为参数传递或结果返回。Lua 中有 8 个基本类型分别为:nil、boolean、number、string、userdata、functon、thread 和 table。

> print(type("Hello world"))
string
> print(type(10.4 * 3))
number
> print(type(print))
function
> print(type(type))
function
> print(type(true))
boolean
> print(type(nil))
nil
> print(type(type(X)))
string
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 基本数据

# nil

nil 类型表示一种没有任何有效值,它只有一个 nil。

> print(type(a))
nil
1
2

对于全局变量和 table,nil 还有一个 “删除” 作用,给全局变量或 table 表里的变量赋予一个 nil 值,等同于把它们删掉。

> tab1 = { key1 = "val1", key2 = "val2", "val3"}
> for k, v in pairs(tab1) do
>> print(k .."-"..v)
>>
>> end
1-val3
key1-val1
key2-val2
> tab1.key1 = nil
> for k, v in pairs(tab1) do                                                    print(k .."-"..v)                                                                                                    end
1-val3
key2-val2
>
1
2
3
4
5
6
7
8
9
10
11
12
13

nit 作比较时应该加上双引号 "

> type(X) == nil
false
> type(X) == 'nil'
true
1
2
3
4
# boolean

boolean 类型只有两个可选值:true(真)和 false(假),Lua 把 false 和 nil 看作是 false,其他的都为 true,数字 0 也是 true:

> print(type(true))
boolean
> print(type(false))
boolean
> print(type(nil))
nil
1
2
3
4
5
6
> if false or nil then
>> print("至少有一个是 true")
>>
>> else
>> print("false 和 nil 都为 false")
>> end
falsenil 都为 false
> if 0 then
>> print("数字 0 是 true")
>> else
>> print("数字 0 为 false")
>> end
数字 0true
1
2
3
4
5
6
7
8
9
10
11
12
13
# number(数字)

Lua 默认只有一种 number 类型:double(双精度)类型(默认类型可以修改 luaconf.h 里定义),以下几种写法都被看作是 number 类型:

> print(type(2))
number
> print(type(2.2))
number
> print(type(0.2))
number
>
> print(type(2e+1))
number
> print(type(0.2e-1))
number
> print(type(7.8263692594256e-06))
number
1
2
3
4
5
6
7
8
9
10
11
12
13
# string(字符串)

字符串由一对双引号或单引号来表示。

> string1 = "this is string1"
> string1
this is string1
1
2
3

也可以用 2 个方括号 [[]] 来表示“一块”字符串。

> html = [[
>> <html>
>> <head></head>
>> <body>
>> <a href="http://www.jecyu.com">Jecyu</a>
>> </body>
>> </html>
>> ]]
> html
<html>
<head></head>
<body>
<a href="http://www.jecyu.com">Jecyu</a>
</body>
</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

在对一个数字字符串上进行算术操作时,Lua 会尝试将这个数字字符串转成一个数字:

> "2" + 6
8.0
> "error" + 1
stdin:1: attempt to perform arithmetic on a string value
stack traceback:
	stdin:1: in main chunk
	[C]: in ?
1
2
3
4
5
6
7

以上 “error” + 1 执行报错了,字符串连接使用的是 ..,如

> "error"..1
error1
1
2

使用 # 来计算字符串的长度,放在字符串前面:

> #html
85
> #string1
15
1
2
3
4

# 数据结构

  • userdata
  • table
# table

通过 table,可以实现数组哈希表集合对象

在 Lua 里,table 的创建是通过“构造表达式”来完成,最简单构造表达式是 {},用来创建一个空表。也可以在表里添加一些数据,直接初始化表:

Lua 中的表(table)其实是一个“关联数组”(associative arrays),数组的索引可以是数字、字符串或表类型。

> a = {}
> a["key"] = "value"
> a
table: 0x7faaf1e02760
> key = 10
> a[key] = 22
> a[key] = a[key] + 11
> for k, v in pairs(a) do
>> print(k..":"..v)
>> end
10:33
key:value
1
2
3
4
5
6
7
8
9
10
11
12

不同于其他语言的数组把 0 作为数组的初始索引,在 Lua 里表的默认索引一般以 1 开始:

> tbl2 = {"apple", "pear", "orange", "grape"}
> tbl2
table: 0x7faaf1c0bd40
> for key, val in pairs(tbl2) do
>> print("Key", key)
>> end
Key	1
Key	2
Key	3
Key	4
>
1
2
3
4
5
6
7
8
9
10
11

table 不会固定长度大小,有新数据添加时 table 长度会自动增长,没初始的 table 都是 nil。

> a = {}
> for i = 1, 10 do
>> a[i] = i
>> end
> a[10]
10
> a3["key"] = "val"
> a["key"] = "val"
> a["key"]
val
> a[10]
10
1
2
3
4
5
6
7
8
9
10
11
12
# userdata(自定义类型)

userdata 是一种用户自定义的数据,用于表示一种由应用程序或 C/C++ 语言库所创建的类型,可以将任意 C/C++ 的任意数据类型的数据(通常是 struct 和指针)存储到 Lua 变量中调用。

表示任意存储在变量中的 C 数据结构。

# funciton(函数)

在 Lua 中,函数是被看作是“第一类值(First-Class Value)”,函数可以存在变量里:

> function factorial(n)
>> if n == 0 then
>> return 1
>> else
>> return n * factorial(n - 1)
>> end
>> end
> factorial(5)
120
> factorial2 = factorial
> factorial2(5)
120
1
2
3
4
5
6
7
8
9
10
11
12

funciton 可以通过匿名(anonymous function)的方式通过参数传递:

> function testFn(tab, fun)
>> for k, v in pairs(tab) do
>> print(fun(k, v))
>>
>> end
>> end
> tab = { key1 = "val1", key2="val2"}
t> testFn(tab, function(key, val) return key.."="..val end)
key2=val2
key1=val1
1
2
3
4
5
6
7
8
9
10

# thread(线程)

在 Lua 里,最主要的线程是协同程序(coroutine)。它跟线程(thread)差不多,拥有自己独立的栈、局部变量和指令指针,可以跟其他协同程序共享全局变量和其他大部分东西

线程与协程的区别:线程可以同时多个运行,而协同程序需要彼此协作的运行,任意时刻只能运行一个,并且处于运行状态的协程只有被挂起(suspend)时才会暂停。

协同程序有点类似同步的多线程,在等待同一个线程锁的几个线程有点类似协同。

# 基本语法
方法 描述
coroutine.create() 创建 coroutine,返回 coroutine,参数是一个函数,当和 resume 配合使用的时候就唤醒函数调用
coroutine.resume() 重启 coroutine,和 create 配合使用
coroutine.yield() 挂起 coroutine,将 coroutine 设置为挂起状态,这个和 resume 配合使用能有很多有用的效果
coroutine.status() 查看 coroutine 的状态:
注:coroutine 的状态有三种:dead、supspended,runinng
coroutine.wrap() 创建 coroutine,返回一个函数,一旦你调用这个函数,就进去 coroutine,和 create 功能重复
coroutine.running() 返回正在跑的 coroutine,一个 coroutine 就是一个线程,当使用 running 的时候,就是返回一个 coroutine 的线程号
> co = coroutine.create(function(i) print(i) end)
> coroutine.resume(co, 1)
1
true
> print(coroutine.status(co))
dead
1
2
3
4
5
6
> co = coroutine.wrap(
>>     function(i)
>>         print(i);
>>     end
>> )
>
> co(1)
1
1
2
3
4
5
6
7
8
> co2 = coroutine.create(
>>     function()
>>         for i=1,10 do
>>             print(i)
>>             if i == 3 then
>>                 print(coroutine.status(co2))  --running
>>                 print(coroutine.running()) --thread:XXXXXX
>>             end
>>             coroutine.yield()
>>         end
>>     end
>> )
>
> coroutine.resume(co2)
1
true
> coroutine.resume(co2)
2
true
> coroutine.resume(co2)
3
running
thread: 0x7fbf7be04ff8	false
true
> print(coroutine.status(co2))
suspended
> print(coroutine.running())
thread: 0x7fbf7c800008	true
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

coroutine.running() 可以看出,coroutine 在底层实现就是一个线程。当 create 一个 coroutine 的时候就是在新线程中注册事件。当使用 resume 触发事件的时候,create 的 coroutine 函数就被执行了,当遇到 yield 的时候就代表挂起当前线程,等候再次 resume 触发事件。

resume 和 yield 的配合强大之处在于,resume 处于主程中,它将外部状态(数据)传入到协同程序内部;而 yield 则将内部的状态(数据)返回到主程序中。

# 生产者和消费者问题

ocal newProductor

function productor()
     local i = 0
     while true do
          i = i + 1
          send(i)     -- 将生产的物品发送给消费者
     end
end

function consumer()
     while true do
          local i = receive()     -- 从生产者那里得到物品
          print(i)
     end
end

function receive()
     local status, value = coroutine.resume(newProductor)
     return value
end

function send(x)
     coroutine.yield(x)     -- x表示需要发送的值,值返回以后,就挂起该协同程序
end

-- 启动程序
newProductor = coroutine.create(productor)
consumer()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

# 控制流

# Lua 条件

控制结构的条件表达式结果可以是任何值,Lua 认为 false 和 nil 为假,true 和 非 nil 为真。要注意的是 Lua 中 0 为 true。

> if (0)
>> then
>> print("0 为 true")
>> end
0true
1
2
3
4
5

# Lua 循环

  • while
  • for
  • repeat...until
  • 循环嵌套(while do...end; for...do...end; repeat...until)

# 对象和方法

Lua 中的表不仅在某种意义上是一种对象。像对象一样,表也有状态(成员变量);也有与对象的值独立的本性,特别是拥有两个不同值的对象(table)代表不同的对象;一个对象在不同的时候也可以有不同的值,但它始终是一个对象;与对象类似,表的生命周期与其由什么创建、在哪创建没有关系。对象有他们的成员函数。

> Account = {balance = 0}                                                 > function Account.withdraw(v)                                                  Account.balance = Account.balance - v                                           end
> Account.withdraw(100.00)
> Account.balance
-100.0
1
2
3
4

#

我们知道,对象由属性和方法组成。LUA 中最基本的结构是 table,所以需要用 table 来描述对象的属性。

lua 中的 function 可以用来表示方法。那么 LUA 中的类可以通过 table + function 模拟出来。

> -- 1. 元类
> Rectangle = {area = 0, length = 0, breadth = 0}
>
> -- 2. 基本类的方法 new
> function Rectangle:new (o,length,breadth)
>>   o = o or {}
>>   setmetatable(o, self) -- 关键函数
>>   self.__index = self
>>   self.length = length or 0
>>   self.breadth = breadth or 0
>>   self.area = length*breadth;
>>   return o
>> end
>
> -- 3. 基本类的方法 printArea
> function Rectangle:printArea ()
>>   print("矩形面积为 ",self.area)
>> end
> Rectangle
table: 0x7fe732c0c460
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
> -- 创建对象
> r = Rectangle:new(nil, 10, 20)
> -- 访问属性
> r.length
10
> -- 访问成员函数
> r:printArea()
矩形面积为 	200
>
1
2
3
4
5
6
7
8
9

# 继承

要实现继承,则需要在声明新变量时直接居于父类实例化一个值,然后再声明 new 函数:

-- 元类
> Shape = { area = 0}
-- 基础类方法 new
> function Shape:new(o, side)
>> o = o or {}
>> setmetatable(o, self) -- 关键函数
>> self.__index = self
>> side = side or 0
>> self.area = side * side
>> return o
-- 基础类方法 printArea
> function Shape:printArea()
>> print("面积为", self.area)
>> end
-- 创建对象
> myshape = Shape:new(nil, 10)
> myshape:printArea()
面积为	100


> Rectangle = Shape:new()
> -- 派生类方法 new
> function Rectangle:new (o,length,breadth)
>>   o = o or Shape:new(o)
>>   setmetatable(o, self)
>>   self.__index = self
>>   self.area = length * breadth
>>   return o
>> end
>
> -- 派生类方法 printArea,函数重写
> function Rectangle:printArea ()
>>   print("矩形面积为 ",self.area)
>> end
>
> -- 创建对象
> myrectangle = Rectangle:new(nil,10,20)
> myrectangle:printArea()
矩形面积为 	200
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39

# 模块和包

模块类似一个封装库,从 Lua 5.1 开始,Lua 假如了标准的模块管理机制,可以把一些公用的代码放在一个文件里,以 API 接口的形式在其他地方调用,有利于代码的重用和降低代码耦合度。

Lua 的模块是由变量、函数等已知元素组成的 table,因此创建一个模块很简单,就是创建一个 table,然后把需要导出的常量、函数放入其中,最后返回这个 table 就行。

-- 定义一个名为 module 的模块
module = {};

-- 定义一个常量
module.constant = "这是一个常量"

-- 定义一个函数
function module.func1()
  io.write("这是一个公有函数!\n")
end

local function func2()
  print("这是一个私有函数!")
end

function module.func3()
  func2()
end

return module;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

使用:

local m = require("./demo01.lua")
print(m.constant)
m.func3()
1
2
3
# 加载机制

对于自定义的模块,模块文件不是放到哪个文件目录都行,函数 require 有它自己的文件路径加载策略,它会尝试从 Lua 文件或 C 程序库中加载模块。

require 用于搜索 Lua 文件的路径是存放在全局变量 package.path 中,当 Lua 启动后,会以环境变量 LUA_PATH 的值来初始化这个环境变量。如果没有找到环境变量,则使用一个编译时定义的默认路径来初始化。

当然,如果没有 LUA_PATH 这个变量,也可以自定义设置,在当前用户根目录下打开 .profile 文件(没有则创建,打开 .bashrc 文件也可以),例如把 “~/lua/” 路径加入 LUA_PATH 环境变量里:

# LUA_PATH
export LUA_PATH = "~/lua/?.lua"
1
2
# C 包(模块)

Lua 和 C 是很容易结合的,使用 C 为 lua 写包。 与 Lua 中写包不同,C 包在使用前必须首先加载并连接,在大多数系统中最容易的实现方式是通过动态链接库机制。

Lua 在一个叫 loadlib 的函数内提供了所有的动态连接的功能。这个函数有两个参数:库的绝对路径和初始化函数。

local path = "/usr/local/lua/lib/libluasocket.so"
local f = loadlib(path, "luaopen_socket")
1
2

loadlib 函数加载指定的库并且连接到 Lua,然后它并不打开库(也就是说没有调用初始化函数),反之他返回初始化函数作为 Lua 的一个函数,这样我们就可以直接在 Lua 中调用它。

local path = "/usr/local/lua/lib/libluasocket.so"
-- 或者 path = "C:\\windows\\luasocket.dll",这是 Window 平台下
local f = assert(loadlib(path, "luaopen_socket"))
f()  -- 真正打开库
1
2
3
4

# 其他特性

# Lua 变量

# Lua 断点调试

# vscode

在 vscode 中搜索 -> lua debug,按照插件。

然后在项目根目录下新建 .vscode 文件夹,新建 launch.json 文件,写入以下配置:

{
  "name": "launch lua process",
  "type": "lua",
  "request": "launch",
  "stopOnEntry": true,
  "program": "${workspaceFolder}/examples/lua/demo01.lua",
  "path": "${workspaceFolder}/?.lua",
  "cpath": "${workspaceFolder}/?.dll",
  "arg": []
}
1
2
3
4
5
6
7
8
9
10
# Lua Debug

模块功能

  • vscode-debugger-client.exe 代理客户端。在 launch 模式中,vscode-debug-client.exe 会用 vscode-debug.dll 创建一个调试器进行调试。在 attach 模式中,vscode-debug-client.exe 会连接一个远程的调试器(也是由 vscode-debug.dll 创建的),vscode-debug-client.exe 只负责在 vscode-debug.dll 和 vscode 之间转发信息。
  • vscode-debug.dll 调试器的核心模块。你可以在你的程序中加载 vscode-debug.dll 并创建调试器,这样 vscode 就可以通过 attach 模式进行调试。
  • luacore.dll lua 核心模块。如果你的程序定制了 lua,你可以替换掉它。
  1. launch 模式,等同于使用 lua.exe 执行的入口文件
    1. program,lua.exe 执行的入口文件
    2. cwd,lua.exe 的当前目录
    3. stopOnEntry,开始调试时是否先暂停
    4. lua带来了,指定 lua dll 的路径,如有不填会加载 luacore.dll
    5. path,用于初始化 package.path
    6. cpath,用于初始化 package。cpath
    7. arg,lua.exe 的命令行参数,用于初始化 arg
    8. console,lua 的标准输出的编码,可选择 utf8、ansi、none。为 none 时不会重定向标准输出到 vscode
    9. soureMaps 源码定向

# 参考资料

Last Updated: 6/17/2020, 3:32:05 PM