api: enforce vla is last and fixed string type

Enforce that variable length fields are the last element of API messages.

Add a 'fixed' version of string type, since dealing with
multiple variable length strings turned out too painful
for the C language bindings.

The string type is now:
{
  string name[64]; // NUL terminated C-string. Essentially decays to u8 name[64]
  string name[];   // Variable length string with embedded len field (vl_api_string_t)
};

The latter notation could be made available to other types as well.
e.g.
{
  vl_api_address_t addresses[];
}
instead of

{
  u32 n_addr;
  vl_api_address_t addresses[n_addr];
};

Type: fix
Change-Id: I18fa17ef47227633752ab50453e8d20a652a9f9b
Signed-off-by: Ole Troan <ot@cisco.com>
diff --git a/src/tools/vppapigen/vppapigen.py b/src/tools/vppapigen/vppapigen.py
index c01fa40..d0391cd 100755
--- a/src/tools/vppapigen/vppapigen.py
+++ b/src/tools/vppapigen/vppapigen.py
@@ -152,6 +152,25 @@
                 self.manual_endian = True
         global_type_add(name, self)
 
+        self.vla = False
+
+        for i, b in enumerate(block):
+            if isinstance(b, Array):
+                if b.length == 0:
+                    self.vla = True
+                    if i + 1 < len(block):
+                        raise ValueError(
+                            'VLA field "{}" must be the last '
+                            'field in message "{}"'
+                            .format(b.fieldname, name))
+            elif b.fieldtype == 'string':
+                self.vla = True
+                if i + 1 < len(block):
+                    raise ValueError(
+                        'VLA field "{}" must be the last '
+                        'field in message "{}"'
+                        .format(b.fieldname, name))
+
     def __repr__(self):
         return self.name + str(self.flags) + str(self.block)
 
@@ -159,12 +178,13 @@
 class Using():
     def __init__(self, name, alias):
         self.name = name
+        self.vla = False
 
         if isinstance(alias, Array):
-            a = { 'type': alias.fieldtype,  # noqa: E201
-                  'length': alias.length }  # noqa: E202
+            a = {'type': alias.fieldtype,
+                 'length': alias.length}
         else:
-            a = { 'type': alias.fieldtype }  # noqa: E201,E202
+            a = {'type': alias.fieldtype}
         self.alias = a
         self.crc = str(alias).encode()
         global_type_add(name, self)
@@ -208,12 +228,29 @@
             elif f == 'autoreply':
                 self.autoreply = True
 
-        for b in block:
+        for i, b in enumerate(block):
             if isinstance(b, Option):
                 if b[1] == 'singular' and b[2] == 'true':
                     self.singular = True
                 block.remove(b)
 
+            if isinstance(b, Array) and b.vla and i + 1 < len(block):
+                raise ValueError(
+                    'VLA field "{}" must be the last field in message "{}"'
+                    .format(b.fieldname, name))
+            elif b.fieldtype.startswith('vl_api_'):
+                if (global_types[b.fieldtype].vla and i + 1 < len(block)):
+                    raise ValueError(
+                        'VLA field "{}" must be the last '
+                        'field in message "{}"'
+                        .format(b.fieldname, name))
+            elif b.fieldtype == 'string' and b.length == 0:
+                if i + 1 < len(block):
+                    raise ValueError(
+                        'VLA field "{}" must be the last '
+                        'field in message "{}"'
+                        .format(b.fieldname, name))
+
     def __repr__(self):
         return self.name + str(self.flags) + str(self.block)
 
@@ -222,6 +259,7 @@
     def __init__(self, name, block, enumtype='u32'):
         self.name = name
         self.enumtype = enumtype
+        self.vla = False
 
         count = 0
         for i, b in enumerate(block):
@@ -272,16 +310,19 @@
 
 
 class Array():
-    def __init__(self, fieldtype, name, length):
+    def __init__(self, fieldtype, name, length, modern_vla=False):
         self.type = 'Array'
         self.fieldtype = fieldtype
         self.fieldname = name
+        self.modern_vla = modern_vla
         if type(length) is str:
             self.lengthfield = length
             self.length = 0
+            self.vla = True
         else:
             self.length = length
             self.lengthfield = None
+            self.vla = False
 
     def __repr__(self):
         return str([self.fieldtype, self.fieldname, self.length,
@@ -292,6 +333,11 @@
     def __init__(self, fieldtype, name, limit=None):
         self.type = 'Field'
         self.fieldtype = fieldtype
+
+        if self.fieldtype == 'string':
+            raise ValueError("The string type {!r} is an "
+                             "array type ".format(name))
+
         if name in keyword.kwlist:
             raise ValueError("Fieldname {!r} is a python keyword and is not "
                              "accessible via the python API. ".format(name))
@@ -521,13 +567,18 @@
         if len(p) == 2:
             p[0] = p[1]
         else:
-            p[0] = { **p[1], **p[2] }
+            p[0] = {**p[1], **p[2]}
 
     def p_field_option(self, p):
-        '''field_option : ID '=' assignee ','
+        '''field_option : ID
+                        | ID '=' assignee ','
                         | ID '=' assignee
+
         '''
-        p[0] = { p[1]: p[3] }
+        if len(p) == 2:
+            p[0] = {p[1]: None}
+        else:
+            p[0] = {p[1]: p[3]}
 
     def p_declaration(self, p):
         '''declaration : type_specifier ID ';'
@@ -540,9 +591,14 @@
             self._parse_error('ERROR')
         self.fields.append(p[2])
 
+    def p_declaration_array_vla(self, p):
+        '''declaration : type_specifier ID '[' ']' ';' '''
+        p[0] = Array(p[1], p[2], 0, modern_vla=True)
+
     def p_declaration_array(self, p):
         '''declaration : type_specifier ID '[' NUM ']' ';'
                        | type_specifier ID '[' ID ']' ';' '''
+
         if len(p) != 7:
             return self._parse_error(
                 'array: %s' % p.value,
@@ -780,7 +836,7 @@
             try:
                 crc = crc_block_combine(t.block, crc)
                 return foldup_blocks(t.block, crc)
-            except:
+            except AttributeError:
                 pass
     return crc
 
diff --git a/src/tools/vppapigen/vppapigen_c.py b/src/tools/vppapigen/vppapigen_c.py
index 19efc99..c1bc11d 100644
--- a/src/tools/vppapigen/vppapigen_c.py
+++ b/src/tools/vppapigen/vppapigen_c.py
@@ -153,8 +153,15 @@
                     if b.lengthfield:
                         output += "    %s %s[0];\n" % (api2c(b.fieldtype), b.fieldname)
                     else:
-                        output += "    %s %s[%s];\n" % (api2c(b.fieldtype), b.fieldname,
-                                                        b.length)
+                        # Fixed length strings decay to nul terminated u8
+                        if b.fieldtype == 'string':
+                            if b.modern_vla:
+                                output += '    {} {};\n'.format(api2c(b.fieldtype), b.fieldname)
+                            else:
+                                output += '    u8 {}[{}];\n'.format(b.fieldname, b.length)
+                        else:
+                            output += "    %s %s[%s];\n" % (api2c(b.fieldtype), b.fieldname,
+                                                            b.length)
                 else:
                     raise ValueError("Error in processing array type %s" % b)