[ffigen] Add a static isInstance method to ObjC classes (#383)

* Add an isInstance method for checking the type of an object.

* Tighten up the test

* Simplify a bit
diff --git a/pkgs/ffigen/lib/src/code_generator/objc_interface.dart b/pkgs/ffigen/lib/src/code_generator/objc_interface.dart
index 95143f2..4c77067 100644
--- a/pkgs/ffigen/lib/src/code_generator/objc_interface.dart
+++ b/pkgs/ffigen/lib/src/code_generator/objc_interface.dart
@@ -44,6 +44,8 @@
   final ObjCBuiltInFunctions builtInFunctions;
   final bool isBuiltIn;
   late final ObjCInternalGlobal _classObject;
+  late final ObjCInternalGlobal _isKindOfClass;
+  late final Func _isKindOfClassMsgSend;
 
   ObjCInterface({
     String? usr,
@@ -95,14 +97,23 @@
       {bool retain = false, bool release = false}) :
           super._(id, lib, retain: retain, release: release);
 
+  /// Returns a [$name] that points to the same underlying object as [other].
   static $name castFrom<T extends _ObjCWrapper>(T other) {
     return $name._(other._id, other._lib, retain: true, release: true);
   }
 
+  /// Returns a [$name] that wraps the given raw object pointer.
   static $name castFromPointer($natLib lib, ffi.Pointer<ObjCObject> other) {
     return $name._(other, lib, retain: true, release: true);
   }
 
+  /// Returns whether [obj] is an instance of [$name].
+  static bool isInstance(_ObjCWrapper obj) {
+    return obj._lib.${_isKindOfClassMsgSend.name}(
+        obj._id, obj._lib.${_isKindOfClass.name},
+        obj._lib.${_classObject.name});
+  }
+
 ''');
 
     if (isNSString) {
@@ -219,6 +230,9 @@
         (Writer w) => '${builtInFunctions.getClass.name}("$originalName")',
         builtInFunctions.getClass)
       ..addDependencies(dependencies);
+    _isKindOfClass = builtInFunctions.getSelObject('isKindOfClass:');
+    _isKindOfClassMsgSend = builtInFunctions.getMsgSendFunc(
+        BooleanType(), [ObjCMethodParam(PointerType(objCObjectType), 'clazz')]);
 
     if (isNSString) {
       _addNSStringMethods();
diff --git a/pkgs/ffigen/test/native_objc_test/is_instance_config.yaml b/pkgs/ffigen/test/native_objc_test/is_instance_config.yaml
new file mode 100644
index 0000000..90ca44b
--- /dev/null
+++ b/pkgs/ffigen/test/native_objc_test/is_instance_config.yaml
@@ -0,0 +1,12 @@
+name: IsInstanceTestObjCLibrary
+description: 'Tests isInstance'
+language: objc
+output: 'test/native_objc_test/is_instance_bindings.dart'
+functions:
+  exclude:
+    - '.*'
+headers:
+  entry-points:
+    - 'test/native_objc_test/is_instance_test.m'
+preamble: |
+  // ignore_for_file: camel_case_types, non_constant_identifier_names, unused_element, unused_field
diff --git a/pkgs/ffigen/test/native_objc_test/is_instance_test.dart b/pkgs/ffigen/test/native_objc_test/is_instance_test.dart
new file mode 100644
index 0000000..bbbfee9
--- /dev/null
+++ b/pkgs/ffigen/test/native_objc_test/is_instance_test.dart
@@ -0,0 +1,47 @@
+// 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 'is_instance_bindings.dart';
+import 'util.dart';
+
+void main() {
+  late IsInstanceTestObjCLibrary lib;
+
+  group('isInstance', () {
+    setUpAll(() {
+      logWarnings();
+      final dylib = File('test/native_objc_test/is_instance_test.dylib');
+      verifySetupFile(dylib);
+      lib = IsInstanceTestObjCLibrary(DynamicLibrary.open(dylib.absolute.path));
+      generateBindingsForCoverage('is_instance');
+    });
+
+    test('Unrelated classes', () {
+      final base = NSObject.castFrom(BaseClass.new1(lib));
+      final unrelated = NSObject.castFrom(UnrelatedClass.new1(lib));
+      expect(BaseClass.isInstance(base), isTrue);
+      expect(BaseClass.isInstance(unrelated), isFalse);
+      expect(UnrelatedClass.isInstance(base), isFalse);
+      expect(UnrelatedClass.isInstance(unrelated), isTrue);
+    });
+
+    test('Base class vs child class', () {
+      final base = NSObject.castFrom(BaseClass.new1(lib));
+      final child = NSObject.castFrom(ChildClass.new1(lib));
+      expect(BaseClass.isInstance(base), isTrue);
+      expect(BaseClass.isInstance(child), isTrue);
+      expect(ChildClass.isInstance(base), isFalse);
+      expect(ChildClass.isInstance(child), isTrue);
+    });
+  });
+}
diff --git a/pkgs/ffigen/test/native_objc_test/is_instance_test.m b/pkgs/ffigen/test/native_objc_test/is_instance_test.m
new file mode 100644
index 0000000..96b2314
--- /dev/null
+++ b/pkgs/ffigen/test/native_objc_test/is_instance_test.m
@@ -0,0 +1,23 @@
+// 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.
+
+#import <Foundation/NSObject.h>
+
+@interface BaseClass : NSObject {}
+@end
+
+@interface ChildClass : BaseClass {}
+@end
+
+@interface UnrelatedClass : NSObject {}
+@end
+
+@implementation BaseClass
+@end
+
+@implementation ChildClass
+@end
+
+@implementation UnrelatedClass
+@end
diff --git a/pkgs/ffigen/test/native_objc_test/setup.dart b/pkgs/ffigen/test/native_objc_test/setup.dart
index 3e9b5e0..22d99c9 100644
--- a/pkgs/ffigen/test/native_objc_test/setup.dart
+++ b/pkgs/ffigen/test/native_objc_test/setup.dart
@@ -51,6 +51,7 @@
   'cast',
   'category',
   'forward_decl',
+  'is_instance',
   'method',
   'native_objc',
   'nullable',