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