da1e01f5a63458abb51e4321fd43513397fe5890
[vpp.git] / extras / japi / java / jvpp / gen / jvppgen / jvpp_model.py
1 #!/usr/bin/env python2
2 #
3 # Copyright (c) 2018 Cisco and/or its affiliates.
4 # Licensed under the Apache License, Version 2.0 (the "License");
5 # you may not use this file except in compliance with the License.
6 # You may obtain a copy of the License at:
7 #
8 #     http://www.apache.org/licenses/LICENSE-2.0
9 #
10 # Unless required by applicable law or agreed to in writing, software
11 # distributed under the License is distributed on an "AS IS" BASIS,
12 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 # See the License for the specific language governing permissions and
14 # limitations under the License.
15 #
16 import json
17 import pprint
18 from collections import OrderedDict
19
20 import binascii
21
22 BASE_PACKAGE = "io.fd.vpp.jvpp"
23
24
25 class ParseException(Exception):
26     pass
27
28
29 class Type(object):
30     def __init__(self, name, java_name, java_name_fqn, jni_signature, jni_type, jni_accessor,
31                  host_to_net_function, net_to_host_function):
32         """
33         Initializes Type class.
34
35         :param name: name of type as defined in .api file, e.g. u8, u32[] or mac_entry
36         :param java_name: corresponding java name, e.g. byte, int[] or MacEntry
37         :param java_name_fqn: fully qualified java name, e.g. io.fd.vpp.jvpp.core.types.MacEntry
38         :param jni_signature: JNI Type signature, e.g. B, [I or Lio.fd.vpp.jvpp.core.types.MacEntry;
39                               See https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/types.html#type_signatures
40         :param jni_type: JNI reference type, e.g. jbyte jintArray, jobject
41                          See https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/types.html#reference_types
42         :param jni_accessor: Java type do by used in Get<type>Field, Set<type>Field and other functions.
43                              See https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/functions.html#accessing_fields_of_objects
44         :param host_to_net_function: name of function host to net byte order swap function
45         :param net_to_host_function: name of function net to host byte order swap function
46         """
47         self.name = name
48         self.java_name = java_name
49
50         # Java generation specific properties, TODO(VPP-1186): move to Java specific subclass
51         self.java_name_fqn = java_name_fqn
52
53         # JNI generation specific properties, TODO(VPP-1186): move to JNI specific subclass
54         self.jni_signature = jni_signature
55
56         # Native type, see:
57         # https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/types.html#primitive_types
58         # and
59         # https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/types.html#reference_types
60         self.jni_type = jni_type
61
62         # Java type do by used in Get<type>Field, Set<type>Field and other functions, see:
63         # https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/functions.html#accessing_fields_of_objects
64         self.jni_accessor = jni_accessor
65
66         self.host_to_net_function = host_to_net_function
67         self.net_to_host_function = net_to_host_function
68         self.is_swap_needed = host_to_net_function and net_to_host_function
69
70
71 class SimpleType(Type):
72     def __init__(self, name, java_name, jni_signature, jni_type, jni_accessor,
73                  host_to_net_function=None, net_to_host_function=None):
74         super(SimpleType, self).__init__(
75             name=name,
76             java_name=java_name,
77             java_name_fqn=java_name,
78             jni_signature=jni_signature,
79             jni_type=jni_type,
80             jni_accessor=jni_accessor,
81             host_to_net_function=host_to_net_function,
82             net_to_host_function=net_to_host_function
83         )
84         self.vpp_name = name
85
86     def get_host_to_net_function(self, host_ref_name, net_ref_name):
87         return "%s = %s(%s)" % (net_ref_name, self.host_to_net_function, host_ref_name)
88
89     def __str__(self):
90         return "SimpleType{name:%s, java_name:%s}" % (self.name, self.java_name)
91
92
93 # TODO(VPP-1187): add array host to net functions to reduce number of members and simplify JNI generation
94 class Array(Type):
95     def __init__(self, base_type, name=None):
96         if name is None:
97             name = base_type.name + _ARRAY_SUFFIX
98         super(Array, self).__init__(
99             name=name,
100             java_name=base_type.java_name + _ARRAY_SUFFIX,
101             java_name_fqn=base_type.java_name_fqn + _ARRAY_SUFFIX,
102             jni_signature="[%s" % base_type.jni_signature,
103             jni_type="%sArray" % base_type.jni_type,
104             jni_accessor="Object",
105             host_to_net_function=base_type.host_to_net_function,
106             net_to_host_function=base_type.net_to_host_function
107         )
108         self.base_type = base_type
109
110     def get_host_to_net_function(self, host_ref_name, net_ref_name):
111         return self.base_type.get_host_to_net_function(host_ref_name, net_ref_name)
112
113     def __str__(self):
114         return "Array{name:%s, java_name:%s}" % (self.name, self.java_name)
115
116
117 class Enum(Type):
118     def __init__(self, name, value, constants, definition, plugin_name):
119         _java_name = _underscore_to_camelcase_upper(name)
120
121         super(Enum, self).__init__(
122             name=name,
123             java_name=_java_name,
124             java_name_fqn="io.fd.vpp.jvpp.%s.types.%s" % (plugin_name, _java_name),
125             jni_signature="Lio/fd/vpp/jvpp/%s/types/%s;" % (plugin_name, _java_name),
126             jni_type="jobject",
127             jni_accessor="Object",
128             host_to_net_function="_host_to_net_%s" % name,
129             net_to_host_function="_net_to_host_%s" % name
130         )
131
132         self.value = value
133         self.constants = constants
134         self.doc = _message_to_javadoc(definition)
135         self.java_name_lower = _underscore_to_camelcase_lower(name)
136         self.vpp_name = "%s%s%s" % (_VPP_TYPE_PREFIX, name, _VPP_TYPE_SUFFIX)
137         # Fully qualified class name used by FindClass function, see:
138         # https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/functions.html#FindClass
139         self.jni_name = "io/fd/vpp/jvpp/%s/types/%s" % (plugin_name, _java_name)
140
141     def get_host_to_net_function(self, host_ref_name, net_ref_name):
142         return "_host_to_net_%s(env, %s, &(%s))" % (self.name, host_ref_name, net_ref_name)
143
144
145 class Class(Type):
146     def __init__(self, name, crc, fields, definition, plugin_name):
147         _java_name = _underscore_to_camelcase_upper(name)
148
149         super(Class, self).__init__(
150             name=name,
151             java_name=_java_name,
152             java_name_fqn="io.fd.vpp.jvpp.%s.types.%s" % (plugin_name, _java_name),
153             jni_signature="Lio/fd/vpp/jvpp/%s/types/%s;" % (plugin_name, _java_name),
154             jni_type="jobject",
155             jni_accessor="Object",
156             host_to_net_function="_host_to_net_%s" % name,
157             net_to_host_function="_net_to_host_%s" % name
158         )
159
160         self.crc = crc
161         self.fields = fields
162         self.doc = _message_to_javadoc(definition)
163         self.java_name_lower = _underscore_to_camelcase_lower(name)
164         self.vpp_name = "%s%s%s" % (_VPP_TYPE_PREFIX, name, _VPP_TYPE_SUFFIX)
165         # Fully qualified class name used by FindClass function, see:
166         # https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/functions.html#FindClass
167         self.jni_name = "io/fd/vpp/jvpp/%s/types/%s" % (plugin_name, _java_name)
168
169     def get_host_to_net_function(self, host_ref_name, net_ref_name):
170         return "_host_to_net_%s(env, %s, &(%s))" % (self.name, host_ref_name, net_ref_name)
171
172
173 class Union(Type):
174     def __init__(self, name, crc, fields, definition, plugin_name):
175         _java_name = _underscore_to_camelcase_upper(name)
176
177         super(Union, self).__init__(
178             name=name,
179             java_name=_java_name,
180             java_name_fqn="io.fd.vpp.jvpp.%s.types.%s" % (plugin_name, _java_name),
181             jni_signature="Lio/fd/vpp/jvpp/%s/types/%s;" % (plugin_name, _java_name),
182             jni_type="jobject",
183             jni_accessor="Object",
184             host_to_net_function="_host_to_net_%s" % name,
185             net_to_host_function="_net_to_host_%s" % name
186         )
187
188         self.crc = crc
189         self.fields = fields
190         self.doc = _message_to_javadoc(definition)
191         self.java_name_lower = _underscore_to_camelcase_lower(name)
192         self.vpp_name = "%s%s%s" % (_VPP_TYPE_PREFIX, name, _VPP_TYPE_SUFFIX)
193         # Fully qualified class name used by FindClass function, see:
194         # https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/functions.html#FindClass
195         self.jni_name = "io/fd/vpp/jvpp/%s/types/%s" % (plugin_name, _java_name)
196
197     def get_host_to_net_function(self, host_ref_name, net_ref_name):
198         return "_host_to_net_%s(env, %s, &(%s))" % (self.name, host_ref_name, net_ref_name)
199
200
201 class Field(object):
202     def __init__(self, name, field_type, array_len=None, array_len_field=None):
203         self.name = name
204         self.java_name = _underscore_to_camelcase_lower(name)
205         self.java_name_upper = _underscore_to_camelcase_upper(name)
206         self.type = field_type
207         self.array_len = array_len
208         self.array_len_field = array_len_field
209
210     def __str__(self):
211         return "Field{name:%s, java_name:%s, type:%s}" % (self.name, self.java_name, self.type)
212
213
214 class Message(object):
215     def __init__(self, name, crc, fields, definition):
216         self.name = name
217         self.java_name_upper = _underscore_to_camelcase_upper(name)
218         self.java_name_lower = _underscore_to_camelcase_lower(name)
219         self.crc = crc[2:]
220         self.fields = fields
221         self.has_fields = fields != []
222         self.doc = _message_to_javadoc(definition)
223
224     def __str__(self):
225         return "Message{name:%s, java_name:%s, crc:%s, fields:%s}" % (
226             self.name, self.java_name_upper, self.crc, self.fields)
227
228
229 class Event(Message):
230     def __init__(self, name, crc, fields, definition):
231         super(Event, self).__init__(name, crc, fields, definition)
232
233
234 class Request(Message):
235     def __init__(self, name, reply, crc, fields, definition):
236         super(Request, self).__init__(name, crc, fields, definition)
237         self.reply = reply
238         self.reply_java = _underscore_to_camelcase_upper(reply)
239
240     def __str__(self):
241         return "Request{name:%s, reply:%s, crc:%s, fields:%s}" % (self.name, self.reply, self.crc, self.fields)
242
243
244 class Reply(Message):
245     def __init__(self, name, request, crc, fields, definition):
246         super(Reply, self).__init__(name, crc, fields, definition)
247         self.request = request
248         self.request_java = _underscore_to_camelcase_upper(request)
249
250     def __str__(self):
251         return "Reply{name:%s, request:%s, crc:%s, fields:%s}" % (self.name, self.request, self.crc, self.fields)
252
253
254 class Dump(Message):
255     def __init__(self, name, details, crc, fields, definition):
256         super(Dump, self).__init__(name, crc, fields, definition)
257         self.details = details
258         self.reply_java = _underscore_to_camelcase_upper(details)
259
260     def __str__(self):
261         return "Dump{name:%s, details:%s, crc:%s, fields:%s}" % (self.name, self.details, self.crc, self.fields)
262
263
264 class Details(Message):
265     def __init__(self, name, dump, crc, fields, definition):
266         super(Details, self).__init__(name, crc, fields, definition)
267         self.dump = dump
268         self.request_java = _underscore_to_camelcase_upper(dump)
269
270     def __str__(self):
271         return "Details{name:%s, dump:%s, crc:%s, fields:%s}" % (self.name, self.dump, self.crc, self.fields)
272
273
274 def is_retval(field):
275     return field.name == u'retval'
276
277
278 def is_array(field):
279     return field.array_len is not None
280
281
282 def is_request(msg):
283     return hasattr(msg, 'reply')
284
285
286 def is_reply(msg):
287     return hasattr(msg, 'request')
288
289
290 def is_dump(msg):
291     return hasattr(msg, 'details')
292
293
294 def is_details(msg):
295     return hasattr(msg, 'dump')
296
297
298 def is_event(msg):
299     return isinstance(msg, Event)
300
301
302 def is_control_ping(msg):
303     return msg.name == u'control_ping'
304
305
306 def is_control_ping_reply(msg):
307     return msg.name == u'control_ping_reply'
308
309
310 def crc(block):
311     s = str(block).encode()
312     return binascii.crc32(s) & 0xffffffff
313
314
315 class JVppModel(object):
316     def __init__(self, logger, json_api_files, plugin_name):
317         self.logger = logger
318         # TODO(VPP-1188): provide json_file_by_definition map to improve javadoc
319         self.json_api_files = json_api_files
320         self.plugin_package = BASE_PACKAGE + "." + plugin_name
321         self.plugin_name = plugin_name
322         self.plugin_java_name = _underscore_to_camelcase_upper(plugin_name)
323         self._load_json_files(json_api_files)
324         self._parse_services()
325         self._parse_messages()
326         self._validate_messages()
327
328     def _load_json_files(self, json_api_files):
329         types = {}
330         self._messages = []
331         self._services = {}
332         self._aliases = {}
333         for file_name in json_api_files:
334             with open(file_name) as f:
335                 j = json.load(f)
336                 types.update({d[0]: {'type': 'enum', 'data': d} for d in j['enums']})
337                 types.update({d[0]: {'type': 'type', 'data': d} for d in j['types']})
338                 types.update({d[0]: {'type': 'union', 'data': d} for d in j['unions']})
339                 self._messages.extend(j['messages'])
340                 self._services.update(j['services'])
341                 self._aliases.update(j['aliases'])
342
343         self._parse_types(types)
344
345     def _parse_aliases(self, types):
346
347         # model aliases
348         for alias_name in self._aliases:
349             alias = self._aliases[alias_name]
350             alias_type = {"type": "type"}
351             java_name_lower = _underscore_to_camelcase_lower(alias_name)
352             vpp_type = alias["type"]
353             crc_value = '0x%08x' % crc(alias_name)
354             if "length" in alias:
355                 length = alias["length"]
356                 alias_type["data"] = [
357                     alias_name,
358                     [
359                         vpp_type,
360                         java_name_lower,
361                         length
362                     ],
363                     {
364                         "crc": crc_value
365                     }
366                 ]
367             else:
368                 alias_type["data"] = [
369                     alias_name,
370                     [
371                         vpp_type,
372                         java_name_lower
373                     ],
374                     {
375                         "crc": crc_value
376                     }
377                 ]
378
379             types[alias_name] = alias_type
380
381     def _parse_types(self, types):
382         self._parse_simple_types()
383         self._parse_aliases(types)
384         i = 0
385         while True:
386             unresolved = {}
387             for name, value in types.items():
388                 if name in self._types_by_name:
389                     continue
390
391                 type = value['type']
392                 data = value['data'][1:]
393                 try:
394                     if type == 'enum':
395                         type = self._parse_enum(name, data)
396                     elif type == 'union':
397                         type = self._parse_union(name, data)
398                     elif type == 'type':
399                         type = self._parse_type(name, data)
400                     else:
401                         self.logger.warning("Unsupported type %s. Ignoring...", type)
402                         continue
403
404                     self._types_by_name[name] = type
405                     self._types_by_name[name + _ARRAY_SUFFIX] = Array(type)
406                 except ParseException as e:
407                     self.logger.debug("Failed to parse %s type in iteration %s: %s.", name, i, e)
408                     unresolved[name] = value
409             if len(unresolved) == 0:
410                 break
411             if i > 3:
412                 raise ParseException('Unresolved type definitions {}'
413                                      .format(unresolved))
414             types = unresolved
415             i += 1
416
417         self.types = self._types_by_name.values()
418
419     def _parse_simple_types(self):
420         # Mapping according to:
421         # http://docs.oracle.com/javase/7/do+'[]'cs/technotes/guides/jni/spec/types.html
422         # and
423         # https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/functions.html#Get_type_Field_routines
424         #
425         # Unsigned types are converted to signed java types that have the same size.
426         # It is the API user responsibility to interpret them correctly.
427
428         self._types_by_name = OrderedDict({
429             'u8': SimpleType('u8', 'byte', 'B', 'jbyte', 'Byte'),
430             'i8': SimpleType('i8', 'byte', 'B', 'jbyte', 'Byte'),
431             'u16': SimpleType('u16', 'short', 'S', 'jshort', 'Short',
432                               host_to_net_function='clib_host_to_net_u16',
433                               net_to_host_function='clib_net_to_host_u16'),
434             'i16': SimpleType('i16', 'short', 'S', 'jshort', 'Short',
435                               host_to_net_function='clib_host_to_net_i16',
436                               net_to_host_function='clib_net_to_host_i16'),
437             'u32': SimpleType('u32', 'int', 'I', 'jint', 'Int',
438                               host_to_net_function='clib_host_to_net_u32',
439                               net_to_host_function='clib_net_to_host_u32'),
440             'i32': SimpleType('i32', 'int', 'I', 'jint', 'Int',
441                               host_to_net_function='clib_host_to_net_i32',
442                               net_to_host_function='clib_net_to_host_i32'),
443             'u64': SimpleType('u64', 'long', 'J', 'jlong', 'Long',
444                               host_to_net_function='clib_host_to_net_u64',
445                               net_to_host_function='clib_net_to_host_u64'),
446             'i64': SimpleType('i64', 'long', 'J', 'jlong', 'Long',
447                               host_to_net_function='clib_host_to_net_i64',
448                               net_to_host_function='clib_net_to_host_i64'),
449             'f64': SimpleType('f64', 'double', 'D', 'jdouble', 'Double'),
450             'string': SimpleType('string', 'String', 'l', 'jstring', 'Object',
451                                  host_to_net_function='_host_to_net_string',
452                                  net_to_host_function='_net_to_host_string')
453         })
454
455         for n, t in self._types_by_name.items():
456             self._types_by_name[n + _ARRAY_SUFFIX] = Array(t)
457
458     def _parse_enum(self, name, definition):
459         self.logger.debug("Parsing enum %s: %s", name, definition)
460         constants = []
461         type_name = None
462         for item in definition:
463             if type(item) is dict and 'enumtype' in item:
464                 type_name = item['enumtype']
465                 continue
466             constants.append({'name': item[0], 'value': item[1]})
467         if not type_name:
468             raise ParseException("'enumtype' was not defined for %s" % definition)
469         return Enum(name, Field('value', self._types_by_name[type_name]), constants, definition, self.plugin_name)
470
471     def _parse_union(self, name, definition):
472         self.logger.debug("Parsing union %s: %s", name, definition)
473         crc, fields = self._parse_fields(definition)
474         return Union(name, crc, fields, definition, self.plugin_name)
475
476     def _parse_type(self, name, definition):
477         self.logger.debug("Parsing type %s: %s", name, definition)
478         crc, fields = self._parse_fields(definition)
479         return Class(name, crc, fields, definition, self.plugin_name)
480
481     def _parse_services(self):
482         self._dumps_by_details = {}
483         self._requests_by_reply = {}
484         for name, service in self._services.iteritems():
485             if _is_stream(service):
486                 self._dumps_by_details[service['reply']] = name
487             else:
488                 self._requests_by_reply[service['reply']] = name
489
490     def _parse_messages(self):
491         # Preserve ordering from JSON file to make debugging easier.
492         self._messages_by_name = OrderedDict()
493         for msg in self._messages:
494             try:
495                 name = msg[0]
496                 definition = msg[1:]
497                 self._messages_by_name[name] = self._parse_message(name, definition)
498             except ParseException as e:
499                 self.logger.warning("Failed to parse message %s: %s. Skipping message.", name, e)
500
501     def _parse_message(self, name, definition):
502         self.logger.debug("Parsing message %s: %s", name, definition)
503         crc, fields = self._parse_fields(definition)
504         if name in self._services:
505             service = self._services[name]
506             reply = service['reply']
507             if _is_stream(service):
508                 return Dump(name, reply, crc, filter(_is_request_field, fields), definition)
509             if reply:
510                 return Request(name, reply, crc, filter(_is_request_field, fields), definition)
511             else:
512                 return Event(name, crc, filter(_is_request_field, fields), definition)
513         elif name in self._requests_by_reply:
514             return Reply(name, self._requests_by_reply[name], crc, filter(_is_reply_field, fields), definition)
515         elif name in self._dumps_by_details:
516             return Details(name, self._dumps_by_details[name], crc, filter(_is_reply_field, fields), definition)
517         else:
518             # TODO: some messages like combined_counters are not visible in the services.
519             # Throw exception instead (requires fixing vppagigen).
520             return Event(name, crc, filter(_is_request_field, fields), definition)
521
522     def _parse_fields(self, definition):
523         crc = None
524         fields = []
525         for item in definition:
526             if type(item) == dict and 'crc' in item:
527                 crc = item['crc']
528             else:
529                 fields.append(self._parse_field(item, fields))
530         if not crc:
531             raise ParseException("CRC was not defined for %s" % definition)
532         return crc, fields
533
534     def _parse_field(self, field, fields):
535         type_name = _extract_type_name(field[0])
536
537         if type_name in self._types_by_name:
538             if len(field) > 2:
539                 # Array field
540                 array_len_field = None
541                 if len(field) == 4:
542                     for f in fields:
543                         if f.name == field[3]:
544                             array_len_field = f
545                     if not array_len_field:
546                         raise ParseException("Could not find field %s declared as length of array %s",
547                                              field[3], field[1])
548                 return Field(field[1], self._types_by_name[type_name + _ARRAY_SUFFIX], field[2], array_len_field)
549             else:
550                 return Field(field[1], self._types_by_name[type_name])
551         else:
552             raise ParseException("Unknown field type %s" % field)
553
554     def _validate_messages(self):
555         """
556         In case if message A is known to be reply for message B, and message B was not correctly parsed,
557         remove message A from the set of all messages.
558         """
559         to_be_removed = []
560         messages = self._messages_by_name
561         for name, msg in messages.iteritems():
562             if (is_request(msg) and msg.reply not in messages) \
563                     or (is_reply(msg) and msg.request not in messages) \
564                     or (is_dump(msg) and msg.details not in messages) \
565                     or (is_details(msg) and msg.dump not in messages):
566                 to_be_removed.append(name)
567
568         for name in to_be_removed:
569             del messages[name]
570
571         self.messages = self._messages_by_name.values()
572
573
574 _ARRAY_SUFFIX = '[]'
575
576
577 def _underscore_to_camelcase_upper(name):
578     return name.title().replace("_", "")
579
580
581 def _underscore_to_camelcase_lower(name):
582     name = name.title().replace("_", "")
583     return name[0].lower() + name[1:]
584
585
586 def _message_to_javadoc(message_definition):
587     """ Converts JSON message definition to javadoc """
588     formatted_message = pprint.pformat(message_definition, indent=4, width=120, depth=None)
589     return " * " + formatted_message.replace("\n", "\n * ")
590
591
592 def _is_stream(service):
593     """
594     Checks if service represents stream, e.g.:
595     "ip_address_dump": {
596         "reply": "ip_address_details",
597         "stream": true
598     }
599     :param service: JSON definition of service
600     :return: value assigned to "stream" or None
601     """
602     return "stream" in service
603
604
605 def _extract_type_name(name):
606     if name.startswith(_VPP_TYPE_PREFIX) and name.endswith(_VPP_TYPE_SUFFIX):
607         return name[len(_VPP_TYPE_PREFIX): - len(_VPP_TYPE_SUFFIX)]
608     return name
609
610 _VPP_TYPE_PREFIX = "vl_api_"
611
612 _VPP_TYPE_SUFFIX = "_t"
613
614
615 def _is_request_field(field):
616     # Skip fields that are hidden to the jvpp user (handled by JNI layer)
617     return field.name not in {'_vl_msg_id', 'client_index', 'context'}
618
619
620 def _is_reply_field(field):
621     # Skip fields that are hidden to the jvpp user:
622     # _vl_msg_id is handled at JNI layer,
623     # Unlike in the request case, context is visible to allow matching replies with requests at Java layer.
624     return field.name not in {'_vl_msg_id'}