Add an objective-C module prefix option (#438)

* Add an objective-C module prefix option

* Fix analysis

* Update readme

* Update readme again
diff --git a/README.md b/README.md
index 437ba54..6442b42 100644
--- a/README.md
+++ b/README.md
@@ -186,7 +186,7 @@
   <tr>
     <td>
       functions<br><br>structs<br><br>unions<br><br>enums<br><br>
-      unnamed-enums<br><br>macros<br><br>globals<br><br>objc-interfaces
+      unnamed-enums<br><br>macros<br><br>globals
     </td>
     <td>Filters for declarations.<br><b>Default: all are included.</b><br><br>
     Options -<br>
@@ -507,6 +507,81 @@
 </tbody>
 </table>
 
+### Objective-C config options
+
+<table>
+<thead>
+  <tr>
+    <th>Key</th>
+    <th>Explaination</th>
+    <th>Example</th>
+  </tr>
+  <colgroup>
+    <col>
+    <col style="width: 100px;">
+  </colgroup>
+</thead>
+<tbody>
+  <tr>
+    <td>
+      objc-interfaces
+    </td>
+    <td>
+      Filters for interface declarations. This option works the same as other
+      declaration filters like `functions` and `structs`.
+    </td>
+    <td>
+
+```yaml
+objc-interfaces:
+  include:
+    # Includes a specific interface.
+    - 'MyInterface'
+    # Includes all interfaces starting with "NS".
+    - 'NS.*'
+  exclude:
+    # Override the above NS.* inclusion, to exclude NSURL.
+    - 'NSURL'
+  rename:
+    # Removes '_' prefix from interface names.
+    '_(.*)': '$1'
+```
+
+  </td>
+  </tr>
+
+  <tr>
+    <td>
+      objc-interfaces -> module
+    </td>
+    <td>
+      Adds a module prefix to the class name when loading the class
+      from the dylib. This is only relevent for ObjC headers that are generated
+      wrappers for a Swift library. See example/swift for more information.
+    </td>
+    <td>
+
+```yaml
+headers:
+  entry-points:
+    # Generated by swiftc to wrap foo_lib.swift.
+    - 'foo_lib-Swift.h'
+objc-interfaces:
+  include:
+    # Eg, foo_lib contains a set of classes prefixed with FL.
+    - 'FL.*'
+  module:
+    # Use 'foo_lib' as the module name for all the FL.* classes.
+    # We don't match .* here because other classes like NSString
+    # shouldn't be given a module prefix.
+    'FL.*': 'foo_lib'
+```
+
+  </td>
+  </tr>
+</tbody>
+</table>
+
 ## Trying out examples
 1. `cd examples/<example_u_want_to_run>`, Run `dart pub get`.
 2. Run `dart run ffigen`.
diff --git a/lib/src/code_generator/objc_interface.dart b/lib/src/code_generator/objc_interface.dart
index 61e88ef..a6099b8 100644
--- a/lib/src/code_generator/objc_interface.dart
+++ b/lib/src/code_generator/objc_interface.dart
@@ -41,6 +41,7 @@
   final methods = <String, ObjCMethod>{};
   bool filled = false;
 
+  final String lookupName;
   final ObjCBuiltInFunctions builtInFunctions;
   final bool isBuiltIn;
   late final ObjCInternalGlobal _classObject;
@@ -51,6 +52,7 @@
     String? usr,
     required String originalName,
     required String name,
+    required this.lookupName,
     String? dartDoc,
     required this.builtInFunctions,
     required this.isBuiltIn,
@@ -228,7 +230,7 @@
 
     _classObject = ObjCInternalGlobal(
         '_class_$originalName',
-        (Writer w) => '${builtInFunctions.getClass.name}("$originalName")',
+        (Writer w) => '${builtInFunctions.getClass.name}("$lookupName")',
         builtInFunctions.getClass)
       ..addDependencies(dependencies);
     _isKindOfClass = builtInFunctions.getSelObject('isKindOfClass:');
diff --git a/lib/src/config_provider/config.dart b/lib/src/config_provider/config.dart
index de60997..56b2df1 100644
--- a/lib/src/config_provider/config.dart
+++ b/lib/src/config_provider/config.dart
@@ -125,6 +125,10 @@
   StructPackingOverride get structPackingOverride => _structPackingOverride;
   late StructPackingOverride _structPackingOverride;
 
+  /// Module prefixes for ObjC interfaces.
+  ObjCModulePrefixer get objcModulePrefixer => _objcModulePrefixer;
+  late ObjCModulePrefixer _objcModulePrefixer;
+
   /// Name of the wrapper class.
   String get wrapperName => _wrapperName;
   late String _wrapperName;
@@ -341,6 +345,15 @@
           _objcInterfaces = result as Declaration;
         },
       ),
+      [strings.objcInterfaces, strings.objcModule]:
+          Specification<Map<String, String>>(
+        requirement: Requirement.no,
+        validator: stringStringMapValidator,
+        extractor: stringStringMapExtractor,
+        defaultValue: () => <String, String>{},
+        extractedResult: (dynamic result) => _objcModulePrefixer =
+            ObjCModulePrefixer(result as Map<String, String>),
+      ),
       [strings.libraryImports]: Specification<Map<String, LibraryImport>>(
         validator: libraryImportsValidator,
         extractor: libraryImportsExtractor,
diff --git a/lib/src/config_provider/config_types.dart b/lib/src/config_provider/config_types.dart
index f602976..8146917 100644
--- a/lib/src/config_provider/config_types.dart
+++ b/lib/src/config_provider/config_types.dart
@@ -373,3 +373,32 @@
     return [];
   }
 }
+
+class _ObjCModulePrefixerEntry {
+  final RegExp pattern;
+  final String moduleName;
+
+  _ObjCModulePrefixerEntry(this.pattern, this.moduleName);
+}
+
+/// Handles applying module prefixes to ObjC classes.
+class ObjCModulePrefixer {
+  final _prefixes = <_ObjCModulePrefixerEntry>[];
+
+  ObjCModulePrefixer(Map<String, String> prefixes) {
+    for (final entry in prefixes.entries) {
+      _prefixes.add(_ObjCModulePrefixerEntry(RegExp(entry.key), entry.value));
+    }
+  }
+
+  /// If any of the prefixing patterns match, applies that module prefix.
+  /// Otherwise returns the class name unmodified.
+  String applyPrefix(String className) {
+    for (final entry in _prefixes) {
+      if (quiver.matchesFull(entry.pattern, className)) {
+        return '${entry.moduleName}.$className';
+      }
+    }
+    return className;
+  }
+}
diff --git a/lib/src/config_provider/spec_utils.dart b/lib/src/config_provider/spec_utils.dart
index 015d92e..ac154ce 100644
--- a/lib/src/config_provider/spec_utils.dart
+++ b/lib/src/config_provider/spec_utils.dart
@@ -179,6 +179,29 @@
   return result;
 }
 
+Map<String, String> stringStringMapExtractor(dynamic yamlConfig) {
+  final resultMap = <String, String>{};
+  final inputMap = yamlConfig as YamlMap?;
+  if (inputMap != null) {
+    for (final key in inputMap.keys) {
+      resultMap[key as String] = inputMap[key] as String;
+    }
+  }
+  return resultMap;
+}
+
+bool stringStringMapValidator(List<String> name, dynamic yamlConfig) {
+  if (!checkType<YamlMap>(name, yamlConfig)) {
+    return false;
+  }
+  for (final key in (yamlConfig as YamlMap).keys) {
+    if (!checkType<String>([...name, key as String], yamlConfig[key])) {
+      return false;
+    }
+  }
+  return true;
+}
+
 Map<String, ImportedType> makeImportTypeMapping(
     Map<String, List<String>> rawTypeMappings,
     Map<String, LibraryImport> libraryImportsMap) {
diff --git a/lib/src/header_parser/sub_parsers/objcinterfacedecl_parser.dart b/lib/src/header_parser/sub_parsers/objcinterfacedecl_parser.dart
index 3c8aab3..bace1c4 100644
--- a/lib/src/header_parser/sub_parsers/objcinterfacedecl_parser.dart
+++ b/lib/src/header_parser/sub_parsers/objcinterfacedecl_parser.dart
@@ -51,6 +51,7 @@
     usr: itfUsr,
     originalName: name,
     name: config.objcInterfaces.renameUsingConfig(name),
+    lookupName: config.objcModulePrefixer.applyPrefix(name),
     dartDoc: getCursorDocComment(cursor),
     builtInFunctions: objCBuiltInFunctions,
     isBuiltIn: cursor.isInSystemHeader(),
diff --git a/lib/src/strings.dart b/lib/src/strings.dart
index 8094df1..6995ada 100644
--- a/lib/src/strings.dart
+++ b/lib/src/strings.dart
@@ -87,6 +87,9 @@
 const exposeFunctionTypedefs = 'expose-typedefs';
 const leafFunctions = 'leaf';
 
+// Sub-fields of ObjC interfaces.
+const objcModule = 'module';
+
 const dependencyOnly = 'dependency-only';
 // Values for `compoundDependencies`.
 const fullCompoundDependencies = 'full';
diff --git a/test/native_objc_test/.gitignore b/test/native_objc_test/.gitignore
index 62dd4b5..ad4daf6 100644
--- a/test/native_objc_test/.gitignore
+++ b/test/native_objc_test/.gitignore
@@ -1 +1,2 @@
 *_bindings.dart
+*-Swift.h
diff --git a/test/native_objc_test/setup.dart b/test/native_objc_test/setup.dart
index 2e6701b..5eff12c 100644
--- a/test/native_objc_test/setup.dart
+++ b/test/native_objc_test/setup.dart
@@ -27,6 +27,27 @@
   print('Generated file: $output');
 }
 
+Future<void> _buildSwift(
+    String input, String outputHeader, String outputLib) async {
+  final args = [
+    '-c',
+    input,
+    '-emit-objc-header-path',
+    outputHeader,
+    '-emit-library',
+    '-o',
+    outputLib,
+  ];
+  final process = await Process.start('swiftc', args);
+  unawaited(stdout.addStream(process.stdout));
+  unawaited(stderr.addStream(process.stderr));
+  final result = await process.exitCode;
+  if (result != 0) {
+    throw ProcessException('swiftc', args, 'Build failed', result);
+  }
+  print('Generated files: $outputHeader and $outputLib');
+}
+
 Future<void> _generateBindings(String config) async {
   final args = [
     'run',
@@ -60,7 +81,19 @@
 Future<void> build(List<String> testNames) async {
   print('Building Dynamic Library for Objective C Native Tests...');
   for (final name in testNames) {
-    await _buildLib('${name}_test.m', '${name}_test.dylib');
+    final mFile = '${name}_test.m';
+    if (await File(mFile).exists()) {
+      await _buildLib(mFile, '${name}_test.dylib');
+    }
+  }
+
+  print('Building Dynamic Library and Header for Swift Tests...');
+  for (final name in testNames) {
+    final swiftFile = '${name}_test.swift';
+    if (await File(swiftFile).exists()) {
+      await _buildSwift(
+          swiftFile, '${name}_test-Swift.h', '${name}_test.dylib');
+    }
   }
 
   print('Generating Bindings for Objective C Native Tests...');
diff --git a/test/native_objc_test/swift_class_config.yaml b/test/native_objc_test/swift_class_config.yaml
new file mode 100644
index 0000000..e845ed1
--- /dev/null
+++ b/test/native_objc_test/swift_class_config.yaml
@@ -0,0 +1,15 @@
+name: SwiftClassTestLibrary
+description: 'Tests Swift classes'
+language: objc
+output: 'test/native_objc_test/swift_class_bindings.dart'
+exclude-all-by-default: true
+objc-interfaces:
+  include:
+    - MySwiftClass
+  module:
+    'MySwiftClass': 'swift_class_test'
+headers:
+  entry-points:
+    - 'test/native_objc_test/swift_class_test-Swift.h'
+preamble: |
+  // ignore_for_file: camel_case_types, non_constant_identifier_names, unused_element, unused_field
diff --git a/test/native_objc_test/swift_class_test.dart b/test/native_objc_test/swift_class_test.dart
new file mode 100644
index 0000000..09a2714
--- /dev/null
+++ b/test/native_objc_test/swift_class_test.dart
@@ -0,0 +1,34 @@
+// Copyright (c) 2022, 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.
+
+// Objective C support is only available on mac.
+@TestOn('mac-os')
+
+import 'dart:ffi';
+import 'dart:io';
+
+import 'package:test/test.dart';
+import '../test_utils.dart';
+import 'swift_class_bindings.dart';
+import 'util.dart';
+
+void main() {
+  late SwiftClassTestLibrary lib;
+  group('swift_class_test', () {
+    setUpAll(() {
+      logWarnings();
+      final dylib = File('test/native_objc_test/swift_class_test.dylib');
+      verifySetupFile(dylib);
+      lib = SwiftClassTestLibrary(DynamicLibrary.open(dylib.absolute.path));
+      generateBindingsForCoverage('swift_class');
+    });
+
+    test('Renamed class', () {
+      final swiftObject = MySwiftClass.new1(lib);
+      expect(swiftObject.getValue(), 123);
+      swiftObject.setValueWithX_(456);
+      expect(swiftObject.getValue(), 456);
+    });
+  });
+}
diff --git a/test/native_objc_test/swift_class_test.swift b/test/native_objc_test/swift_class_test.swift
new file mode 100644
index 0000000..26ae016
--- /dev/null
+++ b/test/native_objc_test/swift_class_test.swift
@@ -0,0 +1,11 @@
+import Foundation
+
+@objc public class MySwiftClass: NSObject {
+  var val = 123;
+  @objc public func getValue() -> Int {
+    return val;
+  }
+  @objc public func setValue(x: Int) {
+    val = x;
+  }
+}