• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright 2023 The Chromium Authors
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5import dataclasses
6from typing import Dict
7from typing import Optional
8from typing import Tuple
9
10import java_lang_classes
11
12_CPP_TYPE_BY_JAVA_TYPE = {
13    'boolean': 'jboolean',
14    'byte': 'jbyte',
15    'char': 'jchar',
16    'double': 'jdouble',
17    'float': 'jfloat',
18    'int': 'jint',
19    'long': 'jlong',
20    'short': 'jshort',
21    'void': 'void',
22    'java/lang/Class': 'jclass',
23    'java/lang/String': 'jstring',
24    'java/lang/Throwable': 'jthrowable',
25}
26
27_DESCRIPTOR_CHAR_BY_PRIMITIVE_TYPE = {
28    'boolean': 'Z',
29    'byte': 'B',
30    'char': 'C',
31    'double': 'D',
32    'float': 'F',
33    'int': 'I',
34    'long': 'J',
35    'short': 'S',
36    'void': 'V',
37}
38
39_PRIMITIVE_TYPE_BY_DESCRIPTOR_CHAR = {
40    v: k
41    for k, v in _DESCRIPTOR_CHAR_BY_PRIMITIVE_TYPE.items()
42}
43
44_DEFAULT_VALUE_BY_PRIMITIVE_TYPE = {
45    'boolean': 'false',
46    'byte': '0',
47    'char': '0',
48    'double': '0',
49    'float': '0',
50    'int': '0',
51    'long': '0',
52    'short': '0',
53    'void': '',
54}
55
56PRIMITIVES = frozenset(_DEFAULT_VALUE_BY_PRIMITIVE_TYPE)
57
58
59@dataclasses.dataclass(frozen=True, order=True)
60class JavaClass:
61  """Represents a reference type."""
62  _fqn: str
63  # This is only meaningful if make_prefix have been called on the original class.
64  _class_without_prefix: 'JavaClass' = None
65
66  def __post_init__(self):
67    assert '.' not in self._fqn, f'{self._fqn} should have / and $, but not .'
68
69  def __str__(self):
70    return self.full_name_with_slashes
71
72  @property
73  def name(self):
74    return self._fqn.rsplit('/', 1)[-1]
75
76  @property
77  def name_with_dots(self):
78    return self.name.replace('$', '.')
79
80  @property
81  def nested_name(self):
82    return self.name.rsplit('$', 1)[-1]
83
84  @property
85  def package_with_slashes(self):
86    return self._fqn.rsplit('/', 1)[0]
87
88  @property
89  def package_with_dots(self):
90    return self.package_with_slashes.replace('/', '.')
91
92  @property
93  def full_name_with_slashes(self):
94    return self._fqn
95
96  @property
97  def full_name_with_dots(self):
98    return self._fqn.replace('/', '.').replace('$', '.')
99
100  @property
101  def class_without_prefix(self):
102    return self._class_without_prefix if self._class_without_prefix else self
103
104  def to_java(self, type_resolver=None):
105    # Empty resolver used to shorted java.lang classes.
106    type_resolver = type_resolver or _EMPTY_TYPE_RESOLVER
107    return type_resolver.contextualize(self)
108
109  def as_type(self):
110    return JavaType(java_class=self)
111
112  def make_prefixed(self, prefix=None):
113    if not prefix:
114      return self
115    prefix = prefix.replace('.', '/')
116    return JavaClass(f'{prefix}/{self._fqn}', self)
117
118  def make_nested(self, name):
119    return JavaClass(f'{self._fqn}${name}')
120
121
122@dataclasses.dataclass(frozen=True)
123class JavaType:
124  """Represents a parameter or return type."""
125  array_dimensions: int = 0
126  primitive_name: Optional[str] = None
127  java_class: Optional[JavaClass] = None
128  annotations: Dict[str, Optional[str]] = \
129      dataclasses.field(default_factory=dict, compare=False)
130
131  @staticmethod
132  def from_descriptor(descriptor):
133    # E.g.: [Ljava/lang/Class;
134    without_arrays = descriptor.lstrip('[')
135    array_dimensions = len(descriptor) - len(without_arrays)
136    descriptor = without_arrays
137
138    if descriptor[0] == 'L':
139      assert descriptor[-1] == ';', 'invalid descriptor: ' + descriptor
140      return JavaType(array_dimensions=array_dimensions,
141                      java_class=JavaClass(descriptor[1:-1]))
142    primitive_name = _PRIMITIVE_TYPE_BY_DESCRIPTOR_CHAR[descriptor[0]]
143    return JavaType(array_dimensions=array_dimensions,
144                    primitive_name=primitive_name)
145
146  @property
147  def non_array_full_name_with_slashes(self):
148    return self.primitive_name or self.java_class.full_name_with_slashes
149
150  # Cannot use dataclass(order=True) because some fields are None.
151  def __lt__(self, other):
152    if self.primitive_name and not other.primitive_name:
153      return -1
154    if other.primitive_name and not self.primitive_name:
155      return 1
156    lhs = (self.array_dimensions, self.primitive_name or self.java_class)
157    rhs = (other.array_dimensions, other.primitive_name or other.java_class)
158    return lhs < rhs
159
160  def is_primitive(self):
161    return self.primitive_name is not None and self.array_dimensions == 0
162
163  def is_void(self):
164    return self.primitive_name == 'void'
165
166  def to_descriptor(self):
167    """Converts a Java type into a JNI signature type."""
168    if self.primitive_name:
169      name = _DESCRIPTOR_CHAR_BY_PRIMITIVE_TYPE[self.primitive_name]
170    else:
171      name = f'L{self.java_class.full_name_with_slashes};'
172    return ('[' * self.array_dimensions) + name
173
174  def to_java(self, type_resolver=None):
175    if self.primitive_name:
176      ret = self.primitive_name
177    else:
178      ret = self.java_class.to_java(type_resolver)
179    return ret + '[]' * self.array_dimensions
180
181  def to_cpp(self):
182    """Returns a C datatype for the given java type."""
183    if self.array_dimensions > 1:
184      return 'jobjectArray'
185    if self.array_dimensions > 0 and self.primitive_name is None:
186      # There is no jstringArray.
187      return 'jobjectArray'
188
189    base = _CPP_TYPE_BY_JAVA_TYPE.get(self.non_array_full_name_with_slashes,
190                                      'jobject')
191    return base + ('Array' * self.array_dimensions)
192
193  def to_cpp_default_value(self):
194    """Returns a valid C return value for the given java type."""
195    if self.is_primitive():
196      return _DEFAULT_VALUE_BY_PRIMITIVE_TYPE[self.primitive_name]
197    return 'nullptr'
198
199  def to_proxy(self):
200    """Converts to types used over JNI boundary."""
201    # All object array types of become jobjectArray in native, but need to be
202    # passed as the original type on the java side.
203    if self.non_array_full_name_with_slashes in _CPP_TYPE_BY_JAVA_TYPE:
204      return self
205
206    # All other types should just be passed as Objects or Object arrays.
207    return dataclasses.replace(self, java_class=_OBJECT_CLASS)
208
209
210@dataclasses.dataclass(frozen=True)
211class JavaParam:
212  """Represents a parameter."""
213  java_type: JavaType
214  name: str
215
216  def to_proxy(self):
217    """Converts to types used over JNI boundary."""
218    return JavaParam(self.java_type.to_proxy(), self.name)
219
220
221class JavaParamList(tuple):
222  """Represents a parameter list."""
223  def to_proxy(self):
224    """Converts to types used over JNI boundary."""
225    return JavaParamList(p.to_proxy() for p in self)
226
227  def to_java_declaration(self, type_resolver=None):
228    return ', '.join('%s %s' % (p.java_type.to_java(type_resolver), p.name)
229                     for p in self)
230
231  def to_call_str(self):
232    return ', '.join(p.name for p in self)
233
234
235@dataclasses.dataclass(frozen=True, order=True)
236class JavaSignature:
237  """Represents a method signature (return type + parameter types)."""
238  return_type: JavaType
239  param_types: Tuple[JavaType]
240  # Signatures should be considered equal if parameter names differ, so exclude
241  # param_list from comparisons.
242  param_list: JavaParamList = dataclasses.field(compare=False)
243
244  @staticmethod
245  def from_params(return_type, param_list):
246    return JavaSignature(return_type=return_type,
247                         param_types=tuple(p.java_type for p in param_list),
248                         param_list=param_list)
249
250  @staticmethod
251  def from_descriptor(descriptor):
252    # E.g.: (Ljava/lang/Object;Ljava/lang/Runnable;)Ljava/lang/Class;
253    assert descriptor[0] == '('
254    i = 1
255    start_idx = i
256    params = []
257    while True:
258      char = descriptor[i]
259      if char == ')':
260        break
261      elif char == '[':
262        i += 1
263        continue
264      elif char == 'L':
265        end_idx = descriptor.index(';', i) + 1
266      else:
267        end_idx = i + 1
268      param_type = JavaType.from_descriptor(descriptor[start_idx:end_idx])
269      params.append(JavaParam(param_type, f'p{len(params)}'))
270      i = end_idx
271      start_idx = end_idx
272
273    return_type = JavaType.from_descriptor(descriptor[i + 1:])
274    return JavaSignature.from_params(return_type, JavaParamList(params))
275
276  def to_descriptor(self):
277    """Returns the JNI signature."""
278    sb = ['(']
279    sb += [t.to_descriptor() for t in self.param_types]
280    sb += [')']
281    sb += [self.return_type.to_descriptor()]
282    return ''.join(sb)
283
284  def to_proxy(self):
285    """Converts to types used over JNI boundary."""
286    return_type = self.return_type.to_proxy()
287    param_list = self.param_list.to_proxy()
288    return JavaSignature.from_params(return_type, param_list)
289
290
291class TypeResolver:
292  """Converts type names to fully qualified names."""
293  def __init__(self, java_class):
294    self.java_class = java_class
295    self.imports = []
296    self.nested_classes = []
297
298  def add_import(self, java_class):
299    self.imports.append(java_class)
300
301  def add_nested_class(self, java_class):
302    self.nested_classes.append(java_class)
303
304  def contextualize(self, java_class):
305    """Return the shortest string that resolves to the given class."""
306    type_package = java_class.package_with_slashes
307    if type_package in ('java/lang', self.java_class.package_with_slashes):
308      return java_class.name_with_dots
309    if java_class in self.imports:
310      return java_class.name_with_dots
311
312    return java_class.full_name_with_dots
313
314  def resolve(self, name):
315    """Return a JavaClass for the given type name."""
316    assert name not in PRIMITIVES
317
318    if '/' in name:
319      # Coming from javap, use the fully qualified name directly.
320      return JavaClass(name)
321
322    if self.java_class.name == name:
323      return self.java_class
324
325    for clazz in self.nested_classes:
326      if name in (clazz.name, clazz.nested_name):
327        return clazz
328
329    # Is it from an import? (e.g. referecing Class from import pkg.Class;
330    # note that referencing an inner class Inner from import pkg.Class.Inner
331    # is not supported).
332    for clazz in self.imports:
333      if name in (clazz.name, clazz.nested_name):
334        return clazz
335
336    # Is it an inner class from an outer class import? (e.g. referencing
337    # Class.Inner from import pkg.Class).
338    if '.' in name:
339      # Assume lowercase means it's a fully qualifited name.
340      if name[0].islower():
341        return JavaClass(name.replace('.', '/'))
342      # Otherwise, try and find the outer class in imports.
343      components = name.split('.')
344      outer = '/'.join(components[:-1])
345      inner = components[-1]
346      for clazz in self.imports:
347        if clazz.name == outer:
348          return clazz.make_nested(inner)
349      name = name.replace('.', '$')
350
351    # java.lang classes always take priority over types from the same package.
352    # To use a type from the same package that has the same name as a java.lang
353    # type, it must be explicitly imported.
354    if java_lang_classes.contains(name):
355      return JavaClass(f'java/lang/{name}')
356
357    # Type not found, falling back to same package as this class.
358    return JavaClass(f'{self.java_class.package_with_slashes}/{name}')
359
360
361_OBJECT_CLASS = JavaClass('java/lang/Object')
362_EMPTY_TYPE_RESOLVER = TypeResolver(_OBJECT_CLASS)
363LONG = JavaType(primitive_name='long')
364VOID = JavaType(primitive_name='void')
365EMPTY_PARAM_LIST = JavaParamList()
366