[jnigen] Using a yaml symbols file instead of hard coding package:jni exported classes (https://github.com/dart-lang/jnigen/issues/289)

* Removes the support for importMap in config.
* Adds the support for importing a yaml symbols file.
diff --git a/pkgs/jni/lib/jni_symbols.yaml b/pkgs/jni/lib/jni_symbols.yaml
new file mode 100644
index 0000000..199df29
--- /dev/null
+++ b/pkgs/jni/lib/jni_symbols.yaml
@@ -0,0 +1,12 @@
+version: 1.0.0
+files:
+  'jni.dart':
+    'java.lang.Object':
+      name: JObject
+      type_class: JObjectType
+      super_count: 0
+    'java.lang.String':
+      name: JString
+      type_class: JStringType
+      super_count: 1
+
diff --git a/pkgs/jnigen/CHANGELOG.md b/pkgs/jnigen/CHANGELOG.md
index 23c1e26..58fb5be 100644
--- a/pkgs/jnigen/CHANGELOG.md
+++ b/pkgs/jnigen/CHANGELOG.md
@@ -1,5 +1,6 @@
 ## 0.5.0-dev.0
-* No JNIGen changes (yet), but keeping version in lockstep with `package:jni`.
+* **Breaking Change** ([#72](https://github.com/dart-lang/jnigen/issues/72)): Removed support for `importMap` in favor of the newly added interop mechanism with importing yaml files.
+* Strings now use UTF16.
 
 ## 0.4.0
 * **Breaking Change** ([#145](https://github.com/dart-lang/jnigen/issues/145)): Type arguments are now named instead of positional.
diff --git a/pkgs/jnigen/lib/src/bindings/dart_generator.dart b/pkgs/jnigen/lib/src/bindings/dart_generator.dart
index 1cd3c2b..a1559cf 100644
--- a/pkgs/jnigen/lib/src/bindings/dart_generator.dart
+++ b/pkgs/jnigen/lib/src/bindings/dart_generator.dart
@@ -31,9 +31,6 @@
 
 // Prefixes and suffixes
 const _typeParamPrefix = '\$';
-// TODO(#143): this is a temporary fix for the name collision.
-const _typeClassPrefix = '\$';
-const _typeClassSuffix = 'Type';
 
 // Misc.
 const _classRef = '_class.reference';
@@ -195,7 +192,12 @@
       if (cBased) {
         s.writeln(cInitCode);
       }
-      final classGenerator = _ClassGenerator(config, s);
+      final resolver = Resolver(
+        importedClasses: config.importedClasses,
+        currentClass: null, // Single file mode.
+        inputClassNames: node.decls.keys.toSet(),
+      );
+      final classGenerator = _ClassGenerator(config, s, resolver);
       for (final classDecl in node.decls.values) {
         classDecl.accept(classGenerator);
       }
@@ -249,11 +251,11 @@
         s.write('import "$initFilePath";');
       }
       final resolver = Resolver(
-        importMap: config.importMap ?? {},
+        importedClasses: config.importedClasses,
         currentClass: fileClassName,
         inputClassNames: node.decls.keys.toSet(),
       );
-      final classGenerator = _ClassGenerator(config, s, resolver: resolver);
+      final classGenerator = _ClassGenerator(config, s, resolver);
       for (final classDecl in classesInFile) {
         classDecl.accept(classGenerator);
       }
@@ -281,13 +283,13 @@
 class _ClassGenerator extends Visitor<ClassDecl, void> {
   final Config config;
   final StringSink s;
-  final Resolver? resolver;
+  final Resolver resolver;
 
   _ClassGenerator(
     this.config,
-    this.s, {
+    this.s,
     this.resolver,
-  });
+  );
 
   static const staticTypeGetter = 'type';
   static const instanceTypeGetter = '\$$staticTypeGetter';
@@ -331,7 +333,7 @@
         .map((typeParam) => 'this.$typeParam,')
         .join(_newLine(depth: 2));
     final superClass = (node.classDecl.superclass!.type as DeclaredType);
-    final superTypeClassesCall = superClass.classDecl == ClassDecl.object
+    final superTypeClassesCall = superClass.classDecl.isObject()
         ? ''
         : superClass.params
             .accept(_TypeClassGenerator(resolver))
@@ -365,7 +367,7 @@
     // Static TypeClass getter
     s.writeln(
         '  /// The type which includes information such as the signature of this class.');
-    final typeClassName = '$_typeClassPrefix$name$_typeClassSuffix';
+    final typeClassName = node.typeClassName;
     if (typeParams.isEmpty) {
       s.writeln('static const $staticTypeGetter = $typeClassName();');
     } else {
@@ -527,7 +529,7 @@
     }
 
     final typeParams = _encloseIfNotEmpty('<', allTypeParams.join(', '), '>');
-    final prefix = resolver?.resolvePrefix(node.classDecl.binaryName) ?? '';
+    final prefix = resolver?.resolvePrefix(node.classDecl) ?? '';
     return '$prefix${node.classDecl.finalName}$typeParams';
   }
 
@@ -563,7 +565,7 @@
 /// Generates the type class.
 class _TypeClassGenerator extends TypeVisitor<_TypeClass> {
   final bool isConst;
-  final Resolver? resolver;
+  final Resolver resolver;
 
   _TypeClassGenerator(this.resolver, {this.isConst = true});
 
@@ -573,19 +575,13 @@
         node.type.accept(_TypeClassGenerator(resolver, isConst: false));
     final ifConst = innerTypeClass.canBeConst && isConst ? 'const ' : '';
     return _TypeClass(
-      '$ifConst$_jArray$_typeClassSuffix(${innerTypeClass.name})',
+      '$ifConst${_jArray}Type(${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();
@@ -596,7 +592,8 @@
 
     // Can be const if all the type parameters are defined and each of them are
     // also const.
-    final canBeConst = definedTypeClasses.every((e) => e.canBeConst);
+    final canBeConst =
+        allTypeParams.isEmpty || definedTypeClasses.every((e) => e.canBeConst);
 
     // Replacing the declared ones. They come at the end.
     // The rest will be `JObjectType`.
@@ -607,7 +604,7 @@
         List.filled(
           allTypeParams.length - node.params.length,
           // Adding const to subexpressions if the entire expression is not const.
-          '${canBeConst ? '' : 'const '}$_jObject$_typeClassSuffix()',
+          '${canBeConst ? '' : 'const '}${_jObject}Type()',
         ),
       );
       allTypeParams.replaceRange(
@@ -621,9 +618,9 @@
 
     final args = allTypeParams.join(', ');
     final ifConst = isConst && canBeConst ? 'const ' : '';
-    final prefix = resolver?.resolvePrefix(node.classDecl.binaryName) ?? '';
+    final prefix = resolver.resolvePrefix(node.classDecl);
     return _TypeClass(
-      '$ifConst$prefix$_typeClassPrefix${node.classDecl.finalName}$_typeClassSuffix($args)',
+      '$ifConst$prefix${node.classDecl.typeClassName}($args)',
       canBeConst,
     );
   }
@@ -631,7 +628,7 @@
   @override
   _TypeClass visitPrimitiveType(PrimitiveType node) {
     final ifConst = isConst ? 'const ' : '';
-    return _TypeClass('$ifConst$_jni.${node.jniType}$_typeClassSuffix()', true);
+    return _TypeClass('$ifConst$_jni.${node.jniType}Type()', true);
   }
 
   @override
@@ -648,7 +645,7 @@
   @override
   _TypeClass visitNonPrimitiveType(ReferredType node) {
     final ifConst = isConst ? 'const ' : '';
-    return _TypeClass('$ifConst$_jObject$_typeClassSuffix()', true);
+    return _TypeClass('$ifConst${_jObject}Type()', true);
   }
 }
 
@@ -759,7 +756,7 @@
 }
 
 class _FromNative extends TypeVisitor<String> {
-  final Resolver? resolver;
+  final Resolver resolver;
   final String value;
 
   const _FromNative(this.resolver, this.value);
@@ -778,7 +775,7 @@
 
 class _FieldGenerator extends Visitor<Field, void> {
   final Config config;
-  final Resolver? resolver;
+  final Resolver resolver;
   final StringSink s;
 
   const _FieldGenerator(this.config, this.resolver, this.s);
@@ -932,7 +929,7 @@
 /// Generates Dart bindings for Java methods.
 class _MethodGenerator extends Visitor<Method, void> {
   final Config config;
-  final Resolver? resolver;
+  final Resolver resolver;
   final StringSink s;
 
   const _MethodGenerator(this.config, this.resolver, this.s);
@@ -1181,7 +1178,7 @@
 /// void bar(Foo foo) => ...
 /// ```
 class _ParamDef extends Visitor<Param, String> {
-  final Resolver? resolver;
+  final Resolver resolver;
 
   const _ParamDef(this.resolver);
 
@@ -1289,7 +1286,7 @@
 /// ((((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;
+  final Resolver resolver;
 
   _ParamTypeLocator({required this.resolver});
 
@@ -1308,7 +1305,7 @@
 }
 
 class _TypeVarLocator extends TypeVisitor<Map<String, List<OutsideInBuffer>>> {
-  final Resolver? resolver;
+  final Resolver resolver;
 
   _TypeVarLocator({required this.resolver});
 
@@ -1334,14 +1331,13 @@
 
   @override
   Map<String, List<OutsideInBuffer>> visitDeclaredType(DeclaredType node) {
-    if (node.classDecl == ClassDecl.object) {
+    if (node.classDecl.isObject()) {
       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';
+    final prefix = resolver.resolvePrefix(node.classDecl);
+    final typeClass = '$prefix${node.classDecl.typeClassName}';
     for (var i = 0; i < node.params.length; ++i) {
       final typeParam = node.classDecl.allTypeParams[i + offset].name;
       final exprs = node.params[i].accept(this);
@@ -1361,7 +1357,7 @@
     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)');
+      e.prependRight(' as ${_jArray}Type).elementType as $_jType)');
     }
     return exprs;
   }
diff --git a/pkgs/jnigen/lib/src/bindings/linker.dart b/pkgs/jnigen/lib/src/bindings/linker.dart
index e50d437..74c14e7 100644
--- a/pkgs/jnigen/lib/src/bindings/linker.dart
+++ b/pkgs/jnigen/lib/src/bindings/linker.dart
@@ -2,26 +2,74 @@
 // 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 'visitor.dart';
 import '../config/config.dart';
 import '../elements/elements.dart';
+import '../logging/logging.dart';
+import 'visitor.dart';
 
 typedef _Resolver = ClassDecl Function(String? binaryName);
 
-/// Adds references from child elements back to their parent elements.
-/// Resolves Kotlin specific `asyncReturnType` for methods.
-class Linker extends Visitor<Classes, void> {
+/// A [Visitor] that adds the correct [ClassDecl] references from the
+/// string binary names.
+///
+/// It adds the following references:
+/// * Links [ClassDecl] objects from imported dependencies.
+/// * Adds references from child elements back to their parent elements.
+/// * Resolves Kotlin specific `asyncReturnType` for methods.
+class Linker extends Visitor<Classes, Future<void>> {
   Linker(this.config);
 
   final Config config;
 
   @override
-  void visit(Classes node) {
-    final classLinker = _ClassLinker(config, (binaryName) {
-      return ClassDecl.predefined[binaryName] ??
+  Future<void> visit(Classes node) async {
+    // Specify paths for this package's classes.
+    final root = config.outputConfig.dartConfig.path;
+    if (config.outputConfig.dartConfig.structure ==
+        OutputStructure.singleFile) {
+      // Connect all to the root if the output is in single file mode.
+      final path = root.toFilePath();
+      for (final decl in node.decls.values) {
+        decl.path = path;
+      }
+    } else {
+      for (final decl in node.decls.values) {
+        final dollarSign = decl.binaryName.indexOf('\$');
+        final className = dollarSign != -1
+            ? decl.binaryName.substring(0, dollarSign)
+            : decl.binaryName;
+        final path = className.replaceAll('.', '/');
+        decl.path = root.resolve(path).toFilePath();
+      }
+    }
+
+    // Find all the imported classes.
+    await config.importClasses();
+
+    if (config.importedClasses.keys
+        .toSet()
+        .intersection(node.decls.keys.toSet())
+        .isNotEmpty) {
+      log.fatal(
+        'Trying to re-import the generated classes.\n'
+        'Try hiding the class(es) in import.',
+      );
+    }
+
+    for (final className in config.importedClasses.keys) {
+      log.finest('Imported $className successfully.');
+    }
+
+    ClassDecl resolve(String? binaryName) {
+      return config.importedClasses[binaryName] ??
           node.decls[binaryName] ??
-          ClassDecl.object;
-    });
+          resolve(TypeUsage.object.name);
+    }
+
+    final classLinker = _ClassLinker(
+      config,
+      resolve,
+    );
     for (final classDecl in node.decls.values) {
       classDecl.accept(classLinker);
     }
@@ -29,20 +77,24 @@
 }
 
 class _ClassLinker extends Visitor<ClassDecl, void> {
-  _ClassLinker(this.config, this.resolve);
-
   final Config config;
   final _Resolver resolve;
-  final Set<ClassDecl> _linked = {...ClassDecl.predefined.values};
+  final Set<ClassDecl> _linked;
+
+  _ClassLinker(
+    this.config,
+    this.resolve,
+  ) : _linked = {...config.importedClasses.values};
 
   @override
   void visit(ClassDecl node) {
     if (_linked.contains(node)) return;
+    log.finest('Linking ${node.binaryName}.');
     _linked.add(node);
 
     node.parent = resolve(node.parentName);
     node.parent!.accept(this);
-    // Adding type params of outer classes to the nested classes
+    // Add type params of outer classes to the nested classes
     final allTypeParams = <TypeParam>[];
     if (!node.modifiers.contains('static')) {
       for (final typeParam in node.parent!.allTypeParams) {
diff --git a/pkgs/jnigen/lib/src/bindings/renamer.dart b/pkgs/jnigen/lib/src/bindings/renamer.dart
index 4f1c440..d5f9a73 100644
--- a/pkgs/jnigen/lib/src/bindings/renamer.dart
+++ b/pkgs/jnigen/lib/src/bindings/renamer.dart
@@ -135,18 +135,13 @@
 
 class _ClassRenamer implements Visitor<ClassDecl, void> {
   final Config config;
+  final Set<ClassDecl> renamed;
   final Map<String, int> classNameCounts = {};
-  final Set<ClassDecl> renamed = {...ClassDecl.predefined.values};
-  final Map<ClassDecl, Map<String, int>> nameCounts = {
-    for (final predefined in ClassDecl.predefined.values) ...{
-      predefined: {..._definedSyms},
-    }
-  };
-  final Map<ClassDecl, Map<String, int>> methodNumsAfterRenaming = {};
+  final Map<ClassDecl, Map<String, int>> nameCounts = {};
 
   _ClassRenamer(
     this.config,
-  );
+  ) : renamed = {...config.importedClasses.values};
 
   /// Returns class name as useful in dart.
   ///
@@ -157,10 +152,11 @@
   @override
   void visit(ClassDecl node) {
     if (renamed.contains(node)) return;
+    log.finest('Renaming ${node.binaryName}.');
     renamed.add(node);
 
     nameCounts[node] = {..._definedSyms};
-    methodNumsAfterRenaming[node] = {};
+    node.methodNumsAfterRenaming = {};
 
     final className = _getSimplifiedClassName(node.binaryName);
     node.uniqueName = _renameConflict(classNameCounts, className);
@@ -170,15 +166,16 @@
     final uniquifyName =
         config.outputConfig.dartConfig.structure == OutputStructure.singleFile;
     node.finalName = uniquifyName ? node.uniqueName : className;
+    // TODO(#143): $ at the beginning is a temporary fix for the name collision.
+    node.typeClassName = '\$${node.finalName}Type';
     log.fine('Class ${node.binaryName} is named ${node.finalName}');
 
     final superClass = (node.superclass!.type as DeclaredType).classDecl;
     superClass.accept(this);
-    nameCounts[node]!.addAll(nameCounts[superClass]!);
+    nameCounts[node]!.addAll(nameCounts[superClass] ?? {});
     final methodRenamer = _MethodRenamer(
       config,
       nameCounts[node]!,
-      methodNumsAfterRenaming,
     );
     for (final method in node.methods) {
       method.accept(methodRenamer);
@@ -192,11 +189,10 @@
 }
 
 class _MethodRenamer implements Visitor<Method, void> {
-  _MethodRenamer(this.config, this.nameCounts, this.methodNumsAfterRenaming);
+  _MethodRenamer(this.config, this.nameCounts);
 
   final Config config;
   final Map<String, int> nameCounts;
-  final Map<ClassDecl, Map<String, int>> methodNumsAfterRenaming;
 
   @override
   void visit(Method node) {
@@ -205,17 +201,17 @@
     // If node is in super class, assign its number, overriding it.
     final superClass =
         (node.classDecl.superclass!.type as DeclaredType).classDecl;
-    final superNum = methodNumsAfterRenaming[superClass]?[sig];
+    final superNum = superClass.methodNumsAfterRenaming[sig];
     if (superNum != null) {
       // Don't rename if superNum == 0
       // Unless the node name is a keyword.
       final superNumText = superNum == 0 ? '' : '$superNum';
       final methodName = superNum == 0 ? _keywordRename(name) : name;
       node.finalName = '$methodName$superNumText';
-      methodNumsAfterRenaming[node.classDecl]?[sig] = superNum;
+      node.classDecl.methodNumsAfterRenaming[sig] = superNum;
     } else {
       node.finalName = _renameConflict(nameCounts, name);
-      methodNumsAfterRenaming[node.classDecl]?[sig] = nameCounts[name]! - 1;
+      node.classDecl.methodNumsAfterRenaming[sig] = nameCounts[name]! - 1;
     }
     log.fine(
         'Method ${node.classDecl.binaryName}#${node.name} is named ${node.finalName}');
diff --git a/pkgs/jnigen/lib/src/bindings/resolver.dart b/pkgs/jnigen/lib/src/bindings/resolver.dart
index b0c8230..68b250b 100644
--- a/pkgs/jnigen/lib/src/bindings/resolver.dart
+++ b/pkgs/jnigen/lib/src/bindings/resolver.dart
@@ -4,18 +4,17 @@
 
 import 'dart:math';
 
+import '../elements/elements.dart';
 import '../logging/logging.dart';
 
 class Resolver {
-  static const Map<String, String> predefined = {
-    'java.lang.String': 'jni.',
-  };
-
   /// Class corresponding to currently writing file.
-  final String currentClass;
+  ///
+  /// Is [null] when in single file mode.
+  final String? currentClass;
 
   /// Explicit import mappings.
-  final Map<String, String> importMap;
+  final Map<String, ClassDecl> importedClasses;
 
   /// Names of all classes in input.
   final Set<String> inputClassNames;
@@ -27,7 +26,7 @@
   final Map<String, String> _classToImportedName = {};
 
   Resolver({
-    required this.importMap,
+    required this.importedClasses,
     required this.currentClass,
     this.inputClassNames = const {},
   });
@@ -50,13 +49,20 @@
   }
 
   /// Get the prefix for the class
-  String resolvePrefix(String binaryName) {
-    if (predefined.containsKey(binaryName)) {
-      return predefined[binaryName]!;
+  String resolvePrefix(ClassDecl classDecl) {
+    if (classDecl.path == 'package:jni/jni.dart') {
+      // For package:jni we don't use a leading underscore.
+      return 'jni.';
     }
+    final binaryName = classDecl.binaryName;
     final target = getFileClassName(binaryName);
 
-    if (target == currentClass && inputClassNames.contains(binaryName)) {
+    // For classes we generate (inside [inputClassNames]) no import
+    // (and therefore no prefix) is necessary when:
+    // * Never necessary in single file mode
+    // * In multi file mode if the target is the same as the current class
+    if ((currentClass == null || target == currentClass) &&
+        inputClassNames.contains(binaryName)) {
       return '';
     }
 
@@ -105,11 +111,11 @@
   /// requested so that classes included in current bindings can be resolved
   /// using relative path.
   String? getImport(String classToResolve, String binaryName) {
-    var prefix = classToResolve;
+    final prefix = classToResolve;
 
     // short circuit if the requested class is specified directly in import map.
-    if (importMap.containsKey(binaryName)) {
-      return importMap[binaryName]!;
+    if (importedClasses.containsKey(binaryName)) {
+      return importedClasses[binaryName]!.path;
     }
 
     if (prefix.isEmpty) {
@@ -117,7 +123,7 @@
     }
 
     final dest = classToResolve.split('.');
-    final src = currentClass.split('.');
+    final src = currentClass!.split('.');
     // Use relative import when the required class is included in current set
     // of bindings.
     if (inputClassNames.contains(binaryName)) {
@@ -138,14 +144,6 @@
       return '$pathToCommon$pathToClass.dart';
     }
 
-    while (prefix.isNotEmpty) {
-      final split = cutFromLast(prefix, '.');
-      final left = split[0];
-      if (importMap.containsKey(prefix)) {
-        return importMap[prefix]!;
-      }
-      prefix = left;
-    }
     return null;
   }
 
diff --git a/pkgs/jnigen/lib/src/config/config_types.dart b/pkgs/jnigen/lib/src/config/config_types.dart
index 6030436..e74718c 100644
--- a/pkgs/jnigen/lib/src/config/config_types.dart
+++ b/pkgs/jnigen/lib/src/config/config_types.dart
@@ -5,12 +5,20 @@
 import 'dart:io';
 
 import 'package:logging/logging.dart';
+import 'package:path/path.dart' as p;
+import 'package:pub_semver/pub_semver.dart';
+import 'package:yaml/yaml.dart';
 
 import '../elements/elements.dart';
+import '../logging/logging.dart';
+import '../util/find_package.dart';
 import 'config_exception.dart';
 import 'filters.dart';
 import 'yaml_reader.dart';
 
+/// Modify this when symbols file format changes according to pub_semver.
+final _currentVersion = Version(1, 0, 0);
+
 /// Configuration for dependencies to be downloaded using maven.
 ///
 /// Dependency names should be listed in groupId:artifactId:version format.
@@ -209,9 +217,9 @@
     this.structure = OutputStructure.packageStructure,
   }) {
     if (structure == OutputStructure.singleFile) {
-      if (!path.toFilePath().endsWith('.dart')) {
+      if (p.extension(path.toFilePath()) != '.dart') {
         throw ConfigException(
-            'output path must end with ".dart" in single file mode');
+            'Dart\'s output path must end with ".dart" in single file mode.');
       }
     } else {
       _ensureIsDirectory('Dart output path', path);
@@ -225,19 +233,32 @@
   OutputStructure structure;
 }
 
+class SymbolsOutputConfig {
+  /// Path to write generated Dart bindings.
+  final Uri path;
+
+  SymbolsOutputConfig(this.path) {
+    if (p.extension(path.toFilePath()) != '.yaml') {
+      throw ConfigException('Symbol\'s output path must end with ".yaml".');
+    }
+  }
+}
+
 class OutputConfig {
   OutputConfig({
+    required this.dartConfig,
     this.bindingsType = BindingsType.cBased,
     this.cConfig,
-    required this.dartConfig,
+    this.symbolsConfig,
   }) {
     if (bindingsType == BindingsType.cBased && cConfig == null) {
-      throw ConfigException("C output config must be provided!");
+      throw ConfigException('C output config must be provided!');
     }
   }
   BindingsType bindingsType;
   DartCodeOutputConfig dartConfig;
   CCodeOutputConfig? cConfig;
+  SymbolsOutputConfig? symbolsConfig;
 }
 
 class BindingExclusions {
@@ -257,12 +278,12 @@
     this.sourcePath,
     this.classPath,
     this.preamble,
-    this.importMap,
     this.androidSdkConfig,
     this.mavenDownloads,
     this.summarizerOptions,
     this.logLevel = Level.INFO,
     this.dumpJsonTo,
+    this.imports,
   });
 
   /// Output configuration for generated bindings
@@ -296,18 +317,6 @@
   /// Common text to be pasted on top of generated C and Dart files.
   final String? preamble;
 
-  /// Additional java package -> dart package mappings (Experimental).
-  ///
-  /// a mapping com.abc.package -> 'package:my_package.dart/my_import.dart'
-  /// in this configuration suggests that any reference to a type from
-  /// com.abc.package shall resolve to an import of 'package:my_package.dart'.
-  ///
-  /// This can be as granular
-  /// `com.abc.package.Class -> 'package:abc/abc.dart'`
-  /// or coarse
-  /// `com.abc.package` -> 'package:abc/abc.dart'`
-  final Map<String, String>? importMap;
-
   /// Whether or not to change Kotlin's suspend functions to Dart async ones.
   ///
   /// This will remove the final Continuation argument.
@@ -321,9 +330,119 @@
   /// along with their transitive dependencies.
   final MavenDownloads? mavenDownloads;
 
-  /// Additional options for the summarizer component
+  /// Additional options for the summarizer component.
   final SummarizerOptions? summarizerOptions;
 
+  /// List of dependencies.
+  final List<Uri>? imports;
+
+  /// Call [importClasses] before using this.
+  late final Map<String, ClassDecl> importedClasses;
+
+  Future<void> importClasses() async {
+    importedClasses = {};
+    for (final import in [
+      // Implicitly importing package:jni symbols.
+      Uri.parse('package:jni/jni_symbols.yaml'),
+      ...?imports,
+    ]) {
+      // Getting the actual uri in case of package uris.
+      final Uri yamlUri;
+      final String importPath;
+      if (import.scheme == 'package') {
+        final packageName = import.pathSegments.first;
+        final packageRoot = await findPackageRoot(packageName);
+        if (packageRoot == null) {
+          log.fatal('package:$packageName was not found.');
+        }
+        yamlUri = packageRoot
+            .resolve('lib/')
+            .resolve(import.pathSegments.sublist(1).join('/'));
+        importPath = 'package:$packageName';
+      } else {
+        yamlUri = import;
+        importPath = ([...import.pathSegments]..removeLast()).join('/');
+      }
+      log.finest('Parsing yaml file in url $yamlUri.');
+      final YamlMap yaml;
+      try {
+        final symbolsFile = File.fromUri(yamlUri);
+        final content = symbolsFile.readAsStringSync();
+        yaml = loadYaml(content, sourceUrl: yamlUri);
+      } on UnsupportedError catch (_) {
+        log.fatal('Could not reference "$import".');
+      } catch (e, s) {
+        log.warning(e);
+        log.warning(s);
+        log.fatal('Error while parsing yaml file "$import".');
+      }
+      final version = Version.parse(yaml['version'] as String);
+      if (!VersionConstraint.compatibleWith(_currentVersion).allows(version)) {
+        log.fatal('"$import" is version "$version" which is not compatible with'
+            'the current JNIgen symbols version $_currentVersion');
+      }
+      final files = yaml['files'] as YamlMap;
+      for (final entry in files.entries) {
+        final filePath = entry.key as String;
+        final classes = entry.value as YamlMap;
+        for (final classEntry in classes.entries) {
+          final binaryName = classEntry.key as String;
+          final decl = classEntry.value as YamlMap;
+          if (importedClasses.containsKey(binaryName)) {
+            log.fatal(
+              'Re-importing "$binaryName" in "$import".\n'
+              'Try hiding the class in import.',
+            );
+          }
+          final classDecl = ClassDecl(
+            simpleName: binaryName.split('.').last,
+            packageName: (binaryName.split('.')..removeLast()).join('.'),
+            binaryName: binaryName,
+          )
+            ..path = '$importPath/$filePath'
+            ..finalName = decl['name']
+            ..typeClassName = decl['type_class']
+            ..superCount = decl['super_count'];
+          for (final typeParamEntry
+              in ((decl['type_params'] as YamlMap?)?.entries) ??
+                  <MapEntry<dynamic, dynamic>>[]) {
+            final typeParamName = typeParamEntry.key as String;
+            final bounds = (typeParamEntry.value as YamlMap).entries.map((e) {
+              final boundName = e.key as String;
+              // Can only be DECLARED or TYPE_VARIABLE
+              if (!['DECLARED', 'TYPE_VARIABLE'].contains(e.value)) {
+                log.fatal(
+                  'Unsupported bound kind "${e.value}" for bound "$boundName" '
+                  'in type parameter "$typeParamName" '
+                  'of "$binaryName".',
+                );
+              }
+              final boundKind = (e.value as String) == 'DECLARED'
+                  ? Kind.declared
+                  : Kind.typeVariable;
+              final ReferredType type;
+              if (boundKind == Kind.declared) {
+                type =
+                    DeclaredType(binaryName: boundName, simpleName: boundName);
+              } else {
+                type = TypeVar(name: boundName);
+              }
+              return TypeUsage(
+                  shorthand: boundName, kind: boundKind, typeJson: {})
+                ..type = type;
+            }).toList();
+            classDecl.allTypeParams.add(
+              TypeParam(name: typeParamName, bounds: bounds),
+            );
+          }
+          classDecl.methodNumsAfterRenaming =
+              (decl['methods'] as YamlMap?)?.cast() ?? {};
+          importedClasses[binaryName] = classDecl;
+        }
+      }
+    }
+  }
+
   /// Directory containing the YAML configuration file, if any.
   Uri? get configRoot => _configRoot;
   Uri? _configRoot;
@@ -337,8 +456,6 @@
 
   static final _levels = Map.fromEntries(
       Level.LEVELS.map((l) => MapEntry(l.name.toLowerCase(), l)));
-  static List<Uri>? _toUris(List<String>? paths) =>
-      paths?.map(Uri.file).toList();
 
   static Config parseArgs(List<String> args) {
     final prov = YamlReader.parseArgs(args);
@@ -354,9 +471,6 @@
       return res;
     }
 
-    Uri? directoryUri(String? path) =>
-        path != null ? Uri.directory(path) : null;
-
     MemberFilter<T>? regexFilter<T extends ClassMember>(String property) {
       final exclusions = prov.getStringList(property);
       if (exclusions == null) return null;
@@ -395,14 +509,13 @@
         configRoot?.resolve(reference).toFilePath() ?? reference;
 
     final config = Config(
-      sourcePath: _toUris(prov.getPathList(_Props.sourcePath)),
-      classPath: _toUris(prov.getPathList(_Props.classPath)),
+      sourcePath: prov.getPathList(_Props.sourcePath),
+      classPath: prov.getPathList(_Props.classPath),
       classes: must(prov.getStringList, [], _Props.classes),
       summarizerOptions: SummarizerOptions(
         extraArgs: prov.getStringList(_Props.summarizerArgs) ?? const [],
         backend: prov.getString(_Props.backend),
-        workingDirectory:
-            directoryUri(prov.getPath(_Props.summarizerWorkingDir)),
+        workingDirectory: prov.getPath(_Props.summarizerWorkingDir),
       ),
       exclude: BindingExclusions(
         methods: regexFilter<Method>(_Props.excludeMethods),
@@ -417,27 +530,32 @@
         cConfig: prov.hasValue(_Props.cCodeOutputConfig)
             ? CCodeOutputConfig(
                 libraryName: must(prov.getString, '', _Props.libraryName),
-                path: Uri.file(must(prov.getPath, '.', _Props.cRoot)),
+                path: must(prov.getPath, Uri.parse('.'), _Props.cRoot),
                 subdir: prov.getString(_Props.cSubdir),
               )
             : null,
         dartConfig: DartCodeOutputConfig(
-          path: Uri.file(must(prov.getPath, '.', _Props.dartRoot)),
+          path: must(prov.getPath, Uri.parse('.'), _Props.dartRoot),
           structure: getOutputStructure(
             prov.getString(_Props.outputStructure),
             OutputStructure.packageStructure,
           ),
         ),
+        symbolsConfig: prov.hasValue(_Props.symbolsOutputConfig)
+            ? SymbolsOutputConfig(
+                must(prov.getPath, Uri.parse('.'), _Props.symbolsOutputConfig),
+              )
+            : null,
       ),
       preamble: prov.getString(_Props.preamble),
-      importMap: prov.getStringMap(_Props.importMap),
+      imports: prov.getPathList(_Props.import),
       mavenDownloads: prov.hasValue(_Props.mavenDownloads)
           ? MavenDownloads(
               sourceDeps: prov.getStringList(_Props.sourceDeps) ?? const [],
-              sourceDir: prov.getPath(_Props.mavenSourceDir) ??
+              sourceDir: prov.getPath(_Props.mavenSourceDir)?.toFilePath() ??
                   resolveFromConfigRoot(MavenDownloads.defaultMavenSourceDir),
               jarOnlyDeps: prov.getStringList(_Props.jarOnlyDeps) ?? const [],
-              jarDir: prov.getPath(_Props.mavenJarDir) ??
+              jarDir: prov.getPath(_Props.mavenJarDir)?.toFilePath() ??
                   resolveFromConfigRoot(MavenDownloads.defaultMavenJarDir),
             )
           : null,
@@ -496,11 +614,12 @@
 
   static const suspendFunToAsync = 'suspend_fun_to_async';
 
-  static const importMap = 'import_map';
+  static const import = 'import';
   static const outputConfig = 'output';
   static const bindingsType = '$outputConfig.bindings_type';
   static const cCodeOutputConfig = '$outputConfig.c';
   static const dartCodeOutputConfig = '$outputConfig.dart';
+  static const symbolsOutputConfig = '$outputConfig.symbols';
   static const cRoot = '$cCodeOutputConfig.path';
   static const cSubdir = '$cCodeOutputConfig.subdir';
   static const dartRoot = '$dartCodeOutputConfig.path';
diff --git a/pkgs/jnigen/lib/src/config/yaml_reader.dart b/pkgs/jnigen/lib/src/config/yaml_reader.dart
index ecae2cb..1cb1f46 100644
--- a/pkgs/jnigen/lib/src/config/yaml_reader.dart
+++ b/pkgs/jnigen/lib/src/config/yaml_reader.dart
@@ -86,8 +86,7 @@
 
   /// Same as [getString] but path is resolved relative to YAML config if it's
   /// from YAML config.
-  String? getPath(String property) =>
-      _config.optionalPath(property)?.toFilePath();
+  Uri? getPath(String property) => _config.optionalPath(property);
 
   List<String>? getStringList(String property) => _config.optionalStringList(
         property,
@@ -95,14 +94,11 @@
         combineAllConfigs: false,
       );
 
-  List<String>? getPathList(String property) {
-    final configResult = _config.optionalPathList(
-      property,
-      combineAllConfigs: false,
-      splitCliPattern: ';',
-    );
-    return configResult?.map((e) => e.path).toList();
-  }
+  List<Uri>? getPathList(String property) => _config.optionalPathList(
+        property,
+        combineAllConfigs: false,
+        splitCliPattern: ';',
+      );
 
   String? getOneOf(String property, Set<String> values) =>
       _config.optionalString(property, validValues: values);
diff --git a/pkgs/jnigen/lib/src/elements/elements.dart b/pkgs/jnigen/lib/src/elements/elements.dart
index 7793c1f..347c818 100644
--- a/pkgs/jnigen/lib/src/elements/elements.dart
+++ b/pkgs/jnigen/lib/src/elements/elements.dart
@@ -29,7 +29,7 @@
 }
 
 class Classes implements Element<Classes> {
-  const Classes._(this.decls);
+  const Classes(this.decls);
 
   final Map<String, ClassDecl> decls;
 
@@ -39,7 +39,7 @@
       final classDecl = ClassDecl.fromJson(declJson);
       decls[classDecl.binaryName] = classDecl;
     }
-    return Classes._(decls);
+    return Classes(decls);
   }
 
   @override
@@ -115,6 +115,12 @@
   @JsonKey(includeFromJson: false)
   late final String finalName;
 
+  /// Name of the type class.
+  ///
+  /// Populated by [Renamer].
+  @JsonKey(includeFromJson: false)
+  late final String typeClassName;
+
   /// Unique name obtained by renaming conflicting names with a number.
   ///
   /// This is used by C bindings instead of fully qualified name to reduce
@@ -130,6 +136,18 @@
   @JsonKey(includeFromJson: false)
   List<TypeParam> allTypeParams = const [];
 
+  /// The path which this class is generated in.
+  ///
+  /// Populated by [Linker].
+  @JsonKey(includeFromJson: false)
+  late final String path;
+
+  /// The numeric suffix of the methods.
+  ///
+  /// Populated by [Renamer].
+  @JsonKey(includeFromJson: false)
+  late final Map<String, int> methodNumsAfterRenaming;
+
   @override
   String toString() {
     return 'Java class declaration for $binaryName';
@@ -137,28 +155,6 @@
 
   String get signature => 'L$internalName;';
 
-  static final object = ClassDecl(
-    binaryName: 'java.lang.Object',
-    packageName: 'java.lang',
-    simpleName: 'Object',
-  )
-    ..finalName = 'JObject'
-    ..superCount = 0;
-
-  static final string = ClassDecl(
-    superclass: TypeUsage.object,
-    binaryName: 'java.lang.String',
-    packageName: 'java.lang',
-    simpleName: 'String',
-  )
-    ..finalName = 'JString'
-    ..superCount = 1;
-
-  static final predefined = {
-    'java.lang.Object': object,
-    'java.lang.String': string,
-  };
-
   factory ClassDecl.fromJson(Map<String, dynamic> json) =>
       _$ClassDeclFromJson(json);
 
@@ -172,6 +168,8 @@
 
   @override
   String get name => finalName;
+
+  bool isObject() => superCount == 0;
 }
 
 @JsonEnum()
@@ -196,15 +194,10 @@
     required this.typeJson,
   });
 
-  static TypeUsage object = () {
-    final typeUsage = TypeUsage.fromJson({
-      "shorthand": "java.lang.Object",
-      "kind": "DECLARED",
-      "type": {"binaryName": "java.lang.Object", "simpleName": "Object"}
-    });
-    (typeUsage.type as DeclaredType).classDecl = ClassDecl.object;
-    return typeUsage;
-  }();
+  static TypeUsage object = TypeUsage(
+      kind: Kind.declared, shorthand: 'JObject', typeJson: {})
+    ..type =
+        (DeclaredType(binaryName: 'java.lang.Object', simpleName: 'Object'));
 
   final String shorthand;
   final Kind kind;
diff --git a/pkgs/jnigen/lib/src/generate_bindings.dart b/pkgs/jnigen/lib/src/generate_bindings.dart
index ab184da..5c9dc9b 100644
--- a/pkgs/jnigen/lib/src/generate_bindings.dart
+++ b/pkgs/jnigen/lib/src/generate_bindings.dart
@@ -34,10 +34,9 @@
     log.fatal(e.message);
   }
 
-  classes
-    ..accept(Excluder(config))
-    ..accept(Linker(config))
-    ..accept(Renamer(config));
+  classes.accept(Excluder(config));
+  await classes.accept(Linker(config));
+  classes.accept(Renamer(config));
 
   final cBased = config.outputConfig.bindingsType == BindingsType.cBased;
   if (cBased) {
diff --git a/pkgs/jnigen/pubspec.yaml b/pkgs/jnigen/pubspec.yaml
index 5cde347..a76d658 100644
--- a/pkgs/jnigen/pubspec.yaml
+++ b/pkgs/jnigen/pubspec.yaml
@@ -14,6 +14,7 @@
   json_annotation: ^4.8.0
   package_config: ^2.1.0
   path: ^1.8.0
+  pub_semver: ^2.1.4
   args: ^2.3.0
   yaml: ^3.1.0
   logging: ^1.0.2
diff --git a/pkgs/jnigen/test/config_test.dart b/pkgs/jnigen/test/config_test.dart
index 97fbb3d..8538ac1 100644
--- a/pkgs/jnigen/test/config_test.dart
+++ b/pkgs/jnigen/test/config_test.dart
@@ -31,10 +31,12 @@
       reason: "cRoot");
   expect(a.outputConfig.dartConfig.path, equals(b.outputConfig.dartConfig.path),
       reason: "dartRoot");
+  expect(a.outputConfig.symbolsConfig?.path,
+      equals(b.outputConfig.symbolsConfig?.path),
+      reason: "symbolsRoot");
   expect(a.sourcePath, equals(b.sourcePath), reason: "sourcePath");
   expect(a.classPath, equals(b.classPath), reason: "classPath");
   expect(a.preamble, equals(b.preamble), reason: "preamble");
-  expect(a.importMap, equals(b.importMap), reason: "importMap");
   final am = a.mavenDownloads;
   final bm = b.mavenDownloads;
   if (am != null) {
diff --git a/pkgs/jnigen/test/package_resolver_test.dart b/pkgs/jnigen/test/package_resolver_test.dart
index 98c0d2b..2f3a2ea 100644
--- a/pkgs/jnigen/test/package_resolver_test.dart
+++ b/pkgs/jnigen/test/package_resolver_test.dart
@@ -3,6 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 
 import 'package:jnigen/src/bindings/resolver.dart';
+import 'package:jnigen/src/elements/elements.dart';
 import 'package:test/test.dart';
 
 import 'test_util/test_util.dart';
@@ -17,21 +18,28 @@
 void main() async {
   await checkLocallyBuiltDependencies();
   final resolver = Resolver(
-      importMap: {
-        'org.apache.pdfbox': 'package:pdfbox/pdfbox.dart',
-        'android.os.Process': 'package:android/os.dart',
-      },
-      currentClass: 'a.b.N',
-      inputClassNames: {
-        'a.b.C',
-        'a.b.c.D',
-        'a.b.c.d.E',
-        'a.X',
-        'e.f.G',
-        'e.F',
-        'a.g.Y',
-        'a.m.n.P'
-      });
+    importedClasses: {
+      'org.apache.pdfbox.pdmodel.PDDocument': ClassDecl(
+        binaryName: 'org.apache.pdfbox.pdmodel.PDDocument',
+        simpleName: 'PDDocument',
+      )..path = 'package:pdfbox/pdfbox.dart',
+      'android.os.Process': ClassDecl(
+        binaryName: 'android.os.Process',
+        simpleName: 'Process',
+      )..path = 'package:android/os.dart',
+    },
+    currentClass: 'a.b.N',
+    inputClassNames: {
+      'a.b.C',
+      'a.b.c.D',
+      'a.b.c.d.E',
+      'a.X',
+      'e.f.G',
+      'e.F',
+      'a.g.Y',
+      'a.m.n.P'
+    },
+  );
 
   final tests = [
     // Absolute imports resolved using import map
@@ -64,6 +72,8 @@
     test(
         'resolve $binaryName',
         () => expect(
-            resolver.resolvePrefix(binaryName), equals(testCase.expectedName)));
+            resolver.resolvePrefix(
+                ClassDecl(binaryName: binaryName, simpleName: '')..path = ''),
+            equals(testCase.expectedName)));
   }
 }