Skip to content
Open
Show file tree
Hide file tree
Changes from 10 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
26 changes: 0 additions & 26 deletions pkgs/ffigen/example/libclang-example/generated_bindings.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3651,26 +3651,6 @@ class LibClang {
late final _clang_getDiagnosticCategory = _clang_getDiagnosticCategoryPtr
.asFunction<DartClang_getDiagnosticCategory>();

/// Retrieve the name of a particular diagnostic category. This
/// is now deprecated. Use clang_getDiagnosticCategoryText()
/// instead.
///
/// \param Category A diagnostic category number, as returned by
/// \c clang_getDiagnosticCategory().
///
/// \returns The name of the given diagnostic category.
CXString clang_getDiagnosticCategoryName(int Category) {
return _clang_getDiagnosticCategoryName(Category);
}

late final _clang_getDiagnosticCategoryNamePtr =
_lookup<ffi.NativeFunction<NativeClang_getDiagnosticCategoryName>>(
'clang_getDiagnosticCategoryName',
);
late final _clang_getDiagnosticCategoryName =
_clang_getDiagnosticCategoryNamePtr
.asFunction<DartClang_getDiagnosticCategoryName>();

/// Retrieve the diagnostic category text for a given diagnostic.
///
/// \returns The text of the given diagnostic category.
Expand Down Expand Up @@ -6441,9 +6421,6 @@ class _SymbolAddresses {
get clang_getDiagnostic => _library._clang_getDiagnosticPtr;
ffi.Pointer<ffi.NativeFunction<NativeClang_getDiagnosticCategory>>
get clang_getDiagnosticCategory => _library._clang_getDiagnosticCategoryPtr;
ffi.Pointer<ffi.NativeFunction<NativeClang_getDiagnosticCategoryName>>
get clang_getDiagnosticCategoryName =>
_library._clang_getDiagnosticCategoryNamePtr;
ffi.Pointer<ffi.NativeFunction<NativeClang_getDiagnosticCategoryText>>
get clang_getDiagnosticCategoryText =>
_library._clang_getDiagnosticCategoryTextPtr;
Expand Down Expand Up @@ -10913,9 +10890,6 @@ typedef DartClang_getDiagnostic =
typedef NativeClang_getDiagnosticCategory =
ffi.UnsignedInt Function(CXDiagnostic);
typedef DartClang_getDiagnosticCategory = int Function(CXDiagnostic);
typedef NativeClang_getDiagnosticCategoryName =
CXString Function(ffi.UnsignedInt Category);
typedef DartClang_getDiagnosticCategoryName = CXString Function(int Category);
typedef NativeClang_getDiagnosticCategoryText = CXString Function(CXDiagnostic);
typedef DartClang_getDiagnosticCategoryText = CXString Function(CXDiagnostic);
typedef NativeClang_getDiagnosticFixIt =
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 @@ -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 || alwaysDeprecated) return Availability.none;

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

Expand All @@ -110,10 +120,6 @@ class ApiAvailability {
return Availability.all;
}

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

Availability? availability_;
for (final (platform, version) in [(ios, iosVer), (macos, macosVer)]) {
// If the user hasn't specified any versions for this platform, defer to
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
Original file line number Diff line number Diff line change
Expand Up @@ -3275,19 +3275,6 @@ class LibClang {
late final _clang_getDiagnosticCategory = _clang_getDiagnosticCategoryPtr
.asFunction<int Function(CXDiagnostic)>();

/// Retrieve the name of a particular diagnostic category. This is now
/// deprecated. Use clang_getDiagnosticCategoryText() instead.
CXString clang_getDiagnosticCategoryName(int Category) {
return _clang_getDiagnosticCategoryName(Category);
}

late final _clang_getDiagnosticCategoryNamePtr =
_lookup<ffi.NativeFunction<CXString Function(ffi.UnsignedInt)>>(
'clang_getDiagnosticCategoryName',
);
late final _clang_getDiagnosticCategoryName =
_clang_getDiagnosticCategoryNamePtr.asFunction<CXString Function(int)>();

/// Retrieve the diagnostic category text for a given diagnostic.
CXString clang_getDiagnosticCategoryText(CXDiagnostic arg0) {
return _clang_getDiagnosticCategoryText(arg0);
Expand Down
12 changes: 6 additions & 6 deletions pkgs/ffigen/test/native_objc_test/deprecated_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -121,8 +121,8 @@ void main() {
expect(bindings, contains('depIos3'));
expect(bindings, contains('depIos3Mac2'));
expect(bindings, contains('depIos3Mac3'));
expect(bindings, contains('alwaysDeprecated'));
expect(bindings, contains('alwaysUnavailable'));
expect(bindings, isNot(contains('alwaysDeprecated')));
expect(bindings, isNot(contains('alwaysUnavailable')));
});

test('interface properties', () {
Expand All @@ -145,8 +145,8 @@ void main() {
expect(bindings, contains('protDepIos3'));
expect(bindings, contains('protDepIos3Mac2'));
expect(bindings, contains('protDepIos3Mac3'));
expect(bindings, contains('protAlwaysDeprecated'));
expect(bindings, contains('protAlwaysUnavailable'));
expect(bindings, isNot(contains('protAlwaysDeprecated')));
expect(bindings, isNot(contains('protAlwaysUnavailable')));
});

test('protocol properties', () {
Expand All @@ -169,8 +169,8 @@ void main() {
expect(bindings, contains('catDepIos3'));
expect(bindings, contains('catDepIos3Mac2'));
expect(bindings, contains('catDepIos3Mac3'));
expect(bindings, contains('catAlwaysDeprecated'));
expect(bindings, contains('catAlwaysUnavailable'));
expect(bindings, isNot(contains('catAlwaysDeprecated')));
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
Loading
Loading