api: split vl_api_prefix into two
[vpp.git] / src / tools / vppapigen / vppapigen.py
index 9d04ec2..861b71d 100755 (executable)
@@ -1,13 +1,16 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
 
-from __future__ import print_function
 import ply.lex as lex
 import ply.yacc as yacc
 import sys
 import argparse
+import keyword
 import logging
 import binascii
 import os
+import sys
+
+log = logging.getLogger('vppapigen')
 
 # Ensure we don't leave temporary files around
 sys.dont_write_bytecode = True
@@ -18,15 +21,17 @@ sys.dont_write_bytecode = True
 
 # Global dictionary of new types (including enums)
 global_types = {}
-global_crc = 0
+
+seen_imports = {}
 
 
-def global_type_add(name):
+def global_type_add(name, obj):
     '''Add new type to the dictionary of types '''
     type_name = 'vl_api_' + name + '_t'
     if type_name in global_types:
-        raise KeyError('Type is already defined: {}'.format(name))
-    global_types[type_name] = True
+        raise KeyError("Attempted redefinition of {!r} with {!r}.".format(
+            name, obj))
+    global_types[type_name] = obj
 
 
 # All your trace are belong to us!
@@ -79,10 +84,23 @@ class VPPAPILexer(object):
 
     t_ignore_LINE_COMMENT = '//.*'
 
+    def t_FALSE(self, t):
+        r'false'
+        t.value = False
+        return t
+
+    def t_TRUE(self, t):
+        r'false'
+        t.value = True
+        return t
+
     def t_NUM(self, t):
-        r'0[xX][0-9a-fA-F]+|\d+'
+        r'0[xX][0-9a-fA-F]+|-?\d+\.?\d*'
         base = 16 if t.value.startswith('0x') else 10
-        t.value = int(t.value, base)
+        if '.' in t.value:
+            t.value = float(t.value)
+        else:
+            t.value = int(t.value, base)
         return t
 
     def t_ID(self, t):
@@ -122,22 +140,52 @@ class VPPAPILexer(object):
     t_ignore = ' \t'
 
 
+def crc_block_combine(block, crc):
+    s = str(block).encode()
+    return binascii.crc32(s, crc) & 0xffffffff
+
+
+def vla_is_last_check(name, block):
+    vla = False
+    for i, b in enumerate(block):
+        if isinstance(b, Array) and b.vla:
+            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.startswith('vl_api_'):
+            if global_types[b.fieldtype].vla:
+                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' and b.length == 0:
+            vla = True
+            if i + 1 < len(block):
+                raise ValueError(
+                    'VLA field "{}" must be the last '
+                    'field in message "{}"'
+                    .format(b.fieldname, name))
+    return vla
+
+
 class Service():
-    def __init__(self, caller, reply, events=[], stream=False):
+    def __init__(self, caller, reply, events=None, stream=False):
         self.caller = caller
         self.reply = reply
         self.stream = stream
-        self.events = events
+        self.events = [] if events is None else events
 
 
 class Typedef():
     def __init__(self, name, flags, block):
-        global global_crc
         self.name = name
         self.flags = flags
         self.block = block
-        self.crc = binascii.crc32(str(block)) & 0xffffffff
-        global_crc = binascii.crc32(str(block), global_crc)
+        self.crc = str(block).encode()
         self.manual_print = False
         self.manual_endian = False
         for f in flags:
@@ -145,23 +193,62 @@ class Typedef():
                 self.manual_print = True
             elif f == 'manual_endian':
                 self.manual_endian = True
-        global_type_add(name)
+
+        global_type_add(name, self)
+
+        self.vla = vla_is_last_check(name, block)
 
     def __repr__(self):
         return self.name + str(self.flags) + str(self.block)
 
 
+class Using():
+    def __init__(self, name, flags, alias):
+        self.name = name
+        self.vla = False
+        self.block = []
+        self.manual_print = True
+        self.manual_endian = True
+
+        self.manual_print = False
+        self.manual_endian = False
+        for f in flags:
+            if f == 'manual_print':
+                self.manual_print = True
+            elif f == 'manual_endian':
+                self.manual_endian = True
+
+        if isinstance(alias, Array):
+            a = {'type': alias.fieldtype,
+                 'length': alias.length}
+        else:
+            a = {'type': alias.fieldtype}
+        self.alias = a
+        self.crc = str(alias).encode()
+        global_type_add(name, self)
+
+    def __repr__(self):
+        return self.name + str(self.alias)
+
+
 class Union():
-    def __init__(self, name, block):
+    def __init__(self, name, flags, block):
         self.type = 'Union'
         self.manual_print = False
         self.manual_endian = False
-        global global_crc
         self.name = name
+
+        for f in flags:
+            if f == 'manual_print':
+                self.manual_print = True
+            elif f == 'manual_endian':
+                self.manual_endian = True
+
         self.block = block
-        self.crc = binascii.crc32(str(block)) & 0xffffffff
-        global_crc = binascii.crc32(str(block), global_crc)
-        global_type_add(name)
+        self.crc = str(block).encode()
+        self.vla = vla_is_last_check(name, block)
+
+        global_type_add(name, self)
 
     def __repr__(self):
         return str(self.block)
@@ -169,12 +256,10 @@ class Union():
 
 class Define():
     def __init__(self, name, flags, block):
-        global global_crc
         self.name = name
         self.flags = flags
         self.block = block
-        self.crc = binascii.crc32(str(block)) & 0xffffffff
-        global_crc = binascii.crc32(str(block), global_crc)
+        self.crc = str(block).encode()
         self.dont_trace = False
         self.manual_print = False
         self.manual_endian = False
@@ -195,6 +280,7 @@ class Define():
                 if b[1] == 'singular' and b[2] == 'true':
                     self.singular = True
                 block.remove(b)
+        self.vla = vla_is_last_check(name, block)
 
     def __repr__(self):
         return self.name + str(self.flags) + str(self.block)
@@ -202,9 +288,9 @@ class Define():
 
 class Enum():
     def __init__(self, name, block, enumtype='u32'):
-        global global_crc
         self.name = name
         self.enumtype = enumtype
+        self.vla = False
 
         count = 0
         for i, b in enumerate(block):
@@ -215,39 +301,54 @@ class Enum():
                 block[i] = [b, count]
 
         self.block = block
-        self.crc = binascii.crc32(str(block)) & 0xffffffff
-        global_crc = binascii.crc32(str(block), global_crc)
-        global_type_add(name)
+        self.crc = str(block).encode()
+        global_type_add(name, self)
 
     def __repr__(self):
         return self.name + str(self.block)
 
 
 class Import():
-    def __init__(self, filename):
-        self.filename = filename
 
-        # Deal with imports
-        parser = VPPAPI(filename=filename)
-        dirlist = dirlist_get()
-        f = filename
-        for dir in dirlist:
-            f = os.path.join(dir, filename)
-            if os.path.exists(f):
-                break
-        with open(f) as fd:
-            self.result = parser.parse_file(fd, None)
+    def __new__(cls, *args, **kwargs):
+        if args[0] not in seen_imports:
+            instance = super().__new__(cls)
+            instance._initialized = False
+            seen_imports[args[0]] = instance
+
+        return seen_imports[args[0]]
+
+    def __init__(self, filename):
+        if self._initialized:
+            return
+        else:
+            self.filename = filename
+            # Deal with imports
+            parser = VPPAPI(filename=filename)
+            dirlist = dirlist_get()
+            f = filename
+            for dir in dirlist:
+                f = os.path.join(dir, filename)
+                if os.path.exists(f):
+                    break
+            if sys.version[0] == '2':
+                with open(f) as fd:
+                    self.result = parser.parse_file(fd, None)
+            else:
+                with open(f, encoding='utf-8') as fd:
+                    self.result = parser.parse_file(fd, None)
+            self._initialized = True
 
     def __repr__(self):
         return self.filename
 
 
 class Option():
-    def __init__(self, option):
-        global global_crc
+    def __init__(self, option, value):
+        self.type = 'Option'
         self.option = option
-        self.crc = binascii.crc32(str(option)) & 0xffffffff
-        global_crc = binascii.crc32(str(option), global_crc)
+        self.value = value
+        self.crc = str(option).encode()
 
     def __repr__(self):
         return str(self.option)
@@ -257,16 +358,19 @@ class Option():
 
 
 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,
@@ -274,10 +378,19 @@ class Array():
 
 
 class Field():
-    def __init__(self, fieldtype, name):
+    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))
         self.fieldname = name
+        self.limit = limit
 
     def __repr__(self):
         return str([self.fieldtype, self.fieldname])
@@ -431,7 +544,9 @@ class VPPAPIParser(object):
         '''define : flist DEFINE ID '{' block_statements_opt '}' ';' '''
         # Legacy typedef
         if 'typeonly' in p[1]:
-            p[0] = Typedef(p[3], p[1], p[5])
+            self._parse_error('legacy typedef. use typedef: {} {}[{}];'
+                              .format(p[1], p[2], p[4]),
+                              self._token_coord(p, 1))
         else:
             p[0] = Define(p[3], p[1], p[5])
 
@@ -457,6 +572,18 @@ class VPPAPIParser(object):
         '''typedef : TYPEDEF ID '{' block_statements_opt '}' ';' '''
         p[0] = Typedef(p[2], [], p[4])
 
+    def p_typedef_flist(self, p):
+        '''typedef : flist TYPEDEF ID '{' block_statements_opt '}' ';' '''
+        p[0] = Typedef(p[3], p[1], p[5])
+
+    def p_typedef_alias(self, p):
+        '''typedef : TYPEDEF declaration '''
+        p[0] = Using(p[2].fieldname, [], p[2])
+
+    def p_typedef_alias_flist(self, p):
+        '''typedef : flist TYPEDEF declaration '''
+        p[0] = Using(p[3].fieldname, p[1], p[3])
+
     def p_block_statements_opt(self, p):
         '''block_statements_opt : block_statements '''
         p[0] = p[1]
@@ -476,7 +603,7 @@ class VPPAPIParser(object):
 
     def p_enum_statements(self, p):
         '''enum_statements : enum_statement
-                            | enum_statements enum_statement'''
+                           | enum_statements enum_statement'''
         if len(p) == 2:
             p[0] = [p[1]]
         else:
@@ -490,16 +617,44 @@ class VPPAPIParser(object):
         else:
             p[0] = p[1]
 
+    def p_field_options(self, p):
+        '''field_options : field_option
+                           | field_options field_option'''
+        if len(p) == 2:
+            p[0] = p[1]
+        else:
+            p[0] = {**p[1], **p[2]}
+
+    def p_field_option(self, p):
+        '''field_option : ID
+                        | ID '=' assignee ','
+                        | ID '=' assignee
+
+        '''
+        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 ';' '''
-        if len(p) != 4:
+        '''declaration : type_specifier ID ';'
+                       | type_specifier ID '[' field_options ']' ';' '''
+        if len(p) == 7:
+            p[0] = Field(p[1], p[2], p[4])
+        elif len(p) == 4:
+            p[0] = Field(p[1], p[2])
+        else:
             self._parse_error('ERROR')
         self.fields.append(p[2])
-        p[0] = Field(p[1], 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,
@@ -521,7 +676,7 @@ class VPPAPIParser(object):
 
     def p_option(self, p):
         '''option : OPTION ID '=' assignee ';' '''
-        p[0] = Option([p[1], p[2], p[4]])
+        p[0] = Option(p[2], p[4])
 
     def p_assignee(self, p):
         '''assignee : NUM
@@ -554,7 +709,11 @@ class VPPAPIParser(object):
 
     def p_union(self, p):
         '''union : UNION ID '{' block_statements_opt '}' ';' '''
-        p[0] = Union(p[2], p[4])
+        p[0] = Union(p[2], [], p[4])
+
+    def p_union_flist(self, p):
+        '''union : flist UNION ID '{' block_statements_opt '}' ';' '''
+        p[0] = Union(p[3], p[1], p[5])
 
     # Error rule for syntax errors
     def p_error(self, p):
@@ -594,8 +753,13 @@ class VPPAPI(object):
         s['Service'] = []
         s['types'] = []
         s['Import'] = []
+        crc = 0
         for o in objs:
             tname = o.__class__.__name__
+            try:
+                crc = binascii.crc32(o.crc, crc)
+            except AttributeError:
+                pass
             if isinstance(o, Define):
                 s[tname].append(o)
                 if o.autoreply:
@@ -606,11 +770,15 @@ class VPPAPI(object):
                 for o2 in o:
                     if isinstance(o2, Service):
                         s['Service'].append(o2)
-            elif isinstance(o, Enum) or isinstance(o, Typedef) or isinstance(o, Union):
+            elif (isinstance(o, Enum) or
+                  isinstance(o, Typedef) or
+                  isinstance(o, Using) or
+                  isinstance(o, Union)):
                 s['types'].append(o)
             else:
                 if tname not in s:
-                    raise ValueError('Unknown class type: {} {}'.format(tname, o))
+                    raise ValueError('Unknown class type: {} {}'
+                                     .format(tname, o))
                 s[tname].append(o)
 
         msgs = {d.name: d for d in s['Define']}
@@ -618,6 +786,8 @@ class VPPAPI(object):
         replies = {s.reply: s for s in s['Service']}
         seen_services = {}
 
+        s['file_crc'] = crc
+
         for service in svcs:
             if service not in msgs:
                 raise ValueError(
@@ -656,7 +826,7 @@ class VPPAPI(object):
                     continue
                 if d[:-5]+'_details' in msgs:
                     s['Service'].append(Service(d, d[:-5]+'_details',
-                                                 stream=True))
+                                                stream=True))
                 else:
                     raise ValueError('{} missing details message'
                                      .format(d))
@@ -686,12 +856,16 @@ class VPPAPI(object):
             if in_import and not (isinstance(o, Enum) or
                                   isinstance(o, Union) or
                                   isinstance(o, Typedef) or
-                                  isinstance(o, Import)):
+                                  isinstance(o, Import) or
+                                  isinstance(o, Using)):
                 continue
             if isinstance(o, Import):
-                self.process_imports(o.result, True, result)
+                result.append(o)
+                result = self.process_imports(o.result, True, result)
             else:
                 result.append(o)
+        return result
+
 
 # Add message ids to each message.
 def add_msg_id(s):
@@ -713,16 +887,44 @@ def dirlist_get():
     return dirlist
 
 
+def foldup_blocks(block, crc):
+    for b in block:
+        # Look up CRC in user defined types
+        if b.fieldtype.startswith('vl_api_'):
+            # Recursively
+            t = global_types[b.fieldtype]
+            try:
+                crc = crc_block_combine(t.block, crc)
+                return foldup_blocks(t.block, crc)
+            except AttributeError:
+                pass
+    return crc
+
+
+def foldup_crcs(s):
+    for f in s:
+        f.crc = foldup_blocks(f.block,
+                              binascii.crc32(f.crc))
+
+
 #
 # Main
 #
 def main():
+    if sys.version_info < (3, 5,):
+        log.exception('vppapigen requires a supported version of python. '
+                      'Please use version 3.5 or greater. '
+                      'Using {}'.format(sys.version))
+        return 1
+
     cliparser = argparse.ArgumentParser(description='VPP API generator')
     cliparser.add_argument('--pluginpath', default=""),
     cliparser.add_argument('--includedir', action='append'),
-    cliparser.add_argument('--input', type=argparse.FileType('r'),
+    cliparser.add_argument('--input',
+                           type=argparse.FileType('r', encoding='UTF-8'),
                            default=sys.stdin)
-    cliparser.add_argument('--output', nargs='?', type=argparse.FileType('w'),
+    cliparser.add_argument('--output', nargs='?',
+                           type=argparse.FileType('w', encoding='UTF-8'),
                            default=sys.stdout)
 
     cliparser.add_argument('output_module', nargs='?', default='C')
@@ -746,20 +948,24 @@ def main():
         logging.basicConfig(stream=sys.stdout, level=logging.WARNING)
     else:
         logging.basicConfig()
-    log = logging.getLogger('vppapigen')
 
     parser = VPPAPI(debug=args.debug, filename=filename, logger=log)
     parsed_objects = parser.parse_file(args.input, log)
 
     # Build a list of objects. Hash of lists.
     result = []
-    parser.process_imports(parsed_objects, False, result)
-    s = parser.process(result)
+
+    if args.output_module == 'C':
+        s = parser.process(parsed_objects)
+    else:
+        result = parser.process_imports(parsed_objects, False, result)
+        s = parser.process(result)
 
     # Add msg_id field
     s['Define'] = add_msg_id(s['Define'])
 
-    file_crc = global_crc & 0xffffffff
+    # Fold up CRCs
+    foldup_crcs(s['Define'])
 
     #
     # Debug
@@ -774,7 +980,7 @@ def main():
     #
     # Generate representation
     #
-    import imp
+    from importlib.machinery import SourceFileLoader
 
     # Default path
     pluginpath = ''
@@ -785,28 +991,35 @@ def main():
                     '/../share/vpp/')
         for c in cand:
             c += '/'
-            if os.path.isfile('{}vppapigen_{}.py'.format(c, args.output_module.lower())):
+            if os.path.isfile('{}vppapigen_{}.py'
+                              .format(c, args.output_module.lower())):
                 pluginpath = c
                 break
     else:
         pluginpath = args.pluginpath + '/'
     if pluginpath == '':
-        raise Exception('Output plugin not found')
-    module_path = '{}vppapigen_{}.py'.format(pluginpath, args.output_module.lower())
+        log.exception('Output plugin not found')
+        return 1
+    module_path = '{}vppapigen_{}.py'.format(pluginpath,
+                                             args.output_module.lower())
 
     try:
-        plugin = imp.load_source(args.output_module, module_path)
-    except Exception, err:
-        raise Exception('Error importing output plugin: {}, {}'
-                        .format(module_path, err))
-
-    result = plugin.run(filename, s, file_crc)
+        plugin = SourceFileLoader(args.output_module,
+                                  module_path).load_module()
+    except Exception as err:
+        log.exception('Error importing output plugin: {}, {}'
+                      .format(module_path, err))
+        return 1
+
+    result = plugin.run(filename, s)
     if result:
         print(result, file=args.output)
     else:
-        raise Exception('Running plugin failed: {} {}'
-                        .format(filename, result))
+        log.exception('Running plugin failed: {} {}'
+                      .format(filename, result))
+        return 1
+    return 0
 
 
 if __name__ == '__main__':
-    main()
+    sys.exit(main())