Lua Development Guide
更新时间: 2025/06/17
在Gitcode上查看源码

openUBMC provides programming framework capabilities in multiple languages, offering users a wide range of choices. Among these, the Lua programming framework is highly mature and easy to use, making it suitable for scenarios that require rapid development and do not have strict requirements for system resources.

The official page of the Programming Language Lua describes the features of the Lua language in detail. openUBMC uses only some basic features and provides more extensive programming library capabilities based on the Lua language itself. This document focuses on how openUBMC uses the Lua language, helping you master Lua source code reading, modification, and incremental development in openUBMC.

Lua Virtual Machine

As a high-level language, Lua requires a virtual machine (VM) to provide a running platform. VMs offer several advantages, such as isolation, flexibility, and manageability.

In openUBMC, developers are not aware of environmental differences where Lua code runs. The same Lua code yields consistent results in any environment, and the code itself does not require differentiated adaptation for different environments.

In openUBMC, each component acts as an independent VM. Therefore, memory, stacks, and information are isolated between components. Users do not need to worry about risks caused by memory or stack conflicts or unsafe operations. Additionally, openUBMC uses VMs to isolate components and prohibit direct calls and dependencies between them. Technically, this ensures that components must interact through resource collaboration interfaces, which maintains the stability of the overall architecture through these interfaces.

Lua Modules and Libraries

In the Lua language, each file is an independent module. Modules are loaded through require.

Example 1

File 1: my_module.lua

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

File 2: openubmc/another_module.lua

lua
global_message = "https://"

File 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://

Loading Paths

openUBMC sets the root paths for loading Lua files as follows:

  • /opt/bmc/app/component_name/lualib
  • /opt/bmc/lualib/

Write the loading path for require based on the relative path of the file under these root paths.

  • Loading within the same component: As shown in Example 1, both my_module.lua and my_app.lua are in the lualib folder of the component. Therefore, you can directly use require "my_module".

  • Cross-component loading: When another_module.lua is in the openubmc directory, add the path require "openubmc/another_module" to require this file in my_app.lua.

Scope

Similar to other languages, Lua has local and global variables. secret_number is declared only in my_module and is not returned. Therefore, it cannot be accessed in my_app. In contrast, global_message is declared as a global variable in openubmc/another_module. This variable does not exist before the module is loaded. After the module is loaded, the variable is shared globally.

Object-Oriented Programming Capabilities

In the Lua language, objects are implemented by introducing the concept of metatables. However, this approach is not convenient to use. Therefore, openUBMC redefines objects.

Example 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

The openUBMC SDK provides encapsulation for class creation, which you can use by loading the mc.class module. Simply call class() during creation.

The ctor function represents the object constructor, used to create specific memory object instances. The init function initializes object properties and is called immediately after ctor. Therefore, it is recommended that initialization-related code be implemented in init instead of ctor.

When you call my_parent_class.new(), both ctor and init functions are triggered, and the created object instance is returned.

: . Differences Between : and . Calls

In Lua, a common error is confusing : and . during function calls or declarations. This is a type of syntactic sugar in Lua.

Code snippet 1

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

Code snippet 2

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

: is a simplified notation that omits self as the first input parameter of the function. Code snippet 1 and code snippet 2 are identical. Similarly, when calling a function, you need to decide whether to pass the object instance itself based on the syntax used.

Code snippet 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

In Code snippet 3, two object instances of my_parent_class are created. Both : and . calls can print the corresponding data.

Note:

You do not need to use : in all scenarios. For example, when calling the new function, you need to call the function of the class rather than the instance. Therefore, use .new() instead of :new().

Object Inheritance

mc.class supports object inheritance, allowing you to quickly create base classes and subclasses.

Example 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() -- The subclass ctor also executes the parent class ctor first.
end

function my_child_class:init()
    my_child_class.super.init(self) -- To call a function of the parent class, use Class.super to get the class object, and then pass its own instance (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()

In Example 3, a my_child_class class is added, which inherits from my_parent_class. The constructor is inherited from the parent class, the init function extends the init function of the parent class, and the print_message function overrides the print_message function of the parent class.

Note:

You need to declare the constructor even if it completely inherits from the parent class function. This is not required for other functions.

Safe Calls

Due to the VM, a Lua script error does not cause the process to crash or exit. However, Lua script execution errors still cause service interruptions. Therefore, when writing code, use safe calls to capture and handle exceptions in places where they might occur, ensuring the stable operation of the overall service.

Example 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)
    -- Exception! Not Ready

    local ok, err = pcall(check_secret_number, 0) -- pcall requires the function and its input parameters.
    print(ok, err)
    -- > false, Not Ready

    if not ok then -- Check if the function call is successful. If it fails, handle it gracefully.
        local retry, res = pcall(function() -- pcall requires a function, so you can create a closure function to call it.
            return check_secret_number(20250101) -- When calling in a closure function, remember to return the value.
        end)
        print(retry, res)
        -- > true, 20250330
    end 
end

main()

Exception Throwing Mechanism

The exception interruption mechanism in Lua is relatively lightweight. When writing service logic, throwing exceptions is an effective way to stop the current code execution and then handle the exception at the calling layer. In Lua, use the error command word to create an exception, passing a error message string as the input parameter.

In example 4, calling check_secret_number(0) directly throws an exception, and the subsequent code does not run.

pcall

When calling a function, you can use pcall for encapsulation to ensure exceptions are handled correctly. pcall returns the execution status of the function and the corresponding return data.

Note:

When using a closure, since a new function is created, ensure that you return the original return data of the function. Otherwise, the return data in normal scenarios will be lost due to the closure, which may cause other exceptions.

Garbage Collection

Garbage collection (GC) is a powerful tool in scripting languages, greatly simplifying memory allocation logic during development. In scenarios that are not sensitive to memory allocation, developers no longer need to worry about memory leaks or illegal memory access, allowing them to focus more on implementing service logic.

Example 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()
    -- Read data from tbl and perform a series of operations.
end

function main()
    for i = 1, 10 do
        temp()  -- Create 10 temporary objects (tbl).
    end
    collectgarbage()  -- All tbl objects are released.
end

In most scenarios, you do not need to call the collectgarbage function manually to trigger GC. The Lua VM schedules GC periodically. You can also manually perform GC to release memory if necessary.

Note:

GC consumes significant performance. Therefore, manual GC is not recommended in most scenarios.

Memory Fragmentation and Resident Memory

Although GC is convenient, it poses another risk: it creates many memory fragments, which often makes the actual memory usage higher than the required memory. Therefore, you must design the memory lifecycle when coding.

Regarding the temporary objects frequently created in Example 5, if main() is called many times, a large number of objects are created and collected, leading to unnecessary performance waste.

By analyzing the code, you can find that tbl is a read-only object and its content is the same for each call. There is no need to create it every time the function runs.

Example 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 is saved permanently and resides in memory after this file is run.

local function temp()
    -- Read data from tbl and perform a series of operations.
end

function main()
    for i = 1, 10 do
        temp()  -- No objects are created.
    end
    collectgarbage()  -- tbl is not released.
end

In Example 6, tbl is moved out of the function and created once when the file is loaded. Subsequent function calls use it directly without re-creation. Therefore, no objects are unloaded during GC.

However, a problem arises: the tbl variable is permanently retained by the file. To avoid loading the same file repeatedly, Lua caches a file in memory after it is loaded via require. The next time the same file is loaded, the file object is returned directly. Therefore, tbl resides in memory until the program exits.

If the main function is not called frequently, this operation increases overall memory resource usage.

Recommendation:

The openUBMC SDK continuously optimizes the GC mechanism and algorithms.

In scenarios where not a large number of temporary objects are created, you are advised to use temporary objects for service implementation.

Loading .so Libraries in Lua

A major advantage of the Lua language is that it can be embedded into other languages like glue. Under the openUBMC framework, you can continue to use C/C++ for service logic development and then execute and call it in Lua.

openUBMC also provides frameworks that simplify calls between Lua and .so libraries.

Lua Encapsulation of C Interfaces (luawrap)

Calls between Lua and C are provided.

Calling C Libraries in Lua

Similar to calling Lua files, you can load .so libraries using require.

mylib.so

lua
local c_lib = require 'mylib'

-- Use the properties and functions exposed by c_lib.

When Lua calls an so library, the program execution switches from Lua to the .so library. Therefore, memory management and exception handling here cannot rely on the capabilities provided by the Lua VM and must be maintained by developers. If a core dump occurs here, the program will exit unexpectedly.

If you use the C++ language, you can also throw exceptions in the .so library. You can use pcall in Lua to capture these exceptions, ensuring the stable operation of the program.

Note:

Coroutine capabilities are effective only at the Lua layer. Therefore, when a function of an .so library is called in Lua, the execution of the entire VM is blocked until the function in the .so library returns to Lua.

Functions in .so libraries should not perform blocking operations and should be designed to be concise and independent.

If blocking operations are required, use the worker mechanism.

Loading Paths

openUBMC sets the root paths for loading Lua files as follows:

  • /opt/bmc/app/component_name/luaclib
  • /opt/bmc/luaclib/
  • /usr/lib64/

C library loading does not support multiple levels. You are advised to place the .so libraries in the preceding paths.

Lua Coroutines

Coroutines are a very powerful feature of the Lua language. However, in openUBMC, Lua coroutines are not used directly. Instead, coroutine operations depend on the Skynet framework. For details, see the Skynet Development Guide.