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 29d07b627..01ab1de65 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 @@ -120,7 +120,7 @@ class ObjCBuiltInFunctions { generateForPackageObjectiveC ? null : builtInCompounds[name]; bool isBuiltInEnum(String name) => !generateForPackageObjectiveC && builtInEnums.contains(name); - bool isNSObject(String name) => name == 'NSObject'; + static bool isNSObject(String name) => name == 'NSObject'; // We need to load a separate instance of objc_msgSend for each signature. If // the return type is a struct, we need to use objc_msgSend_stret instead, and diff --git a/pkgs/ffigen/lib/src/code_generator/objc_interface.dart b/pkgs/ffigen/lib/src/code_generator/objc_interface.dart index ffb7ad729..386854e50 100644 --- a/pkgs/ffigen/lib/src/code_generator/objc_interface.dart +++ b/pkgs/ffigen/lib/src/code_generator/objc_interface.dart @@ -279,8 +279,11 @@ class ObjCInterface extends BindingType with ObjCMethods { // - Methods that return instancetype, because the subclass's copy of the // method needs to return the subclass, not the super class. // Note: instancetype is only allowed as a return type, not an arg type. + final isNSObject = ObjCBuiltInFunctions.isNSObject(originalName); for (final m in superType!.methods) { - if (m.isClassMethod && + if (isNSObject) { + addMethod(m); + } else if (m.isClassMethod && !_excludedNSObjectMethods.contains(m.originalName)) { addMethod(m); } else if (ObjCBuiltInFunctions.isInstanceType(m.returnType)) { @@ -290,8 +293,17 @@ class ObjCInterface extends BindingType with ObjCMethods { } void _copyMethodsFromProtocol(ObjCProtocol proto) { + final isNSObject = ObjCBuiltInFunctions.isNSObject(originalName); for (final m in proto.methods) { - if (!_excludedNSObjectMethods.contains(m.originalName)) { + if (isNSObject) { + if (m.originalName == 'description' || m.originalName == 'hash') { + // TODO(https://github.com/dart-lang/native/issues/1220): Remove this + // special case. These methods only clash because they're sometimes + // declared as getters and sometimes as normal methods. + } else { + addMethod(m); + } + } else if (!_excludedNSObjectMethods.contains(m.originalName)) { addMethod(m); } } diff --git a/pkgs/ffigen/lib/src/code_generator/objc_protocol.dart b/pkgs/ffigen/lib/src/code_generator/objc_protocol.dart index 0d88252bf..015bcc94e 100644 --- a/pkgs/ffigen/lib/src/code_generator/objc_protocol.dart +++ b/pkgs/ffigen/lib/src/code_generator/objc_protocol.dart @@ -189,7 +189,7 @@ ${makeDartDoc(dartDoc ?? originalName)}abstract final class $name { } void _copyMethodsFromSuperType(ObjCProtocol superProtocol) { - if (builtInFunctions.isNSObject(superProtocol.originalName)) { + if (ObjCBuiltInFunctions.isNSObject(superProtocol.originalName)) { // When writing a protocol that doesn't inherit from any other protocols, // it's typical to have it inherit from NSObject instead. But NSObject has // heaps of methods that users are very unlikely to want to implement, so diff --git a/pkgs/ffigen/test/large_integration_tests/large_objc_test.dart b/pkgs/ffigen/test/large_integration_tests/large_objc_test.dart index b48bda736..52409555c 100644 --- a/pkgs/ffigen/test/large_integration_tests/large_objc_test.dart +++ b/pkgs/ffigen/test/large_integration_tests/large_objc_test.dart @@ -47,8 +47,11 @@ void main() { 'attributedString', 'cachePolicy', 'candidateListTouchBarItem', + 'delegate', 'hyphenationFactor', 'image', + 'isProxy', + 'objCType', 'tag', 'title', }; diff --git a/pkgs/ffigen/test/native_objc_test/block_test.dart b/pkgs/ffigen/test/native_objc_test/block_test.dart index 8ca83af17..98593e6d6 100644 --- a/pkgs/ffigen/test/native_objc_test/block_test.dart +++ b/pkgs/ffigen/test/native_objc_test/block_test.dart @@ -688,7 +688,7 @@ void main() { final dummyObject = DummyObject.new1(); DartObjectListenerBlock? block = ObjectListenerBlock.listener((DummyObject obj) { - expect(objectRetainCount(obj.ref.pointer), 1); + expect(objectRetainCount(obj.ref.pointer), greaterThan(0)); completer.complete(); expect(dummyObject, isNotNull); }); diff --git a/pkgs/objective_c/CHANGELOG.md b/pkgs/objective_c/CHANGELOG.md index 7f16f3208..f5115082b 100644 --- a/pkgs/objective_c/CHANGELOG.md +++ b/pkgs/objective_c/CHANGELOG.md @@ -4,6 +4,7 @@ bindings if an optional method is invoked, and the instance doesn't implement the method. - Dispatch all object/block releases to the main thread. +- Add utils for converting Dart `String`s to Objective-C selectors and back. - Require Dart 3.4 or later (due to the use of `dart:ffi` `Struct.create` by `package:ffigen`). - __Breaking change__: Return structs from ObjC methods by value instead of diff --git a/pkgs/objective_c/ffigen_c.yaml b/pkgs/objective_c/ffigen_c.yaml index 6e15e3b17..a3e39a576 100644 --- a/pkgs/objective_c/ffigen_c.yaml +++ b/pkgs/objective_c/ffigen_c.yaml @@ -17,6 +17,7 @@ functions: - 'objc_.*' - 'object_getClass' - 'sel_registerName' + - 'sel_getName' - 'protocol_getMethodDescription' - 'disposeObjCBlockWithClosure' - 'isValidBlock' @@ -37,6 +38,7 @@ functions: - 'newFinalizableBool' rename: 'sel_registerName': 'registerName' + 'sel_getName': 'getName' 'objc_getClass': 'getClass' 'objc_retain': 'objectRetain' 'objc_retainBlock': 'blockRetain' diff --git a/pkgs/objective_c/lib/objective_c.dart b/pkgs/objective_c/lib/objective_c.dart index 1d23ace55..a19f76d19 100644 --- a/pkgs/objective_c/lib/objective_c.dart +++ b/pkgs/objective_c/lib/objective_c.dart @@ -82,3 +82,4 @@ export 'src/objective_c_bindings_generated.dart' NSValue, Protocol; export 'src/protocol_builder.dart'; +export 'src/selector.dart'; diff --git a/pkgs/objective_c/lib/src/c_bindings_generated.dart b/pkgs/objective_c/lib/src/c_bindings_generated.dart index 8fc04acac..22e25ae8d 100644 --- a/pkgs/objective_c/lib/src/c_bindings_generated.dart +++ b/pkgs/objective_c/lib/src/c_bindings_generated.dart @@ -102,6 +102,12 @@ external ObjCMethodDesc getMethodDescription( bool isInstanceMethod, ); +@ffi.Native Function(ffi.Pointer)>( + symbol: "sel_getName", isLeaf: true) +external ffi.Pointer getName( + ffi.Pointer sel, +); + @ffi.Native Function(ffi.Pointer)>( symbol: "object_getClass", isLeaf: true) external ffi.Pointer getObjectClass( diff --git a/pkgs/objective_c/lib/src/objective_c_bindings_generated.dart b/pkgs/objective_c/lib/src/objective_c_bindings_generated.dart index 38f3d89c0..ef9a3bd76 100644 --- a/pkgs/objective_c/lib/src/objective_c_bindings_generated.dart +++ b/pkgs/objective_c/lib/src/objective_c_bindings_generated.dart @@ -6338,8 +6338,8 @@ class NSObject extends objc.ObjCObjectBase { } /// class - static objc.ObjCObjectBase class1() { - final _ret = _objc_msgSend_1unuoxw(_class_NSObject, _sel_class); + objc.ObjCObjectBase class1() { + final _ret = _objc_msgSend_1unuoxw(this.ref.pointer, _sel_class); return objc.ObjCObjectBase(_ret, retain: true, release: true); } @@ -6350,9 +6350,9 @@ class NSObject extends objc.ObjCObjectBase { } /// conformsToProtocol: - static bool conformsToProtocol_(Protocol protocol) { + bool conformsToProtocol_(Protocol aProtocol) { return _objc_msgSend_l8lotg( - _class_NSObject, _sel_conformsToProtocol_, protocol.ref.pointer); + this.ref.pointer, _sel_conformsToProtocol_, aProtocol.ref.pointer); } /// copy @@ -6374,8 +6374,12 @@ class NSObject extends objc.ObjCObjectBase { } /// debugDescription - static NSString debugDescription() { - final _ret = _objc_msgSend_1unuoxw(_class_NSObject, _sel_debugDescription); + NSString debugDescription() { + if (!objc.respondsToSelector(ref.pointer, _sel_debugDescription)) { + throw objc.UnimplementedOptionalMethodException( + 'NSObject', 'debugDescription'); + } + final _ret = _objc_msgSend_1unuoxw(this.ref.pointer, _sel_debugDescription); return NSString.castFromPointer(_ret, retain: true, release: true); } @@ -6639,6 +6643,12 @@ class NSObject extends objc.ObjCObjectBase { _class_NSObject, _sel_resolveInstanceMethod_, sel); } + /// respondsToSelector: + bool respondsToSelector_(ffi.Pointer aSelector) { + return _objc_msgSend_8d7dvc( + this.ref.pointer, _sel_respondsToSelector_, aSelector); + } + /// retain NSObject retain() { final _ret = _objc_msgSend_1unuoxw(this.ref.pointer, _sel_retain); @@ -6668,8 +6678,8 @@ class NSObject extends objc.ObjCObjectBase { } /// superclass - static objc.ObjCObjectBase superclass() { - final _ret = _objc_msgSend_1unuoxw(_class_NSObject, _sel_superclass); + objc.ObjCObjectBase superclass() { + final _ret = _objc_msgSend_1unuoxw(this.ref.pointer, _sel_superclass); return objc.ObjCObjectBase(_ret, retain: true, release: true); } diff --git a/pkgs/objective_c/lib/src/selector.dart b/pkgs/objective_c/lib/src/selector.dart new file mode 100644 index 000000000..93c79be00 --- /dev/null +++ b/pkgs/objective_c/lib/src/selector.dart @@ -0,0 +1,25 @@ +// Copyright (c) 2024, 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:ffi'; + +import 'package:ffi/ffi.dart'; + +import 'c_bindings_generated.dart' as c; +import 'internal.dart'; + +extension StringToSelector on String { + /// Returns an Objective-C selector (aka `SEL`) for this [String]. + /// + /// This is equivalent to the Objective-C `@selector()` directive, or the + /// `NSSelectorFromString` function. + Pointer toSelector() => registerName(this); +} + +extension SelectorToString on Pointer { + /// Returns the string that this Objective-C selector represents. + /// + /// This is equivalent to the Objective-C `NSSelectorFromString` function. + String toDartString() => c.getName(this).cast().toDartString(); +} diff --git a/pkgs/objective_c/pubspec.yaml b/pkgs/objective_c/pubspec.yaml index eb0e05cb1..545e6550e 100644 --- a/pkgs/objective_c/pubspec.yaml +++ b/pkgs/objective_c/pubspec.yaml @@ -15,7 +15,7 @@ topics: environment: sdk: '>=3.4.0 <4.0.0' - flutter: '>=3.3.0' + flutter: '>=3.22.0' dependencies: ffi: ^2.1.0 diff --git a/pkgs/objective_c/src/objective_c_runtime.h b/pkgs/objective_c/src/objective_c_runtime.h index 6f0fd0e99..e4a248560 100644 --- a/pkgs/objective_c/src/objective_c_runtime.h +++ b/pkgs/objective_c/src/objective_c_runtime.h @@ -16,6 +16,7 @@ typedef struct _ObjCObject ObjCObject; typedef struct _ObjCProtocol ObjCProtocol; ObjCSelector *sel_registerName(const char *name); +const char * sel_getName(ObjCSelector* sel); ObjCObject *objc_getClass(const char *name); ObjCObject *objc_retain(ObjCObject *object); ObjCObject *objc_retainBlock(const ObjCObject *object); diff --git a/pkgs/objective_c/test/selector_test.dart b/pkgs/objective_c/test/selector_test.dart new file mode 100644 index 000000000..c7ff5d084 --- /dev/null +++ b/pkgs/objective_c/test/selector_test.dart @@ -0,0 +1,37 @@ +// Copyright (c) 2024, 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') +library; + +import 'dart:ffi'; + +import 'package:objective_c/objective_c.dart'; +import 'package:test/test.dart'; + +void main() { + group('Selector', () { + setUpAll(() { + // TODO(https://github.com/dart-lang/native/issues/1068): Remove this. + DynamicLibrary.open('test/objective_c.dylib'); + }); + + test('from String and back', () { + expect('hello'.toSelector().toDartString(), 'hello'); + expect(''.toSelector().toDartString(), ''); + expect('foo:with:args:'.toSelector().toDartString(), 'foo:with:args:'); + }); + + test('responds to selector', () { + final sel1 = 'addObserver:forKeyPath:options:context:'.toSelector(); + expect(NSObject.new1().respondsToSelector_(sel1), isTrue); + expect(NSObject.new1().respondsToSelector_('foo'.toSelector()), isFalse); + + final sel2 = 'canBeConvertedToEncoding:'.toSelector(); + expect(NSString.new1().respondsToSelector_(sel2), isTrue); + expect(NSString.new1().respondsToSelector_('bar'.toSelector()), isFalse); + }); + }); +}