VPPAPIGEN: Add union and enum support and IP4/IP6 address type.
[vpp.git] / src / tools / vppapigen / vppapigen.py
1 #!/usr/bin/env python
2
3 from __future__ import print_function
4 import ply.lex as lex
5 import ply.yacc as yacc
6 import sys
7 import argparse
8 import logging
9 import binascii
10 import os
11
12 # Ensure we don't leave temporary files around
13 sys.dont_write_bytecode = True
14
15 #
16 # VPP API language
17 #
18
19 # Global dictionary of new types (including enums)
20 global_types = {}
21 global_crc = 0
22
23
24 def global_type_add(name):
25     '''Add new type to the dictionary of types '''
26     type_name = 'vl_api_' + name + '_t'
27     if type_name in global_types:
28         raise KeyError('Type is already defined: {}'.format(name))
29     global_types[type_name] = True
30
31
32 # All your trace are belong to us!
33 def exception_handler(exception_type, exception, traceback):
34     print("%s: %s" % (exception_type.__name__, exception))
35
36
37 #
38 # Lexer
39 #
40 class VPPAPILexer(object):
41     def __init__(self, filename):
42         self.filename = filename
43
44     reserved = {
45         'service': 'SERVICE',
46         'rpc': 'RPC',
47         'returns': 'RETURNS',
48         'null': 'NULL',
49         'stream': 'STREAM',
50         'events': 'EVENTS',
51         'define': 'DEFINE',
52         'typedef': 'TYPEDEF',
53         'enum': 'ENUM',
54         'typeonly': 'TYPEONLY',
55         'manual_print': 'MANUAL_PRINT',
56         'manual_endian': 'MANUAL_ENDIAN',
57         'dont_trace': 'DONT_TRACE',
58         'autoreply': 'AUTOREPLY',
59         'option': 'OPTION',
60         'u8': 'U8',
61         'u16': 'U16',
62         'u32': 'U32',
63         'u64': 'U64',
64         'i8': 'I8',
65         'i16': 'I16',
66         'i32': 'I32',
67         'i64': 'I64',
68         'f64': 'F64',
69         'bool': 'BOOL',
70         'string': 'STRING',
71         'import': 'IMPORT',
72         'true': 'TRUE',
73         'false': 'FALSE',
74         'union': 'UNION',
75     }
76
77     tokens = ['STRING_LITERAL',
78               'ID', 'NUM'] + list(reserved.values())
79
80     t_ignore_LINE_COMMENT = '//.*'
81
82     def t_NUM(self, t):
83         r'0[xX][0-9a-fA-F]+|\d+'
84         base = 16 if t.value.startswith('0x') else 10
85         t.value = int(t.value, base)
86         return t
87
88     def t_ID(self, t):
89         r'[a-zA-Z_][a-zA-Z_0-9]*'
90         # Check for reserved words
91         t.type = VPPAPILexer.reserved.get(t.value, 'ID')
92         return t
93
94     # C string
95     def t_STRING_LITERAL(self, t):
96         r'\"([^\\\n]|(\\.))*?\"'
97         t.value = str(t.value).replace("\"", "")
98         return t
99
100     # C or C++ comment (ignore)
101     def t_comment(self, t):
102         r'(/\*(.|\n)*?\*/)|(//.*)'
103         t.lexer.lineno += t.value.count('\n')
104
105     # Error handling rule
106     def t_error(self, t):
107         raise ParseError("Illegal character '{}' ({})"
108                          "in {}: line {}".format(t.value[0],
109                                                  hex(ord(t.value[0])),
110                                                  self.filename,
111                                                  t.lexer.lineno))
112         t.lexer.skip(1)
113
114     # Define a rule so we can track line numbers
115     def t_newline(self, t):
116         r'\n+'
117         t.lexer.lineno += len(t.value)
118
119     literals = ":{}[];=.,"
120
121     # A string containing ignored characters (spaces and tabs)
122     t_ignore = ' \t'
123
124
125 class Service():
126     def __init__(self, caller, reply, events=[], stream=False):
127         self.caller = caller
128         self.reply = reply
129         self.stream = stream
130         self.events = events
131
132
133 class Typedef():
134     def __init__(self, name, flags, block):
135         global global_crc
136         self.name = name
137         self.flags = flags
138         self.block = block
139         self.crc = binascii.crc32(str(block)) & 0xffffffff
140         global_crc = binascii.crc32(str(block), global_crc)
141         self.manual_print = False
142         self.manual_endian = False
143         for f in flags:
144             if f == 'manual_print':
145                 self.manual_print = True
146             elif f == 'manual_endian':
147                 self.manual_endian = True
148         global_type_add(name)
149
150     def __repr__(self):
151         return self.name + str(self.flags) + str(self.block)
152
153
154 class Union():
155     def __init__(self, name, block):
156         self.type = 'Union'
157         self.manual_print = False
158         self.manual_endian = False
159         global global_crc
160         self.name = name
161         self.block = block
162         self.crc = binascii.crc32(str(block)) & 0xffffffff
163         global_crc = binascii.crc32(str(block), global_crc)
164         global_type_add(name)
165
166     def __repr__(self):
167         return str(self.block)
168
169
170 class Define():
171     def __init__(self, name, flags, block):
172         global global_crc
173         self.name = name
174         self.flags = flags
175         self.block = block
176         self.crc = binascii.crc32(str(block)) & 0xffffffff
177         global_crc = binascii.crc32(str(block), global_crc)
178         self.dont_trace = False
179         self.manual_print = False
180         self.manual_endian = False
181         self.autoreply = False
182         self.singular = False
183         for f in flags:
184             if f == 'dont_trace':
185                 self.dont_trace = True
186             elif f == 'manual_print':
187                 self.manual_print = True
188             elif f == 'manual_endian':
189                 self.manual_endian = True
190             elif f == 'autoreply':
191                 self.autoreply = True
192
193         for b in block:
194             if isinstance(b, Option):
195                 if b[1] == 'singular' and b[2] == 'true':
196                     self.singular = True
197                 block.remove(b)
198
199     def __repr__(self):
200         return self.name + str(self.flags) + str(self.block)
201
202
203 class Enum():
204     def __init__(self, name, block, enumtype='u32'):
205         global global_crc
206         self.name = name
207         self.enumtype = enumtype
208
209         count = 0
210         for i, b in enumerate(block):
211             if type(b) is list:
212                 count = b[1]
213             else:
214                 count += 1
215                 block[i] = [b, count]
216
217         self.block = block
218         self.crc = binascii.crc32(str(block)) & 0xffffffff
219         global_crc = binascii.crc32(str(block), global_crc)
220         global_type_add(name)
221
222     def __repr__(self):
223         return self.name + str(self.block)
224
225
226 class Import():
227     def __init__(self, filename):
228         self.filename = filename
229
230         # Deal with imports
231         parser = VPPAPI(filename=filename)
232         dirlist = dirlist_get()
233         f = filename
234         for dir in dirlist:
235             f = os.path.join(dir, filename)
236             if os.path.exists(f):
237                 break
238         with open(f) as fd:
239             self.result = parser.parse_file(fd, None)
240
241     def __repr__(self):
242         return self.filename
243
244
245 class Option():
246     def __init__(self, option):
247         global global_crc
248         self.option = option
249         self.crc = binascii.crc32(str(option)) & 0xffffffff
250         global_crc = binascii.crc32(str(option), global_crc)
251
252     def __repr__(self):
253         return str(self.option)
254
255     def __getitem__(self, index):
256         return self.option[index]
257
258
259 class Array():
260     def __init__(self, fieldtype, name, length):
261         self.type = 'Array'
262         self.fieldtype = fieldtype
263         self.fieldname = name
264         if type(length) is str:
265             self.lengthfield = length
266             self.length = 0
267         else:
268             self.length = length
269             self.lengthfield = None
270
271     def __repr__(self):
272         return str([self.fieldtype, self.fieldname, self.length,
273                     self.lengthfield])
274
275
276 class Field():
277     def __init__(self, fieldtype, name):
278         self.type = 'Field'
279         self.fieldtype = fieldtype
280         self.fieldname = name
281
282     def __repr__(self):
283         return str([self.fieldtype, self.fieldname])
284
285
286 class Coord(object):
287     """ Coordinates of a syntactic element. Consists of:
288             - File name
289             - Line number
290             - (optional) column number, for the Lexer
291     """
292     __slots__ = ('file', 'line', 'column', '__weakref__')
293
294     def __init__(self, file, line, column=None):
295         self.file = file
296         self.line = line
297         self.column = column
298
299     def __str__(self):
300         str = "%s:%s" % (self.file, self.line)
301         if self.column:
302             str += ":%s" % self.column
303         return str
304
305
306 class ParseError(Exception):
307     pass
308
309
310 #
311 # Grammar rules
312 #
313 class VPPAPIParser(object):
314     tokens = VPPAPILexer.tokens
315
316     def __init__(self, filename, logger):
317         self.filename = filename
318         self.logger = logger
319         self.fields = []
320
321     def _parse_error(self, msg, coord):
322         raise ParseError("%s: %s" % (coord, msg))
323
324     def _parse_warning(self, msg, coord):
325         if self.logger:
326             self.logger.warning("%s: %s" % (coord, msg))
327
328     def _coord(self, lineno, column=None):
329         return Coord(
330                 file=self.filename,
331                 line=lineno, column=column)
332
333     def _token_coord(self, p, token_idx):
334         """ Returns the coordinates for the YaccProduction object 'p' indexed
335             with 'token_idx'. The coordinate includes the 'lineno' and
336             'column'. Both follow the lex semantic, starting from 1.
337         """
338         last_cr = p.lexer.lexdata.rfind('\n', 0, p.lexpos(token_idx))
339         if last_cr < 0:
340             last_cr = -1
341         column = (p.lexpos(token_idx) - (last_cr))
342         return self._coord(p.lineno(token_idx), column)
343
344     def p_slist(self, p):
345         '''slist : stmt
346                  | slist stmt'''
347         if len(p) == 2:
348             p[0] = [p[1]]
349         else:
350             p[0] = p[1] + [p[2]]
351
352     def p_stmt(self, p):
353         '''stmt : define
354                 | typedef
355                 | option
356                 | import
357                 | enum
358                 | union
359                 | service'''
360         p[0] = p[1]
361
362     def p_import(self, p):
363         '''import : IMPORT STRING_LITERAL ';' '''
364         p[0] = Import(p[2])
365
366     def p_service(self, p):
367         '''service : SERVICE '{' service_statements '}' ';' '''
368         p[0] = p[3]
369
370     def p_service_statements(self, p):
371         '''service_statements : service_statement
372                         | service_statements service_statement'''
373         if len(p) == 2:
374             p[0] = [p[1]]
375         else:
376             p[0] = p[1] + [p[2]]
377
378     def p_service_statement(self, p):
379         '''service_statement : RPC ID RETURNS NULL ';'
380                              | RPC ID RETURNS ID ';'
381                              | RPC ID RETURNS STREAM ID ';'
382                              | RPC ID RETURNS ID EVENTS event_list ';' '''
383         if p[2] == p[4]:
384             # Verify that caller and reply differ
385             self._parse_error(
386                 'Reply ID ({}) should not be equal to Caller ID'.format(p[2]),
387                 self._token_coord(p, 1))
388         if len(p) == 8:
389             p[0] = Service(p[2], p[4], p[6])
390         elif len(p) == 7:
391             p[0] = Service(p[2], p[5], stream=True)
392         else:
393             p[0] = Service(p[2], p[4])
394
395     def p_event_list(self, p):
396         '''event_list : events
397                       | event_list events '''
398         if len(p) == 2:
399             p[0] = [p[1]]
400         else:
401             p[0] = p[1] + [p[2]]
402
403     def p_event(self, p):
404         '''events : ID
405                   | ID ',' '''
406         p[0] = p[1]
407
408     def p_enum(self, p):
409         '''enum : ENUM ID '{' enum_statements '}' ';' '''
410         p[0] = Enum(p[2], p[4])
411
412     def p_enum_type(self, p):
413         ''' enum : ENUM ID ':' enum_size '{' enum_statements '}' ';' '''
414         if len(p) == 9:
415             p[0] = Enum(p[2], p[6], enumtype=p[4])
416         else:
417             p[0] = Enum(p[2], p[4])
418
419     def p_enum_size(self, p):
420         ''' enum_size : U8
421                       | U16
422                       | U32 '''
423         p[0] = p[1]
424
425     def p_define(self, p):
426         '''define : DEFINE ID '{' block_statements_opt '}' ';' '''
427         self.fields = []
428         p[0] = Define(p[2], [], p[4])
429
430     def p_define_flist(self, p):
431         '''define : flist DEFINE ID '{' block_statements_opt '}' ';' '''
432         # Legacy typedef
433         if 'typeonly' in p[1]:
434             p[0] = Typedef(p[3], p[1], p[5])
435         else:
436             p[0] = Define(p[3], p[1], p[5])
437
438     def p_flist(self, p):
439         '''flist : flag
440                  | flist flag'''
441         if len(p) == 2:
442             p[0] = [p[1]]
443         else:
444             p[0] = p[1] + [p[2]]
445
446     def p_flag(self, p):
447         '''flag : MANUAL_PRINT
448                 | MANUAL_ENDIAN
449                 | DONT_TRACE
450                 | TYPEONLY
451                 | AUTOREPLY'''
452         if len(p) == 1:
453             return
454         p[0] = p[1]
455
456     def p_typedef(self, p):
457         '''typedef : TYPEDEF ID '{' block_statements_opt '}' ';' '''
458         p[0] = Typedef(p[2], [], p[4])
459
460     def p_block_statements_opt(self, p):
461         '''block_statements_opt : block_statements '''
462         p[0] = p[1]
463
464     def p_block_statements(self, p):
465         '''block_statements : block_statement
466                             | block_statements block_statement'''
467         if len(p) == 2:
468             p[0] = [p[1]]
469         else:
470             p[0] = p[1] + [p[2]]
471
472     def p_block_statement(self, p):
473         '''block_statement : declaration
474                            | option '''
475         p[0] = p[1]
476
477     def p_enum_statements(self, p):
478         '''enum_statements : enum_statement
479                             | enum_statements enum_statement'''
480         if len(p) == 2:
481             p[0] = [p[1]]
482         else:
483             p[0] = p[1] + [p[2]]
484
485     def p_enum_statement(self, p):
486         '''enum_statement : ID '=' NUM ','
487                           | ID ',' '''
488         if len(p) == 5:
489             p[0] = [p[1], p[3]]
490         else:
491             p[0] = p[1]
492
493     def p_declaration(self, p):
494         '''declaration : type_specifier ID ';' '''
495         if len(p) != 4:
496             self._parse_error('ERROR')
497         self.fields.append(p[2])
498         p[0] = Field(p[1], p[2])
499
500     def p_declaration_array(self, p):
501         '''declaration : type_specifier ID '[' NUM ']' ';'
502                        | type_specifier ID '[' ID ']' ';' '''
503         if len(p) != 7:
504             return self._parse_error(
505                 'array: %s' % p.value,
506                 self._coord(lineno=p.lineno))
507
508         # Make this error later
509         if type(p[4]) is int and p[4] == 0:
510             # XXX: Line number is wrong
511             self._parse_warning('Old Style VLA: {} {}[{}];'
512                                 .format(p[1], p[2], p[4]),
513                                 self._token_coord(p, 1))
514
515         if type(p[4]) is str and p[4] not in self.fields:
516             # Verify that length field exists
517             self._parse_error('Missing length field: {} {}[{}];'
518                               .format(p[1], p[2], p[4]),
519                               self._token_coord(p, 1))
520         p[0] = Array(p[1], p[2], p[4])
521
522     def p_option(self, p):
523         '''option : OPTION ID '=' assignee ';' '''
524         p[0] = Option([p[1], p[2], p[4]])
525
526     def p_assignee(self, p):
527         '''assignee : NUM
528                     | TRUE
529                     | FALSE
530                     | STRING_LITERAL '''
531         p[0] = p[1]
532
533     def p_type_specifier(self, p):
534         '''type_specifier : U8
535                           | U16
536                           | U32
537                           | U64
538                           | I8
539                           | I16
540                           | I32
541                           | I64
542                           | F64
543                           | BOOL
544                           | STRING'''
545         p[0] = p[1]
546
547     # Do a second pass later to verify that user defined types are defined
548     def p_typedef_specifier(self, p):
549         '''type_specifier : ID '''
550         if p[1] not in global_types:
551             self._parse_error('Undefined type: {}'.format(p[1]),
552                               self._token_coord(p, 1))
553         p[0] = p[1]
554
555     def p_union(self, p):
556         '''union : UNION ID '{' block_statements_opt '}' ';' '''
557         p[0] = Union(p[2], p[4])
558
559     # Error rule for syntax errors
560     def p_error(self, p):
561         if p:
562             self._parse_error(
563                 'before: %s' % p.value,
564                 self._coord(lineno=p.lineno))
565         else:
566             self._parse_error('At end of input', self.filename)
567
568
569 class VPPAPI(object):
570
571     def __init__(self, debug=False, filename='', logger=None):
572         self.lexer = lex.lex(module=VPPAPILexer(filename), debug=debug)
573         self.parser = yacc.yacc(module=VPPAPIParser(filename, logger),
574                                 write_tables=False, debug=debug)
575         self.logger = logger
576
577     def parse_string(self, code, debug=0, lineno=1):
578         self.lexer.lineno = lineno
579         return self.parser.parse(code, lexer=self.lexer, debug=debug)
580
581     def parse_file(self, fd, debug=0):
582         data = fd.read()
583         return self.parse_string(data, debug=debug)
584
585     def autoreply_block(self, name):
586         block = [Field('u32', 'context'),
587                  Field('i32', 'retval')]
588         return Define(name + '_reply', [], block)
589
590     def process(self, objs):
591         s = {}
592         s['Option'] = {}
593         s['Define'] = []
594         s['Service'] = []
595         s['types'] = []
596         s['Import'] = []
597         for o in objs:
598             tname = o.__class__.__name__
599             if isinstance(o, Define):
600                 s[tname].append(o)
601                 if o.autoreply:
602                     s[tname].append(self.autoreply_block(o.name))
603             elif isinstance(o, Option):
604                 s[tname][o[1]] = o[2]
605             elif type(o) is list:
606                 for o2 in o:
607                     if isinstance(o2, Service):
608                         s['Service'].append(o2)
609             elif isinstance(o, Enum) or isinstance(o, Typedef) or isinstance(o, Union):
610                 s['types'].append(o)
611             else:
612                 if tname not in s:
613                     raise ValueError('Unknown class type: {} {}'.format(tname, o))
614                 s[tname].append(o)
615
616         msgs = {d.name: d for d in s['Define']}
617         svcs = {s.caller: s for s in s['Service']}
618         replies = {s.reply: s for s in s['Service']}
619         seen_services = {}
620
621         for service in svcs:
622             if service not in msgs:
623                 raise ValueError(
624                     'Service definition refers to unknown message'
625                     ' definition: {}'.format(service))
626             if svcs[service].reply != 'null' and \
627                svcs[service].reply not in msgs:
628                 raise ValueError('Service definition refers to unknown message'
629                                  ' definition in reply: {}'
630                                  .format(svcs[service].reply))
631             if service in replies:
632                 raise ValueError('Service definition refers to message'
633                                  ' marked as reply: {}'.format(service))
634             for event in svcs[service].events:
635                 if event not in msgs:
636                     raise ValueError('Service definition refers to unknown '
637                                      'event: {} in message: {}'
638                                      .format(event, service))
639                 seen_services[event] = True
640
641         # Create services implicitly
642         for d in msgs:
643             if d in seen_services:
644                 continue
645             if msgs[d].singular is True:
646                 continue
647             if d.endswith('_reply'):
648                 if d[:-6] in svcs:
649                     continue
650                 if d[:-6] not in msgs:
651                     raise ValueError('{} missing calling message'
652                                      .format(d))
653                 continue
654             if d.endswith('_dump'):
655                 if d in svcs:
656                     continue
657                 if d[:-5]+'_details' in msgs:
658                     s['Service'].append(Service(d, d[:-5]+'_details',
659                                                  stream=True))
660                 else:
661                     raise ValueError('{} missing details message'
662                                      .format(d))
663                 continue
664
665             if d.endswith('_details'):
666                 if d[:-8]+'_dump' not in msgs:
667                     raise ValueError('{} missing dump message'
668                                      .format(d))
669                 continue
670
671             if d in svcs:
672                 continue
673             if d+'_reply' in msgs:
674                 s['Service'].append(Service(d, d+'_reply'))
675             else:
676                 raise ValueError(
677                     '{} missing reply message ({}) or service definition'
678                     .format(d, d+'_reply'))
679
680         return s
681
682     def process_imports(self, objs, in_import, result):
683         imported_objs = []
684         for o in objs:
685             # Only allow the following object types from imported file
686             if in_import and not (isinstance(o, Enum) or
687                                   isinstance(o, Union) or
688                                   isinstance(o, Typedef)):
689                 continue
690             result.append(o)
691
692             if isinstance(o, Import):
693                 self.process_imports(o.result, True, result)
694
695 # Add message ids to each message.
696 def add_msg_id(s):
697     for o in s:
698         o.block.insert(0, Field('u16', '_vl_msg_id'))
699     return s
700
701
702 dirlist = []
703
704
705 def dirlist_add(dirs):
706     global dirlist
707     if dirs:
708         dirlist = dirlist + dirs
709
710
711 def dirlist_get():
712     return dirlist
713
714
715 #
716 # Main
717 #
718 def main():
719     cliparser = argparse.ArgumentParser(description='VPP API generator')
720     cliparser.add_argument('--pluginpath', default=""),
721     cliparser.add_argument('--includedir', action='append'),
722     cliparser.add_argument('--input', type=argparse.FileType('r'),
723                            default=sys.stdin)
724     cliparser.add_argument('--output', nargs='?', type=argparse.FileType('w'),
725                            default=sys.stdout)
726
727     cliparser.add_argument('output_module', nargs='?', default='C')
728     cliparser.add_argument('--debug', action='store_true')
729     cliparser.add_argument('--show-name', nargs=1)
730     args = cliparser.parse_args()
731
732     dirlist_add(args.includedir)
733     if not args.debug:
734         sys.excepthook = exception_handler
735
736     # Filename
737     if args.show_name:
738         filename = args.show_name[0]
739     elif args.input != sys.stdin:
740         filename = args.input.name
741     else:
742         filename = ''
743
744     if args.debug:
745         logging.basicConfig(stream=sys.stdout, level=logging.WARNING)
746     else:
747         logging.basicConfig()
748     log = logging.getLogger('vppapigen')
749
750     parser = VPPAPI(debug=args.debug, filename=filename, logger=log)
751     parsed_objects = parser.parse_file(args.input, log)
752
753     # Build a list of objects. Hash of lists.
754     result = []
755     parser.process_imports(parsed_objects, False, result)
756     s = parser.process(result)
757
758     # Add msg_id field
759     s['Define'] = add_msg_id(s['Define'])
760
761     file_crc = global_crc & 0xffffffff
762
763     #
764     # Debug
765     if args.debug:
766         import pprint
767         pp = pprint.PrettyPrinter(indent=4)
768         for t in s['Define']:
769             pp.pprint([t.name, t.flags, t.block])
770         for t in s['types']:
771             pp.pprint([t.name, t.block])
772
773     #
774     # Generate representation
775     #
776     import imp
777
778     # Default path
779     pluginpath = ''
780     if not args.pluginpath:
781         cand = []
782         cand.append(os.path.dirname(os.path.realpath(__file__)))
783         cand.append(os.path.dirname(os.path.realpath(__file__)) +
784                     '/../share/vpp/')
785         for c in cand:
786             c += '/'
787             if os.path.isfile(c + args.output_module + '.py'):
788                 pluginpath = c
789                 break
790     else:
791         pluginpath = args.pluginpath + '/'
792     if pluginpath == '':
793         raise Exception('Output plugin not found')
794     module_path = pluginpath + args.output_module + '.py'
795
796     try:
797         plugin = imp.load_source(args.output_module, module_path)
798     except Exception, err:
799         raise Exception('Error importing output plugin: {}, {}'
800                         .format(module_path, err))
801
802     result = plugin.run(filename, s, file_crc)
803     if result:
804         print(result, file=args.output)
805     else:
806         raise Exception('Running plugin failed: {} {}'
807                         .format(filename, result))
808
809
810 if __name__ == '__main__':
811     main()