Lua开发指南
更新时间:2024/12/12
在Gitcode上查看源码

openUBMC提供了多种语言编程框架能力,丰富了用户的可选择性。其中Lua编程框架的完备度较高,易上手,适用于需要快速开发、对系统资源无苛刻要求的场景。

The Programming Language Lua 官方页面中详细介绍了Lua语言的特性。openUBMC中仅使用了部分基础特性,同时也基于Lua语言本身提供了更丰富的编程库能力。本篇文章重点介绍openUBMC对Lua语言的使用,方便用户快速掌握openUBMC中的Lua源码阅读、修改,增量开发能力。

Lua虚拟机(Virtual Machine)

Lua作为一款高级语言,在运行态时需要虚拟机提供运行平台。虚拟机提供了一系列的优点:隔离性、灵活性、管理性。

在openUBMC中,开发者不感知Lua代码运行的环境差异,同一份Lua代码在任何环境下运行的结果是一致的,且Lua代码本身不需要针对不同的环境进行差异化适配。

在openUBMC中,每个组件都是一个独立的虚拟机,因此组件与组件之间的内存、堆栈、信息都是隔离的,用户无需担心内存、堆栈上的冲突和不安全操作带来的风险。同时openUBMC也通过虚拟机隔离了组件,禁止了组件之间的直接调用和依赖,在技术层面上确保了组件必须通过资源协作接口进行交互,通过接口确保整体架构上的稳定性。

Lua模块与库(Module and Library)

在Lua语言中,每一个文件都是一个独立的模块,模块与模块之间通过require方式进行加载。

示例1

文件1:my_module.lua

lua
local my_module = {}
local secret_number = 2025330
my_module.message = "Hello OpenUBMC!"
return my_module

文件2:openubmc/another_module.lua

lua
global_message = "https://"

文件3:my_app.lua

lua
local my_module = require 'my_module'
print(my_module.message)
-- > Hello OpenUBMC!
print(my_module.secret_number)
-- > nil
print(global_message)
-- > nil
local another_module = require "openubmc/another_module"
print(global_message)
-- > https://

加载路径

openUBMC设置了lua的加载根路径,分别为:

  • /opt/bmc/app/组件名/lualib
  • /opt/bmc/lualib/

根据文件具体在路径下的相对路径,编写require的加载路径。

  • 同组件内加载: 如示例1中,my_module.luamy_app.lua都在组件的lualib文件夹中,因此可以直接编写require "my_module"

  • 跨组件加载: 当another_module.luaopenubmc路径下是,因此在my_app.lua要require该lua时需要添加路径require "openubmc/another_module"

作用域

与其他语言类似,Lua也有局部变量和全局变量。secret_number仅在my_module中声明,且未返回,因此my_app中无法获取。而global_messageopenubmc/another_module中声明为全局变量,在未加载前,没有此变量,加载后,则全局共享变量。

面对对象编程(OOP)能力

Lua语言中对象是通过引入源表(metatable)的概念来实现的。使用起来不是很方便,因此在openUBMC中重新对对象进行了定义。

示例2

lua
local class = require "mc.class"

local my_parent_class = class()

my_parent_class:ctor(message)
    self.message = message
end

my_parent_class:init()
    if self.message == "" then
        self.message = "Welcome OpenUBMC!"
    end
end

my_parent_class:print_message()
    print(self.message)
end

function main()
    local parent_object = my_parent_class.new()
    parent_object:print_message()
    -- > Welcome OpenUBMC!
end

openUBMC的 SDK中提供了类创建的封装,通过加载mc.class模块方式使用。创建时直接调用class()即可。

ctor函数即代表对象的constructor,用于创建具体的内存对象实列。 init函数用于初始化对象的属性,会在ctor函数调用后立即调用。因此建议初始化相关的代码都在init里面实现,而非ctor里。

在调用my_parent_class.new()的时候,便会出发ctorinit两个函数,并返回创建出来的对象实例。

: . 的调用区别

在Lua中,有一个比较常见的错误,在函数调用、声明时,经常会搞混:.。这是Lua中的一种语法糖。

代码1

lua
my_parent_class:ctor(message)
    self.message = message
end

代码2

lua
my_parent_class.ctor(self, message)
    self.message = message
end

:是一种简化写法,将函数的第一个入参的self简化了。代码1代码2是一致的。同样,在调用的时候,也需要根据使用的语法决定传入对象自身的实例。

代码3

lua
local obj1 = my_parent_class.new("this is obj1")
local obj2 = my_parent_class.new("this is obj2")

obj1:print_message()
-- > this is obj1
obj2.print_message(obj2)
-- > this is obj2
my_parent_class.print_message(obj1)
-- > this is obj1

代码3中创建了两个my_parent_class的对象实例,:.的调用都可以打印对应的数据。

注意

并非所有场景都需要使用:,例如new函数,我们需要调用的是类的函数,而非实例的函数。因此使用.new()而非:new()

对象继承

mc.class支持对象继承能力,可以快速的创建出基类和子类。

示例3

lua
local class = require "mc.class"

local my_parent_class = class()

function my_parent_class:ctor(message)
    self.message = message
end

function my_parent_class:init()
    if self.message == nil then
        self.message = "Welcome OpenUBMC!"
    end
end

function my_parent_class:print_message()
    print(self.message)
end

local my_child_class = class(my_parent_class)

function my_child_class:ctor() -- 子类ctor同时会先执行父类ctor
end

function my_child_class:init()
    my_child_class.super.init(self) --  如需调用父类的函数,可以通过 类.super 获取类对象,然后传入自身实例self
    self.secret_number = 2025330
end

function my_child_class:print_message()
    print(self.message .. " " .. self.secret_number)
end

function main()
    local parent_object = my_parent_class.new()
    parent_object:print_message()
    -- > Welcome OpenUBMC!
    local child_object = my_child_class.new("Secret number is")
    child_object:print_message()
    -- > Secret number is 2025330
end

main()

示例3中,我们增加了一个my_child_class类,继承自my_parent_class。构造函数继承于父类,init函数扩展了父类的init函数,print_message函数覆写了父类的print_message函数。

注意

构造函数即使完全继承父类函数,也需要声明,而且函数则不需要。

安全调用(safe call)

因为虚拟机的原因,即使Lua脚本运行错误,也不会造成进程崩溃退出。但Lua脚本执行错误还是会造成业务中断。因此在编写时,在可能出现异常的地方需要使用安全调用的方式来捕捉并处理异常,保障整体业务的稳定运行。

示例4

lua
local function check_secret_number(num)
    if num <= 20241230 then
        error("Not Ready")
    end
    return 20250330
end

function main()
    local res = check_secret_number(0)
    -- 异常!Not Ready

    local ok, err = pcall(check_secret_number, 0) -- pcall需要传入函数,以及入参
    print(ok, err)
    -- > false, Not Ready

    if not ok then -- 进行函数调用成功检查,如果不成功,可以优雅处理
        local retry, res = pcall(function() -- pcall需要传入函数,因此可以创建一个闭包函数来调用
            return check_secret_number(20250101) -- 在闭包函数中调用,一定要记得返回数值
        end)
        print(retry, res)
        -- > true, 20250330
    end 
end

main()

异常抛出机制

Lua的异常中断机制相对较轻量级,在编写业务时,往往使用异常抛出的方式可以很好的处理中断当前的代码运行,然后在调用层进行异常的处理。Lua中使用error命令字来创建异常,入参为错误消息字符串。

示例4中,直接调用check_secret_number(0)时,便会抛出异常,而后续的代码都不会再运行。

pcall

在调用函数时,可以使用pcall来进行封装,确保异常可以正确的处理。pcall会返回函数执行的状态,以及对应的返回数据。

注意

在使用闭包的时候,由于是创建了一个新的函数,一定要将原本的函数返回数据返回,否则正常场景下的返回数据会被闭包吞掉,反而造成了其他异常。

垃圾回收(Garbage Collection)

垃圾回收是脚本语言中的利器,极大的简化了开发态是对内存的分配逻辑。在对内存分配不敏感的场景下,开发者不再担心内存泄露,内存踩空等内存控制代码,可以更焦距于业务逻辑的编写。

示例5

lua
local a_function_that_create_a_lot_of_data = require 'some_module'

local function temp()
    local tbl = a_function_that_create_a_lot_of_data()
    -- 从tbl里读取数据并进行了一系列操作
end

function main()
    for i = 1, 10 do
        temp()  -- 创建了10个临时对象 tbl
    end
    collectgarbage()  -- 所有tbl都被释放
end

大部分场景下,无需手动调用collectgarbage函数来触发垃圾回收,Lua虚拟机会定期安排垃圾回收。必要时也可以手动进行垃圾回收来释放内存。

注意

垃圾回收的性能消耗较大,因此大部分场景下不建议手动调用。

内存碎片和常驻内存

垃圾回收虽然方便,但是会带来另一个风险,便是创造了很多的内存碎片,导致实际使用内存往往比需要的内存要高。因此在编码时,开发者需要设计内存的生命周期

针对示例5中频繁创建的临时对象,假设main()调用的次数很多,则会创建大量的对象并回收大量的对象,造成了不必要的性能浪费。

通过分析代码即可发现,tbl是只读对象,且每次调用时创建的内容都一样,没有必要每次运行函数时都创建。

示例6

lua
local a_function_that_create_a_lot_of_data = require 'some_module'

local tbl = a_function_that_create_a_lot_of_data() -- tbl被永久的保存,当此文件运行后便会常驻于内存

local function temp()
    -- 从tbl里读取数据并进行了一系列操作
end

function main()
    for i = 1, 10 do
        temp()  -- 不会创造对象
    end
    collectgarbage()  -- 不会释放tbl
end

示例6中,将tbl从函数中抽离了出来,在文件加载时创建一次,后续每次函数调用都直接使用即可,无需创建。因此对垃圾回收时,也不会去卸载任何对象。

同样的我们遇到一个问题,就是tbl变量被文件永久的保留了。Lua中为了避免同一个文件重复加载,文件被require后便会缓存在内存中,下次require同样文件时便可以将文件对象直接返回。因此tbl将常驻于内存中,直到程序退出。

假设main函数并不会被频繁调用,这样的操作反而使得整体内存资源占用变高。

推荐

openUBMC SDK持续的在优化垃圾回收机制和算法。

非大量创建临时对象的场景建议继续使用临时对象的方式完成业务编写。

针对需要创建大量临时对象的场景的最佳实践,可以参考xxxxxxxx

Lua加载so库

Lua语言的一大优势,便是可以像胶水一样嵌入在其他语言中。在openUBMC的框架下,可以继续使用C/C++进行业务逻辑开发,然后在Lua中执行并调用。

openUBMC中也提供了框架,简化了lua与so库之间的调用。

C接口的Lua封装(luawrap)

lua与c的调用

Lua调用C库

与调用lua文件一样,通过require命令字便可以加载so库。

mylib.so

lua
local c_lib = require 'mylib'

-- 使用c_lib暴露的属性和函数

当lua调用so库时,程序运行会从lua中切换至so库中,因此此处的内存管理、异常错误等无法依赖Lua虚拟机提供的能力,需要开发者自行维护。如果此处发生coredump,则会导致程序异常退出。

若使用c++语言,也可以在so库中抛出异常,在Lua中也可以使用pcall进行异常捕获,确保程序的稳定运行。

注意

协程能力仅在lua层生效,C层并不具备此能力。因此在lua中调用so库的函数时将会阻塞整个虚拟机的运行,直到so库中函数返回至Lua。

so库中不应做阻塞式操作,so库的函数设计尽量精简且独立。

如果需要有阻塞性操作,请使用worker机制

加载路径

openUBMC设置了lua的加载根路径,分别为:

  • /opt/bmc/app/组件名/luaclib
  • /opt/bmc/luaclib/
  • /usr/lib64/

C库加载不支持多层级加载,因此建议将so放置于以上路径中。

Lua协程(coroutine)

协程是Lua语言中非常强大的特性,然而在openUBMC中,并不直接使用Lua的协程,而是依赖skynet框架进行协程操作。具体介绍请参考下一篇《Skynet开发指南》