Adapt LuaJIT VPP library and a few examples to API with per-message CRC and JSON

Change-Id: Ibb886e418d338588e61c7bb6cb57f2093c7c5062
Signed-off-by: Andrew Yourtchenko <ayourtch@gmail.com>
diff --git a/vpp-api/lua/bench.lua b/vpp-api/lua/bench.lua
index edff32b..8e5a0b4 100644
--- a/vpp-api/lua/bench.lua
+++ b/vpp-api/lua/bench.lua
@@ -53,12 +53,9 @@
 end
 
 root_dir = "/home/ubuntu/vpp"
-pneum_path = root_dir .. "/build-root/install-vpp_debug-native/vpp-api/lib64/libpneum.so"
-
+pneum_path = root_dir .. "/build-root/install-vpp_lite_debug-native/vpp-api/lib64/libpneum.so"
 vpp:init({ pneum_path = pneum_path })
-
-vpp:consume_api(root_dir .. "/build-root/install-vpp_debug-native/vlib-api/vlibmemory/memclnt.api")
-vpp:consume_api(root_dir .. "/build-root/install-vpp_debug-native/vpp/vpp-api/vpe.api")
+vpp:json_api(root_dir .. "/build-root/install-vpp_lite_debug-native/vpp/vpp-api/vpe.api.json")
 
 vpp:connect("lua-bench")
 local n_tests = 10
diff --git a/vpp-api/lua/examples/cli/lua-cli.lua b/vpp-api/lua/examples/cli/lua-cli.lua
index 1ad5045..b3a24d7 100644
--- a/vpp-api/lua/examples/cli/lua-cli.lua
+++ b/vpp-api/lua/examples/cli/lua-cli.lua
@@ -557,12 +557,14 @@
 
 function init_vpp(vpp)
   local root_dir = "/home/ubuntu/vpp"
-  local pneum_path = root_dir .. "/build-root/install-vpp_debug-native/vpp-api/lib64/libpneum.so"
+  local pneum_path = root_dir .. "/build-root/install-vpp_lite_debug-native/vpp-api/lib64/libpneum.so"
 
   vpp:init({ pneum_path = pneum_path })
 
-  vpp:consume_api(root_dir .. "/build-root/install-vpp_debug-native/vlib-api/vlibmemory/memclnt.api")
-  vpp:consume_api(root_dir .. "/build-root/install-vpp_debug-native/vpp/vpp-api/vpe.api")
+  vpp:init({ pneum_path = pneum_path })
+  vpp:json_api(root_dir .. "/build-root/install-vpp_lite_debug-native/vpp/vpp-api/vpe.api.json")
+
+
 
   vpp:connect("lua_cli")
 end
@@ -652,7 +654,7 @@
   -- print(line, ret)
 end
 
-for msgnum, msgname in ipairs(vpp.msg_number_to_name) do
+for msgnum, msgname in pairs(vpp.msg_number_to_name) do
   local cli, numspaces = string.gsub(msgname, "_", " ")
   device.output["call " .. cli .. " " .. dotdotdot] = function(rl)
     print("ARGH")
diff --git a/vpp-api/lua/examples/example-classifier.lua b/vpp-api/lua/examples/example-classifier.lua
index a3fa45f..ec9c3d3 100644
--- a/vpp-api/lua/examples/example-classifier.lua
+++ b/vpp-api/lua/examples/example-classifier.lua
@@ -20,12 +20,12 @@
 local bit = require("bit")
 
 root_dir = "/home/ubuntu/vpp"
-pneum_path = root_dir .. "/build-root/install-vpp_debug-native/vpp-api/lib64/libpneum.so"
+pneum_path = root_dir .. "/build-root/install-vpp_lite_debug-native/vpp-api/lib64/libpneum.so"
+
 
 vpp:init({ pneum_path = pneum_path })
 
-vpp:consume_api(root_dir .. "/build-root/install-vpp_debug-native/vlib-api/vlibmemory/memclnt.api")
-vpp:consume_api(root_dir .. "/build-root/install-vpp_debug-native/vpp/vpp-api/vpe.api")
+vpp:json_api(root_dir .. "/build-root/install-vpp_lite_debug-native/vpp/vpp-api/vpe.api.json")
 
 vpp:connect("aytest")
 
diff --git a/vpp-api/lua/examples/example-cli.lua b/vpp-api/lua/examples/example-cli.lua
index 656feae..8b84989 100644
--- a/vpp-api/lua/examples/example-cli.lua
+++ b/vpp-api/lua/examples/example-cli.lua
@@ -18,12 +18,11 @@
 vpp = require "vpp-lapi"
 
 root_dir = "/home/ubuntu/vpp"
-pneum_path = root_dir .. "/build-root/install-vpp_debug-native/vpp-api/lib64/libpneum.so"
+pneum_path = root_dir .. "/build-root/install-vpp_lite_debug-native/vpp-api/lib64/libpneum.so"
 
 vpp:init({ pneum_path = pneum_path })
 
-vpp:consume_api(root_dir .. "/build-root/install-vpp_debug-native/vlib-api/vlibmemory/memclnt.api")
-vpp:consume_api(root_dir .. "/build-root/install-vpp_debug-native/vpp/vpp-api/vpe.api")
+vpp:json_api(root_dir .. "/build-root/install-vpp_lite_debug-native/vpp/vpp-api/vpe.api.json")
 
 vpp:connect("aytest")
 
diff --git a/vpp-api/lua/vpp-lapi.lua b/vpp-api/lua/vpp-lapi.lua
index c88b456..ebfd032 100644
--- a/vpp-api/lua/vpp-lapi.lua
+++ b/vpp-api/lua/vpp-lapi.lua
@@ -15,6 +15,155 @@
  */
 ]]
 
+-- json decode/encode from https://gist.github.com/tylerneylon/59f4bcf316be525b30ab
+-- licensed by the author tylerneylon into public domain. Thanks!
+
+local json = {}
+
+-- Internal functions.
+
+local function kind_of(obj)
+  if type(obj) ~= 'table' then return type(obj) end
+  local i = 1
+  for _ in pairs(obj) do
+    if obj[i] ~= nil then i = i + 1 else return 'table' end
+  end
+  if i == 1 then return 'table' else return 'array' end
+end
+
+local function escape_str(s)
+  local in_char  = {'\\', '"', '/', '\b', '\f', '\n', '\r', '\t'}
+  local out_char = {'\\', '"', '/',  'b',  'f',  'n',  'r',  't'}
+  for i, c in ipairs(in_char) do
+    s = s:gsub(c, '\\' .. out_char[i])
+  end
+  return s
+end
+
+-- Returns pos, did_find; there are two cases:
+-- 1. Delimiter found: pos = pos after leading space + delim; did_find = true.
+-- 2. Delimiter not found: pos = pos after leading space;     did_find = false.
+-- This throws an error if err_if_missing is true and the delim is not found.
+local function skip_delim(str, pos, delim, err_if_missing)
+  pos = pos + #str:match('^%s*', pos)
+  if str:sub(pos, pos) ~= delim then
+    if err_if_missing then
+      error('Expected ' .. delim .. ' near position ' .. pos)
+    end
+    return pos, false
+  end
+  return pos + 1, true
+end
+
+-- Expects the given pos to be the first character after the opening quote.
+-- Returns val, pos; the returned pos is after the closing quote character.
+local function parse_str_val(str, pos, val)
+  val = val or ''
+  local early_end_error = 'End of input found while parsing string.'
+  if pos > #str then error(early_end_error) end
+  local c = str:sub(pos, pos)
+  if c == '"'  then return val, pos + 1 end
+  if c ~= '\\' then return parse_str_val(str, pos + 1, val .. c) end
+  -- We must have a \ character.
+  local esc_map = {b = '\b', f = '\f', n = '\n', r = '\r', t = '\t'}
+  local nextc = str:sub(pos + 1, pos + 1)
+  if not nextc then error(early_end_error) end
+  return parse_str_val(str, pos + 2, val .. (esc_map[nextc] or nextc))
+end
+
+-- Returns val, pos; the returned pos is after the number's final character.
+local function parse_num_val(str, pos)
+  local num_str = str:match('^-?%d+%.?%d*[eE]?[+-]?%d*', pos)
+  local val = tonumber(num_str)
+  if not val then error('Error parsing number at position ' .. pos .. '.') end
+  return val, pos + #num_str
+end
+
+
+-- Public values and functions.
+
+function json.stringify(obj, as_key)
+  local s = {}  -- We'll build the string as an array of strings to be concatenated.
+  local kind = kind_of(obj)  -- This is 'array' if it's an array or type(obj) otherwise.
+  if kind == 'array' then
+    if as_key then error('Can\'t encode array as key.') end
+    s[#s + 1] = '['
+    for i, val in ipairs(obj) do
+      if i > 1 then s[#s + 1] = ', ' end
+      s[#s + 1] = json.stringify(val)
+    end
+    s[#s + 1] = ']'
+  elseif kind == 'table' then
+    if as_key then error('Can\'t encode table as key.') end
+    s[#s + 1] = '{'
+    for k, v in pairs(obj) do
+      if #s > 1 then s[#s + 1] = ', ' end
+      s[#s + 1] = json.stringify(k, true)
+      s[#s + 1] = ':'
+      s[#s + 1] = json.stringify(v)
+    end
+    s[#s + 1] = '}'
+  elseif kind == 'string' then
+    return '"' .. escape_str(obj) .. '"'
+  elseif kind == 'number' then
+    if as_key then return '"' .. tostring(obj) .. '"' end
+    return tostring(obj)
+  elseif kind == 'boolean' then
+    return tostring(obj)
+  elseif kind == 'nil' then
+    return 'null'
+  else
+    error('Unjsonifiable type: ' .. kind .. '.')
+  end
+  return table.concat(s)
+end
+
+json.null = {}  -- This is a one-off table to represent the null value.
+
+function json.parse(str, pos, end_delim)
+  pos = pos or 1
+  if pos > #str then error('Reached unexpected end of input.') end
+  local pos = pos + #str:match('^%s*', pos)  -- Skip whitespace.
+  local first = str:sub(pos, pos)
+  if first == '{' then  -- Parse an object.
+    local obj, key, delim_found = {}, true, true
+    pos = pos + 1
+    while true do
+      key, pos = json.parse(str, pos, '}')
+      if key == nil then return obj, pos end
+      if not delim_found then error('Comma missing between object items.') end
+      pos = skip_delim(str, pos, ':', true)  -- true -> error if missing.
+      obj[key], pos = json.parse(str, pos)
+      pos, delim_found = skip_delim(str, pos, ',')
+    end
+  elseif first == '[' then  -- Parse an array.
+    local arr, val, delim_found = {}, true, true
+    pos = pos + 1
+    while true do
+      val, pos = json.parse(str, pos, ']')
+      if val == nil then return arr, pos end
+      if not delim_found then error('Comma missing between array items.') end
+      arr[#arr + 1] = val
+      pos, delim_found = skip_delim(str, pos, ',')
+    end
+  elseif first == '"' then  -- Parse a string.
+    return parse_str_val(str, pos + 1)
+  elseif first == '-' or first:match('%d') then  -- Parse a number.
+    return parse_num_val(str, pos)
+  elseif first == end_delim then  -- End of an object or array.
+    return nil, pos + 1
+  else  -- Parse true, false, or null.
+    local literals = {['true'] = true, ['false'] = false, ['null'] = json.null}
+    for lit_str, lit_val in pairs(literals) do
+      local lit_end = pos + #lit_str - 1
+      if str:sub(pos, lit_end) == lit_str then return lit_val, lit_end + 1 end
+    end
+    local pos_info_str = 'position ' .. pos .. ': ' .. str:sub(pos, pos + 10)
+    error('Invalid json syntax starting at ' .. pos_info_str)
+  end
+end
+
+
 local vpp = {}
 
 local ffi = require("ffi")
@@ -278,6 +427,7 @@
  int pneum_read(char **data, int *l);
  int pneum_write(char *data, int len);
  void pneum_free(char *data);
+ uint32_t pneum_get_msg_index(unsigned char * name);
 ]]
 
   vpp.pneum_path = args.pneum_path
@@ -294,6 +444,7 @@
   vpp.msg_number_to_name = {}
   vpp.msg_number_to_type = {}
   vpp.msg_number_to_pointer_type = {}
+  vpp.msg_name_to_crc = {}
   vpp.c_type_to_fields = {}
   vpp.events = {}
   vpp.plugin_version = {}
@@ -523,6 +674,19 @@
   return vpp
 end
 
+function vpp.resolve_message_number(msgname)
+  local name = msgname .. "_" .. vpp.msg_name_to_crc[msgname]
+  local idx = vpp.pneum.pneum_get_msg_index(vpp.c_str(name))
+  if vpp.debug_dump then
+    print("Index for " .. tostring(name) .. " is " .. tostring(idx))
+  end
+  vpp.msg_name_to_number[msgname] = idx
+  vpp.msg_number_to_name[idx] = msgname
+  vpp.msg_number_to_type[idx] = "vl_api_" .. msgname .. "_t"
+  vpp.msg_number_to_pointer_type[idx] = vpp.msg_number_to_type[idx] .. " *"
+  ffi.cdef("\n\n enum { vl_msg_" .. msgname .. " = " .. idx .. " };\n\n")
+end
+
 function vpp.connect(vpp, client_name)
     local name = "lua_client"
     if client_name then
@@ -532,12 +696,97 @@
     if tonumber(ret) == 0 then
       vpp.is_connected = true
     end
+    for k, v in pairs(vpp.msg_name_to_number) do
+      vpp.resolve_message_number(k)
+    end
   end
 
 function vpp.disconnect(vpp)
     vpp.pneum.pneum_disconnect()
   end
 
+function vpp.json_api(vpp, path, plugin_name)
+    -- print("Consuming the VPP api from "..path)
+    local ffii = {}
+    local f = io.open(path, "r")
+    if not f then
+      print("Could not open " .. path)
+      return nil
+    end
+    local data = f:read("*all")
+    local json = json.parse(data)
+    if not (json.types or json.messages) then
+      print("Can not parse " .. path)
+      return nil
+    end
+
+    local all_types = {}
+
+    for i, v in ipairs(json.types) do
+      table.insert(all_types, { typeonly = 1, desc = v })
+    end
+    for i, v in ipairs(json.messages) do
+      table.insert(all_types, { typeonly = 0, desc = v })
+    end
+    for i, v in ipairs(all_types) do
+      local typeonly = v.typeonly
+      local name = v.desc[1]
+      local c_type = "vl_api_" .. name .. "_t"
+
+      local fields = {}
+      -- vpp.msg_name_to_fields[name] = fields
+      -- print("CTYPE " .. c_type)
+      vpp.c_type_to_fields[c_type] = fields
+      vpp.t_lua2c[c_type] = vpp.t_lua2c["__MSG__"]
+      vpp.t_c2lua[c_type] = vpp.t_c2lua["__MSG__"]
+
+      local cdef = { "\n\n#pragma pack(1)\ntypedef struct _vl_api_", name, " {\n" }
+      for ii, vv in ipairs(v.desc) do
+        if type(vv) == "table" then
+          if vv.crc then
+            vpp.msg_name_to_crc[name] = string.sub(vv.crc, 3) -- strip the leading 0x
+          else
+            local fieldtype = vv[1]
+            local fieldname = vv[2]
+            local fieldcount = vv[3]
+            local fieldcountvar = vv[4]
+            local fieldrec = { name = fieldname, c_type = fieldtype, array = fieldcount, array_size = fieldcountvar }
+            if fieldcount then
+              table.insert(cdef, "  " .. fieldtype .. " " .. fieldname .. "[" .. fieldcount .. "];\n")
+              if fieldtype == "u8" then
+                -- any array of bytes is treated as a string
+              elseif vpp.t_lua2c[fieldtype] then
+                -- print("Array of " .. fieldtype .. " is ok!")
+              else
+                print("Unknown array type: ", name,  " : " , fieldname, " : ", fieldtype, ":", fieldcount, ":", fieldcountvar)
+              end
+            else
+              table.insert(cdef, "  " .. fieldtype .. " " .. fieldname .. ";\n")
+            end
+            fields[fieldname] = fieldrec
+          end
+        end
+      end
+
+      table.insert(cdef, "} vl_api_" .. name .. "_t;")
+      table.insert(ffii, table.concat(cdef))
+
+      if typeonly == 0 then
+        -- we will want to resolve this later
+        if vpp.debug_dump then
+          print("Remember to resolve " .. name)
+        end
+        vpp.msg_name_to_number[name] = -1
+        if vpp.is_connected then
+          vpp.resolve_message_number(name)
+        end
+      end
+
+    end
+    local cdef_full = table.concat(ffii)
+    ffi.cdef(cdef_full)
+end
+
 function vpp.consume_api(vpp, path, plugin_name)
     -- print("Consuming the VPP api from "..path)
     local ffii = {}
@@ -636,7 +885,8 @@
   if lua2c then
     return(lua2c(c_type, src, dst_c_ptr))
   else
-    print("vpp.lua2c: do not know how to store type " .. c_type)
+    print("vpp.lua2c: do not know how to store type " .. tostring(c_type))
+    local x = "a" .. nil
     return 0
   end
 end
@@ -708,6 +958,9 @@
     local cstruct = ""
     local options = options_in or {}
     if msg_num then
+      if vpp.debug_dump then
+        print("Message #" .. tostring(msg_num) .. " for name " .. tostring(api_name))
+      end
       vpp:api_write(api_name, req_table)
       if not vpp.msg_name_to_number[end_message_name] or options.force_ping then
         end_message_name = "control_ping_reply"