| // Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file |
| // for details. All rights reserved. Use of this source code is governed by a |
| // BSD-style license that can be found in the LICENSE file. |
| |
| import 'dart:io'; |
| |
| import 'package:meta/meta.dart'; |
| |
| import '../config/config.dart'; |
| import '../elements/elements.dart'; |
| import '../logging/logging.dart'; |
| import '../writers/bindings_writer.dart'; |
| import 'c_generator.dart'; |
| import 'resolver.dart'; |
| import 'visitor.dart'; |
| |
| // Import prefixes |
| const _jni = 'jni'; |
| const _ffi = 'ffi'; |
| |
| // package:jni types |
| const _jType = '$_jni.JObjType'; |
| const _jPointer = '$_jni.JObjectPtr'; |
| const _jArray = '$_jni.JArray'; |
| const _jObject = '$_jni.JObject'; |
| const _jThrowable = '$_jni.JThrowablePtr'; |
| const _jResult = '$_jni.JniResult'; |
| const _jCallType = '$_jni.JniCallType'; |
| |
| // package:ffi types |
| const _voidPointer = '$_ffi.Pointer<$_ffi.Void>'; |
| |
| // Prefixes and suffixes |
| const _typeParamPrefix = '\$'; |
| // TODO(#143): this is a temporary fix for the name collision. |
| const _typeClassPrefix = '\$'; |
| const _typeClassSuffix = 'Type'; |
| |
| // Misc. |
| const _classRef = '_classRef'; |
| const _env = 'jniEnv'; |
| const _accessors = 'jniAccessors'; |
| const _lookup = 'jniLookup'; |
| const _selfPointer = 'reference'; |
| |
| // Docs |
| const _deleteInstruction = |
| ' /// The returned object must be deleted after use, ' |
| 'by calling the `delete` method.'; |
| |
| extension on String { |
| String capitalize() { |
| return '${this[0].toUpperCase()}${substring(1)}'; |
| } |
| |
| /// Reverses an ASCII string. |
| String get reversed => split('').reversed.join(); |
| } |
| |
| extension on Iterable<String> { |
| /// Similar to [join] but adds the [separator] to the end as well. |
| String delimited([String separator = '']) { |
| return map((e) => '$e$separator').join(); |
| } |
| } |
| |
| /// Encloses [inside] in the middle of [open] and [close] |
| /// if [inside] is not empty. |
| String _encloseIfNotEmpty(String open, String inside, String close) { |
| if (inside == '') return ''; |
| return '$open$inside$close'; |
| } |
| |
| String _newLine({int depth = 0}) { |
| return '\n${' ' * depth}'; |
| } |
| |
| /// Merges two maps. For the same keys, their value lists will be concatenated. |
| /// |
| /// ** After calling this, the original maps might get modified! ** |
| Map<K, List<V>> _mergeMapValues<K, V>(Map<K, List<V>> a, Map<K, List<V>> b) { |
| final merged = <K, List<V>>{}; |
| for (final key in {...a.keys, ...b.keys}) { |
| if (!a.containsKey(key)) { |
| merged[key] = b[key]!; |
| continue; |
| } |
| if (!b.containsKey(key)) { |
| merged[key] = a[key]!; |
| continue; |
| } |
| |
| // Merging the smaller one to the bigger one |
| if (a[key]!.length > b[key]!.length) { |
| merged[key] = a[key]!; |
| merged[key]!.addAll(b[key]!); |
| } else { |
| merged[key] = b[key]!; |
| merged[key]!.addAll(a[key]!); |
| } |
| } |
| return merged; |
| } |
| |
| /// **Naming Convention** |
| /// |
| /// Let's take the following code as an example: |
| /// |
| /// ```dart |
| /// Method definition |
| /// void f<T extends num, U>(JType<T> $T, JType<U> $U, T t, U u) { |
| /// // ... |
| /// } |
| /// f<int, String>($T, $U, t, u); // Calling the Method |
| /// ``` |
| /// |
| /// Here `f` will be replaced according to the place of usage. |
| /// |
| /// * `fArgsDef` refers to `T t, U u` – the arguments in the method |
| /// definition. |
| /// * `fArgsCall` refer to `t, u` – the arguments passed to call the method. |
| /// * `fTypeParamsDef` refers to `<T extends num, U>` – the type parameters |
| /// of the method at the point of definition. |
| /// * `fTypeParamsCall` refers to `<int, String>` – the type parameters when |
| /// calling, or whenever we don't want to have the `extends` keyword. |
| /// * `fTypeClassesDef` refers to `JType<T> $T, JType<U> $U`. |
| /// * `fTypeClassesCall` refers to `$T, $U` when calling the method. |
| class DartGenerator extends Visitor<Classes, Future<void>> { |
| final Config config; |
| |
| DartGenerator(this.config); |
| |
| static const cInitImport = 'import "dart:ffi" as ffi;\n' |
| 'import "package:jni/internal_helpers_for_jnigen.dart";\n'; |
| |
| /// Initialization code for C based bindings. |
| /// |
| /// Should be called once in a package. In package-structured bindings |
| /// this is placed in _init.dart in package root. |
| String get cInitCode => ''' |
| // Auto-generated initialization code. |
| |
| final $_ffi.Pointer<T> Function<T extends $_ffi.NativeType>(String sym) $_lookup = |
| ProtectedJniExtensions.initGeneratedLibrary("${config.outputConfig.cConfig!.libraryName}"); |
| |
| |
| '''; |
| |
| static const dartOnlyInitImport = 'import "package:jni/jni.dart" as jni;\n'; |
| |
| static const dartOnlyInitCode = ''' |
| // Auto-generated initialization code. |
| |
| final $_env = $_jni.Jni.env; |
| final $_accessors = $_jni.Jni.accessors; |
| |
| |
| '''; |
| |
| static const autoGeneratedNotice = '// Autogenerated by jnigen. ' |
| 'DO NOT EDIT!\n\n'; |
| static const defaultImports = ''' |
| import "dart:isolate" show ReceivePort; |
| import "dart:ffi" as ffi; |
| import "package:jni/internal_helpers_for_jnigen.dart"; |
| import "package:jni/jni.dart" as jni; |
| |
| '''; |
| static const defaultLintSuppressions = ''' |
| // ignore_for_file: annotate_overrides |
| // ignore_for_file: camel_case_extensions |
| // ignore_for_file: camel_case_types |
| // ignore_for_file: constant_identifier_names |
| // ignore_for_file: file_names |
| // ignore_for_file: no_leading_underscores_for_local_identifiers |
| // ignore_for_file: non_constant_identifier_names |
| // ignore_for_file: overridden_fields |
| // ignore_for_file: unnecessary_cast |
| // ignore_for_file: unused_element |
| // ignore_for_file: unused_field |
| // ignore_for_file: unused_import |
| // ignore_for_file: unused_shown_name |
| |
| '''; |
| static const preImportBoilerplate = |
| autoGeneratedNotice + defaultLintSuppressions + defaultImports; |
| |
| @override |
| Future<void> visit(Classes node) async { |
| final cBased = config.outputConfig.bindingsType == BindingsType.cBased; |
| final root = config.outputConfig.dartConfig.path; |
| final preamble = config.preamble ?? ''; |
| if (config.outputConfig.dartConfig.structure == |
| OutputStructure.singleFile) { |
| final file = File.fromUri(root); |
| await file.create(recursive: true); |
| log.info("Generating ${cBased ? "C + Dart" : "Pure Dart"} Bindings"); |
| final s = file.openWrite(); |
| s.writeln(preamble); |
| s.writeln(autoGeneratedNotice); |
| s.writeln(defaultLintSuppressions); |
| s.writeln(defaultImports); |
| if (cBased) { |
| s.writeln(cInitCode); |
| } else { |
| s.writeln(dartOnlyInitCode); |
| } |
| final classGenerator = _ClassGenerator(config, s); |
| node.decls.values.accept(classGenerator).toList(); |
| await s.close(); |
| await runDartFormat(file.path); |
| return; |
| } |
| final files = <String, List<ClassDecl>>{}; |
| final packages = <String, Set<String>>{}; |
| for (final classDecl in node.decls.values) { |
| final fileClass = Resolver.getFileClassName(classDecl.binaryName); |
| |
| files.putIfAbsent(fileClass, () => <ClassDecl>[]); |
| files[fileClass]!.add(classDecl); |
| |
| packages.putIfAbsent(classDecl.packageName, () => {}); |
| packages[classDecl.packageName]!.add(fileClass.split('.').last); |
| } |
| |
| log.info("Using dart root = $root"); |
| const initFileName = '_init.dart'; |
| final initFileUri = root.resolve(initFileName); |
| final initFile = File.fromUri(initFileUri); |
| await initFile.create(recursive: true); |
| final initStream = initFile.openWrite(); |
| initStream.writeln(preamble); |
| initStream.writeln(cBased ? cInitImport : dartOnlyInitImport); |
| initStream.writeln(cBased ? cInitCode : dartOnlyInitCode); |
| await initStream.close(); |
| for (var fileClassName in files.keys) { |
| final relativeFileName = '${fileClassName.replaceAll('.', '/')}.dart'; |
| final dartFileUri = root.resolve(relativeFileName); |
| final dartFile = await File.fromUri(dartFileUri).create(recursive: true); |
| log.fine('$fileClassName -> ${dartFile.path}'); |
| |
| final classesInFile = files[fileClassName]!; |
| final dartFileStream = dartFile.openWrite(); |
| dartFileStream.writeln(preamble); |
| dartFileStream.writeln(autoGeneratedNotice); |
| dartFileStream.writeln(defaultLintSuppressions); |
| dartFileStream.writeln(defaultImports); |
| final s = StringBuffer(); |
| final initFilePath = ('../' * |
| relativeFileName.codeUnits |
| .where((cu) => cu == '/'.codeUnitAt(0)) |
| .length) + |
| initFileName; |
| s.write('import "$initFilePath";'); |
| final resolver = Resolver( |
| importMap: config.importMap ?? {}, |
| currentClass: fileClassName, |
| inputClassNames: node.decls.keys.toSet(), |
| ); |
| classesInFile |
| .accept(_ClassGenerator(config, s, resolver: resolver)) |
| .toList(); |
| dartFileStream.writeAll(resolver.getImportStrings(), '\n'); |
| dartFileStream.writeln(s.toString()); |
| await dartFileStream.close(); |
| } |
| |
| // write _package.dart export files |
| for (var package in packages.keys) { |
| final dirUri = root.resolve('${package.replaceAll('.', '/')}/'); |
| final exportFileUri = dirUri.resolve("_package.dart"); |
| final exportFile = File.fromUri(exportFileUri); |
| exportFile.createSync(recursive: true); |
| final exports = |
| packages[package]!.map((cls) => 'export "$cls.dart";').join('\n'); |
| exportFile.writeAsStringSync(exports); |
| } |
| await runDartFormat(root.toFilePath()); |
| log.info('Completed.'); |
| } |
| } |
| |
| /// Generates the Dart class definition, type class, and the array extension |
| class _ClassGenerator extends Visitor<ClassDecl, void> { |
| final Config config; |
| final StringSink s; |
| final Resolver? resolver; |
| |
| _ClassGenerator( |
| this.config, |
| this.s, { |
| this.resolver, |
| }); |
| |
| static const staticTypeGetter = 'type'; |
| static const instanceTypeGetter = '\$$staticTypeGetter'; |
| |
| @override |
| void visit(ClassDecl node) { |
| final isDartOnly = |
| config.outputConfig.bindingsType == BindingsType.dartOnly; |
| |
| // Docs |
| s.write('/// from: ${node.binaryName}\n'); |
| node.javadoc?.accept(_DocGenerator(s, depth: 0)); |
| |
| // Class definition |
| final name = node.finalName; |
| final superName = node.superclass!.accept(_TypeGenerator(resolver)); |
| final typeParamsDef = _encloseIfNotEmpty( |
| '<', |
| node.allTypeParams |
| .accept(const _TypeParamGenerator(withExtends: true)) |
| .join(', '), |
| '>', |
| ); |
| final typeParams = node.allTypeParams |
| .accept(const _TypeParamGenerator(withExtends: false)); |
| final typeParamsCall = _encloseIfNotEmpty( |
| '<', |
| typeParams.map((typeParam) => '$_typeParamPrefix$typeParam').join(', '), |
| '>', |
| ); |
| final staticTypeGetterCallArgs = _encloseIfNotEmpty( |
| '(', |
| typeParams.join(', '), |
| ')', |
| ); |
| final typeClassDefinitions = typeParams |
| .map((typeParam) => |
| 'final $_jType<$_typeParamPrefix$typeParam> $typeParam;') |
| .join(_newLine(depth: 1)); |
| final ctorTypeClassesDef = typeParams |
| .map((typeParam) => 'this.$typeParam,') |
| .join(_newLine(depth: 2)); |
| final superClass = (node.classDecl.superclass!.type as DeclaredType); |
| final superTypeClassesCall = superClass.classDecl == ClassDecl.object |
| ? '' |
| : superClass.params |
| .accept(_TypeClassGenerator(resolver)) |
| .map((typeClass) => '${typeClass.name},') |
| .join(_newLine(depth: 2)); |
| s.write(''' |
| class $name$typeParamsDef extends $superName { |
| @override |
| late final $_jType $instanceTypeGetter = $staticTypeGetter$staticTypeGetterCallArgs; |
| |
| $typeClassDefinitions |
| |
| $name.fromRef( |
| $ctorTypeClassesDef |
| $_jPointer ref, |
| ): super.fromRef( |
| $superTypeClassesCall |
| ref |
| ); |
| |
| '''); |
| |
| if (isDartOnly) { |
| final internalName = node.internalName; |
| s.write(''' |
| static final $_classRef = $_accessors.getClassOf(r"$internalName"); |
| |
| '''); |
| } |
| |
| // Static TypeClass getter |
| s.writeln( |
| ' /// The type which includes information such as the signature of this class.'); |
| final typeClassName = '$_typeClassPrefix$name$_typeClassSuffix'; |
| if (typeParams.isEmpty) { |
| s.writeln('static const $staticTypeGetter = $typeClassName();'); |
| } else { |
| final staticTypeGetterTypeClassesDef = typeParams |
| .map( |
| (typeParam) => '$_jType<$_typeParamPrefix$typeParam> $typeParam,') |
| .join(_newLine(depth: 2)); |
| final typeClassesCall = |
| typeParams.map((typeParam) => '$typeParam,').join(_newLine(depth: 3)); |
| s.write(''' |
| static $typeClassName$typeParamsCall $staticTypeGetter$typeParamsDef( |
| $staticTypeGetterTypeClassesDef |
| ) { |
| return $typeClassName( |
| $typeClassesCall |
| ); |
| } |
| |
| '''); |
| } |
| |
| // Fields and Methods |
| final fieldGenerator = _FieldGenerator(config, resolver, s); |
| for (final field in node.fields) { |
| field.accept(fieldGenerator); |
| } |
| final methodGenerator = _MethodGenerator(config, resolver, s); |
| for (final method in node.methods) { |
| method.accept(methodGenerator); |
| } |
| |
| // End of Class definition |
| s.writeln('}'); |
| |
| // TypeClass definition |
| final typeClassesCall = |
| typeParams.map((typeParam) => '$typeParam,').join(_newLine(depth: 2)); |
| final signature = node.signature; |
| final superTypeClass = superClass.accept(_TypeClassGenerator(resolver)); |
| final hashCodeTypeClasses = typeParams.join(', '); |
| final equalityTypeClasses = typeParams |
| .map((typeParam) => ' && $typeParam == other.$typeParam') |
| .join(); |
| final hashCode = typeParams.isEmpty |
| ? '($typeClassName).hashCode' |
| : 'Object.hash($typeClassName, $hashCodeTypeClasses)'; |
| s.write(''' |
| class $typeClassName$typeParamsDef extends $_jType<$name$typeParamsCall> { |
| $typeClassDefinitions |
| |
| const $typeClassName( |
| $ctorTypeClassesDef |
| ); |
| |
| @override |
| String get signature => r"$signature"; |
| |
| @override |
| $name$typeParamsCall fromRef($_jPointer ref) => $name.fromRef( |
| $typeClassesCall |
| ref |
| ); |
| |
| @override |
| $_jType get superType => ${superTypeClass.name}; |
| |
| @override |
| final superCount = ${node.superCount}; |
| |
| @override |
| int get hashCode => $hashCode; |
| |
| @override |
| bool operator ==(Object other) { |
| return other.runtimeType == $typeClassName && other is $typeClassName$equalityTypeClasses; |
| } |
| } |
| |
| '''); |
| log.finest('Generated bindings for class ${node.binaryName}'); |
| } |
| } |
| |
| /// Generates the JavaDoc comments. |
| class _DocGenerator extends Visitor<JavaDocComment, void> { |
| final StringSink s; |
| final int depth; |
| |
| const _DocGenerator(this.s, {required this.depth}); |
| |
| @override |
| void visit(JavaDocComment node) { |
| final link = RegExp('{@link ([^{}]+)}'); |
| final indent = ' ' * depth; |
| final comments = node.comment |
| .replaceAllMapped(link, (match) => match.group(1) ?? '') |
| .replaceAll('#', '\\#') |
| .replaceAll('<p>', '') |
| .replaceAll('</p>', '\n') |
| .replaceAll('<b>', '__') |
| .replaceAll('</b>', '__') |
| .replaceAll('<em>', '_') |
| .replaceAll('</em>', '_') |
| .split('\n') |
| .join('\n$indent///'); |
| s.write(''' |
| $indent/// |
| $indent/// $comments |
| '''); |
| } |
| } |
| |
| /// Generates the user-facing Dart type. |
| class _TypeGenerator extends TypeVisitor<String> { |
| final Resolver? resolver; |
| |
| const _TypeGenerator(this.resolver); |
| |
| @override |
| String visitArrayType(ArrayType node) { |
| final innerType = node.type; |
| if (innerType.kind == Kind.primitive) { |
| return '$_jArray<$_jni.${(innerType.type as PrimitiveType).jniType}>'; |
| } |
| return '$_jArray<${innerType.accept(this)}>'; |
| } |
| |
| @override |
| String visitDeclaredType(DeclaredType node) { |
| if (node.classDecl.binaryName == 'java.lang.Object' || |
| node.classDecl.binaryName == 'java.lang.String') { |
| return '$_jni.${node.classDecl.finalName}'; |
| } |
| |
| // All type parameters of this type |
| final allTypeParams = node.classDecl.allTypeParams |
| .accept(const _TypeParamGenerator(withExtends: false)) |
| .toList(); |
| // The ones that are declared. |
| final definedTypeParams = node.params.accept(this).toList(); |
| |
| // Replacing the declared ones. They come at the end. |
| // The rest will be JObject. |
| if (allTypeParams.length >= node.params.length) { |
| allTypeParams.replaceRange( |
| 0, |
| allTypeParams.length - node.params.length, |
| List.filled( |
| allTypeParams.length - node.params.length, |
| _jObject, |
| ), |
| ); |
| allTypeParams.replaceRange( |
| allTypeParams.length - node.params.length, |
| allTypeParams.length, |
| definedTypeParams, |
| ); |
| } |
| |
| final typeParams = _encloseIfNotEmpty('<', allTypeParams.join(', '), '>'); |
| final prefix = resolver?.resolvePrefix(node.classDecl.binaryName) ?? ''; |
| return '$prefix${node.classDecl.finalName}$typeParams'; |
| } |
| |
| @override |
| String visitPrimitiveType(PrimitiveType node) { |
| return node.dartType; |
| } |
| |
| @override |
| String visitTypeVar(TypeVar node) { |
| return '$_typeParamPrefix${node.name}'; |
| } |
| |
| @override |
| String visitWildcard(Wildcard node) { |
| // TODO(#141): Support wildcards |
| return super.visitWildcard(node); |
| } |
| |
| @override |
| String visitNonPrimitiveType(ReferredType node) { |
| return _jObject; |
| } |
| } |
| |
| class _TypeClass { |
| final String name; |
| final bool canBeConst; |
| |
| const _TypeClass(this.name, this.canBeConst); |
| } |
| |
| /// Generates the type class. |
| class _TypeClassGenerator extends TypeVisitor<_TypeClass> { |
| final bool isConst; |
| final Resolver? resolver; |
| |
| _TypeClassGenerator(this.resolver, {this.isConst = true}); |
| |
| @override |
| _TypeClass visitArrayType(ArrayType node) { |
| final innerTypeClass = |
| node.type.accept(_TypeClassGenerator(resolver, isConst: false)); |
| final ifConst = innerTypeClass.canBeConst && isConst ? 'const ' : ''; |
| return _TypeClass( |
| '$ifConst$_jArray$_typeClassSuffix(${innerTypeClass.name})', |
| innerTypeClass.canBeConst, |
| ); |
| } |
| |
| @override |
| _TypeClass visitDeclaredType(DeclaredType node) { |
| if (node.classDecl.binaryName == 'java.lang.Object' || |
| node.classDecl.binaryName == 'java.lang.String') { |
| final ifConst = isConst ? 'const ' : ''; |
| return _TypeClass( |
| '$ifConst$_jni.${node.classDecl.finalName}$_typeClassSuffix()', true); |
| } |
| final allTypeParams = node.classDecl.allTypeParams |
| .accept(const _TypeParamGenerator(withExtends: false)) |
| .toList(); |
| |
| // The ones that are declared. |
| final definedTypeClasses = |
| node.params.accept(_TypeClassGenerator(resolver, isConst: false)); |
| |
| // Can be const if all the type parameters are defined and each of them are |
| // also const. |
| final canBeConst = definedTypeClasses.every((e) => e.canBeConst); |
| |
| // Replacing the declared ones. They come at the end. |
| // The rest will be `JObjectType`. |
| if (allTypeParams.length >= node.params.length) { |
| allTypeParams.replaceRange( |
| 0, |
| allTypeParams.length - node.params.length, |
| List.filled( |
| allTypeParams.length - node.params.length, |
| // Adding const to subexpressions if the entire expression is not const. |
| '${canBeConst ? '' : 'const '}$_jObject$_typeClassSuffix()', |
| ), |
| ); |
| allTypeParams.replaceRange( |
| allTypeParams.length - node.params.length, |
| allTypeParams.length, |
| // Adding const to subexpressions if the entire expression is not const. |
| definedTypeClasses.map((param) => |
| '${param.canBeConst && !canBeConst ? 'const ' : ''}${param.name}'), |
| ); |
| } |
| |
| final args = allTypeParams.join(', '); |
| final ifConst = isConst && canBeConst ? 'const ' : ''; |
| final prefix = resolver?.resolvePrefix(node.classDecl.binaryName) ?? ''; |
| return _TypeClass( |
| '$ifConst$prefix$_typeClassPrefix${node.classDecl.finalName}$_typeClassSuffix($args)', |
| canBeConst, |
| ); |
| } |
| |
| @override |
| _TypeClass visitPrimitiveType(PrimitiveType node) { |
| final ifConst = isConst ? 'const ' : ''; |
| return _TypeClass('$ifConst$_jni.${node.jniType}$_typeClassSuffix()', true); |
| } |
| |
| @override |
| _TypeClass visitTypeVar(TypeVar node) { |
| return _TypeClass(node.name, false); |
| } |
| |
| @override |
| _TypeClass visitWildcard(Wildcard node) { |
| // TODO(#141): Support wildcards |
| return super.visitWildcard(node); |
| } |
| |
| @override |
| _TypeClass visitNonPrimitiveType(ReferredType node) { |
| final ifConst = isConst ? 'const ' : ''; |
| return _TypeClass('$ifConst$_jObject$_typeClassSuffix()', true); |
| } |
| } |
| |
| class _TypeParamGenerator extends Visitor<TypeParam, String> { |
| final bool withExtends; |
| |
| const _TypeParamGenerator({required this.withExtends}); |
| |
| @override |
| String visit(TypeParam node) { |
| if (!withExtends) { |
| return node.name; |
| } |
| // TODO(#144): resolve the actual type being extended, if any. |
| return '$_typeParamPrefix${node.name} extends $_jObject'; |
| } |
| } |
| |
| class _JniResultGetter extends TypeVisitor<String> { |
| const _JniResultGetter(); |
| |
| @override |
| String visitPrimitiveType(PrimitiveType node) { |
| if (node.name == 'void') return 'check()'; |
| if (node.name == 'double') return 'doubleFloat'; |
| if (node.name == 'int') return 'integer'; |
| return node.name; |
| } |
| |
| @override |
| String visitNonPrimitiveType(ReferredType node) { |
| return 'object'; |
| } |
| } |
| |
| /// Type signature for C-based bindings. |
| /// |
| /// When `isFFi` is `true`, it generates the ffi type signature and when it's |
| /// false, it generates the dart type signature. |
| /// |
| /// For example `ffi.Int32` is an ffi type signature while `int` is a Dart one: |
| /// ```dart |
| /// jniLookup<ffi.NativeFunction<jni.JniResult Function(ffi.Int32, ffi.Int32)>>( |
| /// "sum") |
| /// .asFunction<jni.JniResult Function(int, int)>(); |
| /// ``` |
| class _TypeSig extends TypeVisitor<String> { |
| final bool isFfi; |
| |
| const _TypeSig({required this.isFfi}); |
| |
| @override |
| String visitPrimitiveType(PrimitiveType node) { |
| if (isFfi) return '$_ffi.${node.ffiType}'; |
| if (node.name == 'boolean') return 'int'; |
| return node.dartType; |
| } |
| |
| @override |
| String visitNonPrimitiveType(ReferredType node) { |
| return _voidPointer; |
| } |
| } |
| |
| class _TypeName extends TypeVisitor<String> { |
| const _TypeName(); |
| |
| @override |
| String visitPrimitiveType(PrimitiveType node) { |
| return node.name; |
| } |
| |
| @override |
| String visitNonPrimitiveType(ReferredType node) { |
| return 'object'; |
| } |
| } |
| |
| class _CallType extends TypeVisitor<String> { |
| const _CallType(); |
| |
| @override |
| String visitPrimitiveType(PrimitiveType node) { |
| return '$_jCallType.${node.name}Type'; |
| } |
| |
| @override |
| String visitNonPrimitiveType(ReferredType node) { |
| return '$_jCallType.objectType'; |
| } |
| } |
| |
| class _ToNativeSuffix extends TypeVisitor<String> { |
| const _ToNativeSuffix(); |
| |
| @override |
| String visitPrimitiveType(PrimitiveType node) { |
| if (node.name == 'boolean') { |
| return ' ? 1 : 0'; |
| } |
| return ''; |
| } |
| |
| @override |
| String visitNonPrimitiveType(ReferredType node) { |
| return '.$_selfPointer'; |
| } |
| } |
| |
| class _FromNative extends TypeVisitor<String> { |
| final Resolver? resolver; |
| final String value; |
| |
| const _FromNative(this.resolver, this.value); |
| |
| @override |
| String visitPrimitiveType(PrimitiveType node) { |
| return value; |
| } |
| |
| @override |
| String visitNonPrimitiveType(ReferredType node) { |
| final typeClass = node.accept(_TypeClassGenerator(resolver)).name; |
| return '$typeClass.fromRef($value)'; |
| } |
| } |
| |
| class _FieldGenerator extends Visitor<Field, void> { |
| final Config config; |
| final Resolver? resolver; |
| final StringSink s; |
| |
| const _FieldGenerator(this.config, this.resolver, this.s); |
| |
| void writeCAccessor(Field node) { |
| final name = node.finalName; |
| final cName = node.accept(const CFieldName()); |
| final ifRef = node.isStatic ? '' : '$_jPointer, '; |
| |
| s.write(''' |
| static final _get_$name = |
| $_lookup<$_ffi.NativeFunction<$_jResult Function($ifRef)>>( |
| "get_$cName") |
| .asFunction<$_jResult Function($ifRef)>(); |
| |
| '''); |
| |
| if (!node.isFinal) { |
| final ffiSig = node.type.accept(const _TypeSig(isFfi: true)); |
| final dartSig = node.type.accept(const _TypeSig(isFfi: false)); |
| s.write(''' |
| static final _set_$name = |
| $_lookup<$_ffi.NativeFunction<$_jThrowable Function($ifRef$ffiSig)>>( |
| "set_$cName") |
| .asFunction<$_jThrowable Function($ifRef$dartSig)>(); |
| |
| '''); |
| } |
| } |
| |
| String cGetter(Field node) { |
| final name = node.finalName; |
| final getter = node.type.accept(const _JniResultGetter()); |
| final self = node.isStatic ? '' : _selfPointer; |
| return '_get_$name($self).$getter'; |
| } |
| |
| String cSetter(Field node) { |
| final name = node.finalName; |
| final self = node.isStatic ? '' : '$_selfPointer, '; |
| final toNativeSuffix = node.type.accept(const _ToNativeSuffix()); |
| return '_set_$name(${self}value$toNativeSuffix)'; |
| } |
| |
| void writeDartOnlyAccessor(Field node) { |
| final name = node.finalName; |
| final ifStatic = node.isStatic ? 'Static' : ''; |
| final descriptor = node.type.accept(const Descriptor()); |
| s.write(''' |
| static final _id_$name = |
| $_accessors.get${ifStatic}FieldIDOf( |
| $_classRef, |
| r"${node.name}", |
| r"$descriptor", |
| ); |
| '''); |
| } |
| |
| String dartOnlyGetter(Field node) { |
| final name = node.finalName; |
| final self = node.isStatic ? _classRef : _selfPointer; |
| final ifStatic = node.isStatic ? 'Static' : ''; |
| final callType = node.type.accept(const _CallType()); |
| final resultGetter = node.type.accept(const _JniResultGetter()); |
| return '$_accessors.get${ifStatic}Field($self, _id_$name, $callType)' |
| '.$resultGetter'; |
| } |
| |
| String dartOnlySetter(Field node) { |
| final name = node.finalName; |
| final self = node.isStatic ? _classRef : _selfPointer; |
| final ifStatic = node.isStatic ? 'Static' : ''; |
| final fieldType = node.type.accept(const _TypeName()).capitalize(); |
| final toNativeSuffix = node.type.accept(const _ToNativeSuffix()); |
| return '$_env.Set$ifStatic${fieldType}Field($self, _id_$name, value$toNativeSuffix)'; |
| } |
| |
| void writeDocs(Field node, {required bool writeDeleteInstructions}) { |
| final originalDecl = '${node.type.shorthand} ${node.name}'; |
| s.writeln(' /// from: ${node.modifiers.join(' ')} $originalDecl'); |
| if (node.type.kind != Kind.primitive && writeDeleteInstructions) { |
| s.writeln(_deleteInstruction); |
| } |
| node.javadoc?.accept(_DocGenerator(s, depth: 1)); |
| } |
| |
| @override |
| void visit(Field node) { |
| final isCBased = config.outputConfig.bindingsType == BindingsType.cBased; |
| |
| // Check if it should be a `static const` getter. |
| if (node.isFinal && node.isStatic && node.defaultValue != null) { |
| final name = node.finalName; |
| final value = node.defaultValue!; |
| // TODO(#31): Should we leave String as a normal getter instead? |
| if (value is String || value is num || value is bool) { |
| writeDocs(node, writeDeleteInstructions: false); |
| s.write(' static const $name = '); |
| if (value is String) { |
| s.write('r"""$value"""'); |
| } else { |
| s.write(value); |
| } |
| s.writeln(';\n'); |
| return; |
| } |
| } |
| |
| // Accessors |
| (isCBased ? writeCAccessor : writeDartOnlyAccessor)(node); |
| |
| // Getter docs |
| writeDocs(node, writeDeleteInstructions: true); |
| |
| final name = node.finalName; |
| final ifStatic = node.isStatic ? 'static ' : ''; |
| final type = node.type.accept(_TypeGenerator(resolver)); |
| s.write('$ifStatic$type get $name => '); |
| s.write(node.type.accept( |
| _FromNative(resolver, (isCBased ? cGetter : dartOnlyGetter)(node)), |
| )); |
| s.writeln(';\n'); |
| if (!node.isFinal) { |
| // Setter docs |
| writeDocs(node, writeDeleteInstructions: true); |
| |
| s.write('${ifStatic}set $name($type value) => '); |
| s.write((isCBased ? cSetter : dartOnlySetter)(node)); |
| s.writeln(';\n'); |
| } |
| } |
| } |
| |
| class _MethodTypeSig extends Visitor<Method, String> { |
| final bool isFfi; |
| |
| const _MethodTypeSig({required this.isFfi}); |
| |
| @override |
| String visit(Method node) { |
| final args = [ |
| if (!node.isCtor && !node.isStatic) _voidPointer, |
| ...node.params.map((param) => param.type).accept( |
| _TypeSig(isFfi: isFfi), |
| ) |
| ].join(', '); |
| return '$_jResult Function($args)'; |
| } |
| } |
| |
| /// Generates Dart bindings for Java methods. |
| class _MethodGenerator extends Visitor<Method, void> { |
| final Config config; |
| final Resolver? resolver; |
| final StringSink s; |
| |
| const _MethodGenerator(this.config, this.resolver, this.s); |
| |
| void writeCAccessor(Method node) { |
| final name = node.finalName; |
| final cName = node.accept(const CMethodName()); |
| final ffiSig = node.accept(const _MethodTypeSig(isFfi: true)); |
| final dartSig = node.accept(const _MethodTypeSig(isFfi: false)); |
| s.write(''' |
| static final _$name = |
| $_lookup<$_ffi.NativeFunction<$ffiSig>>("$cName") |
| .asFunction<$dartSig>(); |
| |
| '''); |
| } |
| |
| void writeDartOnlyAccessor(Method node) { |
| final name = node.finalName; |
| final ifStatic = node.isStatic ? 'Static' : ''; |
| final signature = node.accept(const MethodSignature()); |
| s.write(''' |
| static final _id_$name = $_accessors.get${ifStatic}MethodIDOf( |
| $_classRef, r"${node.name}", r"$signature"); |
| |
| '''); |
| } |
| |
| bool isSuspendFun(Method node) { |
| return node.asyncReturnType != null; |
| } |
| |
| String cCtor(Method node) { |
| final name = node.finalName; |
| final params = |
| node.params.accept(const _ParamCall(isCBased: true)).join(', '); |
| return '_$name($params)'; |
| } |
| |
| String dartOnlyCtor(Method node) { |
| final name = node.finalName; |
| final params = |
| node.params.accept(const _ParamCall(isCBased: false)).join(', '); |
| return '$_accessors.newObjectWithArgs($_classRef, _id_$name, [$params])'; |
| } |
| |
| String cMethodCall(Method node) { |
| final name = node.finalName; |
| final params = [ |
| if (!node.isStatic) _selfPointer, |
| ...node.params.accept(const _ParamCall(isCBased: true)), |
| ].join(', '); |
| final resultGetter = node.returnType.accept(const _JniResultGetter()); |
| return '_$name($params).$resultGetter'; |
| } |
| |
| String dartOnlyMethodCall(Method node) { |
| final name = node.finalName; |
| final ifStatic = node.isStatic ? 'Static' : ''; |
| final self = node.isStatic ? _classRef : _selfPointer; |
| final callType = node.returnType.accept(const _CallType()); |
| final params = |
| node.params.accept(const _ParamCall(isCBased: false)).join(', '); |
| final resultGetter = node.returnType.accept(const _JniResultGetter()); |
| return '$_accessors.call${ifStatic}MethodWithArgs' |
| '($self, _id_$name, $callType, [$params]).$resultGetter'; |
| } |
| |
| @override |
| void visit(Method node) { |
| final isCBased = config.outputConfig.bindingsType == BindingsType.cBased; |
| |
| // Accessors |
| (isCBased ? writeCAccessor : writeDartOnlyAccessor)(node); |
| |
| // Docs |
| s.write(' /// from: '); |
| s.writeAll(node.modifiers.map((m) => '$m ')); |
| s.write('${node.returnType.shorthand} ${node.name}('); |
| s.writeAll(node.params.map((p) => '${p.type.shorthand} ${p.name}'), ', '); |
| s.writeln(')'); |
| if (node.returnType.kind != Kind.primitive || node.isCtor) { |
| s.writeln(_deleteInstruction); |
| } |
| node.javadoc?.accept(_DocGenerator(s, depth: 1)); |
| |
| // Used for inferring the type parameter from the given parameters. |
| final typeLocators = node.params |
| .accept(_ParamTypeLocator(resolver: resolver)) |
| .fold(<String, List<String>>{}, _mergeMapValues).map( |
| (key, value) => MapEntry( |
| key, |
| _encloseIfNotEmpty( |
| '[', |
| value.delimited(', '), |
| ']', |
| ), |
| )); |
| |
| bool isRequired(TypeParam typeParam) { |
| return (typeLocators[typeParam.name] ?? '').isEmpty; |
| } |
| |
| final typeInference = |
| (node.isCtor ? node.classDecl.allTypeParams : node.typeParams) |
| .where((tp) => !isRequired(tp)) |
| .map((tp) => tp.name) |
| .map((tp) => '$tp ??= $_jni.lowestCommonSuperType' |
| '(${typeLocators[tp]}) as $_jType<$_typeParamPrefix$tp>;') |
| .join(_newLine(depth: 2)); |
| |
| if (node.isCtor) { |
| final className = node.classDecl.finalName; |
| final name = node.finalName; |
| final ctorName = name == 'ctor' ? className : '$className.$name'; |
| final paramsDef = node.params.accept(_ParamDef(resolver)).delimited(', '); |
| final typeClassDef = _encloseIfNotEmpty( |
| '{', |
| node.classDecl.allTypeParams |
| .map((typeParam) => typeParam |
| .accept(_CtorTypeClassDef(isRequired: isRequired(typeParam)))) |
| .delimited(', '), |
| '}', |
| ); |
| final typeClassCall = node.classDecl.allTypeParams |
| .map((typeParam) => |
| typeParam.accept(const _TypeParamGenerator(withExtends: false))) |
| .delimited(', '); |
| |
| final ctorExpr = (isCBased ? cCtor : dartOnlyCtor)(node); |
| s.write(''' |
| factory $ctorName($paramsDef$typeClassDef) { |
| $typeInference |
| return ${node.classDecl.finalName}.fromRef( |
| $typeClassCall |
| $ctorExpr.object |
| ); |
| } |
| |
| '''); |
| return; |
| } |
| |
| final name = node.finalName; |
| final returnType = isSuspendFun(node) |
| ? 'Future<${node.asyncReturnType!.accept(_TypeGenerator(resolver))}>' |
| : node.returnType.accept(_TypeGenerator(resolver)); |
| final returnTypeClass = (node.asyncReturnType ?? node.returnType) |
| .accept(_TypeClassGenerator(resolver)) |
| .name; |
| final ifStatic = node.isStatic ? 'static ' : ''; |
| final defArgs = node.params.accept(_ParamDef(resolver)).toList(); |
| final typeClassDef = _encloseIfNotEmpty( |
| '{', |
| node.typeParams |
| .map((typeParam) => typeParam.accept(_MethodTypeClassDef( |
| isRequired: isRequired(typeParam), |
| ))) |
| .delimited(', '), |
| '}', |
| ); |
| final typeParamsDef = _encloseIfNotEmpty( |
| '<', |
| node.typeParams |
| .accept(const _TypeParamGenerator(withExtends: true)) |
| .join(', '), |
| '>', |
| ); |
| if (isSuspendFun(node)) { |
| defArgs.removeLast(); |
| } |
| final params = defArgs.delimited(', '); |
| s.write(' $ifStatic$returnType $name$typeParamsDef($params$typeClassDef)'); |
| final callExpr = (isCBased ? cMethodCall : dartOnlyMethodCall)(node); |
| if (isSuspendFun(node)) { |
| final returning = |
| node.asyncReturnType!.accept(_FromNative(resolver, '\$o')); |
| s.write('''async { |
| $typeInference |
| final \$p = ReceivePort(); |
| final \$c = $_jObject.fromRef($_jni.Jni.newPortContinuation(\$p)); |
| $callExpr; |
| final \$o = $_jPointer.fromAddress(await \$p.first); |
| final \$k = $returnTypeClass.getClass().reference; |
| if ($_jni.Jni.env.IsInstanceOf(\$o, \$k) == 0) { |
| throw "Failed"; |
| } |
| return $returning; |
| } |
| |
| '''); |
| } else { |
| final returning = node.returnType.accept(_FromNative(resolver, callExpr)); |
| s.writeln('''{ |
| $typeInference |
| return $returning; |
| } |
| '''); |
| } |
| } |
| } |
| |
| /// Generates the method type param definition. |
| /// |
| /// For example `required JObjType<T> $T` in: |
| /// ```dart |
| /// void bar(..., {required JObjType<T> $T}) => ... |
| /// ``` |
| class _MethodTypeClassDef extends Visitor<TypeParam, String> { |
| final bool isRequired; |
| |
| const _MethodTypeClassDef({required this.isRequired}); |
| |
| @override |
| String visit(TypeParam node) { |
| return '${isRequired ? 'required ' : ''}$_jType' |
| '<$_typeParamPrefix${node.name}>${isRequired ? '' : '?'} ${node.name}'; |
| } |
| } |
| |
| /// Generates the class type param definition. Used only in constructors. |
| /// |
| /// For example `required this.$T` in: |
| /// ```dart |
| /// class Foo { |
| /// final JObjType<T> $T; |
| /// Foo(..., {required this.$T}) => ... |
| /// } |
| /// ``` |
| class _CtorTypeClassDef extends Visitor<TypeParam, String> { |
| final bool isRequired; |
| |
| const _CtorTypeClassDef({required this.isRequired}); |
| |
| @override |
| String visit(TypeParam node) { |
| return '${isRequired ? 'required ' : ''} $_jType' |
| '<$_typeParamPrefix${node.name}>${isRequired ? '' : '?'} ${node.name}'; |
| } |
| } |
| |
| /// Method parameter's definition. |
| /// |
| /// For example `Foo foo` in: |
| /// ```dart |
| /// void bar(Foo foo) => ... |
| /// ``` |
| class _ParamDef extends Visitor<Param, String> { |
| final Resolver? resolver; |
| |
| const _ParamDef(this.resolver); |
| |
| @override |
| String visit(Param node) { |
| final type = node.type.accept(_TypeGenerator(resolver)); |
| return '$type ${node.finalName}'; |
| } |
| } |
| |
| /// Method parameter used in calling the native method. |
| /// |
| /// For example `foo.reference` in: |
| /// ```dart |
| /// void bar(Foo foo) => _bar(foo.reference); |
| /// ``` |
| class _ParamCall extends Visitor<Param, String> { |
| final bool isCBased; |
| |
| const _ParamCall({required this.isCBased}); |
| |
| @override |
| String visit(Param node) { |
| final nativeSuffix = node.type.accept(const _ToNativeSuffix()); |
| final paramCall = '${node.finalName}$nativeSuffix'; |
| if (!isCBased) { |
| // We need to wrap [paramCall] in the appropriate wrapper class. |
| return node.type.accept(_JValueWrapper(paramCall)); |
| } |
| return paramCall; |
| } |
| } |
| |
| /// Wraps the parameter in the appropriate JValue wrapper class. |
| /// |
| /// For instance, `int` in Dart can be mapped to `long` or `int` or ... in Java. |
| /// The wrapper class is how we identify the type in pure dart bindings. |
| class _JValueWrapper extends TypeVisitor<String> { |
| final String param; |
| |
| _JValueWrapper(this.param); |
| |
| @override |
| String visitNonPrimitiveType(ReferredType node) { |
| return param; |
| } |
| |
| @override |
| String visitPrimitiveType(PrimitiveType node) { |
| if (node.name == 'long' || |
| node.name == 'double' || |
| node.name == 'boolean') { |
| return param; |
| } |
| return '$_jni.JValue${node.name.capitalize()}($param)'; |
| } |
| } |
| |
| /// A pair of [StringBuffer]s that can create an expression from the outer layer |
| /// inwards. |
| /// |
| /// For example: |
| /// ```dart |
| /// final buffer = OutsideInBuffer(); // asterisk (*) is used to show the middle |
| /// buffer.appendLeft('f('); // f(* |
| /// buffer.prependRight('x)'); // f(*x) |
| /// buffer.appendLeft('g('); // f(g(*x) |
| /// buffer.prependRight('y) + '); // f(g(*y) + x) |
| /// buffer.toString(); // f(g(y) + x) |
| /// ``` |
| @visibleForTesting |
| class OutsideInBuffer { |
| final StringBuffer _leftBuffer; |
| final StringBuffer _rightBuffer; |
| |
| OutsideInBuffer() |
| : _leftBuffer = StringBuffer(), |
| _rightBuffer = StringBuffer(); |
| |
| void prependRight(Object? object) { |
| final s = object.toString(); |
| for (var i = 0; i < s.length; ++i) { |
| _rightBuffer.write(s[s.length - i - 1]); |
| } |
| } |
| |
| void appendLeft(Object? object) { |
| _leftBuffer.write(object); |
| } |
| |
| @override |
| String toString() { |
| return _leftBuffer.toString() + _rightBuffer.toString().reversed; |
| } |
| } |
| |
| /// The ways to locate each type parameter. |
| /// |
| /// For example in `JArray<JMap<$T, $T>> a`, `T` can be retreived using |
| /// ```dart |
| /// ((((a.$type as jni.JArrayType).elementType) as $JMapType).K) as jni.JObjType<$T> |
| /// ``` |
| /// and |
| /// ```dart |
| /// ((((a.$type as jni.JArrayType).elementType) as $JMapType).V) as jni.JObjType<$T> |
| /// ``` |
| class _ParamTypeLocator extends Visitor<Param, Map<String, List<String>>> { |
| final Resolver? resolver; |
| |
| _ParamTypeLocator({required this.resolver}); |
| |
| @override |
| Map<String, List<String>> visit(Param node) { |
| return node.type.accept(_TypeVarLocator(resolver: resolver)).map( |
| (key, value) => MapEntry( |
| key, |
| value |
| .map((e) => |
| (e..appendLeft('${node.finalName}.\$type')).toString()) |
| .toList(), |
| ), |
| ); |
| } |
| } |
| |
| class _TypeVarLocator extends TypeVisitor<Map<String, List<OutsideInBuffer>>> { |
| final Resolver? resolver; |
| |
| _TypeVarLocator({required this.resolver}); |
| |
| @override |
| Map<String, List<OutsideInBuffer>> visitNonPrimitiveType(ReferredType node) { |
| return {}; |
| } |
| |
| @override |
| Map<String, List<OutsideInBuffer>> visitWildcard(Wildcard node) { |
| // TODO(#141): Support wildcards |
| return super.visitWildcard(node); |
| } |
| |
| @override |
| Map<String, List<OutsideInBuffer>> visitTypeVar(TypeVar node) { |
| return { |
| node.name: [ |
| OutsideInBuffer(), |
| ], |
| }; |
| } |
| |
| @override |
| Map<String, List<OutsideInBuffer>> visitDeclaredType(DeclaredType node) { |
| if (node.classDecl == ClassDecl.object) { |
| return {}; |
| } |
| final offset = node.classDecl.allTypeParams.length - node.params.length; |
| final result = <String, List<OutsideInBuffer>>{}; |
| final prefix = resolver?.resolvePrefix(node.binaryName) ?? ''; |
| final typeClass = |
| '$prefix$_typeClassPrefix${node.classDecl.finalName}$_typeClassSuffix'; |
| for (var i = 0; i < node.params.length; ++i) { |
| final typeParam = node.classDecl.allTypeParams[i + offset].name; |
| final exprs = node.params[i].accept(this); |
| for (final expr in exprs.entries) { |
| for (final buffer in expr.value) { |
| buffer.appendLeft('('); |
| buffer.prependRight(' as $typeClass).$typeParam'); |
| result[expr.key] = (result[expr.key] ?? [])..add(buffer); |
| } |
| } |
| } |
| return result; |
| } |
| |
| @override |
| Map<String, List<OutsideInBuffer>> visitArrayType(ArrayType node) { |
| final exprs = node.type.accept(this); |
| for (final e in exprs.values.expand((i) => i)) { |
| e.appendLeft('(('); |
| e.prependRight(' as $_jArray$_typeClassSuffix).elementType as $_jType)'); |
| } |
| return exprs; |
| } |
| |
| @override |
| Map<String, List<OutsideInBuffer>> visitPrimitiveType(PrimitiveType node) { |
| return {}; |
| } |
| } |