作者:糖果

LazyTable源码

local ngx_req = {
      headers = function()
          return "testcase"
      end,
      method = function()
          return "GET"
      end,
}


local lazy_tbl
lazy_tbl = function(tbl, index)
  return setmetatable(tbl, {
    __index = function(self, key)
      for k,v in pairs(index) do
          print(k, v)
      end
      --print(key)
      for k,v in pairs(self) do
          print(k, v)
      end
      local fn = index[key]
      if fn then
        do
          --此处fn(self)和fn(),传入self形参与否的效果是一样,table中的匿名函数
          --定义是没有参数的,这个形参是moonscript翻译后加上去的。
          local res = fn(self)
          --local res = fn(self)        
          local res = fn()
          self[key] = res
          --实际把return res去掉也不会影响这个程序的运行结果, 在此函数中
          --从了这个return语句再也没有return调用了,而设定工作在self[key] = res这句就已经完成。
          return res
        end
      end
    end
  })
end


local build_request
build_request = function(unlazy)
  if unlazy == nil then
    unlazy = false
    --unlazy = true
  end
  do
    local t = lazy_tbl({ }, ngx_req)
    if unlazy then
      for k in pairs(ngx_req) do
        --这里遍历的ngx_req,但是调用的函数是t的。
        local _ = t[k]
      end
    end
    return t
  end
end


req = build_request("unlazy")


for k,v in pairs(req) do
    print(k, v)
end

重置setmetatable

在一个匿名函数中,return setmetatable,通过对函数形参传入的table的变量的__index属 性进行统一修改,而新设定的__index对应函数的形参self和key,分别对应新table的本身, 和table对应的key。

 __index = function(self, key)
  lazy_tbl({ }, ngx_req)

self:是ngx_req的引用。 key:是ngx_req的key。

如果发现,ngx_req的元素类型是function就执行下,然后,把返回结果(字符串) 替换原value值。

核心原理

lazy table实现的核心部分是,是在return setmetatable做table复制时,并统一的设定 新table的__index属性, 然后在遍历我们要批量设定的table时,得用table的__index对 应函数中的设置,修改要修改table的成员变量值,把table中,对应key的值是function 的元素,改成key的值等于,这个key对应匿名函数的返回值。

修改table元素的是由setmetatable指定的__index方法来完成。而遍历循环执行table中的 所有匿名函数,由一次外层的循环来完成。

moonscript的lua翻译特征

do end 结构是为了让local型的局部变量,在 do end 结构外不可见。

 do 
    local tmp = 1
 end 
 print(tmp)

结果是:nil

 do 
    tmp = 1
 end 
 print(tmp)

结果是:1

下面的代码有明显的moonscript翻译成lua的特征:

local test 
test = function()
    do  
        local tmp = "do end"
        return tmp 
    end 
    return "ret"
end

ret = test()
print(ret)

简化后的LazyTable代码

local ngx = { 
    url = function() 
        return "url"
    end,
    method = function()
        return "method"
    end,
}

local lazy

lazy = function(tbl, index)
    return setmetatable (tbl, {
        __index = function(self, key)
            print(key)
        end 
    })  
end


local build_request
build_request = function(unlazy)
        ret = lazy({}, ngx)
        for k in pairs(ngx) do
             local _ = ret[k]
        end
end

build_request("")

lazy的核心是通过lazy函数,重新设置table的__index对应函数 ,在函数中调用元素值是function类型的函数,用函数返回结果修 改当前元素的value值。

通过LazyTable可把Table表的声明和元素值的动态设定在一个 封装调用周期内完成。

应用场景

LazyTable在应用在Lapis的获取nginx变量的一个功能,我们把这个功能 移到HiLua演示框架里,看LazyTable如何在实际应用场景应用的。

local ngx_request = {
  headers = function()
    return ngx.req.get_headers()
  end,
  cmd_meth = function()
    return ngx.var.request_method
  end,
  cmd_url = function()
    return ngx.var.request_uri
  end,
}

local lazy_tbl
lazy_tbl = function(tbl, index)
  return setmetatable(tbl, {
    __index = function(self, key)
      local fn = index[key]
      if fn then
        do  
          local res = fn(self)
          self[key] = res 
          return res 
        end 
      end 
    end 
  })  
end

return ngx_request

Moonscript是有OO的类的组织结构,但是用Moonscript写的Lapis框架,有的一些文件并没有 按类的形式组织,比如上面这个截取的代码,最后返回的就是一个ngx_request的table,通过 引用table中的函数类型元素进行调用,即可返回你想要的数据,这个类似之前lapis ES的写法 一个孙数的调用过程,可以整个组织成一个table声明的过程,在其过程中就完成了各种行为的 调用,看起来很整洁,调用时序隐含在声明里,而不是传统的若干个函数过程顺序调用的方式。

在一个数据结构中定义若干函数的调用,要是和路由器比较的话,路由器需要正则匹配判断 而这个是顺序执行。

在HiLua中的调用形式如下:

local req = require "nginx"

app:get("/hilua", function(request,id)
    ngx.say(type(req.cmd_meth))
end

nginx中ngx_request取得HTTP请求阶段的各种变量,这个数据结构在HiLua进行路由匹配处理 从这个数据结构中,取得相关变量。

local req = require "nginx"
function Route:run(router)
        local url = req.cmd_url()
        local method = req.cmd_meth()
end

LazyTable本身是一种设计实现的思路,可以用Moonscript实现,也可以用Lua实现,也可以 C/CPP实现。

作者:糖果

Lapis使用JSON解析的代层库就是CJSON。

遍历Table变量的所有元素。

util.moon

json_encodable = (obj, seen={}) ->
  switch type obj
    when "table"
      unless seen[obj]
        seen[obj] = true
        { k, json_encodable(v) for k,v in pairs(obj) when type(k) == "string" or type(k) == "number" }
    when "function", "userdata", "thread"
      nil
    else
      obj

to_json = (obj) -> json.encode json_encodable obj
from_json = (obj) -> json.decode obj

json_encodable递归调用检查形参,seen={} 在形参列表里的应用。 检查出所有字符中含有的JSON数据,放入Table返回。

util.lua

json_encodable = function(obj, seen)
  if seen == nil then
    seen = { }
  end
  local _exp_0 = type(obj)
  if "table" == _exp_0 then
    if not (seen[obj]) then
      seen[obj] = true
      local _tbl_0 = { }
      for k, v in pairs(obj) do
        if type(k) == "string" or type(k) == "number" then
          _tbl_0[k] = json_encodable(v)
        end
      end
      return _tbl_0
    end
  elseif "function" == _exp_0 or "userdata" == _exp_0 or "thread" == _exp_0 then
    return nil
  else
    return obj
  end
end

to_json = function(obj)
  return json.encode(json_encodable(obj))
end

from_json = function(obj)
  return json.decode(obj)
end

Lua版因为没有像MoonScript支持When语句,被翻译成了很多的if elseif end语句。

Lapis调用的JSON基础就是lua-cjson这个库,这个库同样有一个类似的递归调用就是 :serialise_value。


local function is_array(table)
    local max = 0 
    local count = 0 
    for k, v in pairs(table) do
        if type(k) == "number" then
            if k > max then max = k end 
            count = count + 1 
        else
            return -1
        end 
    end 
    if max > count * 2 then
        return -1
    end 

    return max 
end
local serialise_value

local function serialise_table(value, indent, depth)
    local spacing, spacing2, indent2
    if indent then
        spacing = "\n" .. indent
        spacing2 = spacing .. "  "
        indent2 = indent .. "  "
    else
        spacing, spacing2, indent2 = " ", " ", false
    end 
    depth = depth + 1 
    if depth > 50 then
        return "Cannot serialise any further: too many nested tables"
    end

    local max = is_array(value)

    local comma = false
    local fragment = { "{" .. spacing2 }

    if max > 0 then
        -- Serialise array
        for i = 1, max do
            if comma then
                table.insert(fragment, "," .. spacing2)
            end
            table.insert(fragment, serialise_value(value[i], indent2, depth))
            comma = true
        end
    elseif max < 0 then
        -- Serialise table
        for k, v in pairs(value) do
            if comma then
                table.insert(fragment, "," .. spacing2)
            end
            table.insert(fragment,
                ("[%s] = %s"):format(serialise_value(k, indent2, depth),
                                     serialise_value(v, indent2, depth)))
            comma = true
        end
    end
    table.insert(fragment, spacing .. "}")

    return table.concat(fragment)
end



function serialise_value(value, indent, depth)
    if indent == nil then indent = "" end
    if depth == nil then depth = 0 end

    if value == json.null then
        return "json.null"
    elseif type(value) == "string" then
        return ("%q"):format(value)
    elseif type(value) == "nil" or type(value) == "number" or
           type(value) == "boolean" then
        return tostring(value)
    elseif type(value) == "table" then
        return serialise_table(value, indent, depth)
    else
        return "\"<" .. type(value) .. ">\""
    end
end

调用序列化,如果table变量里的value还是table就递归的调用serialise_value函数自己。

meta_info = { 
    key = "test key:",
    values = { 
        k = "key",
        v = "value"
    },  
    testcase = "null"
}

print(serialise_value(meta_info))

以上函数是直接从CJSON中提取出来的,可以遍历任意的table,相关于pprint这种功能。

打印结果如下:

{
  ["testcase"] = json.null,
  ["key"] = "test key:",
  ["values"] = {
    ["k"] = "key",
    ["v"] = "value"
  }
}

cjson.null与nil、NULL是否等价。

下面是@hambut老师的测试代码:

local cjson = require "cjson"

local s = [[{"key":null,"key1":"value"}]]
local sd = cjson.decode(s)
sd.key2 = "value2"

ngx.say(cjson.encode(sd))

打印结果如下:

{"key1":"value","key":null,"key2":"value2"}

再做一个实验, 我们直接写一个so库,同句函数cjson_new,只返回一个table结构数据。

extern int cjson_new(lua_State* L); 

static luaL_Reg libtangguo[] = { 
    {"cjson_new", cjson_new},
    {NULL, NULL}
};


int cjson_new(lua_State* L) {
    lua_newtable(L);
    /* Set cjson.null */
    lua_pushlightuserdata(L, NULL);
    lua_setfield(L, -2, "null");
    return 1;
}

cjson.null 就是.so库返回的一个table变量,中的一个key名为 “null”, value是userdata(nil) 的userdata类型的table元素,lightuserdata不归GC管理,就是一个的指针,一般就用于和 其它的lightsuerdata比较, 而这个其它的lightuserdata变量,一般也都是C产生的,是产生 lightuserdata之间比,不是C的lightuserdata和lua的lightuserdata比法。

lightuserdata和userdata也不一样, lightuserdata是指针,userdata是buffer。

local cjson_new = package.loadlib("libtangguo.so", "cjson_new")
local cjson = cjson_new()

for k,v in pairs(cjson) do
    print(k, v, type(v))
end

print(cjson.null)
tmp = cjson.null
print(tmp, type(tmp))


if cjson.null == nil then
    print("OK")
end

if cjson.null == "null" then
    print("OK")
end


if cjson.null == tmp then
    print("OK")
end

cjson.null不是nil, 更不是”null”。数据在内存空间的存储形式是不一样的,但表示的现实意义是一样的,表示“啥也没有”。

为了进一步说明是这样的,我们直接看一下pushlightusredata的C源码:

lapi.c

/*
** Union of all Lua values
*/
typedef union Value {
  GCObject *gc;    /* collectable objects */
  void *p;         /* light userdata */
  int b;           /* booleans */
  lua_CFunction f; /* light C functions */
  lua_Integer i;   /* integer numbers */
  lua_Number n;    /* float numbers */
} Value;


LUA_API void lua_pushlightuserdata (lua_State *L, void *p) {
  lua_lock(L);
  setpvalue(L->top, p);
  api_incr_top(L);
  lua_unlock(L);
}

cjson.so

    lua_newtable(L);
    lua_pushlightuserdata(L, NULL);
    lua_setfield(L, -2, "null");

这样在实际调用时, setpvalue(L->top, p); 相当于 void *p = NULL, 最后是被封装到table变量里返回的。

作者:糖果

上一篇是用.so作为框架的库,这是接上回,用MoonScript实现库。

在HiLua工程中,创建/libs/moon目录,建立MoonScript库代码,如下:

HiLog.moon

class HiLog
    @log: =>
        print("HiLog...")
        return "HiLog..."

HiLog.lua

local HiLog
do
  local _class_0
  local _base_0 = { } 
  _base_0.__index = _base_0
  _class_0 = setmetatable({
    __init = function() end,
    __base = _base_0,
    __name = "HiLog"
  }, {
    __index = _base_0,
    __call = function(cls, ...)
      local _self_0 = setmetatable({}, _base_0)
      cls.__init(_self_0, ...)
      return _self_0
    end 
  })  
  _base_0.__class = _class_0
  local self = _class_0
  self.log = function(self)
    print("HiLog...")
    return "HiLog..."
  end 
  HiLog = _class_0
  return _class_0
end

创建新工程与app:

hi project Test-HiLua
hi app Test-HiLua

修改一下app.lua

require "log"
local HiLog = require "HiLog"
local Application = require "orc"
app = Application.new()

app:get("/hilua", function(request,id)
    ret = HiLog:log()   
    ngx.say(ret)
    ngx.say('hilua') 
end)

return app.run()

库可以用C写生成SO共享库,也可以用MoonScript翻译成Lua,然后与框架路由结合起 来,这种依赖就是纯调用依赖关联,尽量不产生数据关联。

原文地址:

源码地址: