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