Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion pkgs/ffigen/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
4 changes: 3 additions & 1 deletion pkgs/ffigen/lib/src/code_generator/objc_methods.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ class ApiAvailability {
static ApiAvailability fromCursor(
clang_types.CXCursor cursor,
Context context,
// { bool treatSwiftUnavailableAsUnavailable = false,}
) {
final platformsLength = clang.clang_getCursorPlatformAvailability(
cursor,
Expand Down Expand Up @@ -64,6 +65,7 @@ class ApiAvailability {

PlatformAvailability? ios;
PlatformAvailability? macos;
var swiftIsUnavailable = false;

for (var i = 0; i < platformsLength; ++i) {
final platform = platforms[i];
Expand All @@ -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,
Expand All @@ -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);

Expand All @@ -110,7 +119,7 @@ class ApiAvailability {
return Availability.all;
}

if (alwaysDeprecated || alwaysUnavailable) {
if (alwaysDeprecated) {
return Availability.none;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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: '
Expand Down
5 changes: 4 additions & 1 deletion pkgs/ffigen/lib/src/visitor/apply_config_filters.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -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);
});
Expand All @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -64,7 +63,6 @@ class CopyMethodsFromSuperTypesVisitation extends Visitation {
}
}
}

// Copy all methods from all the interface's protocols.
_copyMethodFromProtocols(node, node.protocols, node.addMethod);

Expand Down
6 changes: 3 additions & 3 deletions pkgs/ffigen/test/native_objc_test/deprecated_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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', () {
Expand All @@ -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', () {
Expand All @@ -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', () {
Expand Down
83 changes: 83 additions & 0 deletions pkgs/ffigen/test/native_objc_test/swift_unavailable_test.dart
Original file line number Diff line number Diff line change
@@ -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()')));
});
});
}
20 changes: 20 additions & 0 deletions pkgs/ffigen/test/native_objc_test/swift_unavailable_test.m
Original file line number Diff line number Diff line change
@@ -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 <Foundation/Foundation.h>

#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
2 changes: 1 addition & 1 deletion pkgs/ffigen/test/unit_tests/api_availability_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ void main() {
alwaysUnavailable: true,
externalVersions: const ExternalVersions(),
).availability,
Availability.all,
Availability.none,
);
expect(
ApiAvailability(
Expand Down
1 change: 1 addition & 0 deletions pkgs/objective_c/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
18 changes: 0 additions & 18 deletions pkgs/objective_c/lib/src/objective_c_bindings_generated.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading