[ffigen] Make properties that set/get objective-c instances nullable (#348)

diff --git a/pkgs/ffigen/lib/src/code_generator/objc_built_in_functions.dart b/pkgs/ffigen/lib/src/code_generator/objc_built_in_functions.dart
index 95bce8b..1ab0798 100644
--- a/pkgs/ffigen/lib/src/code_generator/objc_built_in_functions.dart
+++ b/pkgs/ffigen/lib/src/code_generator/objc_built_in_functions.dart
@@ -83,11 +83,22 @@
     utilsExist = true;
 
     final objType = PointerType(objCObjectType).getCType(w);
-    s.write('\nclass _ObjCWrapper {\n');
-    s.write('  final $objType _id;\n');
-    s.write('  final ${w.className} _lib;\n');
-    s.write('  _ObjCWrapper._(this._id, this._lib);\n');
-    s.write('}\n');
+    s.write('''
+class _ObjCWrapper {
+  final $objType _id;
+  final ${w.className} _lib;
+
+  _ObjCWrapper._(this._id, this._lib);
+
+  @override
+  bool operator ==(Object other) {
+    return other is _ObjCWrapper && _id == other._id;
+  }
+
+  @override
+  int get hashCode => _id.hashCode;
+}
+''');
   }
 
   void addDependencies(Set<Binding> dependencies) {
diff --git a/pkgs/ffigen/lib/src/code_generator/objc_interface.dart b/pkgs/ffigen/lib/src/code_generator/objc_interface.dart
index 6b9c4c6..ab4da9e 100644
--- a/pkgs/ffigen/lib/src/code_generator/objc_interface.dart
+++ b/pkgs/ffigen/lib/src/code_generator/objc_interface.dart
@@ -121,7 +121,8 @@
       s.write('  ');
       if (isStatic) {
         s.write('static ');
-        s.write(_getConvertedType(returnType, w, name));
+        s.write(
+            _getConvertedReturnType(returnType, w, name, m.isNullableReturn));
 
         switch (m.kind) {
           case ObjCMethodKind.method:
@@ -147,18 +148,20 @@
         switch (m.kind) {
           case ObjCMethodKind.method:
             // returnType methodName(...)
-            s.write(_getConvertedType(returnType, w, name));
+            s.write(_getConvertedReturnType(
+                returnType, w, name, m.isNullableReturn));
             s.write(' $methodName');
             s.write(paramsToString(m.params, isStatic: false));
             break;
           case ObjCMethodKind.propertyGetter:
             // returnType get methodName
-            s.write(_getConvertedType(returnType, w, name));
+            s.write(_getConvertedReturnType(
+                returnType, w, name, m.isNullableReturn));
             s.write(' get $methodName');
             break;
           case ObjCMethodKind.propertySetter:
             // set methodName(...)
-            s.write('set $methodName');
+            s.write(' set $methodName');
             s.write(paramsToString(m.params, isStatic: false));
             break;
         }
@@ -181,7 +184,8 @@
       }
       s.write(');\n');
       if (convertReturn) {
-        final result = _doReturnConversion(returnType, '_ret', name, '_lib');
+        final result = _doReturnConversion(
+            returnType, '_ret', name, '_lib', m.isNullableReturn);
         s.write('    return $result;');
       }
 
@@ -311,6 +315,15 @@
     return type.getDartType(w);
   }
 
+  String _getConvertedReturnType(
+      Type type, Writer w, String enclosingClass, bool isNullableReturn) {
+    final result = _getConvertedType(type, w, enclosingClass);
+    if (isNullableReturn) {
+      return result + "?";
+    }
+    return result;
+  }
+
   String _doArgConversion(ObjCMethodParam arg) {
     if (arg.type is ObjCInterface ||
         _isObject(arg.type) ||
@@ -324,12 +337,22 @@
     return arg.name;
   }
 
-  String _doReturnConversion(
-      Type type, String value, String enclosingClass, String library) {
-    if (type is ObjCInterface) return '${type.name}._($value, $library)';
-    if (_isObject(type)) return 'NSObject._($value, $library)';
-    if (_isInstanceType(type)) return '$enclosingClass._($value, $library)';
-    return value;
+  String _doReturnConversion(Type type, String value, String enclosingClass,
+      String library, bool isNullable) {
+    String prefix = "";
+    if (isNullable) {
+      prefix += "$value.address == 0 ? null : ";
+    }
+    if (type is ObjCInterface) {
+      return prefix + '${type.name}._($value, $library)';
+    }
+    if (_isObject(type)) {
+      return prefix + 'NSObject._($value, $library)';
+    }
+    if (_isInstanceType(type)) {
+      return prefix + '$enclosingClass._($value, $library)';
+    }
+    return prefix + value;
   }
 }
 
@@ -351,6 +374,7 @@
   final String originalName;
   final ObjCProperty? property;
   Type? returnType;
+  final bool isNullableReturn;
   final List<ObjCMethodParam> params;
   final ObjCMethodKind kind;
   final bool isClass;
@@ -364,6 +388,7 @@
     required this.kind,
     required this.isClass,
     this.returnType,
+    this.isNullableReturn = false,
     List<ObjCMethodParam>? params_,
   }) : params = params_ ?? [];
 
diff --git a/pkgs/ffigen/lib/src/header_parser/sub_parsers/objcinterfacedecl_parser.dart b/pkgs/ffigen/lib/src/header_parser/sub_parsers/objcinterfacedecl_parser.dart
index c9451cf..658a038 100644
--- a/pkgs/ffigen/lib/src/header_parser/sub_parsers/objcinterfacedecl_parser.dart
+++ b/pkgs/ffigen/lib/src/header_parser/sub_parsers/objcinterfacedecl_parser.dart
@@ -119,6 +119,9 @@
   final isReadOnly = propertyAttributes &
           clang_types.CXObjCPropertyAttrKind.CXObjCPropertyAttr_readonly >
       0;
+  // TODO(#334): Use the nullable attribute to decide this.
+  final isNullable =
+      cursor.type().kind == clang_types.CXTypeKind.CXType_ObjCObjectPointer;
 
   final property = ObjCProperty(fieldName);
 
@@ -133,8 +136,9 @@
     dartDoc: dartDoc,
     kind: ObjCMethodKind.propertyGetter,
     isClass: isClass,
+    returnType: fieldType,
+    isNullableReturn: isNullable,
   );
-  getter.returnType = fieldType;
   itf.addMethod(getter);
 
   if (!isReadOnly) {
@@ -148,7 +152,8 @@
         kind: ObjCMethodKind.propertySetter,
         isClass: isClass);
     setter.returnType = NativeType(SupportedNativeType.Void);
-    setter.params.add(ObjCMethodParam(fieldType, 'value'));
+    setter.params
+        .add(ObjCMethodParam(fieldType, 'value', isNullable: isNullable));
     itf.addMethod(setter);
   }
 }
diff --git a/pkgs/ffigen/test/header_parser_tests/expected_bindings/_expected_objc_config_bindings.dart b/pkgs/ffigen/test/header_parser_tests/expected_bindings/_expected_objc_config_bindings.dart
index 3a2029d..a3ab4bf 100644
--- a/pkgs/ffigen/test/header_parser_tests/expected_bindings/_expected_objc_config_bindings.dart
+++ b/pkgs/ffigen/test/header_parser_tests/expected_bindings/_expected_objc_config_bindings.dart
@@ -423,7 +423,16 @@
 class _ObjCWrapper {
   final ffi.Pointer<ObjCObject> _id;
   final NativeLibrary _lib;
+
   _ObjCWrapper._(this._id, this._lib);
+
+  @override
+  bool operator ==(Object other) {
+    return other is _ObjCWrapper && _id == other._id;
+  }
+
+  @override
+  int get hashCode => _id.hashCode;
 }
 
 class Foo extends NSObject {
diff --git a/pkgs/ffigen/test/header_parser_tests/expected_bindings/_expected_objc_interface_bindings.dart b/pkgs/ffigen/test/header_parser_tests/expected_bindings/_expected_objc_interface_bindings.dart
index 45201d7..9502b9f 100644
--- a/pkgs/ffigen/test/header_parser_tests/expected_bindings/_expected_objc_interface_bindings.dart
+++ b/pkgs/ffigen/test/header_parser_tests/expected_bindings/_expected_objc_interface_bindings.dart
@@ -519,7 +519,16 @@
 class _ObjCWrapper {
   final ffi.Pointer<ObjCObject> _id;
   final NativeLibrary _lib;
+
   _ObjCWrapper._(this._id, this._lib);
+
+  @override
+  bool operator ==(Object other) {
+    return other is _ObjCWrapper && _id == other._id;
+  }
+
+  @override
+  int get hashCode => _id.hashCode;
 }
 
 class Foo extends NSObject {
diff --git a/pkgs/ffigen/test/native_objc_test/cast_bindings.dart b/pkgs/ffigen/test/native_objc_test/cast_bindings.dart
index fcac886..d167a6c 100644
--- a/pkgs/ffigen/test/native_objc_test/cast_bindings.dart
+++ b/pkgs/ffigen/test/native_objc_test/cast_bindings.dart
@@ -643,7 +643,16 @@
 class _ObjCWrapper {
   final ffi.Pointer<ObjCObject> _id;
   final CastTestObjCLibrary _lib;
+
   _ObjCWrapper._(this._id, this._lib);
+
+  @override
+  bool operator ==(Object other) {
+    return other is _ObjCWrapper && _id == other._id;
+  }
+
+  @override
+  int get hashCode => _id.hashCode;
 }
 
 class Castaway extends NSObject {
diff --git a/pkgs/ffigen/test/native_objc_test/cast_test.dart b/pkgs/ffigen/test/native_objc_test/cast_test.dart
index 20f2e18..d4a93f3 100644
--- a/pkgs/ffigen/test/native_objc_test/cast_test.dart
+++ b/pkgs/ffigen/test/native_objc_test/cast_test.dart
@@ -3,6 +3,7 @@
 // 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';
@@ -52,17 +53,29 @@
     });
 
     test('castFrom', () {
-      final meAsInt = testInstance.meAsInt();
-      expect(Castaway.castFrom(testInstance.meAsNSObject()).meAsInt(), meAsInt);
+      final fromCast = Castaway.castFrom(testInstance.meAsNSObject());
+      expect(fromCast, testInstance);
     });
 
     test('castFromPointer', () {
       final meAsInt = testInstance.meAsInt();
-      expect(
-          Castaway.castFromPointer(
-                  lib, Pointer<ObjCObject>.fromAddress(meAsInt))
-              .meAsInt(),
-          meAsInt);
+      final fromCast = Castaway.castFromPointer(
+          lib, Pointer<ObjCObject>.fromAddress(meAsInt));
+      expect(fromCast, testInstance);
+    });
+
+    test('equality equals', () {
+      final meAsInt = testInstance.meAsInt();
+      final fromCast = Castaway.castFromPointer(
+          lib, Pointer<ObjCObject>.fromAddress(meAsInt));
+      expect(fromCast, testInstance);
+    });
+
+    test('equality not equals', () {
+      final meAsInt = testInstance.meAsInt();
+      final fromCast = Castaway.castFromPointer(
+          lib, Pointer<ObjCObject>.fromAddress(meAsInt));
+      expect(fromCast, isNot(equals(NSObject.new1(lib))));
     });
   });
 }
diff --git a/pkgs/ffigen/test/native_objc_test/method_bindings.dart b/pkgs/ffigen/test/native_objc_test/method_bindings.dart
index 27920b0..c695b60 100644
--- a/pkgs/ffigen/test/native_objc_test/method_bindings.dart
+++ b/pkgs/ffigen/test/native_objc_test/method_bindings.dart
@@ -696,7 +696,16 @@
 class _ObjCWrapper {
   final ffi.Pointer<ObjCObject> _id;
   final MethodTestObjCLibrary _lib;
+
   _ObjCWrapper._(this._id, this._lib);
+
+  @override
+  bool operator ==(Object other) {
+    return other is _ObjCWrapper && _id == other._id;
+  }
+
+  @override
+  int get hashCode => _id.hashCode;
 }
 
 class MethodInterface extends NSObject {
diff --git a/pkgs/ffigen/test/native_objc_test/native_objc_test_bindings.dart b/pkgs/ffigen/test/native_objc_test/native_objc_test_bindings.dart
index cf75b1c..53b8516 100644
--- a/pkgs/ffigen/test/native_objc_test/native_objc_test_bindings.dart
+++ b/pkgs/ffigen/test/native_objc_test/native_objc_test_bindings.dart
@@ -1146,7 +1146,16 @@
 class _ObjCWrapper {
   final ffi.Pointer<ObjCObject> _id;
   final NativeObjCLibrary _lib;
+
   _ObjCWrapper._(this._id, this._lib);
+
+  @override
+  bool operator ==(Object other) {
+    return other is _ObjCWrapper && _id == other._id;
+  }
+
+  @override
+  int get hashCode => _id.hashCode;
 }
 
 class Foo extends NSObject {
diff --git a/pkgs/ffigen/test/native_objc_test/nullable_bindings.dart b/pkgs/ffigen/test/native_objc_test/nullable_bindings.dart
index 6f6b1b8..90533ce 100644
--- a/pkgs/ffigen/test/native_objc_test/nullable_bindings.dart
+++ b/pkgs/ffigen/test/native_objc_test/nullable_bindings.dart
@@ -510,6 +510,32 @@
       _registerName1("isNullWithNullableNSObjectArg:");
   late final ffi.Pointer<ObjCSel> _sel_isNullWithNotNullableNSObjectPtrArg_1 =
       _registerName1("isNullWithNotNullableNSObjectPtrArg:");
+  late final ffi.Pointer<ObjCSel> _sel_returnNil_1 =
+      _registerName1("returnNil:");
+  ffi.Pointer<ObjCObject> _objc_msgSend_15(
+    ffi.Pointer<ObjCObject> obj,
+    ffi.Pointer<ObjCSel> sel,
+    bool r,
+  ) {
+    return __objc_msgSend_15(
+      obj,
+      sel,
+      r ? 1 : 0,
+    );
+  }
+
+  late final __objc_msgSend_15Ptr = _lookup<
+      ffi.NativeFunction<
+          ffi.Pointer<ObjCObject> Function(ffi.Pointer<ObjCObject>,
+              ffi.Pointer<ObjCSel>, ffi.Uint8)>>('objc_msgSend');
+  late final __objc_msgSend_15 = __objc_msgSend_15Ptr.asFunction<
+      ffi.Pointer<ObjCObject> Function(
+          ffi.Pointer<ObjCObject>, ffi.Pointer<ObjCSel>, int)>();
+
+  late final ffi.Pointer<ObjCSel> _sel_nullableObjectProperty1 =
+      _registerName1("nullableObjectProperty");
+  late final ffi.Pointer<ObjCSel> _sel_setNullableObjectProperty_1 =
+      _registerName1("setNullableObjectProperty:");
 }
 
 abstract class NSComparisonResult {
@@ -611,7 +637,16 @@
 class _ObjCWrapper {
   final ffi.Pointer<ObjCObject> _id;
   final NullableTestObjCLibrary _lib;
+
   _ObjCWrapper._(this._id, this._lib);
+
+  @override
+  bool operator ==(Object other) {
+    return other is _ObjCWrapper && _id == other._id;
+  }
+
+  @override
+  int get hashCode => _id.hashCode;
 }
 
 class NullableInterface extends NSObject {
@@ -639,6 +674,22 @@
         _lib._sel_isNullWithNotNullableNSObjectPtrArg_1, x?._id ?? ffi.nullptr);
   }
 
+  static NSObject returnNil(NullableTestObjCLibrary _lib, bool r) {
+    final _ret = _lib._objc_msgSend_15(
+        _lib._class_NullableInterface1, _lib._sel_returnNil_1, r);
+    return NSObject._(_ret, _lib);
+  }
+
+  NSObject? get nullableObjectProperty {
+    final _ret = _lib._objc_msgSend_1(_id, _lib._sel_nullableObjectProperty1);
+    return _ret.address == 0 ? null : NSObject._(_ret, _lib);
+  }
+
+  set nullableObjectProperty(NSObject? value) {
+    _lib._objc_msgSend_8(
+        _id, _lib._sel_setNullableObjectProperty_1, value?._id ?? ffi.nullptr);
+  }
+
   static NullableInterface new1(NullableTestObjCLibrary _lib) {
     final _ret =
         _lib._objc_msgSend_1(_lib._class_NullableInterface1, _lib._sel_new1);
diff --git a/pkgs/ffigen/test/native_objc_test/nullable_test.dart b/pkgs/ffigen/test/native_objc_test/nullable_test.dart
index fe15f8d..c93d56e 100644
--- a/pkgs/ffigen/test/native_objc_test/nullable_test.dart
+++ b/pkgs/ffigen/test/native_objc_test/nullable_test.dart
@@ -17,6 +17,7 @@
 
 void main() {
   late NullableTestObjCLibrary lib;
+  late NullableInterface nullableInterface;
   late NSObject obj;
   group('method calls', () {
     setUpAll(() {
@@ -24,6 +25,7 @@
       final dylib = File('test/native_objc_test/nullable_test.dylib');
       verifySetupFile(dylib);
       lib = NullableTestObjCLibrary(DynamicLibrary.open(dylib.absolute.path));
+      nullableInterface = NullableInterface.new1(lib);
       obj = NSObject.new1(lib);
     });
 
@@ -50,6 +52,26 @@
       }
     });
 
+    group('Nullable property', () {
+      test('Not null', () {
+        nullableInterface.nullableObjectProperty = obj;
+        expect(nullableInterface.nullableObjectProperty, obj);
+      });
+      test('Null', () {
+        nullableInterface.nullableObjectProperty = null;
+        expect(nullableInterface.nullableObjectProperty, null);
+      });
+    });
+
+    group('Nullable return', () {
+      test('Not null', () {
+        expect(NullableInterface.returnNil(lib, false), isA<NSObject>());
+      });
+      test('Null', () {
+        expect(NullableInterface.returnNil(lib, true), null);
+      });
+    }, skip: "TODO(#334): enable this test");
+
     group('Nullable arguments', () {
       test('Not null', () {
         expect(
diff --git a/pkgs/ffigen/test/native_objc_test/nullable_test.m b/pkgs/ffigen/test/native_objc_test/nullable_test.m
index 4733493..abf1b02 100644
--- a/pkgs/ffigen/test/native_objc_test/nullable_test.m
+++ b/pkgs/ffigen/test/native_objc_test/nullable_test.m
@@ -5,6 +5,9 @@
 
 +(BOOL) isNullWithNullableNSObjectArg:(nullable NSObject *)x;
 +(BOOL) isNullWithNotNullableNSObjectPtrArg:(NSObject *)x;
++(nullable NSObject *) returnNil:(BOOL)r;
+
+@property (nullable, retain) NSObject *nullableObjectProperty;
 
 @end
 
@@ -18,4 +21,12 @@
   return x == NULL;
 }
 
++(nullable NSObject *) returnNil:(BOOL)r {
+  if (r) {
+    return nil;
+  } else {
+    return [NSObject new];
+  }
+}
+
 @end
diff --git a/pkgs/ffigen/test/native_objc_test/property_bindings.dart b/pkgs/ffigen/test/native_objc_test/property_bindings.dart
index 0b7ecc3..90ec1be 100644
--- a/pkgs/ffigen/test/native_objc_test/property_bindings.dart
+++ b/pkgs/ffigen/test/native_objc_test/property_bindings.dart
@@ -662,7 +662,16 @@
 class _ObjCWrapper {
   final ffi.Pointer<ObjCObject> _id;
   final PropertyTestObjCLibrary _lib;
+
   _ObjCWrapper._(this._id, this._lib);
+
+  @override
+  bool operator ==(Object other) {
+    return other is _ObjCWrapper && _id == other._id;
+  }
+
+  @override
+  int get hashCode => _id.hashCode;
 }
 
 class PropertyInterface extends NSObject {
diff --git a/pkgs/ffigen/test/native_objc_test/string_bindings.dart b/pkgs/ffigen/test/native_objc_test/string_bindings.dart
index d9ea394..bfadf87 100644
--- a/pkgs/ffigen/test/native_objc_test/string_bindings.dart
+++ b/pkgs/ffigen/test/native_objc_test/string_bindings.dart
@@ -2627,7 +2627,16 @@
 class _ObjCWrapper {
   final ffi.Pointer<ObjCObject> _id;
   final StringTestObjCLibrary _lib;
+
   _ObjCWrapper._(this._id, this._lib);
+
+  @override
+  bool operator ==(Object other) {
+    return other is _ObjCWrapper && _id == other._id;
+  }
+
+  @override
+  int get hashCode => _id.hashCode;
 }
 
 class NSValue extends NSObject {
@@ -3112,9 +3121,9 @@
     return _lib._objc_msgSend_11(_id, _lib._sel_unsignedIntegerValue1);
   }
 
-  NSObject get stringValue {
+  NSObject? get stringValue {
     final _ret = _lib._objc_msgSend_1(_id, _lib._sel_stringValue1);
-    return NSObject._(_ret, _lib);
+    return _ret.address == 0 ? null : NSObject._(_ret, _lib);
   }
 
   int compare(NSObject? otherNumber) {
@@ -3627,14 +3636,14 @@
     return NSOrderedCollectionDifference._(_ret, _lib);
   }
 
-  NSObject get insertions {
+  NSObject? get insertions {
     final _ret = _lib._objc_msgSend_1(_id, _lib._sel_insertions1);
-    return NSObject._(_ret, _lib);
+    return _ret.address == 0 ? null : NSObject._(_ret, _lib);
   }
 
-  NSObject get removals {
+  NSObject? get removals {
     final _ret = _lib._objc_msgSend_1(_id, _lib._sel_removals1);
-    return NSObject._(_ret, _lib);
+    return _ret.address == 0 ? null : NSObject._(_ret, _lib);
   }
 
   bool get hasChanges {
@@ -3839,10 +3848,10 @@
         loadHandler._id);
   }
 
-  NSObject get registeredTypeIdentifiers {
+  NSObject? get registeredTypeIdentifiers {
     final _ret =
         _lib._objc_msgSend_1(_id, _lib._sel_registeredTypeIdentifiers1);
-    return NSObject._(_ret, _lib);
+    return _ret.address == 0 ? null : NSObject._(_ret, _lib);
   }
 
   bool hasItemConformingToTypeIdentifier(NSObject? typeIdentifier) {
@@ -3891,13 +3900,14 @@
     return NSProgress._(_ret, _lib);
   }
 
-  NSObject get suggestedName {
+  NSObject? get suggestedName {
     final _ret = _lib._objc_msgSend_1(_id, _lib._sel_suggestedName1);
-    return NSObject._(_ret, _lib);
+    return _ret.address == 0 ? null : NSObject._(_ret, _lib);
   }
 
-  set suggestedName(NSObject value) {
-    _lib._objc_msgSend_8(_id, _lib._sel_setSuggestedName_1, value._id);
+  set suggestedName(NSObject? value) {
+    _lib._objc_msgSend_8(
+        _id, _lib._sel_setSuggestedName_1, value?._id ?? ffi.nullptr);
   }
 
   NSItemProvider initWithObject(NSObject? object) {