Skip to content
Open
Show file tree
Hide file tree
Changes from 6 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
3 changes: 3 additions & 0 deletions 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,6 +311,7 @@ class ObjCMethod extends AstNode with HasLocalScope {
kind == ObjCMethodKind.propertySetter;
bool get isRequired => !isOptional;
bool get isInstanceMethod => !isClassMethod;
bool get unavailable => apiAvailability.alwaysUnavailable;

ObjCMsgSendFunc fillMsgSend() {
return msgSend ??= context.objCBuiltInFunctions.getMsgSendFunc(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,9 @@ class ApiAvailability {

static ApiAvailability fromCursor(
clang_types.CXCursor cursor,
Context context,
) {
Context context, {
bool treatSwiftUnavailableAsUnavailable = false,
}) {
final platformsLength = clang.clang_getCursorPlatformAvailability(
cursor,
nullptr,
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,18 @@ class ApiAvailability {
case 'macos':
macos = platformAvailability..name = 'macOS';
break;
case 'swift':
if (platformAvailability.unavailable &&
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this still necessary? I thought your fixes to the deprecated test meant you didn't need this extra treatSwiftUnavailableAsUnavailable logic anymore?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@liamappelbe The deprecated fix means the guard isn't needed for filterMethods. But it still needed for _shouldReplaceMethod. Without it _shouldReplaceMethod block NSObject's methods from being copied to subclasses, So when i tried to remove it, alot of tests fail

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah ok, then the correct fix is to update _shouldReplaceMethod with this special case. Is this issue specific to NSObject, or does it apply to all isObjCImports? Could you upload a version of the PR that shows those errors, so I can see them on the bots?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@liamappelbe Yeah it's apply to all isObjCImports, Do you want me to push a version without the guard so you can see the failures on CI?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Hassnaa9 yes please. I'm curious about the error.

Copy link
Contributor Author

@Hassnaa9 Hassnaa9 Mar 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@liamappelbe Ok, i pushed it

treatSwiftUnavailableAsUnavailable) {
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 +110,8 @@ class ApiAvailability {
}

Availability _getAvailability(ExternalVersions? externalVersions) {
if (alwaysUnavailable) return Availability.none;

final macosVer = _normalizeVersions(externalVersions?.macos);
final iosVer = _normalizeVersions(externalVersions?.ios);

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
9 changes: 8 additions & 1 deletion pkgs/ffigen/lib/src/visitor/apply_config_filters.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import '../code_generator.dart';
import '../config_provider/config.dart' show Config, Declarations;
import '../header_parser/sub_parsers/api_availability.dart';

import 'ast.dart';

Expand Down Expand Up @@ -46,7 +47,9 @@ class ApplyConfigFiltersVisitation extends Visitation {
if (objcInterfaces == null) return;

node.filterMethods(
(m) => objcInterfaces.includeMember(node, m.originalName),
(m) =>
m.apiAvailability.availability != Availability.none &&
objcInterfaces.includeMember(node, m.originalName),
);
_visitImpl(node, objcInterfaces);

Expand All @@ -63,6 +66,8 @@ class ApplyConfigFiltersVisitation extends Visitation {
final objcCategories = config.objectiveC?.categories;
if (objcCategories == null) return;
node.filterMethods((m) {
if (m.unavailable) return false;
if (m.apiAvailability.availability == Availability.none) return false;
if (node.shouldCopyMethodToInterface(m)) return false;
return objcCategories.includeMember(node, m.originalName);
});
Expand All @@ -80,6 +85,8 @@ 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.apiAvailability.availability == Availability.none) return false;
if (m.isClassMethod) return false;

return objcProtocols.includeMember(node, m.originalName);
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
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 @@ -15911,24 +15911,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
56 changes: 0 additions & 56 deletions pkgs/swiftgen/test/integration/classes_bindings.dart
Original file line number Diff line number Diff line change
Expand Up @@ -59,32 +59,9 @@ extension type TestClassWrapper._(objc.ObjCObject object$)
final $ret = _objc_msgSend_151sglz(_class_TestClassWrapper, _sel_create);
return TestClassWrapper.fromPointer($ret, retain: true, release: true);
}

/// new
static TestClassWrapper new$() {
final $ret = _objc_msgSend_151sglz(_class_TestClassWrapper, _sel_new);
return TestClassWrapper.fromPointer($ret, retain: false, release: true);
}

/// Returns a new instance of TestClassWrapper constructed with the default `new` method.
TestClassWrapper() : this.as(new$().object$);
}

extension TestClassWrapper$Methods on TestClassWrapper {
/// init
TestClassWrapper init() {
objc.checkOsVersionInternal(
'TestClassWrapper.init',
iOS: (false, (2, 0, 0)),
macOS: (false, (10, 0, 0)),
);
final $ret = _objc_msgSend_151sglz(
object$.ref.retainAndReturnPointer(),
_sel_init,
);
return TestClassWrapper.fromPointer($ret, retain: false, release: true);
}

/// myMethod
TestOtherClassWrapper myMethod() {
final $ret = _objc_msgSend_151sglz(object$.ref.pointer, _sel_myMethod);
Expand Down Expand Up @@ -144,40 +121,9 @@ extension type TestOtherClassWrapper._(objc.ObjCObject object$)
release: true,
);
}

/// new
static TestOtherClassWrapper new$() {
final $ret = _objc_msgSend_151sglz(_class_TestOtherClassWrapper, _sel_new);
return TestOtherClassWrapper.fromPointer(
$ret,
retain: false,
release: true,
);
}

/// Returns a new instance of TestOtherClassWrapper constructed with the default `new` method.
TestOtherClassWrapper() : this.as(new$().object$);
}

extension TestOtherClassWrapper$Methods on TestOtherClassWrapper {
/// init
TestOtherClassWrapper init() {
objc.checkOsVersionInternal(
'TestOtherClassWrapper.init',
iOS: (false, (2, 0, 0)),
macOS: (false, (10, 0, 0)),
);
final $ret = _objc_msgSend_151sglz(
object$.ref.retainAndReturnPointer(),
_sel_init,
);
return TestOtherClassWrapper.fromPointer(
$ret,
retain: false,
release: true,
);
}

/// times10WithX:
int times10WithX(int x) {
return _objc_msgSend_12hwf9n(object$.ref.pointer, _sel_times10WithX_, x);
Expand Down Expand Up @@ -257,10 +203,8 @@ final _objc_msgSend_1cwp428 = objc.msgSendPointer
late final _sel_alloc = objc.registerName("alloc");
late final _sel_allocWithZone_ = objc.registerName("allocWithZone:");
late final _sel_create = objc.registerName("create");
late final _sel_init = objc.registerName("init");
late final _sel_isKindOfClass_ = objc.registerName("isKindOfClass:");
late final _sel_myMethod = objc.registerName("myMethod");
late final _sel_new = objc.registerName("new");
late final _sel_times10WithX_ = objc.registerName("times10WithX:");
typedef instancetype = ffi.Pointer<objc.ObjCObjectImpl>;
typedef Dartinstancetype = objc.ObjCObject;
Loading