diff --git a/pkgs/ffigen/CHANGELOG.md b/pkgs/ffigen/CHANGELOG.md index c9b6f4e3cf..287ee853f9 100644 --- a/pkgs/ffigen/CHANGELOG.md +++ b/pkgs/ffigen/CHANGELOG.md @@ -28,7 +28,12 @@ Xcode installations. [#3134](https://github.com/dart-lang/native/issues/3134) - Add allocate constructor for native C structs: `$allocate(Allocator $allocator, {required ...})` - +- Fix a [bug](https://github.com/dart-lang/native/issues/2665) where ObjC + classes with `SWIFT_UNAVAILABLE` annotated `init`/`new` methods were + generating runtime-crashing no-arg constructors. +- Fix a bug where methods marked with `__attribute__((unavailable))` or + `__attribute__((deprecated))` were incorrectly included in bindings when no platform version info was configured. + ## 20.1.1 - Update tests and examples now that package:objective_c is using native assets. diff --git a/pkgs/ffigen/lib/src/code_generator/objc_methods.dart b/pkgs/ffigen/lib/src/code_generator/objc_methods.dart index 7629b53e4c..b440aa5c1e 100644 --- a/pkgs/ffigen/lib/src/code_generator/objc_methods.dart +++ b/pkgs/ffigen/lib/src/code_generator/objc_methods.dart @@ -50,6 +50,8 @@ mixin ObjCMethods { } bool _shouldReplaceMethod(ObjCMethod oldMethod, ObjCMethod newMethod) { + if (oldMethod.unavailable || newMethod.unavailable) return false; + // Typically we ignore duplicate methods. However, property setters and // getters are duplicated in the AST. One copy is marked with // ObjCMethodKind.propertyGetter/Setter. The other copy is missing @@ -309,7 +311,7 @@ class ObjCMethod extends AstNode with HasLocalScope { kind == ObjCMethodKind.propertySetter; bool get isRequired => !isOptional; bool get isInstanceMethod => !isClassMethod; - + bool get unavailable => apiAvailability.availability == Availability.none; ObjCMsgSendFunc fillMsgSend() { return msgSend ??= context.objCBuiltInFunctions.getMsgSendFunc( returnType, diff --git a/pkgs/ffigen/lib/src/header_parser/sub_parsers/api_availability.dart b/pkgs/ffigen/lib/src/header_parser/sub_parsers/api_availability.dart index a5cdde0d78..8be1685b04 100644 --- a/pkgs/ffigen/lib/src/header_parser/sub_parsers/api_availability.dart +++ b/pkgs/ffigen/lib/src/header_parser/sub_parsers/api_availability.dart @@ -35,6 +35,7 @@ class ApiAvailability { static ApiAvailability fromCursor( clang_types.CXCursor cursor, Context context, + // { bool treatSwiftUnavailableAsUnavailable = false,} ) { final platformsLength = clang.clang_getCursorPlatformAvailability( cursor, @@ -64,6 +65,7 @@ class ApiAvailability { PlatformAvailability? ios; PlatformAvailability? macos; + var swiftIsUnavailable = false; for (var i = 0; i < platformsLength; ++i) { final platform = platforms[i]; @@ -80,12 +82,17 @@ class ApiAvailability { case 'macos': macos = platformAvailability..name = 'macOS'; break; + case 'swift': + if (platformAvailability.unavailable) { + swiftIsUnavailable = true; + } + break; } } final api = ApiAvailability( alwaysDeprecated: alwaysDeprecated.value != 0, - alwaysUnavailable: alwaysUnavailable.value != 0, + alwaysUnavailable: alwaysUnavailable.value != 0 || swiftIsUnavailable, ios: ios, macos: macos, externalVersions: context.config.objectiveC?.externalVersions, @@ -102,6 +109,8 @@ class ApiAvailability { } Availability _getAvailability(ExternalVersions? externalVersions) { + if (alwaysUnavailable) return Availability.none; + final macosVer = _normalizeVersions(externalVersions?.macos); final iosVer = _normalizeVersions(externalVersions?.ios); @@ -110,7 +119,7 @@ class ApiAvailability { return Availability.all; } - if (alwaysDeprecated || alwaysUnavailable) { + if (alwaysDeprecated) { return Availability.none; } 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 cd3de4bc07..db132ee8de 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 @@ -152,13 +152,11 @@ void _parseSuperType( final fieldName = cursor.spelling(); final fieldType = cursor.type().toCodeGenType(context); - final apiAvailability = ApiAvailability.fromCursor(cursor, context); - if (apiAvailability.availability == Availability.none) { - context.logger.info( - 'Omitting deprecated property ${decl.originalName}.$fieldName', - ); - return (null, null); - } + final apiAvailability = ApiAvailability.fromCursor( + cursor, + context, + // treatSwiftUnavailableAsUnavailable: !cursor.isInSystemHeader(), + ); if (fieldType.isIncompleteCompound) { context.logger.warning( @@ -260,13 +258,11 @@ ObjCMethod? parseObjCMethod( return null; } - final apiAvailability = ApiAvailability.fromCursor(cursor, context); - if (apiAvailability.availability == Availability.none) { - logger.info( - 'Omitting deprecated method ${itfDecl.originalName}.$methodName', - ); - return null; - } + final apiAvailability = ApiAvailability.fromCursor( + cursor, + context, + // treatSwiftUnavailableAsUnavailable: !cursor.isInSystemHeader(), + ); logger.fine( ' > ${isClassMethod ? 'Class' : 'Instance'} method: ' diff --git a/pkgs/ffigen/lib/src/visitor/apply_config_filters.dart b/pkgs/ffigen/lib/src/visitor/apply_config_filters.dart index 42ab89288c..38b87a8a92 100644 --- a/pkgs/ffigen/lib/src/visitor/apply_config_filters.dart +++ b/pkgs/ffigen/lib/src/visitor/apply_config_filters.dart @@ -46,7 +46,8 @@ class ApplyConfigFiltersVisitation extends Visitation { if (objcInterfaces == null) return; node.filterMethods( - (m) => objcInterfaces.includeMember(node, m.originalName), + (m) => + !m.unavailable && objcInterfaces.includeMember(node, m.originalName), ); _visitImpl(node, objcInterfaces); @@ -63,6 +64,7 @@ class ApplyConfigFiltersVisitation extends Visitation { final objcCategories = config.objectiveC?.categories; if (objcCategories == null) return; node.filterMethods((m) { + if (m.unavailable) return false; if (node.shouldCopyMethodToInterface(m)) return false; return objcCategories.includeMember(node, m.originalName); }); @@ -80,6 +82,7 @@ class ApplyConfigFiltersVisitation extends Visitation { // methods on protocols if there's a use case. For now filter them. We // filter here instead of during parsing so that these methods are still // copied to any interfaces that implement the protocol. + if (m.unavailable) return false; if (m.isClassMethod) return false; return objcProtocols.includeMember(node, m.originalName); diff --git a/pkgs/ffigen/lib/src/visitor/copy_methods_from_super_type.dart b/pkgs/ffigen/lib/src/visitor/copy_methods_from_super_type.dart index f583989e6c..f8c213eee4 100644 --- a/pkgs/ffigen/lib/src/visitor/copy_methods_from_super_type.dart +++ b/pkgs/ffigen/lib/src/visitor/copy_methods_from_super_type.dart @@ -44,13 +44,12 @@ class CopyMethodsFromSuperTypesVisitation extends Visitation { void visitObjCInterface(ObjCInterface node) { node.visitChildren(visitor, typeGraphOnly: true); - final isNSObject = ObjCBuiltInFunctions.isNSObject(node.originalName); - // We need to copy certain methods from the super type: // - Class methods, because Dart classes don't inherit static methods. // - 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(node.originalName); final superType = node.superType; if (superType != null) { for (final m in superType.methods) { @@ -64,7 +63,6 @@ class CopyMethodsFromSuperTypesVisitation extends Visitation { } } } - // Copy all methods from all the interface's protocols. _copyMethodFromProtocols(node, node.protocols, node.addMethod); diff --git a/pkgs/ffigen/test/native_objc_test/deprecated_test.dart b/pkgs/ffigen/test/native_objc_test/deprecated_test.dart index 6b12b4a9a6..2c1be6596a 100644 --- a/pkgs/ffigen/test/native_objc_test/deprecated_test.dart +++ b/pkgs/ffigen/test/native_objc_test/deprecated_test.dart @@ -122,7 +122,7 @@ void main() { expect(bindings, contains('depIos3Mac2')); expect(bindings, contains('depIos3Mac3')); expect(bindings, contains('alwaysDeprecated')); - expect(bindings, contains('alwaysUnavailable')); + expect(bindings, isNot(contains('alwaysUnavailable'))); }); test('interface properties', () { @@ -146,7 +146,7 @@ void main() { expect(bindings, contains('protDepIos3Mac2')); expect(bindings, contains('protDepIos3Mac3')); expect(bindings, contains('protAlwaysDeprecated')); - expect(bindings, contains('protAlwaysUnavailable')); + expect(bindings, isNot(contains('protAlwaysUnavailable'))); }); test('protocol properties', () { @@ -170,7 +170,7 @@ void main() { expect(bindings, contains('catDepIos3Mac2')); expect(bindings, contains('catDepIos3Mac3')); expect(bindings, contains('catAlwaysDeprecated')); - expect(bindings, contains('catAlwaysUnavailable')); + expect(bindings, isNot(contains('catAlwaysUnavailable'))); }); test('category properties', () { diff --git a/pkgs/ffigen/test/native_objc_test/swift_unavailable_test.dart b/pkgs/ffigen/test/native_objc_test/swift_unavailable_test.dart new file mode 100644 index 0000000000..559bda4e24 --- /dev/null +++ b/pkgs/ffigen/test/native_objc_test/swift_unavailable_test.dart @@ -0,0 +1,83 @@ +// Copyright (c) 2025, 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:io'; + +import 'package:ffigen/ffigen.dart'; +import 'package:path/path.dart' as path; +import 'package:test/test.dart'; + +import '../test_utils.dart'; + +void main() { + group('swift_unavailable', () { + late final String bindings; + + setUpAll(() { + FfiGenerator( + output: Output( + dartFile: Uri.file( + path.join( + packagePathForTests, + 'test', + 'native_objc_test', + 'swift_unavailable_bindings.dart', + ), + ), + format: false, + style: const DynamicLibraryBindings( + wrapperName: 'SwiftUnavailableTestLibrary', + wrapperDocComment: 'Tests SWIFT_UNAVAILABLE annotation', + ), + ), + headers: Headers( + entryPoints: [ + Uri.file( + path.join( + packagePathForTests, + 'test', + 'native_objc_test', + 'swift_unavailable_test.m', + ), + ), + ], + ), + objectiveC: ObjectiveC( + interfaces: Interfaces( + include: (decl) => {'Animal'}.contains(decl.originalName), + ), + ), + ).generate(logger: createTestLogger()); + + bindings = File( + path.join( + packagePathForTests, + 'test', + 'native_objc_test', + 'swift_unavailable_bindings.dart', + ), + ).readAsStringSync(); + }); + + test('initWithName is generated (designated initializer)', () { + expect(bindings, contains('initWithName')); + }); + + test('init is NOT generated (SWIFT_UNAVAILABLE)', () { + expect(RegExp(r"'init'\b").hasMatch(bindings), isFalse); + }); + + test('new is NOT generated (SWIFT_UNAVAILABLE_MSG)', () { + expect(bindings, isNot(contains("'new'"))); + }); + + test('no-arg Animal() constructor is NOT generated', () { + expect(bindings, isNot(contains('Animal()'))); + }); + }); +} diff --git a/pkgs/ffigen/test/native_objc_test/swift_unavailable_test.m b/pkgs/ffigen/test/native_objc_test/swift_unavailable_test.m new file mode 100644 index 0000000000..54284bec0b --- /dev/null +++ b/pkgs/ffigen/test/native_objc_test/swift_unavailable_test.m @@ -0,0 +1,20 @@ +// Copyright (c) 2025, 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 + +#define SWIFT_UNAVAILABLE \ + __attribute__((availability(swift, unavailable))) +#define SWIFT_UNAVAILABLE_MSG(msg) \ + __attribute__((availability(swift, unavailable, message = msg))) +#define OBJC_DESIGNATED_INITIALIZER \ + __attribute__((objc_designated_initializer)) + +@interface Animal : NSObject +@property(nonatomic, copy) NSString* _Nonnull name; +- (nonnull instancetype)initWithName:(NSString* _Nonnull)name + OBJC_DESIGNATED_INITIALIZER; +- (nonnull instancetype)init SWIFT_UNAVAILABLE; ++ (nonnull instancetype)new SWIFT_UNAVAILABLE_MSG("-init is unavailable"); +@end diff --git a/pkgs/ffigen/test/unit_tests/api_availability_test.dart b/pkgs/ffigen/test/unit_tests/api_availability_test.dart index 79a383228e..4ead77fb62 100644 --- a/pkgs/ffigen/test/unit_tests/api_availability_test.dart +++ b/pkgs/ffigen/test/unit_tests/api_availability_test.dart @@ -193,7 +193,7 @@ void main() { alwaysUnavailable: true, externalVersions: const ExternalVersions(), ).availability, - Availability.all, + Availability.none, ); expect( ApiAvailability( diff --git a/pkgs/objective_c/CHANGELOG.md b/pkgs/objective_c/CHANGELOG.md index 48d7f17353..07f364babd 100644 --- a/pkgs/objective_c/CHANGELOG.md +++ b/pkgs/objective_c/CHANGELOG.md @@ -1,5 +1,6 @@ ## 9.4.0 - Fix (https://github.com/dart-lang/native/issues/2877) such that all occurances of ObjCObject `isA` now accepts a nullable `ObjCObject?` and returns `false` when input is`null` +- Removed `NSOrderedCollectionChange.init()`. This is not a breaking change because this method never would have worked, as it doesn't exist in Objective-C. ## 9.3.0 - `autoReleasePool` now returns the value produced by its callback. 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 42598bb0a2..198d1126de 100644 --- a/pkgs/objective_c/lib/src/objective_c_bindings_generated.dart +++ b/pkgs/objective_c/lib/src/objective_c_bindings_generated.dart @@ -15947,24 +15947,6 @@ extension NSOrderedCollectionChange$Methods on NSOrderedCollectionChange { return _objc_msgSend_xw2lbc(object$.ref.pointer, _sel_index); } - /// init - NSOrderedCollectionChange init() { - objc.checkOsVersionInternal( - 'NSOrderedCollectionChange.init', - iOS: (false, (2, 0, 0)), - macOS: (false, (10, 0, 0)), - ); - final $ret = _objc_msgSend_151sglz( - object$.ref.retainAndReturnPointer(), - _sel_init, - ); - return NSOrderedCollectionChange.fromPointer( - $ret, - retain: false, - release: true, - ); - } - /// initWithObject:type:index: /// /// iOS: introduced 13.0.0