Skip to content

[ffigen] ObjC id<Foo> -> Dart Foo #1959

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 14 commits into from
Feb 3, 2025
Merged
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
10 changes: 10 additions & 0 deletions pkgs/ffigen/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,16 @@
- Use package:objective_c 5.0.0
- Support transitive categories of built-in types:
https://github.com/dart-lang/native/issues/1820
- __Breaking change__: Maintain protocol conformance when translating from ObjC
to Dart. For example, ObjC's `id<FooProtocol>` is now translated to Dart's
`FooProtocol`. Generally this shouldn't be a breaking change for code that is
using protocols correctly, with a few caveats:
- For more advanced use cases that use `ObjCProtocolBuilder` directly, after
calling `build()` you will need to cast the generated object to the target
protocol: `FooProtocol.castFrom(protocolBuilder.build())`.
- Due to limitations in the Dart type system, only the first protocol of an
`id` is used: `id<FooProtocol, BarProtocol>` becomes `FooProtocol`. The
`FooProtocol.castFrom` method can help work around issues this may cause.
- Fix the handling of global arrays to remove the extra pointer reference.

## 16.1.0
Expand Down
2 changes: 2 additions & 0 deletions pkgs/ffigen/lib/src/code_generator/imports.dart
Original file line number Diff line number Diff line change
Expand Up @@ -167,3 +167,5 @@ final objCSelType = ImportedType(
objcPkgImport, 'ObjCSelector', 'ObjCSelector', 'struct objc_selector');
final objCBlockType =
ImportedType(objcPkgImport, 'ObjCBlockImpl', 'ObjCBlockImpl', 'id');
final objCProtocolType =
ImportedType(objcPkgImport, 'ObjCProtocol', 'ObjCProtocol', 'void');
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,8 @@ class ObjCBuiltInFunctions {
'NSCoding': 'NSCoding',
'NSCopying': 'NSCopying',
'NSFastEnumeration': 'NSFastEnumeration',
'NSItemProviderReading': 'NSItemProviderReading',
'NSItemProviderWriting': 'NSItemProviderWriting',
'NSMutableCopying': 'NSMutableCopying',
'NSObject': 'NSObjectProtocol',
'NSSecureCoding': 'NSSecureCoding',
Expand Down
8 changes: 3 additions & 5 deletions pkgs/ffigen/lib/src/code_generator/objc_interface.dart
Original file line number Diff line number Diff line change
Expand Up @@ -73,17 +73,15 @@ class ObjCInterface extends BindingType with ObjCMethods {

final rawObjType = PointerType(objCObjectType).getCType(w);
final wrapObjType = ObjCBuiltInFunctions.objectBase.gen(w);
final superTypeIsInPkgObjc = superType == null;
final protoImpl = protocols.isEmpty
? ''
: 'implements ${protocols.map((p) => p.getDartType(w)).join(', ')} ';

final superCtor = superType == null ? 'super' : 'super.castFromPointer';
s.write('''
class $name extends ${superType?.getDartType(w) ?? wrapObjType} $protoImpl{
$name._($rawObjType pointer,
{bool retain = false, bool release = false}) :
${superTypeIsInPkgObjc ? 'super' : 'super.castFromPointer'}
(pointer, retain: retain, release: release);
$name._($rawObjType pointer, {bool retain = false, bool release = false}) :
$superCtor(pointer, retain: retain, release: release);

/// Constructs a [$name] that points to the same underlying object as [other].
$name.castFrom($wrapObjType other) :
Expand Down
66 changes: 51 additions & 15 deletions pkgs/ffigen/lib/src/code_generator/objc_protocol.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ class ObjCProtocol extends BindingType with ObjCMethods {
final superProtocols = <ObjCProtocol>[];
final String lookupName;
final ObjCInternalGlobal _protocolPointer;
late final ObjCInternalGlobal _conformsTo;
late final ObjCMsgSendFunc _conformsToMsgSend;

// Filled by ListBindingsVisitation.
bool generateAsStub = false;
Expand All @@ -33,9 +35,18 @@ class ObjCProtocol extends BindingType with ObjCMethods {
(Writer w) =>
'${ObjCBuiltInFunctions.getProtocol.gen(w)}("$lookupName")'),
super(
name: name ??
builtInFunctions.getBuiltInProtocolName(originalName) ??
originalName);
name: builtInFunctions.getBuiltInProtocolName(originalName) ??
name ??
originalName) {
_conformsTo = builtInFunctions.getSelObject('conformsToProtocol:');
_conformsToMsgSend = builtInFunctions.getMsgSendFunc(BooleanType(), [
Parameter(
name: 'protocol',
type: PointerType(objCProtocolType),
objCConsumed: false,
)
]);
}

@override
bool get isObjCImport =>
Expand All @@ -52,6 +63,7 @@ class ObjCProtocol extends BindingType with ObjCMethods {
ObjCBuiltInFunctions.protocolListenableMethod.gen(w);
final protocolBuilder = ObjCBuiltInFunctions.protocolBuilder.gen(w);
final objectBase = ObjCBuiltInFunctions.objectBase.gen(w);
final rawObjType = PointerType(objCObjectType).getCType(w);
final getSignature = ObjCBuiltInFunctions.getProtocolMethodSignature.gen(w);

final s = StringBuffer();
Expand All @@ -65,10 +77,23 @@ class ObjCProtocol extends BindingType with ObjCMethods {
}
s.write(makeDartDoc(dartDoc ?? originalName));

final protoImpl = superProtocols.isEmpty
? protocolBase
: superProtocols.map((p) => p.getDartType(w)).join(', ');
s.write('abstract interface class $name implements $protoImpl {');
final sp = superProtocols.map((p) => p.getDartType(w));
final impls = superProtocols.isEmpty ? '' : 'implements ${sp.join(', ')}';
s.write('''
interface class $name extends $protocolBase $impls{
$name._($rawObjType pointer, {bool retain = false, bool release = false}) :
super(pointer, retain: retain, release: release);

/// Constructs a [$name] that points to the same underlying object as [other].
$name.castFrom($objectBase other) :
this._(other.ref.pointer, retain: true, release: true);

/// Constructs a [$name] that wraps the given raw object pointer.
$name.castFromPointer($rawObjType other,
{bool retain = false, bool release = false}) :
this._(other, retain: retain, release: release);

''');

if (!generateAsStub) {
final buildArgs = <String>[];
Expand Down Expand Up @@ -150,10 +175,10 @@ class ObjCProtocol extends BindingType with ObjCMethods {
final builders = '''
/// Builds an object that implements the $originalName protocol. To implement
/// multiple protocols, use [addToBuilder] or [$protocolBuilder] directly.
static $objectBase implement($args) {
static $name implement($args) {
final builder = $protocolBuilder();
$buildImplementations
return builder.build();
return $name.castFrom(builder.build());
}

/// Adds the implementation of the $originalName protocol to an existing
Expand All @@ -169,10 +194,10 @@ class ObjCProtocol extends BindingType with ObjCMethods {
/// Builds an object that implements the $originalName protocol. To implement
/// multiple protocols, use [addToBuilder] or [$protocolBuilder] directly. All
/// methods that can be implemented as listeners will be.
static $objectBase implementAsListener($args) {
static $name implementAsListener($args) {
final builder = $protocolBuilder();
$buildListenerImplementations
return builder.build();
return $name.castFrom(builder.build());
}

/// Adds the implementation of the $originalName protocol to an existing
Expand All @@ -185,10 +210,10 @@ class ObjCProtocol extends BindingType with ObjCMethods {
/// Builds an object that implements the $originalName protocol. To implement
/// multiple protocols, use [addToBuilder] or [$protocolBuilder] directly. All
/// methods that can be implemented as blocking listeners will be.
static $objectBase implementAsBlocking($args) {
static $name implementAsBlocking($args) {
final builder = $protocolBuilder();
$buildBlockingImplementations
return builder.build();
return $name.castFrom(builder.build());
}

/// Adds the implementation of the $originalName protocol to an existing
Expand All @@ -201,6 +226,16 @@ class ObjCProtocol extends BindingType with ObjCMethods {
}

s.write('''
/// Returns whether [obj] is an instance of [$name].
static bool conformsTo($objectBase obj) {
return ${_conformsToMsgSend.invoke(
w,
'obj.ref.pointer',
_conformsTo.name,
[_protocolPointer.name],
)};
}

$builders
$listenerBuilders
$methodFields
Expand Down Expand Up @@ -264,8 +299,7 @@ Protocol* _${wrapName}_$originalName(void) { return @protocol($originalName); }
required bool objCRetain,
String? objCEnclosingClass,
}) =>
ObjCInterface.generateConstructor(
'${w.objcPkgPrefix}.ObjCObjectBase', value, objCRetain);
ObjCInterface.generateConstructor(getDartType(w), value, objCRetain);

@override
String? generateRetain(String value) =>
Expand Down Expand Up @@ -305,6 +339,8 @@ Protocol* _${wrapName}_$originalName(void) { return @protocol($originalName); }
super.visitChildren(visitor);
visitor.visit(_protocolPointer);
visitor.visitAll(superProtocols);
visitor.visit(_conformsTo);
visitor.visit(_conformsToMsgSend);
visitMethods(visitor);
}
}
48 changes: 48 additions & 0 deletions pkgs/ffigen/lib/src/code_generator/pointer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -185,3 +185,51 @@ class ObjCBlockPointer extends ObjCObjectPointer {
return other is ObjCBlockPointer || other is ObjCBlock;
}
}

/// A pointer to an Objective C object with protocols.
class ObjCObjectPointerWithProtocols extends ObjCObjectPointer {
List<ObjCProtocol> protocols;

ObjCObjectPointerWithProtocols(this.protocols)
: assert(protocols.isNotEmpty),
super._();

@override
String getDartType(Writer w) => protocols.first.getDartType(w);

@override
bool isSupertypeOf(Type other) {
other = other.typealiasType;
if (other is ObjCObjectPointerWithProtocols) {
// The "correct" logic would be to return true if each of our protocols
// was a supertype of one of the other's protocols. But this method is
// designed to reflect the subtyping rules of the *Dart bindings*, not the
// ObjC types. So since the codegen just uses the first protocol, we do
// the same here.
return protocols.first.isSupertypeOf(other.protocols.first);
}
return false;
}

@override
String toString() => 'id<${protocols.join(', ')}>';

@override
String cacheKey() => 'id<${protocols.map((p) => p.cacheKey()).join(', ')}>';

@override
String convertFfiDartTypeToDartType(
Writer w,
String value, {
required bool objCRetain,
String? objCEnclosingClass,
}) =>
protocols.first.convertFfiDartTypeToDartType(w, value,
objCRetain: objCRetain, objCEnclosingClass: objCEnclosingClass);

@override
void visitChildren(Visitor visitor) {
super.visitChildren(visitor);
visitor.visitAll(protocols);
}
}
Loading
Loading