编程语言Lua(一):入门介绍、学习资料、项目管理与调试方法
作者: 李佶澳 转载请保留:原文地址 发布时间:2018/10/22 17:10:00
说明
最近在学习API网关Kong(六):Kong数据平面的实现分析,Kong的数据平面使用的语言是Lua,需要学习一下Lua这门语言的使用方法、项目组织方式以及配套的开发调试工具等。
Lua简介
Lua网站上说这门语言已经有25年
的历史了,维基百科上的Lua (programming language)页面上给出了具体的时间:
Lua was created in 1993 by Roberto Ierusalimschy, Luiz Henrique de Figueiredo, and Waldemar Celes, members of the Computer Graphics Technology Group (Tecgraf) at the Pontifical Catholic University of Rio de Janeiro, in Brazil.
有点吃惊,Lua是一门挺古老的语言,而且是巴西的大学开发的,顺便查了一下其它几种常见语言的诞生时间:
C,1969年至1973年间,贝尔实验室的丹尼斯·里奇与肯·汤普逊,为了移植与开发UNIX操作系统,以B语言为基础设计、开发出来。
C++,1979年,Bjarne Stroustrup(比雅尼·斯特劳斯特鲁普)决定为C语言增强一些类似Simula的特点,1985年10月出现了第一个商业化发布。
Python,1989年的圣诞节期间,吉多·范罗苏姆为了打发时间
,在荷兰的阿姆斯特丹的开发的脚本解释程序,第一版发布于1991年。(他得有多闲?…)
Java,1990年Sun公司的一个内部项目研究的技术,最开始的名称是Oak
,1993年夏天能够使用,1994年决定将该技术用于互联网,1994年Java 1.0a提供下载。1996年Sun公司成立Java业务集团,专门开发Java技术,因为Oak商标已经被注册,从Oak改名为Java。
Lua,1977~1992年,巴西在计算机软件和硬件上设置了贸易壁垒,位于巴西的Tecgraf公司不能从国外购买软件,于是从零开始开发基础软件,lua是葡萄牙语,意思是月亮。
Brainfuck,1993年,Urban Müller创建的奇葩语言,一共只有8个命令和1个结构指针,它的代码是这个样子的:
++++++++[>++++[>++>+++>+++>+<<<<-]>+>+>->>+[<]<-]>>.>---.+++++++..+++.>>.<-.<.+++.------.--------.>>+.>++.
你没看错,上面的++++等就是用Brainfuck语言写的代码。
Lua的学习资料
Programming in Lua,Lua的设计者Roberto Ierusalimschy
写的Lua语言教程,2016年出版了第4版,覆盖了Lua 5.3。百度云下载地址:Download:Programming in Lua, 4th Edition。
Lua 5.3 Reference Manual,Lua语言的概念、语法定义和标准库函数列表。
Lua Directory,收集了大量的Lua资料。
Lua wiki,Lua的wiki。
另外还有邮件列表、IRC、StackOverFlow主页等,这里不列出了,在Lua Getting Started中可以找到。
Lua的第三方库和配套工具
Lua的标准库很小,只有11个文件,Lua Addons中收录了大量拓展Lua功能的资源:
Lua可以调用的Library,包括用其它语言实现的、提供了Lua接口的Library
此外还有:
awesome-lua,一组精选的、高质量的Lua Package。
LuaRocks,一个Lua Package管理工具,收录了大量以rocks
方式发布的Lua Package。
luadist,一个跨平台的Lua Package管理工具。
zerobrane,一个纯粹的Lua IDE 。(装起来看了一下,功能很少,不支持安装插件,感觉没有Idea+EmmyLua好用)
如果使用Idea,可以用EmmyLua插件。
Lua的安装
在windows上可以使用luadist安装,在Linux和Mac上,可以直接使用对应的包管理工具安装:
yum install -y lua # for centos
brew install lua # for mac
也可以自己编译安装,lua download:
curl -R -O http://www.lua.org/ftp/lua-5.3.5.tar.gz
tar zxf lua-5.3.5.tar.gz
cd lua-5.3.5
make linux test # for linux
make macosx test # for mac
从OpenResty最佳实践 的lua介绍中了解到Lua和 LuaJIT的关系和区别:
LuaJIT 就是一个为了再榨出一些速度的尝试,它利用即时编译(Just-in Time)技术把 Lua 代码编译成本地机器码后交由 CPU 直接执行。LuaJIT 2 的测评报告表明,在数值运算、循环与函数调用、协程切换、字符串操作等许多方面它的加速效果都很显著。
对性能要求更高,可以使用luaJIT,下面是mac上的安装方法:
brew install luajit
后面用到lua
命令的地方,用luajit
命令替代,执行效率会更高。
Lua代码的运行
安装lua之后,直接执行lua
,进入lua的命令行后,可以直接输入代码,回车执行:
➜ ~ lua
Lua 5.3.5 Copyright (C) 1994-2018 Lua.org, PUC-Rio
> print("hello world!")
hello world!
>
也可以将代码写到.lua文件中,lua中注释符号是--
:
$ cat 01-hello-world.lua
#! /usr/bin/env lua
--
-- 01-hello-world.lua
-- Copyright (C) 2018 lijiaocn <lijiaocn@foxmail.com>
--
-- Distributed under terms of the GPL license.
--
print("Hello World")
用lua命令加载执行.lua文件:
$ lua 01-hello-world.lua
Hello World
Lua的语法
这块的内容比较多,单独开一篇笔记记录。
建议直接学习Programming in Lua, 4th Edition。
百度网盘下载地址:Download:Programming in Lua, 4th Edition
Lua代码组织方式
Lua5.1版本开始,定义了Module和Package,Module
是可复用的Lua代码文件,Package
是一组平铺的或有树状包含关系的Module。
Lua Module
Module是一个返回一个Table变量的.lua文件,使用函数require
引入:
-- 引入math.lua
local m = require "math"
print(m.sin(3.14))
模块名称也可以用变量,动态加载模块:
local cmd = require("kong.cmd." .. cmd_name)
Lua的标准模块
会被用下面的方式加载默认加载:
math = require "math"
string = require "string"
...
因此标准模块可以直接用模块名引用:
math.sin(3.14)
Lua特别小巧,标准库数量只有一下几个,可以到Lua的手册中查看标准库中函数和用法:
coroutine.lua
debug.lua
global.lua
io.lua
math.lua
os.lua
package.lua
stdfuncs.lua
stdlibrary.doclua string.lua
table.lua
除了直接用操作.
调用modules中的函数,还可以将函数复制给其它变量,通过其它变量调用:
local m = require "mod"
local f = m.foo
f()
还可以只导入module中的一个函数:
local f = require "mod".foo
require函数加载Module的过程
require函数的内部流程如下:
1 require首先检查package.loaded
,查看目标module是否已经加载过,如果加载过,直接返回上次加载得到的table;
2 如果没有,用package.path
中的目录模版中查找module对应的lua文件,找到之后用loadfile
函数加载,生成一个loader变量;
3 如果package.path
指定的目录模版中没有对应的lua文件,require转为到package.cpath
指定的目录模板中查找与module同名的C Library
。找到同名的C library之后,使用函数package.loadlib
加载,生成一个loader变量。这个loader变量指向的是C library中的luaopen_modname
函数;
4 调用loader变量指向的函数,传入两个参数:module名称和包含loader的文件名;
5 loader执行返回的数值被存入package.loaded
,如果没有返回数值,package.loaded
保持原状。
如果要强制重新加载Module,可以将package.loaded
中对应Module的记录清除,清除后,遇到require加载时,会重新加载:
package.loaded.modname = nil
require函数没有传入参数,这是为了防止同一个module被使用不同的参数多次加载。如果要为module设置参数,可以在module中创建一个设置函数的方式实现:
local mod = require "mod"
mod.init(0, 0)
需要注意的是如果引入的Module是一个目录,加载目录的下的init.lua
文件,这是由package.path
的值决定的:
> print("%s",package.path)
%s ./?.lua;/usr/local/share/lua/5.1/?.lua;/usr/local/share/lua/5.1/?/init.lua;/usr/local/lib/lua/5.1/?.lua;/usr/local/lib/lua/5.1/?/init.lua
require函数查找Module的路径
另外需要特别注意的是,和其它语言非常不同,Lua加载模块时不是按照指定的顺序到几个目录中查找,而是使用路径匹配的方式查找 (这样做的根源是ISO C中没有目录的概念,ISO C是lua的抽象运行平台,abstract platform)。
package.path
和package.cpath
中,记录的不是具体的目录,而是目录的模版,多个模版用”;”分隔,例如:
?;?.lua;c:\windows\?;/usr/local/lua/?/?.lua
require在查找Module时,将?
替换为Module的名字,对于require "sql"
,上面的模版对应的文件分别是:
sql
sql.lua
c:\windows\sql
/usr/local/luc/sql/sql.lua
package.path
是查找用Lua写的Module的路径,值依次
来自于(如果第一个没有值就找第二个):
环境变量: LUA_PATH_5_3 (或者其它版本)
环境变量: LUA_PATH
编译在Lua命令中的默认路径
package.cpath
是查找C写的链接库的路径,值依次
来自于(如果第一个没有值就找第二个):
环境变量:LUA_CPATH_5_3
环境变量:LUA_CPATH
编译在Lua命令中的默认路径
Lua5.2开始,可以加上-E
参数强制使用编译在Lua命令中的默认路径。
可以用package.searchpath()
函数按照同样的加载规则,查找Module:
> path = ".\\?.dll;C:\\Program Files\\Lua502\\dll\\?.dll"
> print(package.searchpath("X", path))
nil
no file '.\X.dll'
no file 'C:\Program Files\Lua502\dll\X.dll'
Module的版本管理
module名称中可以带有-版本号
后缀,例如:
m1 = require "mod-v1"
这主要是针对C library中,对于用Lua写的Module,如果有多个版本直接用不同的文件名就可以了,但是对于C library就不行了。
加载C library的时候,Module的名称不仅对应到C library的文件名,还对应到其中的函数名,仅仅修改C library的文件名是不行的,但是如果函数名中带版本号,显然也是非常不好的。
因此Lua规定对于命名为模块名-版本号
样式Module,从C library查找名称格式为luaopen_XXX
的函数的时候,忽略Module名称中的版本后缀。
对于mod-v1,在对应的C Library中查找的函数是luaopen_mod
,这样每个版本的C library中都可以用同样的函数名。
package
module支持层级,多个module可以呈树形结构组织,层级关系用.
标记,例如:
a.b
a.b.c
package就是一个module目录树,是比module更高一层的概念,Lua的代码发布以package为单位。
Lua的项目管理工具
依赖管理与发布打包:luarocks
LuaRocks既可以管理lua项目依赖的package,也可以将lua项目发布、安装,以及推送到luarocks中共享,类似于nodejs的npm,可以参考luarocks documents。
luarocks支持多个版本的lua,可以用--lua-dir
指定另一个版本的lua,例如在lua@5.1中安装say:
luarocks --lua-dir=/usr/local/opt/lua@5.1 install say
luarocks初始化项目
init
子命令初始化一个lua项目:
luarocks init
生成以下文件:
$ tree .
.
├── lua
├── lua_modules
│ └── lib
│ └── luarocks
│ └── rocks-5.3
├── luarocks
└── luarocks-dev-1.rockspec
其中lua
和luarocks
是两个可执行脚本,使用项目中的./lua
和./luarocks
,可以将依赖的文件安装到项目本地目录中,不干扰系统上的Lua安装目录。
luarocks-dev-1.rockspec是自动生成的rockspec文件,用途在后面讲。
luarocks安装依赖
使用初始化时,在项目本地生成的./luarocks
执行search
子命令,查找可以安装的依赖以及版本,例如查找名为mobdebug
的Package:
$ ./luarocks search mobdebug
mobdebug - Search results for Lua 5.1:
======================================
Rockspecs and source rocks:
---------------------------
mobdebug
0.70-1 (rockspec) - https://luarocks.org
0.64-1 (rockspec) - https://luarocks.org
0.63-1 (rockspec) - https://luarocks.org
0.55-1 (rockspec) - https://luarocks.org
0.55-1 (src) - https://luarocks.org
0.51-1 (rockspec) - https://luarocks.org
0.51-1 (src) - https://luarocks.org
0.50-1 (rockspec) - https://luarocks.org
0.50-1 (src) - https://luarocks.org
0.49-1 (rockspec) - https://luarocks.org
0.49-1 (src) - https://luarocks.org
0.48-1 (rockspec) - https://luarocks.org
0.48-1 (src) - https://luarocks.org
用install
子命令安装指定的版本:
./luarocks install mobdebug 0.70-1
注意rockspec文件不会自动更新,好像没有自动更新项目的rockspec文件的命令 2018-10-27 16:10:36
用list
子命令可以查看已经安装的Package:
$ ./luarocks list
Rocks installed for Lua 5.1
---------------------------
luarocks
3.0.3-1 (installed) - /usr/local/lib/luarocks/rocks-5.1
luasocket
3.0rc1-2 (installed) - /Users/lijiao/Work-Finup/workspace/studys/study-Lua/first-demo/lua_modules/lib/luarocks/rocks-5.1
mobdebug
0.70-1 (installed) - /Users/lijiao/Work-Finup/workspace/studys/study-Lua/first-demo/lua_modules/lib/luarocks/rocks-5.1
依赖的package被安装在lua_modules目录中:
$ ls lua_modules/share/lua/5.1
ltn12.lua mime.lua mobdebug.lua socket socket.lua
lua_modules目录中将lua package按照Lua的版本分开了。 在安装了EmmyLua插件的IntelliJ Idea中,代码跳转的时候,会从lua_modules中寻找代码,优先使用最新的版本的代码。
记录依赖的rockspec文件
rockspec文件不仅记录Lua项目依赖的package,还包括了Lua项目打包发布方式,以kong的rockspec文件为例:
package = "kong"
version = "0.14.1-0"
supported_platforms = {"linux", "macosx"}
source = {
url = "git://github.com/Kong/kong",
tag = "0.14.1"
}
description = {
summary = "Kong is a scalable and customizable API Management Layer built on top of Nginx.",
homepage = "http://getkong.org",
license = "MIT"
}
dependencies = {
"inspect == 3.1.1",
"luasec == 0.6",
"luasocket == 3.0-rc1",
...省略...
}
build = {
type = "builtin",
modules = {
["kong"] = "kong/init.lua",
["kong.meta"] = "kong/meta.lua",
["kong.cache"] = "kong/cache.lua",
...省略...
}
}
dependencies
中记录了项目依赖,build
中记录项目代码的发布方式,即lua文件与module的对应。
还没有找到自动更新rockspec文件的方法,如果需要手工在rockspec中输入依赖的package,就太麻烦了。最好有一个自动将依赖的package更新到rockspec文件中的方法。2018-10-27 16:22:44
项目的安装
build
子命令安装当前目录中的lua代码,自动下载项目的rockspec文件中指定的lua package:
$ ./luarocks build
Missing dependencies for kong 0.14.1-0:
inspect 3.1.1 (not installed)
luasec 0.6 (not installed)
luasocket 3.0-rc1 (not installed)
penlight 1.5.4 (not installed)
lua-resty-http 0.12 (not installed)
lua-resty-jit-uuid 0.0.7 (not instal
...
kong 0.14.1-0 depends on inspect 3.1.1 (not installed)
Installing https://luarocks.org/inspect-3.1.1-0.src.rock
...
如果项目中的./luarocks
,将会安装到项目本地的lua_modules中,如果使用系统的luarocks
命令,将会安装到系统的目录中。
也可以用make
子命令,make子命令只安装项目,不自动下载依赖。
IntelliJ Idea
IntelliJ Idea中有两个Lua插件,一个是Lua
,一个是EmmyLua
,实测EmmyLua对代码跳转的支持更好一些,国人开发的,也比较活跃。
可以先用luarocks初始化一个lua项目,然后在Idea中直接将项目目录导入:
$ luarocks init --lua-dir=/usr/local/opt/lua@5.3
$ tree .
.
├── lua
├── lua_modules
│ └── lib
│ └── luarocks
│ └── rocks-5.3
├── luarocks
└── luarocks-dev-1.rockspec
EmmyLua插件在执行代码跳转时,先从项目源代码中查找,然后从lua_modules/lib/luarocks
中最新
版本的Lua目录中查找,最后从SDK的ClassPath/SourcePath目录中查找。
不在项目目录内的Lua代码,要正确跳转过去,需要它们所在的目录将添加到Idea的SDK中:
File -> Project Structure->SDKs
在SDK窗口中选择当前使用的SDK,在它的ClassPath/SourcePath目录中添加项目目录之外的代码目录
通常至少要把本地安装的Lua的模块目录添加到SDK的ClassPath/SourcePath中。
例如在Mac上,lua代码被安装在:
/usr/local/share/lua/5.1/`
需要把这个目录添加到SDK的ClassPath/SourcePath。
在Mac上,为Idea的SDK添加代码目录时,可能看不到/usr目录,可以做一个符号连接,将符号连接添加到SDK中,例如:
ln -s /usr/local/share/lua/5.1 ~/Bin/lua-5.1-sdk
如果项目用到了OpenResty,还需要将OpenResty的模块添加到SDK的ClassPath/SourcePath中。
Mac上用brew安装的OpenResty的package目录是:
/usr/local/Cellar/openresty/1.13.6.2/lualib/
可能同样需要为它做一个符号连接,将符号连接加到SDK中:
ln -s /usr/local/Cellar/openresty/1.13.6.2/lualib ~/Bin/openresty-1.13.6.2-lualib
Lua代码的调试方法
依赖管理可以使用前面提到的luarocks,Lua项目开发可以用Intelli Idea + EmmyLua插件
。
Lua代码的运行前面也提到过,可以直接用lua
或者luajit
等lua代码解释器加载运行。
剩下的一个比较关键的问题就是lua代码的调试方法了。
在IntelliJ IDEA中调试Lua代码
在IntelliJ IDEA中运行Lua代码比较简单。在Run
菜单中选择Run,会弹出一个运行配置对话框,在0 Edit Configuration
中,设置Lua代码的运行环境。
最主要的加载执行lua代码的命令的完整路径:
Program: /usr/local/bin/lua-5.1 执行lua代码的lua命令
可以用快捷键ctrl+alt+r
直接调出运行窗口。
IDEA中使用Remote Debugger
在Idea中调试Lua代码,用快捷键ctrl+alt+d
直接调出Debug窗口,在0 Edit Configuration
中选择Debugger:
Remote Debugger(MobDebug)
Apply之后选择Debug运行,这时候Idea窗口下方会弹出Debugger对话框。
alt+command+r
开始执行到下一个断点,F8
单步执行不进入函数,F7
单步执行进入函数,command+r
重新执行。
可能需要引用mobdebug模块
EmmyLua的文档中说设置Remote Debug远程调试,需要在lua代码中引入mobdebug模块:
require("mobdebug").start()
-- 或者
require("mobdebug").start("host-ip", port) --默认值为 "localhost", 8172
不过我在Idea中试验了下,不安装mobdebug也可以进行调试,或许还有我不知道的与mobdebug有关的内容,这里先记录下这件事。2018-10-27 17:03:13
可以用luarocks安装mobdebug,先到项目目录中执行luarock init
完成初始化设置:
$ luarocks init
Initializing project first-demo ...
然后用项目本地生成的./luarocks
执行search
子命令,查看可以安装的版本:
$ ./luarocks search mobdebug
mobdebug - Search results for Lua 5.1:
======================================
Rockspecs and source rocks:
---------------------------
mobdebug
0.70-1 (rockspec) - https://luarocks.org
0.64-1 (rockspec) - https://luarocks.org
0.63-1 (rockspec) - https://luarocks.org
0.55-1 (rockspec) - https://luarocks.org
0.55-1 (src) - https://luarocks.org
0.51-1 (rockspec) - https://luarocks.org
0.51-1 (src) - https://luarocks.org
0.50-1 (rockspec) - https://luarocks.org
0.50-1 (src) - https://luarocks.org
0.49-1 (rockspec) - https://luarocks.org
0.49-1 (src) - https://luarocks.org
0.48-1 (rockspec) - https://luarocks.org
0.48-1 (src) - https://luarocks.org
最后用install
子命令安装指定的版本:
./luarocks install mobdebug 0.70-1