lua 高级特性

这一篇来记录下 lua 的某些高级特性,以便在实际应用中得心应手。

模块和包

为了方便代码复用和扩展,可以使用 table 实现模块 module,在模块中封装通用代码。把同类型的函数放在一个文件中,然后在其它脚本中调用。

module = {}

module.version = "V0.1"
module.author = "litreily"

function module.func1 ()
    function-body
end

function module.func2 ()
    function-body
end

return module

然后在其它文件通过 require 导入。

require ("module")
-- or
require "module"

print(module.version)
module.func1()

-- or
local m = require ("module")

print(m.version)
m.func1()

举例说明,为了复用一些通用方法比如文件读写,异常处理等功能,可以将其放在一起。

-- utils functions
utils = {}

function utils.errorhandler (err)
    print("ERROR: ", err)
end

function utils.xpcall (statements)
    return xpcall (function () return statements end, utils.errorhandler)
end

-- cat file
function utils.catfile (file)
    local f = assert(io.open(file, "r"))
    local content = f:read("*all")
    f:close()
    return content
end

-- @mode: w a r+ w+ a+ b
-- @content: strings
function utils.savefile (file, mode, content)
    local f = assert(io.open(file, mode))
    f:write(content)
    f:close()
end

return utils

上面对文件读写函数以及异常处理函数进行了封装,在其它文件中就可以通过 require 调用了。

require "utils"

-- cat demo.lua and write into test.txt
utils.savefile("test.txt", "w", utils.catfile("demo.lua"))

xpcall(function () print(utils.catfile("utils.lua")) end, utils.errorhandler)
print(utils.xpcall(os.execute("ls /")))

闭包

function 变量

首先要知道的是,在 Lua 中,function 本身就是基本的数据类型之一,也可以和普通的变量一样定义和赋值。

function foo(x)
    print(x)
end

foo = function (x) print(x) end

是等价的。因此,函数也可以当做普通变量一样被返回、被赋值。下面再来看闭包。

闭包概念

闭包 (Closure) 的概念并不是 Lua 特有的,许多其它语言也有,比如java, js等。在 Lua 中,闭包表现为匿名函数或者函数嵌套,是由一个函数和一个非局部变量(upvalue)组成的。看个例子就清楚了。

function foo(x)
    local function func1()
        print(x)
    end
    return func1
end

func = foo("hello")
func()

内部函数也可以不定义函数名,直接return。

function foo(x)
    return function ()
        print(x)
    end
end

func = foo("hello")
func()

以上的func就是创建的闭包。通过闭包函数,可以实现多个内部函数之间的资源共享。

function Produce(n)
   local function func1()
      print(n)
   end
   local function func2()
      n = n + 1
   end
   return func1, func2
end

f1, f2 = Produce(2020)
f1() -- /* print 2020 */

f2()
f1() -- /* print 2021 */

f2()
f1() -- /* print 2022 */

上面两个闭包 f1, f2 通过 n 实现资源共享。

闭包应用

  1. 闭包作为高阶函数的参数
  2. 回调函数
  3. 创建安全的运行环境,类似沙盒
  4. 迭代器

迭代器

Lua 中的迭代器就可以通过闭包实现的。以下代码就是迭代函数 ipairs 的实现方式。

function iter (a, i)
    i = i + 1
    local v = a[i]
    if v then
       return i, v
    end
end

function ipairs (a)
    return iter, a, 0
end

ipairs 的应用如下:

data = {"hello", "world", "!"}
for i, v in ipairs(data)
do
    print(i, v)
end

面向对象

Lua 是使用C语言编写的,但其不仅支持面向过程,同时也支持面向对象。而面向对象也是通过万能的 table 实现的。

构造函数

Animal = {name = "", height = 0, weight = 0}
-- The constructor function
function Animal:new (object, name, height, weight)
    object = object or {}
    setmetatable(object, self)
    self.__index = self
    self.name = name or ""
    self.height = height or 0
    self.weight = weight or 0
    return object
end

-- The member function
function Animal:printHeight()
    print("the height of " .. self.name .. " is " .. self.height)
end

创建对象

a = Animal:new(nil, "cat", 0.2, 15)

访问属性

print(a.name)

访问成员函数

a:printHeight()

继承

在Animal类的基础上继承,派生出人类 human.

Human = Animal:new("human", 0, 0)
function Human:new (object, name, height, weight, sex)
    object = object or Animal:new("name", height, weight)
    setmetatable(object, self)
    self.__index = self
    self.sex = sex or "male"
    return object
end

function Human:printSex()
    print("the sex of " .. self.name .. " is " .. self.sex)
end

boy = Hunman:new("mike", 1.7, 65, "male")
print(boy.name)
boy:printSex()

错误处理

assert and error

assert 是很多语言都会用到的处理函数,在Python、C++等大部分高级语言中都会集成。通过它可以预先判断是否会有错误产生,如果有则直接处理,而不用等待真正执行到错误处才报错。比如下面的类型检查,可以规避输入参数不合法导致的错误问题,提前退出。

local function add(a,b)
   assert(type(a) == "number", "a is not a number")
   assert(type(b) == "number", "b is not a number")
   return a+b
end
add(10)

执行会有错误提示

lua: test.lua:3: b is not a number
stack traceback:
    [C]: in function 'assert'
    test.lua:3: in local 'add'
    test.lua:6: in main chunk
    [C]: in ?

error 函数可以用来打印log, 而且可以指定level,控制输出包含哪些信息。

error (messag [, level])
  • level=1 (default) : 输出调用error位置(文件,行号)
  • level=2 : 指出调用 error 的函数
  • level=0 : 不添加位置信息

pcall and xpcall

pcall (protected call) 有点类似 Python中的 try...catch, 尝试捕获错误,不过其 接收的是一个函数,而不是表达式 .

if pcall(function_name, ….) then
-- no error
else
-- some error
end

举例如下:

> =pcall(function(i) print(i) end, 10)
10
true

> =pcall(function(i) print(i) error('error message') end, 10)
10
false        stdin:1: error message

当然第二个例子中error是人为故意添加的,倒不是说真的有错误。

理论上pcall 可以捕获函数执行过程的任意错误,但是它主要是返回错误的位置,把部分调用栈信息丢失了。此时就是 xpcall 的出场时刻了。

xpcall 第二个参数是一个错误处理函数 errorhandler, 可以将发生错误时的栈信息打印出来。本文头部讲述模块的时候已经用到了。

function foo (n)
   n = n/nil
end

function errorhandler( err )
   print( "ERROR:", err )
end

status = xpcall(foo, errorhandler, 10)
print(status)

执行后输出以下错误。

ERROR:  stdin:1: attempt to perform arithmetic on local 'n' (a nil value)

C 与 Lua 相互调用

在C与Lua之间相互调用,需要安装有lua库,通常在编译安装时就会生成所需的头文件和库文件

  • lua.h
  • lualib.h
  • lauxlib.h
  • liblua.so or liblua.a

C 调用 Lua 脚本

如果单单只是要执行 Lua 脚本,非常简单。直接在C代码中通过 system 调用即可。

system("lua demo.lua");

但是对于嵌入式设备而言,可能没有将lua编译到设备中,此时可以通过lua的库函数 luaL_dofile 执行脚本。

#include  <stdio.h>
#include  <lua.h>
#include  <lualib.h>
#include  <lauxlib.h>

int main(int argc, char *argv[])
{
    lua_State* L;

    L = luaL_newstate(); /* 创建lua状态机 */
    luaL_openlibs(L); /* 打开Lua状态机中所有Lua标准库 */
    luaL_dofile(L, "demo.lua"); /*加载lua脚本*/
    lua_close(L); /*清除Lua*/
    return 0;
}

C 调用 Lua 函数

如果要在C代码中调用Lua脚本中定义的函数,以最简单的加法为例,在lua中写一个 add 函数,然后在 C 代码中调用。

-- add.lua
function add(x,y)
    return x+y
end

在 C 代码中调用 lua 相关库函数。

#include  <stdio.h>
#include  <lua.h>
#include  <lualib.h>
#include  <lauxlib.h>

lua_State* L;

int luaadd(int x, int y)
{
    int sum;

    lua_getglobal(L,"add");/*函数名*/
    lua_pushnumber(L, x); /*参数入栈*/
    lua_pushnumber(L, y); /*参数入栈*/
    lua_call(L, 2, 1); /*开始调用函数,有2个参数,1个返回值*/
    sum = (int)lua_tonumber(L, -1); /*取出返回值*/
    lua_pop(L,1); /*清除返回值的栈*/

    return sum;
}

int main(int argc, char *argv[])
{
    int sum;

    L = luaL_newstate(); /* 创建lua状态机 */
    luaL_openlibs(L); /* 打开Lua状态机中所有Lua标准库 */
    luaL_dofile(L, "add.lua"); /*加载lua脚本*/

    sum = luaadd(1000, 24); /*调用C函数,这个里面会调用lua函数*/
    printf("The sum is %d \n",sum);

    lua_close(L); /*清除Lua*/
    return 0;
}

注意包含头文件,并确保头文件所在目录包含在环境变量 $PATH 中,否则编译会出错。

下面进行编译,编译的时候一定要链接好对应的库,否则同样会编译出错。

  • liblua.a (对应安装lua时安装的 liblua.a 静态库)。
  • libm.so
  • libdl.so
$ gcc test.c -o test -llua -lm -ldl
$ ./test
The sum is 1024

如果不添加 -llua, 会提示以下错误。

/usr/bin/ld: /tmp/ccQVKqF9.o: in function `luaadd':
test.c:(.text+0x24): undefined reference to `lua_getglobal'
/usr/bin/ld: test.c:(.text+0x38): undefined reference to `lua_pushnumber'
/usr/bin/ld: test.c:(.text+0x4c): undefined reference to `lua_pushnumber'
/usr/bin/ld: test.c:(.text+0x70): undefined reference to `lua_callk'
/usr/bin/ld: test.c:(.text+0x89): undefined reference to `lua_tonumberx'
/usr/bin/ld: test.c:(.text+0xa4): undefined reference to `lua_settop'
/usr/bin/ld: /tmp/ccQVKqF9.o: in function `main':
test.c:(.text+0xc1): undefined reference to `luaL_newstate'
/usr/bin/ld: test.c:(.text+0xd7): undefined reference to `luaL_openlibs'
/usr/bin/ld: test.c:(.text+0xf2): undefined reference to `luaL_loadfilex'
/usr/bin/ld: test.c:(.text+0x120): undefined reference to `lua_pcallk'
/usr/bin/ld: test.c:(.text+0x159): undefined reference to `lua_close'
collect2: error: ld returned 1 exit status

如果不添加 -lm, 会提示以下错误。

/usr/bin/ld: /usr/local/lib/liblua.a(lobject.o): in function `numarith.isra.0':
lobject.c:(.text+0x1fb): undefined reference to `fmod'
/usr/bin/ld: lobject.c:(.text+0x221): undefined reference to `pow'
/usr/bin/ld: /usr/local/lib/liblua.a(lvm.o): in function `luaV_execute':
lvm.c:(.text+0x2145): undefined reference to `pow'
/usr/bin/ld: lvm.c:(.text+0x24e0): undefined reference to `fmod'
/usr/bin/ld: /usr/local/lib/liblua.a(lmathlib.o): in function `math_log10':
lmathlib.c:(.text+0xa3): undefined reference to `log10'
/usr/bin/ld: /usr/local/lib/liblua.a(lmathlib.o): in function `math_pow':
lmathlib.c:(.text+0x1b8): undefined reference to `pow'
/usr/bin/ld: /usr/local/lib/liblua.a(lmathlib.o): in function `math_tanh':
lmathlib.c:(.text+0x1e3): undefined reference to `tanh'
/usr/bin/ld: /usr/local/lib/liblua.a(lmathlib.o): in function `math_sinh':
lmathlib.c:(.text+0x213): undefined reference to `sinh'
/usr/bin/ld: /usr/local/lib/liblua.a(lmathlib.o): in function `math_cosh':
lmathlib.c:(.text+0x243): undefined reference to `cosh'
/usr/bin/ld: /usr/local/lib/liblua.a(lmathlib.o): in function `math_tan':
lmathlib.c:(.text+0x273): undefined reference to `tan'
/usr/bin/ld: /usr/local/lib/liblua.a(lmathlib.o): in function `math_sqrt':
lmathlib.c:(.text+0x2d6): undefined reference to `sqrt'
/usr/bin/ld: /usr/local/lib/liblua.a(lmathlib.o): in function `math_sin':
lmathlib.c:(.text+0x303): undefined reference to `sin'
/usr/bin/ld: /usr/local/lib/liblua.a(lmathlib.o): in function `math_log':
lmathlib.c:(.text+0x649): undefined reference to `log10'
/usr/bin/ld: lmathlib.c:(.text+0x656): undefined reference to `log'
/usr/bin/ld: lmathlib.c:(.text+0x678): undefined reference to `log2'
/usr/bin/ld: lmathlib.c:(.text+0x68c): undefined reference to `log'
/usr/bin/ld: lmathlib.c:(.text+0x6a0): undefined reference to `log'
/usr/bin/ld: /usr/local/lib/liblua.a(lmathlib.o): in function `math_exp':
lmathlib.c:(.text+0x723): undefined reference to `exp'
/usr/bin/ld: /usr/local/lib/liblua.a(lmathlib.o): in function `math_cos':
lmathlib.c:(.text+0x753): undefined reference to `cos'
/usr/bin/ld: /usr/local/lib/liblua.a(lmathlib.o): in function `math_atan':
lmathlib.c:(.text+0x7b0): undefined reference to `atan2'
/usr/bin/ld: /usr/local/lib/liblua.a(lmathlib.o): in function `math_asin':
lmathlib.c:(.text+0x7e3): undefined reference to `asin'
/usr/bin/ld: /usr/local/lib/liblua.a(lmathlib.o): in function `math_acos':
lmathlib.c:(.text+0x813): undefined reference to `acos'
/usr/bin/ld: /usr/local/lib/liblua.a(lmathlib.o): in function `math_fmod':
lmathlib.c:(.text+0xca3): undefined reference to `fmod'
collect2: error: ld returned 1 exit status

如果不添加 -ldl, 会提示以下错误。

/usr/bin/ld: /usr/local/lib/liblua.a(loadlib.o): in function `lookforfunc':
loadlib.c:(.text+0x565): undefined reference to `dlsym'
/usr/bin/ld: loadlib.c:(.text+0x5c6): undefined reference to `dlopen'
/usr/bin/ld: loadlib.c:(.text+0x649): undefined reference to `dlerror'
/usr/bin/ld: loadlib.c:(.text+0x671): undefined reference to `dlerror'
/usr/bin/ld: /usr/local/lib/liblua.a(loadlib.o): in function `gctm':
loadlib.c:(.text+0x831): undefined reference to `dlclose'
collect2: error: ld returned 1 exit status

所以编译时要根据错误提示添加指定的库。

Lua 调用 C 函数

要在 Lua 中调用 C 函数,主要有以下两种方式。

  1. C 中注册函数给 Lua
  2. C 编译出动态链接库,在 Lua 中使用 require

先来看看注册函数的方式。

#include <stdio.h>
#include <string.h>
#include <lua.h>
#include <lualib.h>
#include <lauxlib.h>

static int lua_SayHello(lua_State *L)
{
    const char *d = luaL_checkstring(L, 1); /* 获取参数,字符串类型 */
    char str[100] = "hello ";

    strcat(str, d);
    lua_pushstring(L, str); /* 返回给lua的值压栈 */

    return 1; /* 返回值个数 */
}

int main(int argc, char *argv[])
{
    lua_State *L = luaL_newstate(); /* 创建lua状态机 */
    luaL_openlibs(L); 

    lua_register(L, "SayHello", lua_SayHello); /*注册C函数到lua */

    const char* testfunc = "print(SayHello('world'))"; /*lua中调用c函数 */
    if(luaL_dostring(L, testfunc)) /* 执行Lua命令。*/
        printf("Failed to invoke.\n");

    lua_close(L); /*清除Lua*/
    return 0;
}

这里虽然没有看到脚本,但实际上也是用lua的语法写的指令 print(SayHello('world')), 然后通过 luaL_dostring函数执行 lua 指令。

需要注意的是,注册的函数 l_SayHello return 的是返回值个数,这里1代表只有一个返回值,也就是压栈的字符串。

接下来看另外一种,通过动态链接库的方式调用C函数。

/* mylualib.c
 * use to compile mylualib.so */
#include <stdio.h>
#include <string.h>
#include <lua.h>
#include <lauxlib.h>
#include <lualib.h>

static int add(lua_State* L) 
{
    double op1 = luaL_checknumber(L,1);
    double op2 = luaL_checknumber(L,2);
    lua_pushnumber(L,op1 + op2);
    return 1;
}

static int sub(lua_State* L)
{
    double op1 = luaL_checknumber(L,1);
    double op2 = luaL_checknumber(L,2);
    lua_pushnumber(L,op1 - op2);
    return 1;
}

/* 第一个字段用于Lua调用,第二个字段为C的函数指针
 * 结构体数组中的最后一个元素的两个字段均为NULL,用于提示Lua注册函数已经到达数组的末尾。*/
static const struct luaL_Reg mylibs[] = { 
    {"add", add},
    {"sub", sub},
    {NULL, NULL} 
}; 

/* 函数名必须为luaopen_xxx,xxx表示lib名称。Lua代码 require "xxx"需要与之对应。*/
extern int luaopen_mylualib(lua_State* L) 
{
    luaL_newlib(L, mylibs);
    return 1;
}

以上注册了简单加法运算函数,编写后编译成动态链接库。

gcc mylualib.c -shared -fPIC -o mylualib.so

这里要注意的是,编译以上动态链接库需要 liblua.so , 但是默认编译安装lua的时候只会生成静态库 liblua.a, 并不会生成这个文件,所以需要修改lua的 Makefile,添加 liblua.so 相关配置,最后重新编译安装lua。

然后写个简单的lua脚本 my.lua.

-- my.lua
local mylib = require "mylualib"

print(mylib.add(12,33))
print(mylib.sub(33,22))

执行下看看效果。

$ lua my.lua
45.0
11.0

完美执行。

示例

最后来看个Lua的示例程序。

getopt

Lua 没有官方的getopt库,但是 GitHub 上有很多库用于替代它的自定义库。比如下面这个,是相对更符合shell习惯的。

--- /*@from: https://github.com/skeeto/getopt-lua
--- getopt(3)-like functionality for Lua 5.1 and later
-- This is free and unencumbered software released into the public domain.

--- getopt(argv, optstring [, nonoptions])
--
-- Returns a closure suitable for "for ... in" loops. On each call the
-- closure returns the next (option, optarg). For unknown options, it
-- returns ('?', option). When a required optarg is missing, it returns
-- (':', option). It is reasonable to continue parsing after errors.
-- Returns nil when done.
--
-- The optstring follows the same format as POSIX getopt(3). However,
-- this function will never print output on its own.
--
-- Non-option arguments are accumulated, in order, in the optional
-- "nonoptions" table. If a "--" argument is encountered, appends the
-- remaining arguments to the nonoptions table and returns nil.
--
-- The input argv table is left unmodified.*/

local function getopt(argv, optstring, nonoptions)
    local optind = 1
    local optpos = 2
    nonoptions = nonoptions or {}
    return function()
        while true do
            local arg = argv[optind]
            if arg == nil then
                return nil
            elseif arg == '--' then
                for i = optind + 1, #argv do
                    table.insert(nonoptions, argv[i])
                end
                return nil
            elseif arg:sub(1, 1) == '-' then
                local opt = arg:sub(optpos, optpos)
                local start, stop = optstring:find(opt .. ':?')
                if not start then
                    optind = optind + 1
                    optpos = 2
                    return '?', opt
                elseif stop > start and #arg > optpos then
                    local optarg = arg:sub(optpos + 1)
                    optind = optind + 1
                    optpos = 2
                    return opt, optarg
                elseif stop > start then
                    local optarg = argv[optind + 1]
                    optind = optind + 2
                    optpos = 2
                    if optarg == nil then
                        return ':', opt
                    end
                    return opt, optarg
                else
                    optpos = optpos + 1
                    if optpos > #arg then
                        optind = optind + 1
                        optpos = 2
                    end
                    return opt, nil
                end
            else
                optind = optind + 1
                table.insert(nonoptions, arg)
            end
        end
    end
end

return getopt

--[[ /*Examples:
getopt = require('getopt')

local append = false
local binary = false
local color = 'white'
local nonoptions = {}
local infile = io.input()

for opt, arg in getopt(arg, 'abc:h', nonoptions) do
    if opt == 'a' then
        append = true
    elseif opt == 'b' then
        binary = true
    elseif opt == 'c' then
        color = arg
    elseif opt == 'h' then
        usage()
        os.exit(0)
    elseif opt == '?' then
        print('error: unknown option: ' .. arg)
        os.exit(1)
    elseif opt == ':' then
        print('error: missing argument: ' .. arg)
        os.exit(1)
    end
end

if #nonoptions == 1 then
    infile = io.open(nonoptions[1], 'r')
elseif #nonoptions > 1 then
    print('error: wrong number of arguments: ' .. #nonoptions)
    os.exit(1)
end
*/]]

参考