openUBMC社区通用编码规范
更新时间: 2026/05/14
在Gitcode上查看源码

openUBMC社区通用编码规范

1 概述


  • 本规范适用于openUBMC社区代码开发,目的是引导社区开发者养成良好的编程习惯,编写出风格统一、容易阅读、安全可靠的代码
  • 本规范同样适用于指导openUBMC社区门禁和工具开发

2 代码风格


2.1 命名

P.01 标识符的命名遵循阅读习惯

符合英文阅读习惯的命名将明显提高代码可读性。命名贴近英文语法,使代码更容易阅读

lua
【正例】
if found then
   ...
end

【反例】
if is_found then
   ...
end

P.02 变量命名尽量简短,前提是不影响阅读理解

变量应在其作用域中无二义性,在不影响阅读理解的前提下,命名应该尽量简短

lua
【正例】
local function print_error_msg()
    local msg
    ...
end

【反例】
local function print_error_msg()
    local error_msg
    ...
end

G.NAM.B01 标识符满足openUBMC标识符命名规范

结合openUBMC的业务特点和现状,针对Lua语言的标识符命名制定了如下规范:

表1.1 openUBMC Lua语言标识符命名规范

类别规定命名风格正确示例
仓库名snake_case风格libmc_lua
组件名snake_case风格key_mgmt
文件snake_case风格project_file.lua
模块snake_case风格local module_name = {}
snake_case风格local class_name = {}
RPC方法大驼峰风格function module_name:SetName()
RPC方法参数大驼峰风格function module_name:SetName(ExampleName)
函数snake_case风格local function func_name()
全局变量snake_case风格var_name = 0
局部变量snake_case风格local var_name = 0
函数参数snake_case风格local function set_name(example_name)
常量全大写,下划线分割local DEFAULT_PORT = 8080
枚举类型snake_case风格local enum_name = {}
枚举值全大写,下划线分割local enum_name = { MODE_RED = 0, MODE_GREEN = 1 }
表名snake_case风格local table_name = {}
表索引snake_case风格local table_name = { table_index = 'example' }

注:表中未包含的其他标识符命名,建议统一使用snake_case

G.NAM.B02 函数和参数的命名能够正确描述功能

  • 命名被认为是软件开发过程中最困难,也是最重要的事情之一
  • 符号命名要简洁、准确,符合阅读习惯,容易理解

G.NAM.B03 标识符拼写正确,符合通用习惯缩写

表1.2 常用英文缩写对照表

完整单词缩写完整单词缩写完整单词缩写完整单词缩写
actionactaddressaddrargumentargasynchronousasync
attributeattraverageavgbufferbufcalculatecalc
classclscolumncolcommandcmdcomparecmp
configurationcfgcontextctxcontrolctrlcountcnt
currentcurdecreasedecdefinedefdeletedel
descriptordescdestinationdestdirectorydirdivisiondiv
documentdocdriverdrvenvironmentenverrorerr
ethernetethexecuteexecexpressionexprfrequencyfreq
functionfuncgenerategenhexdecimalhexidentificationid
imageimgincreaseincindexidxinformationinfo
initialinitinterfaceintflengthlenlibrarylib
managementmgmtmaximummaxmessagemsgminimummin
modulemodmultiplemultinumbernumobjectobj
packagepkgparameterparampasswordpwdphysicalphy
pointerptrpositionpospreviousprevprocessproc
publicpubreceivercvreferencerefregisterreg
repositoryreporesponserespresultretsecondsec
sequenceseqserial numbersnsourcesrcstandardstd
stringstrsynchronizesyncsystemsystemperaturetemp
temporarytmpvaluevalvariablevarvectorvec
versionvervoltagevolt

2.2 注释

P.03 注释跟代码一样重要,应按需注释

G.CMT.B01 较为晦涩的代码、特殊处理场景代码要尽量详尽的进行注释

  • ​ 难理解的代码,建议在注释中简述相关知识说明,比如增加协议字段格式、自定义配置文件格式等方便理解代码处理逻辑
  • ​ 代码中的特殊处理,尽量介绍清楚背景、业务场景、处理逻辑等;也可以在README中增加功能详细说明
  • ​ 当需要规避周边组件(硬件、开源第三方软件等)的问题时,必须添加注释并且注明规避背景、规避原因和影响范围

3 编程实践


3.1 表

P.04 禁止弱引用表与__gc元方法混用

  • openUBMC基于LuaJIT开发,LuaJIT只完全兼容Lua 5.1。当弱引用表与__gc元方法混用时可能会引发无法预期的错误
lua
【反例】
local table_a = setmetatable({}, {__mode = 'kv', __gc = function()
    ...
end}) -- Bad, 弱引用表与__gc元方法混用

G.TBL.B01 Lua 表不支持直接比较,需要比较所有的Key和Value是否相同

可以使用框架utils.table_compare库函数完成Lua 表的比较

lua
【反例】
local table_a = {}
local table_b = {}
...
if table_a == {} then      -- Bad
    ...
end
if table_a == table_b then -- Bad
    ...
end
【正例】
 local table_a = {}
 local table_b = {}
...
if utils.table_compare(table_a, {}) then      -- Good
    ...
end

if utils.table_compare(table_a, table_b) then -- Good
    ...
end

if next(table_a) == nil then                  -- Good
    ...
end

G.TBL.B02 表使用的时候需要保证先构造成员变量然后使用,避免多线程下出现访问nil

lua
【反例】

-- 线程一
local value = {
    ['Type'] = 'ABC'
}

-- 线程二
-- 可能先于线程一执行,value.Type为nil导致程序抛错
table_a[value.Type] = 1

【正例】

-- 划分到同一个线程先后执行,保证成员变量已构造
local value = {
    ['Type'] = 'ABC'
}

table_a[value.Type] = 1

G.TBL.B03 使用ipairs有序遍历数组,使用pairs遍历不连续的数组

  • 不论遍历的是数组还是字典,pairs均不保证遍历的顺序,它依赖于表内部的哈希实现。
  • 要有序遍历数组,使用ipairs遍历。这里的有序是指从下标1开始依次访问,下标必须连续。table的键值如果包含字母则不属于数组的范畴,更谈不上有序。
  • 要完整遍历不连续的数组,使用pairs遍历
lua
【反例】
local a = { 1, 2, 3, 4, 5 }
for k, v in pairs(a) do
    print(v) -- Bad, 使用pairs遍历,不保证顺序
end

local b = {
    [1] = '1',
    ['a'] = 'a'
}
for k, v in ipairs(b) do
    print(v) -- Bad, 使用ipairs遍历,未完整遍历所有数据
end

【正例】
local a = {1, 2, 3, 4, 5}
for k, v in ipairs(a) do
    print(v) -- Good, 使用ipairs遍历有序数组
end

local b = {
    [1] = '1',
    ['a'] = 'a'
}
for k, v in pairs(b) do
    print(v) -- Good, 使用pairs遍历所有元素,但不保证顺序
end

G.TBL.B04 使用#对表取长度应当先确保表是一个序列且未包含其他类型键值

  • 如果表的__len元方法没有给出,表的长度只在表是一个序列时有定义
  • 序列指表的正数键集等于 {1..n}
lua
【反例】
local a = {
    [2] = 1,
    [3] = 2
} -- Bad,正数健集{2,3},缺少键值1,a不是序列

local b = {
    [1] = 1,
    [3] = 2,
    [4] = 3
} -- Bad,正数键集{1,3,4},键值不连续。禁止对不连续的数组表求长度,结果不可预测

local c = {
    [-1] = 1,
    [0] = 2,
    [1] = 3,
    [2] = 4,
    ['s'] = 5
} -- Bad,正数健集{1,2},c是序列,但不推荐使用#取长度,容易误认为其他键值也参与长度计算
【正例】
local d = {
    [1] = 1,
    [2] = 2,
    [3] = 3,
} -- Good,正数键集{1,2,3},d是序列,且未包含其他类型键值,#d是预期的3

G.TBL.B05 使用remove方法删除表中元素应当先确保表是一个序列且未包含其他类型键值,否则应当使用将元素置为nil的方式删除

  • 序列指表的正数键集等于 {1..n}
lua
【反例】
local a = {
    [2] = 1,
    [3] = 2
}
table.remove(a, 2) -- Bad,报错bad argument #1 to 'remove' (position out of bounds)
【正例】
local a = {
    [1] = 1,
    [2] = 2,
    [3] = 3
}
table.remove(a, 2) -- Good,成功删除键为2的元素,删除后该表为{[1] = 1, [2] = 3}

local b = {
    [2] = 1,
    [3] = 2
}
b[2] = nil -- Good,成功删除键为2的元素,删除后该表为{[3] = 2}

注意:使用t[i] = nil会导致数组下标不连续,使用table.remove才会自动调整数组下标

G.TBL.B06 通过pairs遍历表时,循环体中不能同时对表做插入和删除操作

lua
【反例】
local a = { 1, 2, 3 }
for k, v in pairs(a) do
    a[k] = nil
    a[k + 3] = v -- Bad,报错invalid key to 'next'
end

G.TBL.B07 使用Lua弱引用表时确保表外有对变量进行引用,或变量垃圾回收不影响业务功能

  • Lua弱引用表(__mode = 'kv'__mode = 'k'__mode = 'v')作为键/值保存在表中的变量不会增加其引用计数,当变量在表外没有被引用时会被垃圾回收
lua
【反例】
-- 弱引用表db_objs用来存放db对象
local db_objs = setmetatable({}, {__mode = 'kv'})
function add_db_obj(obj)
    -- 将需要持久化的obj添加到待持久化的对象列表db_objs中
    db_objs[obj] = context.get_context() or context.new()
    -- 由于db_objs是弱引用表,离开当前函数作用域之后obj可能被垃圾回收
end
local function save_all_objs()
    for obj, ctx in pairs(db_objs) do
        -- 遍历db_objs时,表中对象可能已经被垃圾回收,造成数据丢失
        obj:save()
    end
end

【正例】
-- 弱引用表g_obj_pool的作用是提供缓存池,避免频繁重复创建对象
local g_obj_pool = setmetatable({}, {__mode = 'kv'})
local function new_obj()
    local obj = table.remove(g_obj_pool)
    if not obj then
        -- 表中对象已被垃圾回收时可以重新创建,不影响业务功能
        return new_object()
    end
    -- 表中对象没有被垃圾回收时可以重复利用,避免频繁创建新对象的消耗
    return obj
end

G.TBL.B08 应用插件机制的场景下,推荐使用深拷贝的方式传递table类型参数

  • table类型的参数在传入插件处理后,可能会在插件内部被修改。如果该参数后续在组件内需要继续使用,可能会引起异常
  • Lua不支持声明const类型的入参,因此推荐使用深拷贝的方式(utils.table_copy)传递table类型参数到插件
lua
【反例】
function plugin_method.set_property(parameter)
    local ok, rsp = pcall(func, parameter) -- Bad,table参数直接传入插件
    ...
end

【正例】
function plugin_method.set_property(parameter)
    local input_location = utils.table_copy(parameter)     -- Good,深拷贝一份table参数传入插件
    local ok, rsp = pcall(func, parameter)
    ...
end

G.TBL.B09 高频调用场景中禁止通过赋空表的方式实现表的清空

  • 赋空表会申请新的内存,在高频调用场景中会导致内存无法及时GC,产生内存碎片,最终导致内存持续增长
  • 可以通过两种方式实现:1、依次对表的变量赋nil;2、使用封装的table_cache类
lua
local table_cache = require 'mc.table_cache'
local instance = table_cache.new()  -- 实例化
local config = instance:allocate()  -- 申请空表

instance:deallocate(config)  -- 销毁

3.2 函数

G.FUN.B01 信号处理函数应该轻量

  • 信号处理函数处理时间过长可能会引起程序非预期结果,使用时应谨慎
  • 不允许在信号处理函数中添加延时函数,如skynet.sleep

G.FUN.B02 循环体中避免定义local变量,放在循环体外定义,可以减少内存申请

lua
【反例】
for i = 1, #array do
    local a
    a = array[i].number
    ...
end
【正例】
local a
for i = 1, #array do
    a = array[i].number
    ...
end

G.FUN.B03 使用Lua闭包特性,避免在高频调用函数中申请local变量

  • 循环体和高频调用函数中定义local变量,会反复申请内存,增加Lua GC机制负担,当GC无法及时回收时,会导致内存占用持续增加。

G.FUN.B04 禁止将_作为函数入参

_作为Lua中的占位符,与其他变量并无区别,默认值为nil,未通过local显式申明的情况下为全局变量。以占位符作为函数入参,可能引起异常

lua
【反例】
local res, _ = func(cmd, _, request.data_out)  -- Bad, _可能已作为全局变量被赋值
...

function func(cmd, data_in, data_out)
    if data_in then -- 如果data_in非空,执行额外命令
        ...
    end
end
【正例】
local res, _ = func(cmd, nil, request.data_out, nil)  -- Good, 直接通过nil占位

3.3 程序块

G.CHK.B01 Lua除法运算使用'//'时,结果会向下取整。使用'/'时,结果为浮点数

lua
【示例】
local a = 5 / 2
local b = 5 // 2

print('a = ', a)      -- 输出为 a =     2.5
print('b = ', b)      -- 输出为 b =     2

G.CHK.B02 任意基本类型变量都可以进行'=='和'~='比较,但只有数值类型变量可以进行'>'、'<'、'>='、'<='比较

lua
【反例】
if a > b then  -- a、b为字符串类型
    ...
end

G.CHK.B03 Lua中基本数据类型是值类型,table是引用类型。对新的值类型变量赋值不会改变原始值

lua
-- 错误示例
local flag = table[Id]
if flag ~= 0 then
    ...
end
flag = 1  -- 对flag赋值不会改变table[Id]

G.CHK.B04 对变量使用#取长度前,须确保变量为string/table类型或者有__len()元方法

  • Lua中string/table类型(特指序列,参见G.TBL.B04说明)对#有定义,非string类型可以通过__len元方法修改取长度的操作行为
lua
【反例】
if #v.SRVersion > 0 then  -- Bad,未确保变量为table或string类型,程序可能抛错
    ...
end
【正例】
if type(v.SRVersion) == 'string' and #v.SRVersion > 0 then  -- Good,先确保变量为string类型再取长度
    ...
end

G.CHK.B05 北向接口Script和Plugin编码时,使用cjson接口保证字典和数组的有序性

  • 北向接口通常要求字典和数组的有序性,使用Lua的table默认无序,可能导致接口返回结果不符合预期
  • 在北向提供的Sript和Plugin机制中,建议使用框架提供的cjson接口json_object_new_array/json_object_new_object,保证返回结果的有序性
lua
【反例】
function get_selLogEntries(processObj)
    local eventList = processObj.EventList
    local selLogEntries = {}
    for i, v in pairs(eventList) do
        selLogEntries[i] = {        -- Bad, 使用Lua的table,无序
            ["eventid"] = eventList[i].RecordId,
            ["subjecttype"] = "TODO",
            ["eventdesc"] = eventList[i].Description,
        }
    end
    return selLogEntries
end

【正例】
function get_selLogEntries(processObj)
    local eventList = processObj.EventList
    local selLogEntries = cjson.json_object_new_array()     -- Good, 使用cjson接口保证数据的有序性
    for i, v in pairs(eventList) do
        selLogEntries[i] = cjson.json_object_new_object()
        selLogEntries[i].eventid = eventList[i].RecordId
        selLogEntries[i].subjecttype = 'TODO'
        selLogEntries[i].eventdesc = eventList[i].Description
    end
    return selLogEntries
end

G.CHK.B06 使用string.find匹配字符串时,应注意plain参数的正确使用

  • string.find用于在指定的目标字符串中搜索指定的模式,函数的完整声明为string.find(s, pattern [, init [, plain]])
  • string.find具有两个可选参数,第3个参数是一个索引,用于说明从目标字符串的哪个位置开始搜索。第4个参数是一个布尔值,用于说明是否进行简单搜索(plain search)
  • plain参数不传时,默认为false,表示进行模式匹配,如果传入true,则进行简单搜索,使用时应注意是否与实际查询情况相符
  • 如果没有第3个参数,则不能传入第4个可选参数
lua
【示例】
-- 从uri中匹配文件后缀名.ISO
-- plain参数设置为true,表示进行简单搜索,不进行模式匹配,'.'不会被视为特殊字符
-- 如果需要传入第4个参数,第3个参数必须也传入,1表示从第1个字符开始搜索
local start_pos = string.find(uri, '.ISO', 1, true)

G.CHK.B07 使用字符串匹配时,应注意与标准正则匹配规则的差异

  • Lua使用“%”进行转义,而标准正则使用“\”进行转义
  • Lua不支持大小写不敏感模式

G.CHK.B08 使用字符串变量时,应注意单双引号的使用

  • 如果字符串中有双引号,要用单引号包括;如果字符串中有单引号,要用双引号包括
  • 字符串不能又包含单引号又包含双引号

G.CHK.B09 使用A and B or C进行三目运算时,应避免B取值为false

  • B取值为false时,无论A为真或假,三目运算的结果都为C,不符合预期
  • 针对B取值可能为false的场景,使用if...else表达式
lua
【反例】
local res1 = (val_a == 1) and false or true  -- Bad,结果恒为true

local res2 = (val_a == 1) and (val_b == 1) or true  -- Bad,当不满足val_b == 1时,结果恒为true

【正例】
local res1 = (val_a ~= 1)

local res2
if val_a == 1 then
    res2 = (val_b == 1)
else
    res2 = true
end

G.CHK.B10 不推荐使用Lua的goto语句

  • goto将程序的控制点转移到一个标签处,Lua机制会检测跳转到的新作用域内的变量是否全部初始化,如果跳过了初始化则会报错
  • 这违反了Lua 5.3+的标签作用域规则(ISO/IEC 2375:2017 §3.3.8),实际执行时将触发"undefined label 'xxx'"错误(Lua 5.3+严格模式)

G.CHK.B11 资源消耗大的高频日志打印应当先判断日志级别

  • 在高频日志打印中如果存在cjson.encode、table.concat等资源消耗大的操作,会对BMC性能造成一定影响
  • 可以通过判断当前的日志级别来避免不必要的资源消耗操作
lua
【示例】
-- 如果当前日志级别小于INFO,则不会进入分支执行 cjson.encode
if log:getLevel() >= log.INFO then
    log:info('RefVolumes:%s RefDrives:%s.', cjson.encode(array_obj.RefVolumes),
        cjson.encode(array_obj.RefDrives))
end

3.4 多线程

G.MTH.B01 openUBMC使用skynet框架,在Lua中创建协程只能使用skynet.fork,禁止使用coroutine

  • skynet框架消息处理服务使用了Lua的coroutine来调度各个服务处理消息,在skynet框架中的服务应该使用skynet.fork创建子任务,由skynet统一调度。直接使用Lua的coroutine会和skynet框架的coroutine冲突,产生不可预期的结果。

G.MTH.B02 使用skynet.wait/skynet.wakeup对协程进行挂起、唤醒操作时,避免中途有其他挂起协程的操作

  • 当协程使用skynet.wait/skynet.wakeup对协程进行挂起、唤醒操作时,假如插入其他挂起协程的操作,如skynet.sleep,协程唤醒可能与预期不一致
lua
【反例】
function func()
    local co = coroutine.running()

    local function check()
        ...
        -- 此处的skynet.wakeup可能唤醒的是skynet.sleep挂起的协程,非代码预期情况
        skynet.wakeup(co)
    end

    -- 创建协程,在协程中唤醒co
    skynet.fork(check)
    -- skynet.sleep相当于skynet.wait(),挂起当前协程
    skynet.sleep(10)

    -- 预期在此处挂起和唤醒当前协程
    skynet.wait()
    co = nil
end

【正例】
function func()
    local co = coroutine.running()

    local function check()
        ···
        -- 唤醒的为skynet.wait()挂起的协程,与预期相符
        skynet.wakeup(co)
    end

    skynet.fork(function()
        skynet.fork(check)
        -- skynet.sleep挂起新协程,不会影响原协程
        skynet.sleep(10)
    end)

    -- 把创建协程与skynet.sleep的操作放在新协程中,当前skynet.wait()正常挂起
    skynet.wait()
    co = nil
end

G.MTH.B03 禁止在ORM对象中通过skynet创建协程

  • ORM(Object Relational Mapping)是openUBMC实现的一种OOP范式。
  • 当ORM对象卸载时,通过skynet创建的协程不会跟随对象消亡。如果协程中存在访问对象的行为,可能会导致程序发生异常。
  • 可以通过self:next_tick替代skynet创建协程
lua
【反例】
function c_network_adapter:set_npu_max_sfp_temp()
    local ops = c_optical_module.collection:fetch({NetworkAdapterId = self.NodeId})
    skynet.fork(function()   -- Bad,通过skynet.fork创建协程
        while true do
            self:update_npu_max_sfp_temp(ops)
        end
    end)
end
【正例】
function c_network_adapter:set_npu_max_sfp_temp()
    local ops = c_optical_module.collection:fetch({NetworkAdapterId = self.NodeId})
    self:next_tick(function()  -- Good,通过ORM自带方法创建协程
        while true do
            self:update_npu_max_sfp_temp(ops)
        end
    end)
end

G.MTH.B04 避免重复初始化同一个文件锁

  • 通过flock函数对某文件加完互斥锁之后,当文件锁被再次初始化时(代码流程可能会重复调用初始化函数),文件描述符会发生变化
  • 锁释放时操作的是变化后的文件描述符,文件未成功解锁,当再次加锁时就会导致代码流程死锁
c
/* 示例 */
int32 db_init(void)
{
    // 确保只初始化一次
    if (db_is_inited()) {
        return RET_OK;
    }
    // 初始化文件锁
    g_db_lock = open(DB_SYNC_FILE, O_RDWR | O_CREAT, S_IRUSR | S_IWUSR);
    ...
}

3.5 数据库

G.DBS.B01 数据库批量插入数据时,应使用事务机制进行批量提交

  • openUBMC使用的sqlite数据库基于B+树结构存储数据,每次插入/删除一条数据时,都可能涉及周边数据节点和上级索引节点的调整,频繁的结构调整导致写入量远大于数据量,实测插入200条数据,数据总大小300KB,写入量会放大到20M。
  • 当批量写入数据库操作达到5条及以上时,应该使用事务机制进行批量提交。
  • 事务机制由框架db:tx回调函数实现,db:tx使用sqlite的事务机制,将回调中的所有数据库操作作为一个事务处理, 具体实现为:先执行BEGIN,再执行回调函数,如果回调中的数据库操作执行成功,则执行COMMIT将修改写入数据库,如果执行失败则执行ROLLBACK回滚操作。
  • db:tx()方法在操作失败时会抛错,建议使用pcall调用。
  • 批量操作中如果有一个操作失败会回滚全部操作,建议将相关性强的操作放到一起。
lua
【反例】
local_db:delete(local_db['Power']):exec()
for _, v in pairs(power_table) do
    local power_row_data = local_db['Power'](saved_data)
    power_row_data:save() -- Bad, 在循环中进行数据库插入
end

【正例】
local ok, err = pcall(local_db.db.tx, local_db.db, function ()
    local_db:delete(local_db.Power):exec()
    for _, v in pairs(power_table) do
        local_db.Power(v):save() -- Good, 将数据库插入放在事务中执行
    end
end)

G.DBS.B02 避免使用枚举类型的属性作为持久化数据库表的主键

  • 当枚举类型的属性作为主键时,扩展场景下新增枚举值,当升级新版本并且数据库表中添加了包含新增枚举值的记录,再回退到旧版本,持久化数据加载并校验时会由于找不到新增的枚举值而失败,从而引发兼容性问题
  • 建议根据主键实际的数据类型进行定义即可,具有可扩展性和兼容性
json
【反例】
"properties": {
    "Id": {
        "baseType": "Enum",
        "$ref": "types.json#/defs/EnumType",
        "primaryKey": true
    },
    ...
}

【正例】
"properties": {
    "Id": {
        "baseType": "U8",
        "primaryKey": true
    },
    ...
}

G.DBS.B03 对于自建数据库,需要有可靠性修复机制,避免业务功能受损

  • 此处的自建数据库是指组件内部通过sqlite库指定路径创建的数据库,不同于通过MDS定义由框架创建的数据库
  • 数据库完整性校验可通过sqlite命令"PRAGMA quick_check"
c
/* 示例 */
#define SQLITE3_PATH    "/usr/sbin/sqlite3"

/* DEST_DB_PATH为被校验的数据库路径 */
FILE *ptr = popen(SQLITE3_PATH " " DEST_DB_PATH " \"PRAGMA integrity_check\"", "r");
if (ptr == NULL) {
    debug_log(DLOG_ERROR, "[%s] excute integrity check failed", __FUNCTION__);
    return ret;
}

(void)vos_fgets_s(buf, sizeof(buf) - 1, ptr);
if (strncasecmp(buf, "ok", strlen("ok")) == 0) { // 返回ok表示成功,否则都是有问题
    ret = RET_OK;
} else {
    debug_log(DLOG_ERROR, "[%s] check failed, buf = %s", __FUNCTION__, buf);
}
pclose(ptr);
return ret;
  • 数据库修复可通过sqlite命令".dump"先导出历史命令,然后在新创建数据库文件中执行.dump导出的历史sql命令
c
/* 示例 */
#define SQLITE3_PATH    "/usr/sbin/sqlite3"

// 导出数据库内容,DEST_DB_PATH为被修复的数据库路径,TEMP_SQL_PATH为导出的历史命令
gint32 ret = snprintf_s(cmd_str, sizeof(cmd_str), sizeof(cmd_str) - 1, "%s %s .dump > %s",
    SQLITE3_PATH, DEST_DB_PATH, TEMP_SQL_PATH);

argv[0] = "/bin/sh";
argv[1] = "-c";
argv[2] = cmd_str;
argv[3] = NULL;
ret = vos_system_s(argv[0], argv);

// 重建一个数据库,TEMP_DB_PATH为重建的临时数据库
ret = snprintf_s(cmd_str, sizeof(cmd_str), sizeof(cmd_str) - 1, "%s %s < %s",
    SQLITE3_PATH, TEMP_DB_PATH, TEMP_SQL_PATH);

ret = vos_system_s(argv[0], argv);

// 用重建的数据库替换原来的数据库
(void)vos_file_copy(DEST_DB_PATH, TEMP_DB_PATH);

G.DBS.B04 禁止变更已持久化的枚举属性的取值范围

  • 如果已持久化的枚举属性的取值范围发生变更,BMC版本升降级都可能会出现从数据库中加载未定义的枚举值的场景,此时会由于校验不通过而导致加载失败
  • 以下是一个属性示例,该属性为string类型,已通过enum关键字定义了取值范围,并且为掉电持久化
json
"AuthenticationProtocol": {
    "baseType": "String",
    "usage": [
       "PoweroffPer"
    ],
    "readOnly": false,
    "enum": [
        "SHA256",
        "SHA512"
    ]
}

G.DBS.B05 禁止通过路径访问数据库

  • 允许通过路径访问数据库则默许了本组件可以访问其他组件管理的数据库,这可能会造成多线程或多进程并行访问数据库的场景,最终导致数据库出现异常
  • 开发者应当重新设计代码方案以避免这种场景
lua
【错误示例】
-- 以下通过路径直接访问了pcie_device组件的复位持久化数据库,可能会造成数据库异常
local Databases = require 'database'
local db = Databases('/opt/bmc/pram/persistence.local/pcie_device.db')
local vm = db:prepare(sql_cmd)

3.6 其他

P.05 禁止使用对象名处理业务流程

  • 包括但不限于对象名称比对、对象名分段解析等处理
  • 对象名及命名规则无法保证不发生变化,不能作为资源协作接口之间的约束
lua
【反例】
-- 截取position后两位做为slot,若sr配置变化,则可能出现功能问题。Slot属性改为在sr配置即可
object.Slot = tonumber(string.sub(position, -2))

P.06 合理设计调试打印、日志代码

  • 代码中的调试、日志部分,作为问题定位辅助手段,应尽量少,不可以喧宾夺主
  • 一个模块尽量只在出入口或异常时记录调试、日志信息,其中间行为过程尽量通过白盒测试保证可靠性

G.OTH.B01 日志打印满足openUBMC日志规范要求

txt
Error:程序运行过程中的错误信息,当前处理必须中止,并向上抛出错误,比如无法连接数据库,解析JSON字符串失败、文件创建失败

Warning:程序运行过程中有负面影响的信息或事件,当前处理还可以继续,但是可能会有更严重的故障发生,比如内存占用率高于指定阈值,某处理过程超过了预期耗时,flash写入量超标等

Notice:程序运行过程中的重要信息或事件,对程序运行没有负面影响,比如收到了某个关键的系统信号/事件,定时任务成功的完成执行、程序开始监听某个端口等

Info:程序运行过程中的一般信息或事件,重要性比Notice级别低,比如某个任务的某个部分完成执行

Debug:程序运行过程的详细信息,比如报文数据,函数调用轨迹(入口、出口)等

G.OTH.B02 使用mc.logging库中格式化日志输出函数,格式化符'%d'和'%u'的输出值必须为整型值

lua
local log = require 'mc.logging'

local c = 5 / 2
local d = 5 // 2

log:info('c = %d', c)      -- Bad,  c = 2.5 程序会抛异常
log:info('d = %d', d)      -- Good, d = 2

G.OTH.B03 推荐以%s对number类型的变量进行打印,可避免该变量在异常情况下为nil时导致的程序抛错

lua
【示例】
log:error('fail count = %s', a)  -- a为number类型,即使为nil也能正常运行

G.OTH.B04 禁止使用系统时间来计算时间间隔

  • 系统时间可能发生跳变(例如从NTP服务器同步时间),会导致时间间隔计算不准确。系统时间只能用于时刻记录。常用获取系统时间的函数如下:
名称简要说明
vos.vos_get_cur_time_stamp()对C函数time(0)的封装,单位s
os.time()没有入参时,返回系统当前时间
utils.time_ms()对C函数clock_gettime的封装,时钟类型为CLOCK_REALTIME
  • 运行时间不会发生跳变,可以用来计算时间间隔。常用获取运行时间的函数如下:
名称简要说明
vos.vos_tick_get()基于jiffies计算系统运行时间,单位ms
skynet.now()返回当前进程的运行时间,单位0.01s

G.OTH.B05 调用C库返回的字符串使用lua_pushlstring替代lua_pushstring,可避免包含0x00的字符串被截断

  • lua_pushstring:将指针 s 指向的零结尾的字符串压栈
  • lua_pushlstring:将指针 s 指向的长度为len的字符串压栈,字符串内可以是任意二进制数据,包括零字符
C
【反例】
char *s;
// 省略部分代码
lua_pushstring(L, s);
【正例】
char *s;
// 省略部分代码
lua_pushlstring(L, s, len); // len为数据长度

G.OTH.B06 禁止使用0XXX表示8进制数

  • Lua不支持像C语言的方式表示8进制数
lua
【反例】
utils.chmod(file_path, 0640)
【正例】
utils.chmod(file_path, utils.S_IRUSR | utils.S_IWUSR | utils.S_IRGRP) -- 0640权限

G.OTH.B07 禁止对有热插拔场景的csr对象使用单例模式

  • 对有热插拔场景的csr对象使用单例模式,当该对象被卸载之后再次分发时,由于单例对象已被创建过则不会再次创建,此时单例对象里的参数仍为上一次的脏数据,会导致业务代码出现异常。

G.OTH.B08 使用skynet.queue队列进行处理时应考虑范围是否合理,避免使用全局队列

  • 使用skynet.queue队列控制任务顺序执行时,应注意队列包含的任务范围应尽量小,避免使用全局队列,队列混用时容易导致阻塞时间过长,引发性能问题
lua
【反例】
local queue = require 'skynet.queue'
local cs = queue() -- 全局队列

local s = class()

function s:fun1()
    cs(funa, ...)
end

function s:fun2()
    cs(funb, ...) -- funa和funb共用全局队列,funa的执行也会对funb产生影响;不同的s实例对象之间的funa与funb执行也会互相影响;队列混用容易导致阻塞时间长,引发性能问题
end

【正例】
local queue = require 'skynet.queue'

local s = class()

function s:ctor()
    self.cs_a = queue()
    self.cs_b = queue() -- 不同对象以及不同函数的队列分开声明,避免互相影响
end

function s:fun1()
    self.cs_a(funa, ...)
end

function s:fun2()
    self.cs_b(funb, ...)
end

G.OTH.B09 使用sed命令禁止带有硬编码行号

  • 原文件行号一旦发生变化,可能导致修改内容错位
  • 对于文件内容的修改应当通过查找或者插入文件首/文件尾实现
lua
【反例】
self.run("sed -i '12 i\  chown root:root /dev/shm/dbus/.dbus' rundbus.sh") -- Bad,通过行号查找

【正例】
self.run("sed -i '$ i\chown root:root /dev/shm/dbus/.dbus' rundbus.sh") -- Good,基于行尾查找

G.OTH.B10 跨skynet服务调用若需要获取执行结果应使用skynet.call而非skynet.send

  • skynet.call为同步阻塞式调用,执行skynet.call能获取到执行情况及返回值结果
  • skynet.send为异步非阻塞式调用,无论目标服务是否存在或是否能执行成功该调用都能执行成功
lua
【反例】
local ok, ret = pcall(function ()
    return skynet.send('main', 'lua', 'exec_function', parameter)  -- Bad,无论是否存在main服务或exec_function是否执行成功,pcall返回结果恒为true
end)

【正例】
local ok, ret = pcall(function ()
    return skynet.call('main', 'lua', 'exec_function', parameter)  -- Good,当main服务不存在或exec_function执行异常,pcall能正常捕获,ret也能正常接收到执行返回值
end)

4 异常处理

4.1 断言

G.AST.B01 正式代码禁止使用断言

【规则说明】

通常,断言函数用于评估边界条件并暴露问题,以便在代码中进行定位和修复。断言对于调试很有用。在大多数语言中,断言在代码的发布版本中都是关闭的。在Lua语言中没有明显的调试和发布模式,这意味着程序中的断言会导致错误。除非正确对错误进行处理,否则会出现程序硬退出。下面两个函数都会抛出异常:

  • error(message [, level]):打印出message后,会终止程序运行。
  • assert(v [, messafe]):即v是假(nilfalse)时,调用error函数,否则返回所有所有参数。其中message默认值是"assertion failed!"

除以下例外场景,其他场合禁止使用errorassert函数:

  • 当前错误引擎机制依赖error函数,在错误引擎相关代码中可以使用error函数抛异常
  • 集成测试或单元测试中允许使用error或assert函数

G.AST.B02 使用pcall或xpcall来调用可能抛异常的函数

【规则说明】

一些skynet库函数大量使用了assert断言,为了避免异常情况下程序退出,应该使用pcall或xpcall来调用这些库函数。例如skynet的socket库和websocket库,均大量使用assert断言。

lua
function socket.read(id, sz)
    local s = socket_pool[id]
    assert(s)
    ...
end

function websocket.write(id, data, fmt, masking_key)
    local ws_obj = assert(ws_pool[id])
    fmt = fmt or "text"
    assert(fmt == "text" or fmt == "binary")
    write_frame(ws_obj, fmt, data, masking_key)
end
  • pcall(f, arg1, arg2, ...):以一种"保护模式"来调用第一个参数,能够捕获执行中的任何错误。
  • xpcall(f, msgh, arg1, arg2, ...):相比pcall增加了一个msgh参数,作为错误处理回调函数。

G.AST.B03 函数的异常处理不能混用return和抛异常两种方法

  • 函数的异常处理要么使用return来返回异常状态;要么使用error错误引擎抛出错误。不可两种方法混用,容易导致接口使用错误。

5 SR配置

5.1 字符串操作

G.SR.STR.B01 string.format的参数不允许使用表达式

SR语法限制。当string.format的参数为表达式时,不返回表达式的结果

lua
【反例】
"Name": "${Slot} |> string.format('Cpu%sChannel0', $1 * 2)"  -- Bad, string.format的参数使用了表达式。若${Slot}为1,结果为"Cpu1 * 2Channel0"
【正例】
"Name": "${Slot} |> expr($1 * 2) |> string.format('Cpu%sChannel0', $1)" -- Good, 将表达式的结果作为string.format的参数。若${Slot}为1,结果为"Cpu2Channel0"