vppapigen: api crc checker
[vpp.git] / src / tools / vppapigen / vppapigen.py
1 #!/usr/bin/env python3
2
3 import ply.lex as lex
4 import ply.yacc as yacc
5 import sys
6 import argparse
7 import keyword
8 import logging
9 import binascii
10 import os
11 import sys
12 from subprocess import Popen, PIPE
13
14 log = logging.getLogger('vppapigen')
15
16 # Ensure we don't leave temporary files around
17 sys.dont_write_bytecode = True
18
19 #
20 # VPP API language
21 #
22
23 # Global dictionary of new types (including enums)
24 global_types = {}
25
26 seen_imports = {}
27
28
29 def global_type_add(name, obj):
30     '''Add new type to the dictionary of types '''
31     type_name = 'vl_api_' + name + '_t'
32     if type_name in global_types:
33         raise KeyError("Attempted redefinition of {!r} with {!r}.".format(
34             name, obj))
35     global_types[type_name] = obj
36
37
38 # All your trace are belong to us!
39 def exception_handler(exception_type, exception, traceback):
40     print("%s: %s" % (exception_type.__name__, exception))
41
42
43 #
44 # Lexer
45 #
46 class VPPAPILexer(object):
47     def __init__(self, filename):
48         self.filename = filename
49
50     reserved = {
51         'service': 'SERVICE',
52         'rpc': 'RPC',
53         'returns': 'RETURNS',
54         'null': 'NULL',
55         'stream': 'STREAM',
56         'events': 'EVENTS',
57         'define': 'DEFINE',
58         'typedef': 'TYPEDEF',
59         'enum': 'ENUM',
60         'typeonly': 'TYPEONLY',
61         'manual_print': 'MANUAL_PRINT',
62         'manual_endian': 'MANUAL_ENDIAN',
63         'dont_trace': 'DONT_TRACE',
64         'autoreply': 'AUTOREPLY',
65         'option': 'OPTION',
66         'u8': 'U8',
67         'u16': 'U16',
68         'u32': 'U32',
69         'u64': 'U64',
70         'i8': 'I8',
71         'i16': 'I16',
72         'i32': 'I32',
73         'i64': 'I64',
74         'f64': 'F64',
75         'bool': 'BOOL',
76         'string': 'STRING',
77         'import': 'IMPORT',
78         'true': 'TRUE',
79         'false': 'FALSE',
80         'union': 'UNION',
81     }
82
83     tokens = ['STRING_LITERAL',
84               'ID', 'NUM'] + list(reserved.values())
85
86     t_ignore_LINE_COMMENT = '//.*'
87
88     def t_FALSE(self, t):
89         r'false'
90         t.value = False
91         return t
92
93     def t_TRUE(self, t):
94         r'false'
95         t.value = True
96         return t
97
98     def t_NUM(self, t):
99         r'0[xX][0-9a-fA-F]+|-?\d+\.?\d*'
100         base = 16 if t.value.startswith('0x') else 10
101         if '.' in t.value:
102             t.value = float(t.value)
103         else:
104             t.value = int(t.value, base)
105         return t
106
107     def t_ID(self, t):
108         r'[a-zA-Z_][a-zA-Z_0-9]*'
109         # Check for reserved words
110         t.type = VPPAPILexer.reserved.get(t.value, 'ID')
111         return t
112
113     # C string
114     def t_STRING_LITERAL(self, t):
115         r'\"([^\\\n]|(\\.))*?\"'
116         t.value = str(t.value).replace("\"", "")
117         return t
118
119     # C or C++ comment (ignore)
120     def t_comment(self, t):
121         r'(/\*(.|\n)*?\*/)|(//.*)'
122         t.lexer.lineno += t.value.count('\n')
123
124     # Error handling rule
125     def t_error(self, t):
126         raise ParseError("Illegal character '{}' ({})"
127                          "in {}: line {}".format(t.value[0],
128                                                  hex(ord(t.value[0])),
129                                                  self.filename,
130                                                  t.lexer.lineno))
131         t.lexer.skip(1)
132
133     # Define a rule so we can track line numbers
134     def t_newline(self, t):
135         r'\n+'
136         t.lexer.lineno += len(t.value)
137
138     literals = ":{}[];=.,"
139
140     # A string containing ignored characters (spaces and tabs)
141     t_ignore = ' \t'
142
143
144 def crc_block_combine(block, crc):
145     s = str(block).encode()
146     return binascii.crc32(s, crc) & 0xffffffff
147
148
149 def vla_is_last_check(name, block):
150     vla = False
151     for i, b in enumerate(block):
152         if isinstance(b, Array) and b.vla:
153             vla = True
154             if i + 1 < len(block):
155                 raise ValueError(
156                     'VLA field "{}" must be the last field in message "{}"'
157                     .format(b.fieldname, name))
158         elif b.fieldtype.startswith('vl_api_'):
159             if global_types[b.fieldtype].vla:
160                 vla = True
161                 if i + 1 < len(block):
162                     raise ValueError(
163                         'VLA field "{}" must be the last '
164                         'field in message "{}"'
165                         .format(b.fieldname, name))
166         elif b.fieldtype == 'string' and b.length == 0:
167             vla = True
168             if i + 1 < len(block):
169                 raise ValueError(
170                     'VLA field "{}" must be the last '
171                     'field in message "{}"'
172                     .format(b.fieldname, name))
173     return vla
174
175
176 class Service():
177     def __init__(self, caller, reply, events=None, stream=False):
178         self.caller = caller
179         self.reply = reply
180         self.stream = stream
181         self.events = [] if events is None else events
182
183
184 class Typedef():
185     def __init__(self, name, flags, block):
186         self.name = name
187         self.flags = flags
188         self.block = block
189         self.crc = str(block).encode()
190         self.manual_print = False
191         self.manual_endian = False
192         for f in flags:
193             if f == 'manual_print':
194                 self.manual_print = True
195             elif f == 'manual_endian':
196                 self.manual_endian = True
197
198         global_type_add(name, self)
199
200         self.vla = vla_is_last_check(name, block)
201
202     def __repr__(self):
203         return self.name + str(self.flags) + str(self.block)
204
205
206 class Using():
207     def __init__(self, name, flags, alias):
208         self.name = name
209         self.vla = False
210         self.block = []
211         self.manual_print = True
212         self.manual_endian = True
213
214         self.manual_print = False
215         self.manual_endian = False
216         for f in flags:
217             if f == 'manual_print':
218                 self.manual_print = True
219             elif f == 'manual_endian':
220                 self.manual_endian = True
221
222         if isinstance(alias, Array):
223             a = {'type': alias.fieldtype,
224                  'length': alias.length}
225         else:
226             a = {'type': alias.fieldtype}
227         self.alias = a
228         self.crc = str(alias).encode()
229         global_type_add(name, self)
230
231     def __repr__(self):
232         return self.name + str(self.alias)
233
234
235 class Union():
236     def __init__(self, name, flags, block):
237         self.type = 'Union'
238         self.manual_print = False
239         self.manual_endian = False
240         self.name = name
241
242         for f in flags:
243             if f == 'manual_print':
244                 self.manual_print = True
245             elif f == 'manual_endian':
246                 self.manual_endian = True
247
248         self.block = block
249         self.crc = str(block).encode()
250         self.vla = vla_is_last_check(name, block)
251
252         global_type_add(name, self)
253
254     def __repr__(self):
255         return str(self.block)
256
257
258 class Define():
259     def __init__(self, name, flags, block):
260         self.name = name
261         self.flags = flags
262         self.block = block
263         self.dont_trace = False
264         self.manual_print = False
265         self.manual_endian = False
266         self.autoreply = False
267         self.singular = False
268         self.options = {}
269         for f in flags:
270             if f == 'dont_trace':
271                 self.dont_trace = True
272             elif f == 'manual_print':
273                 self.manual_print = True
274             elif f == 'manual_endian':
275                 self.manual_endian = True
276             elif f == 'autoreply':
277                 self.autoreply = True
278
279         remove = []
280         for b in block:
281             if isinstance(b, Option):
282                 if b[1] == 'singular' and b[2] == 'true':
283                     self.singular = True
284                 else:
285                     self.options[b.option] = b.value
286                 remove.append(b)
287
288         block = [x for x in block if not x in remove]
289         self.block = block
290         self.vla = vla_is_last_check(name, block)
291         self.crc = str(block).encode()
292
293     def __repr__(self):
294         return self.name + str(self.flags) + str(self.block)
295
296
297 class Enum():
298     def __init__(self, name, block, enumtype='u32'):
299         self.name = name
300         self.enumtype = enumtype
301         self.vla = False
302
303         count = 0
304         for i, b in enumerate(block):
305             if type(b) is list:
306                 count = b[1]
307             else:
308                 count += 1
309                 block[i] = [b, count]
310
311         self.block = block
312         self.crc = str(block).encode()
313         global_type_add(name, self)
314
315     def __repr__(self):
316         return self.name + str(self.block)
317
318
319 class Import():
320
321     def __new__(cls, *args, **kwargs):
322         if args[0] not in seen_imports:
323             instance = super().__new__(cls)
324             instance._initialized = False
325             seen_imports[args[0]] = instance
326
327         return seen_imports[args[0]]
328
329     def __init__(self, filename, revision):
330         if self._initialized:
331             return
332         else:
333             self.filename = filename
334             # Deal with imports
335             parser = VPPAPI(filename=filename, revision=revision)
336             dirlist = dirlist_get()
337             f = filename
338             for dir in dirlist:
339                 f = os.path.join(dir, filename)
340                 if os.path.exists(f):
341                     break
342             self.result = parser.parse_filename(f, None)
343             self._initialized = True
344
345     def __repr__(self):
346         return self.filename
347
348
349 class Option():
350     def __init__(self, option, value):
351         self.type = 'Option'
352         self.option = option
353         self.value = value
354         self.crc = str(option).encode()
355
356     def __repr__(self):
357         return str(self.option)
358
359     def __getitem__(self, index):
360         return self.option[index]
361
362
363 class Array():
364     def __init__(self, fieldtype, name, length, modern_vla=False):
365         self.type = 'Array'
366         self.fieldtype = fieldtype
367         self.fieldname = name
368         self.modern_vla = modern_vla
369         if type(length) is str:
370             self.lengthfield = length
371             self.length = 0
372             self.vla = True
373         else:
374             self.length = length
375             self.lengthfield = None
376             self.vla = False
377
378     def __repr__(self):
379         return str([self.fieldtype, self.fieldname, self.length,
380                     self.lengthfield])
381
382
383 class Field():
384     def __init__(self, fieldtype, name, limit=None):
385         self.type = 'Field'
386         self.fieldtype = fieldtype
387
388         if self.fieldtype == 'string':
389             raise ValueError("The string type {!r} is an "
390                              "array type ".format(name))
391
392         if name in keyword.kwlist:
393             raise ValueError("Fieldname {!r} is a python keyword and is not "
394                              "accessible via the python API. ".format(name))
395         self.fieldname = name
396         self.limit = limit
397
398     def __repr__(self):
399         return str([self.fieldtype, self.fieldname])
400
401
402 class Coord(object):
403     """ Coordinates of a syntactic element. Consists of:
404             - File name
405             - Line number
406             - (optional) column number, for the Lexer
407     """
408     __slots__ = ('file', 'line', 'column', '__weakref__')
409
410     def __init__(self, file, line, column=None):
411         self.file = file
412         self.line = line
413         self.column = column
414
415     def __str__(self):
416         str = "%s:%s" % (self.file, self.line)
417         if self.column:
418             str += ":%s" % self.column
419         return str
420
421
422 class ParseError(Exception):
423     pass
424
425
426 #
427 # Grammar rules
428 #
429 class VPPAPIParser(object):
430     tokens = VPPAPILexer.tokens
431
432     def __init__(self, filename, logger, revision=None):
433         self.filename = filename
434         self.logger = logger
435         self.fields = []
436         self.revision = revision
437
438     def _parse_error(self, msg, coord):
439         raise ParseError("%s: %s" % (coord, msg))
440
441     def _parse_warning(self, msg, coord):
442         if self.logger:
443             self.logger.warning("%s: %s" % (coord, msg))
444
445     def _coord(self, lineno, column=None):
446         return Coord(
447                 file=self.filename,
448                 line=lineno, column=column)
449
450     def _token_coord(self, p, token_idx):
451         """ Returns the coordinates for the YaccProduction object 'p' indexed
452             with 'token_idx'. The coordinate includes the 'lineno' and
453             'column'. Both follow the lex semantic, starting from 1.
454         """
455         last_cr = p.lexer.lexdata.rfind('\n', 0, p.lexpos(token_idx))
456         if last_cr < 0:
457             last_cr = -1
458         column = (p.lexpos(token_idx) - (last_cr))
459         return self._coord(p.lineno(token_idx), column)
460
461     def p_slist(self, p):
462         '''slist : stmt
463                  | slist stmt'''
464         if len(p) == 2:
465             p[0] = [p[1]]
466         else:
467             p[0] = p[1] + [p[2]]
468
469     def p_stmt(self, p):
470         '''stmt : define
471                 | typedef
472                 | option
473                 | import
474                 | enum
475                 | union
476                 | service'''
477         p[0] = p[1]
478
479     def p_import(self, p):
480         '''import : IMPORT STRING_LITERAL ';' '''
481         p[0] = Import(p[2], revision=self.revision)
482
483     def p_service(self, p):
484         '''service : SERVICE '{' service_statements '}' ';' '''
485         p[0] = p[3]
486
487     def p_service_statements(self, p):
488         '''service_statements : service_statement
489                         | service_statements service_statement'''
490         if len(p) == 2:
491             p[0] = [p[1]]
492         else:
493             p[0] = p[1] + [p[2]]
494
495     def p_service_statement(self, p):
496         '''service_statement : RPC ID RETURNS NULL ';'
497                              | RPC ID RETURNS ID ';'
498                              | RPC ID RETURNS STREAM ID ';'
499                              | RPC ID RETURNS ID EVENTS event_list ';' '''
500         if p[2] == p[4]:
501             # Verify that caller and reply differ
502             self._parse_error(
503                 'Reply ID ({}) should not be equal to Caller ID'.format(p[2]),
504                 self._token_coord(p, 1))
505         if len(p) == 8:
506             p[0] = Service(p[2], p[4], p[6])
507         elif len(p) == 7:
508             p[0] = Service(p[2], p[5], stream=True)
509         else:
510             p[0] = Service(p[2], p[4])
511
512     def p_event_list(self, p):
513         '''event_list : events
514                       | event_list events '''
515         if len(p) == 2:
516             p[0] = [p[1]]
517         else:
518             p[0] = p[1] + [p[2]]
519
520     def p_event(self, p):
521         '''events : ID
522                   | ID ',' '''
523         p[0] = p[1]
524
525     def p_enum(self, p):
526         '''enum : ENUM ID '{' enum_statements '}' ';' '''
527         p[0] = Enum(p[2], p[4])
528
529     def p_enum_type(self, p):
530         ''' enum : ENUM ID ':' enum_size '{' enum_statements '}' ';' '''
531         if len(p) == 9:
532             p[0] = Enum(p[2], p[6], enumtype=p[4])
533         else:
534             p[0] = Enum(p[2], p[4])
535
536     def p_enum_size(self, p):
537         ''' enum_size : U8
538                       | U16
539                       | U32 '''
540         p[0] = p[1]
541
542     def p_define(self, p):
543         '''define : DEFINE ID '{' block_statements_opt '}' ';' '''
544         self.fields = []
545         p[0] = Define(p[2], [], p[4])
546
547     def p_define_flist(self, p):
548         '''define : flist DEFINE ID '{' block_statements_opt '}' ';' '''
549         # Legacy typedef
550         if 'typeonly' in p[1]:
551             self._parse_error('legacy typedef. use typedef: {} {}[{}];'
552                               .format(p[1], p[2], p[4]),
553                               self._token_coord(p, 1))
554         else:
555             p[0] = Define(p[3], p[1], p[5])
556
557     def p_flist(self, p):
558         '''flist : flag
559                  | flist flag'''
560         if len(p) == 2:
561             p[0] = [p[1]]
562         else:
563             p[0] = p[1] + [p[2]]
564
565     def p_flag(self, p):
566         '''flag : MANUAL_PRINT
567                 | MANUAL_ENDIAN
568                 | DONT_TRACE
569                 | TYPEONLY
570                 | AUTOREPLY'''
571         if len(p) == 1:
572             return
573         p[0] = p[1]
574
575     def p_typedef(self, p):
576         '''typedef : TYPEDEF ID '{' block_statements_opt '}' ';' '''
577         p[0] = Typedef(p[2], [], p[4])
578
579     def p_typedef_flist(self, p):
580         '''typedef : flist TYPEDEF ID '{' block_statements_opt '}' ';' '''
581         p[0] = Typedef(p[3], p[1], p[5])
582
583     def p_typedef_alias(self, p):
584         '''typedef : TYPEDEF declaration '''
585         p[0] = Using(p[2].fieldname, [], p[2])
586
587     def p_typedef_alias_flist(self, p):
588         '''typedef : flist TYPEDEF declaration '''
589         p[0] = Using(p[3].fieldname, p[1], p[3])
590
591     def p_block_statements_opt(self, p):
592         '''block_statements_opt : block_statements '''
593         p[0] = p[1]
594
595     def p_block_statements(self, p):
596         '''block_statements : block_statement
597                             | block_statements block_statement'''
598         if len(p) == 2:
599             p[0] = [p[1]]
600         else:
601             p[0] = p[1] + [p[2]]
602
603     def p_block_statement(self, p):
604         '''block_statement : declaration
605                            | option '''
606         p[0] = p[1]
607
608     def p_enum_statements(self, p):
609         '''enum_statements : enum_statement
610                            | enum_statements enum_statement'''
611         if len(p) == 2:
612             p[0] = [p[1]]
613         else:
614             p[0] = p[1] + [p[2]]
615
616     def p_enum_statement(self, p):
617         '''enum_statement : ID '=' NUM ','
618                           | ID ',' '''
619         if len(p) == 5:
620             p[0] = [p[1], p[3]]
621         else:
622             p[0] = p[1]
623
624     def p_field_options(self, p):
625         '''field_options : field_option
626                            | field_options field_option'''
627         if len(p) == 2:
628             p[0] = p[1]
629         else:
630             p[0] = {**p[1], **p[2]}
631
632     def p_field_option(self, p):
633         '''field_option : ID
634                         | ID '=' assignee ','
635                         | ID '=' assignee
636
637         '''
638         if len(p) == 2:
639             p[0] = {p[1]: None}
640         else:
641             p[0] = {p[1]: p[3]}
642
643     def p_declaration(self, p):
644         '''declaration : type_specifier ID ';'
645                        | type_specifier ID '[' field_options ']' ';' '''
646         if len(p) == 7:
647             p[0] = Field(p[1], p[2], p[4])
648         elif len(p) == 4:
649             p[0] = Field(p[1], p[2])
650         else:
651             self._parse_error('ERROR', self._token_coord(p, 1))
652         self.fields.append(p[2])
653
654     def p_declaration_array_vla(self, p):
655         '''declaration : type_specifier ID '[' ']' ';' '''
656         p[0] = Array(p[1], p[2], 0, modern_vla=True)
657
658     def p_declaration_array(self, p):
659         '''declaration : type_specifier ID '[' NUM ']' ';'
660                        | type_specifier ID '[' ID ']' ';' '''
661
662         if len(p) != 7:
663             return self._parse_error(
664                 'array: %s' % p.value,
665                 self._coord(lineno=p.lineno))
666
667         # Make this error later
668         if type(p[4]) is int and p[4] == 0:
669             # XXX: Line number is wrong
670             self._parse_warning('Old Style VLA: {} {}[{}];'
671                                 .format(p[1], p[2], p[4]),
672                                 self._token_coord(p, 1))
673
674         if type(p[4]) is str and p[4] not in self.fields:
675             # Verify that length field exists
676             self._parse_error('Missing length field: {} {}[{}];'
677                               .format(p[1], p[2], p[4]),
678                               self._token_coord(p, 1))
679         p[0] = Array(p[1], p[2], p[4])
680
681     def p_option(self, p):
682         '''option : OPTION ID '=' assignee ';' '''
683         p[0] = Option(p[2], p[4])
684
685     def p_assignee(self, p):
686         '''assignee : NUM
687                     | TRUE
688                     | FALSE
689                     | STRING_LITERAL '''
690         p[0] = p[1]
691
692     def p_type_specifier(self, p):
693         '''type_specifier : U8
694                           | U16
695                           | U32
696                           | U64
697                           | I8
698                           | I16
699                           | I32
700                           | I64
701                           | F64
702                           | BOOL
703                           | STRING'''
704         p[0] = p[1]
705
706     # Do a second pass later to verify that user defined types are defined
707     def p_typedef_specifier(self, p):
708         '''type_specifier : ID '''
709         if p[1] not in global_types:
710             self._parse_error('Undefined type: {}'.format(p[1]),
711                               self._token_coord(p, 1))
712         p[0] = p[1]
713
714     def p_union(self, p):
715         '''union : UNION ID '{' block_statements_opt '}' ';' '''
716         p[0] = Union(p[2], [], p[4])
717
718     def p_union_flist(self, p):
719         '''union : flist UNION ID '{' block_statements_opt '}' ';' '''
720         p[0] = Union(p[3], p[1], p[5])
721
722     # Error rule for syntax errors
723     def p_error(self, p):
724         if p:
725             self._parse_error(
726                 'before: %s' % p.value,
727                 self._coord(lineno=p.lineno))
728         else:
729             self._parse_error('At end of input', self.filename)
730
731
732 class VPPAPI(object):
733
734     def __init__(self, debug=False, filename='', logger=None, revision=None):
735         self.lexer = lex.lex(module=VPPAPILexer(filename), debug=debug)
736         self.parser = yacc.yacc(module=VPPAPIParser(filename, logger,
737                                                     revision=revision),
738                                 write_tables=False, debug=debug)
739         self.logger = logger
740         self.revision = revision
741         self.filename = filename
742
743     def parse_string(self, code, debug=0, lineno=1):
744         self.lexer.lineno = lineno
745         return self.parser.parse(code, lexer=self.lexer, debug=debug)
746
747     def parse_fd(self, fd, debug=0):
748         data = fd.read()
749         return self.parse_string(data, debug=debug)
750
751     def parse_filename(self, filename, debug=0):
752         if self.revision:
753             git_show = f'git show  {self.revision}:{filename}'
754             with Popen(git_show.split(), stdout=PIPE, encoding='utf-8') as git:
755                 return self.parse_fd(git.stdout, None)
756         else:
757             try:
758                 with open(filename, encoding='utf-8') as fd:
759                     return self.parse_fd(fd, None)
760             except FileNotFoundError:
761                 print(f'File not found: {filename}', file=sys.stderr)
762                 sys.exit(2)
763
764     def autoreply_block(self, name, parent):
765         block = [Field('u32', 'context'),
766                  Field('i32', 'retval')]
767         # inherhit the parent's options
768         for k,v in parent.options.items():
769             block.append(Option(k, v))
770         return Define(name + '_reply', [], block)
771
772     def process(self, objs):
773         s = {}
774         s['Option'] = {}
775         s['Define'] = []
776         s['Service'] = []
777         s['types'] = []
778         s['Import'] = []
779         crc = 0
780         for o in objs:
781             tname = o.__class__.__name__
782             try:
783                 crc = binascii.crc32(o.crc, crc) & 0xffffffff
784             except AttributeError:
785                 pass
786             if isinstance(o, Define):
787                 s[tname].append(o)
788                 if o.autoreply:
789                     s[tname].append(self.autoreply_block(o.name, o))
790             elif isinstance(o, Option):
791                 s[tname][o.option] = o.value
792             elif type(o) is list:
793                 for o2 in o:
794                     if isinstance(o2, Service):
795                         s['Service'].append(o2)
796             elif (isinstance(o, Enum) or
797                   isinstance(o, Typedef) or
798                   isinstance(o, Using) or
799                   isinstance(o, Union)):
800                 s['types'].append(o)
801             else:
802                 if tname not in s:
803                     raise ValueError('Unknown class type: {} {}'
804                                      .format(tname, o))
805                 s[tname].append(o)
806
807         msgs = {d.name: d for d in s['Define']}
808         svcs = {s.caller: s for s in s['Service']}
809         replies = {s.reply: s for s in s['Service']}
810         seen_services = {}
811
812         s['file_crc'] = crc
813
814         for service in svcs:
815             if service not in msgs:
816                 raise ValueError(
817                     'Service definition refers to unknown message'
818                     ' definition: {}'.format(service))
819             if svcs[service].reply != 'null' and \
820                svcs[service].reply not in msgs:
821                 raise ValueError('Service definition refers to unknown message'
822                                  ' definition in reply: {}'
823                                  .format(svcs[service].reply))
824             if service in replies:
825                 raise ValueError('Service definition refers to message'
826                                  ' marked as reply: {}'.format(service))
827             for event in svcs[service].events:
828                 if event not in msgs:
829                     raise ValueError('Service definition refers to unknown '
830                                      'event: {} in message: {}'
831                                      .format(event, service))
832                 seen_services[event] = True
833
834         # Create services implicitly
835         for d in msgs:
836             if d in seen_services:
837                 continue
838             if msgs[d].singular is True:
839                 continue
840             if d.endswith('_reply'):
841                 if d[:-6] in svcs:
842                     continue
843                 if d[:-6] not in msgs:
844                     raise ValueError('{} missing calling message'
845                                      .format(d))
846                 continue
847             if d.endswith('_dump'):
848                 if d in svcs:
849                     continue
850                 if d[:-5]+'_details' in msgs:
851                     s['Service'].append(Service(d, d[:-5]+'_details',
852                                                 stream=True))
853                 else:
854                     raise ValueError('{} missing details message'
855                                      .format(d))
856                 continue
857
858             if d.endswith('_details'):
859                 if d[:-8]+'_dump' not in msgs:
860                     raise ValueError('{} missing dump message'
861                                      .format(d))
862                 continue
863
864             if d in svcs:
865                 continue
866             if d+'_reply' in msgs:
867                 s['Service'].append(Service(d, d+'_reply'))
868             else:
869                 raise ValueError(
870                     '{} missing reply message ({}) or service definition'
871                     .format(d, d+'_reply'))
872
873         return s
874
875     def process_imports(self, objs, in_import, result):
876         imported_objs = []
877         for o in objs:
878             # Only allow the following object types from imported file
879             if in_import and not (isinstance(o, Enum) or
880                                   isinstance(o, Union) or
881                                   isinstance(o, Typedef) or
882                                   isinstance(o, Import) or
883                                   isinstance(o, Using)):
884                 continue
885             if isinstance(o, Import):
886                 result.append(o)
887                 result = self.process_imports(o.result, True, result)
888             else:
889                 result.append(o)
890         return result
891
892
893 # Add message ids to each message.
894 def add_msg_id(s):
895     for o in s:
896         o.block.insert(0, Field('u16', '_vl_msg_id'))
897     return s
898
899
900 dirlist = []
901
902
903 def dirlist_add(dirs):
904     global dirlist
905     if dirs:
906         dirlist = dirlist + dirs
907
908
909 def dirlist_get():
910     return dirlist
911
912
913 def foldup_blocks(block, crc):
914     for b in block:
915         # Look up CRC in user defined types
916         if b.fieldtype.startswith('vl_api_'):
917             # Recursively
918             t = global_types[b.fieldtype]
919             try:
920                 crc = crc_block_combine(t.block, crc)
921                 return foldup_blocks(t.block, crc)
922             except AttributeError:
923                 pass
924     return crc
925
926
927 def foldup_crcs(s):
928     for f in s:
929         f.crc = foldup_blocks(f.block,
930                               binascii.crc32(f.crc) & 0xffffffff)
931
932
933 #
934 # Main
935 #
936 def main():
937     if sys.version_info < (3, 5,):
938         log.exception('vppapigen requires a supported version of python. '
939                       'Please use version 3.5 or greater. '
940                       'Using {}'.format(sys.version))
941         return 1
942
943     cliparser = argparse.ArgumentParser(description='VPP API generator')
944     cliparser.add_argument('--pluginpath', default=""),
945     cliparser.add_argument('--includedir', action='append'),
946     cliparser.add_argument('--outputdir', action='store'),
947     cliparser.add_argument('--input')
948     cliparser.add_argument('--output', nargs='?',
949                            type=argparse.FileType('w', encoding='UTF-8'),
950                            default=sys.stdout)
951
952     cliparser.add_argument('output_module', nargs='?', default='C')
953     cliparser.add_argument('--debug', action='store_true')
954     cliparser.add_argument('--show-name', nargs=1)
955     cliparser.add_argument('--git-revision',
956                            help="Git revision to use for opening files")
957     args = cliparser.parse_args()
958
959     dirlist_add(args.includedir)
960     if not args.debug:
961         sys.excepthook = exception_handler
962
963     # Filename
964     if args.show_name:
965         filename = args.show_name[0]
966     elif args.input:
967         filename = args.input
968     else:
969         filename = ''
970
971     if args.debug:
972         logging.basicConfig(stream=sys.stdout, level=logging.WARNING)
973     else:
974         logging.basicConfig()
975
976     parser = VPPAPI(debug=args.debug, filename=filename, logger=log,
977                     revision=args.git_revision)
978
979     try:
980         if not args.input:
981             parsed_objects = parser.parse_fd(sys.stdin, log)
982         else:
983             parsed_objects = parser.parse_filename(args.input, log)
984     except ParseError as e:
985         print('Parse error: ', e, file=sys.stderr)
986         sys.exit(1)
987
988     # Build a list of objects. Hash of lists.
989     result = []
990
991     if args.output_module == 'C':
992         s = parser.process(parsed_objects)
993     else:
994         result = parser.process_imports(parsed_objects, False, result)
995         s = parser.process(result)
996
997     # Add msg_id field
998     s['Define'] = add_msg_id(s['Define'])
999
1000     # Fold up CRCs
1001     foldup_crcs(s['Define'])
1002
1003     #
1004     # Debug
1005     if args.debug:
1006         import pprint
1007         pp = pprint.PrettyPrinter(indent=4, stream=sys.stderr)
1008         for t in s['Define']:
1009             pp.pprint([t.name, t.flags, t.block])
1010         for t in s['types']:
1011             pp.pprint([t.name, t.block])
1012
1013     #
1014     # Generate representation
1015     #
1016     from importlib.machinery import SourceFileLoader
1017
1018     # Default path
1019     pluginpath = ''
1020     if not args.pluginpath:
1021         cand = []
1022         cand.append(os.path.dirname(os.path.realpath(__file__)))
1023         cand.append(os.path.dirname(os.path.realpath(__file__)) +
1024                     '/../share/vpp/')
1025         for c in cand:
1026             c += '/'
1027             if os.path.isfile('{}vppapigen_{}.py'
1028                               .format(c, args.output_module.lower())):
1029                 pluginpath = c
1030                 break
1031     else:
1032         pluginpath = args.pluginpath + '/'
1033     if pluginpath == '':
1034         log.exception('Output plugin not found')
1035         return 1
1036     module_path = '{}vppapigen_{}.py'.format(pluginpath,
1037                                              args.output_module.lower())
1038
1039     try:
1040         plugin = SourceFileLoader(args.output_module,
1041                                   module_path).load_module()
1042     except Exception as err:
1043         log.exception('Error importing output plugin: {}, {}'
1044                       .format(module_path, err))
1045         return 1
1046
1047     result = plugin.run(args, filename, s)
1048     if result:
1049         print(result, file=args.output)
1050     else:
1051         log.exception('Running plugin failed: {} {}'
1052                       .format(filename, result))
1053         return 1
1054     return 0
1055
1056
1057 if __name__ == '__main__':
1058     sys.exit(main())