blob: 66f9d1c39f158e937085aba1c4810a9da63bd70c [file] [log] [blame]
// Copyright (c) 2024, 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 '../code_generator.dart';
import '../context.dart';
import '../header_parser/sub_parsers/api_availability.dart';
import '../visitor/ast.dart';
import 'binding_string.dart';
import 'scope.dart';
import 'utils.dart';
import 'writer.dart';
class ObjCProtocol extends BindingType with ObjCMethods, HasLocalScope {
@override
final Context context;
final superProtocols = <ObjCProtocol>[];
final String lookupName;
final ObjCInternalGlobal _protocolPointer;
late final ObjCInternalGlobal _conformsTo;
late final ObjCMsgSendFunc _conformsToMsgSend;
final ApiAvailability apiAvailability;
// Filled by ListBindingsVisitation.
bool generateAsStub = false;
ObjCProtocol({
super.usr,
required String super.originalName,
String? name,
String? lookupName,
super.dartDoc,
required this.apiAvailability,
required this.context,
}) : lookupName = lookupName ?? originalName,
_protocolPointer = ObjCInternalGlobal(
'_protocol_$originalName',
() =>
'${ObjCBuiltInFunctions.getProtocol.gen(context)}("$lookupName")',
),
super(
name:
context.objCBuiltInFunctions.getBuiltInProtocolName(
originalName,
) ??
name ??
originalName,
) {
_conformsTo = context.objCBuiltInFunctions.getSelObject(
'conformsToProtocol:',
);
_conformsToMsgSend = context.objCBuiltInFunctions
.getMsgSendFunc(BooleanType(), [
Parameter(
name: 'protocol',
type: PointerType(objCProtocolType),
objCConsumed: false,
),
]);
}
@override
bool get isObjCImport =>
context.objCBuiltInFunctions.getBuiltInProtocolName(originalName) != null;
bool get unavailable => apiAvailability.availability == Availability.none;
@override
BindingString toBindingString(Writer w) {
final protocolClass = ObjCBuiltInFunctions.protocolClass.gen(context);
final protocolBase = ObjCBuiltInFunctions.protocolBase.gen(context);
final protocolMethod = ObjCBuiltInFunctions.protocolMethod.gen(context);
final protocolListenableMethod = ObjCBuiltInFunctions
.protocolListenableMethod
.gen(context);
final protocolBuilder = ObjCBuiltInFunctions.protocolBuilder.gen(context);
final objectBase = ObjCBuiltInFunctions.objectBase.gen(context);
final rawObjType = PointerType(objCObjectType).getCType(context);
final getSignature = ObjCBuiltInFunctions.getProtocolMethodSignature.gen(
context,
);
final s = StringBuffer();
s.write('\n');
if (generateAsStub) {
s.write('''
/// WARNING: $name is a stub. To generate bindings for this class, include
/// $originalName in your config's objc-protocols list.
///
''');
}
s.write(makeDartDoc(dartDoc ?? originalName));
final sp = [
protocolBase,
...superProtocols.map((p) => p.getDartType(context)),
];
s.write('''
extension type $name._($protocolBase object\$) implements ${sp.join(', ')} {
/// Constructs a [$name] that points to the same underlying object as [other].
$name.as($objectBase other) : object\$ = other;
/// Constructs a [$name] that wraps the given raw object pointer.
$name.fromPointer($rawObjType other,
{bool retain = false, bool release = false}) :
object\$ = $protocolBase(other, retain: retain, release: release);
''');
if (!generateAsStub) {
final msgSendInvoke = _conformsToMsgSend.invoke(
context,
'obj.ref.pointer',
_conformsTo.name,
[_protocolPointer.name],
);
s.write('''
/// Returns whether [obj] is an instance of [$name].
static bool conformsTo($objectBase obj) {
return $msgSendInvoke;
}
''');
}
s.write('''
}
''');
if (!generateAsStub) {
s.write('''
extension $name\$Methods on $name {
${generateInstanceMethodBindings(w, this)}
}
''');
}
if (!generateAsStub) {
final builder = '$name\$Builder';
s.write('''
interface class $builder {
''');
final buildArgs = <String>[];
final buildImplementations = StringBuffer();
final buildListenerImplementations = StringBuffer();
final buildBlockingImplementations = StringBuffer();
final methodFields = StringBuffer();
var anyListeners = false;
for (final method in methods) {
final methodName = method.protocolMethodName!.name;
final fieldName = methodName;
final argName = methodName;
final block = method.protocolBlock!;
final blockUtils = block.name;
final methodClass = block.hasListener
? protocolListenableMethod
: protocolMethod;
// The function type omits the first arg of the block, which is unused.
final func = FunctionType(
returnType: block.returnType,
parameters: [...block.params.skip(1)],
);
final funcType = func.getDartType(context, writeArgumentNames: false);
if (method.isOptional) {
buildArgs.add('$funcType? $argName');
} else {
buildArgs.add('required $funcType $argName');
}
final blockFirstArg = block.params[0].type.getDartType(context);
final argsReceived = func.parameters
.map((p) => '${p.type.getDartType(context)} ${p.name}')
.join(', ');
final argsPassed = func.parameters.map((p) => p.name).join(', ');
final wrapper =
'($blockFirstArg _, $argsReceived) => func($argsPassed)';
var listenerBuilders = '';
var maybeImplementAsListener = 'implement';
var maybeImplementAsBlocking = 'implement';
if (block.hasListener) {
listenerBuilders =
'''
($funcType func) => $blockUtils.listener($wrapper),
($funcType func) => $blockUtils.blocking($wrapper),
''';
maybeImplementAsListener = 'implementAsListener';
maybeImplementAsBlocking = 'implementAsBlocking';
anyListeners = true;
}
buildImplementations.write('''
$builder.$fieldName.implement(builder, $argName);''');
buildListenerImplementations.write('''
$builder.$fieldName.$maybeImplementAsListener(builder, $argName);''');
buildBlockingImplementations.write('''
$builder.$fieldName.$maybeImplementAsBlocking(builder, $argName);''');
methodFields.write(makeDartDoc(method.dartDoc ?? method.originalName));
methodFields.write('''static final $fieldName = $methodClass<$funcType>(
${_protocolPointer.name},
${method.selObject.name},
${_trampolineAddress(block)},
$getSignature(
${_protocolPointer.name},
${method.selObject.name},
isRequired: ${method.isRequired},
isInstanceMethod: ${method.isInstanceMethod},
),
($funcType func) => $blockUtils.fromFunction($wrapper),
$listenerBuilders
);
''');
}
buildArgs.add('bool \$keepIsolateAlive = true');
final args = '{${buildArgs.join(', ')}}';
final builders =
'''
/// Returns the [$protocolClass] object for this protocol.
static $protocolClass get \$protocol =>
$protocolClass.fromPointer(${_protocolPointer.name}.cast());
/// Builds an object that implements the $originalName protocol. To implement
/// multiple protocols, use [addToBuilder] or [$protocolBuilder] directly.
///
/// If `\$keepIsolateAlive` is true, this protocol will keep this isolate
/// alive until it is garbage collected by both Dart and ObjC.
static $name implement($args) {
final builder = $protocolBuilder(debugName: '$originalName');
$buildImplementations
builder.addProtocol(\$protocol);
return $name.as(builder.build(keepIsolateAlive: \$keepIsolateAlive));
}
/// Adds the implementation of the $originalName protocol to an existing
/// [$protocolBuilder].
///
/// Note: You cannot call this method after you have called `builder.build`.
static void addToBuilder($protocolBuilder builder, $args) {
$buildImplementations
builder.addProtocol(\$protocol);
}
''';
var listenerBuilders = '';
if (anyListeners) {
listenerBuilders =
'''
/// 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.
///
/// If `\$keepIsolateAlive` is true, this protocol will keep this isolate
/// alive until it is garbage collected by both Dart and ObjC.
static $name implementAsListener($args) {
final builder = $protocolBuilder(debugName: '$originalName');
$buildListenerImplementations
builder.addProtocol(\$protocol);
return $name.as(builder.build(keepIsolateAlive: \$keepIsolateAlive));
}
/// Adds the implementation of the $originalName protocol to an existing
/// [$protocolBuilder]. All methods that can be implemented as listeners will
/// be.
///
/// Note: You cannot call this method after you have called `builder.build`.
static void addToBuilderAsListener($protocolBuilder builder, $args) {
$buildListenerImplementations
builder.addProtocol(\$protocol);
}
/// 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.
///
/// If `\$keepIsolateAlive` is true, this protocol will keep this isolate
/// alive until it is garbage collected by both Dart and ObjC.
static $name implementAsBlocking($args) {
final builder = $protocolBuilder(debugName: '$originalName');
$buildBlockingImplementations
builder.addProtocol(\$protocol);
return $name.as(builder.build(keepIsolateAlive: \$keepIsolateAlive));
}
/// Adds the implementation of the $originalName protocol to an existing
/// [$protocolBuilder]. All methods that can be implemented as blocking
/// listeners will be.
///
/// Note: You cannot call this method after you have called `builder.build`.
static void addToBuilderAsBlocking($protocolBuilder builder, $args) {
$buildBlockingImplementations
builder.addProtocol(\$protocol);
}
''';
}
s.write('''
$builders
$listenerBuilders
$methodFields
}
''');
}
return BindingString(
type: BindingStringType.objcProtocol,
string: s.toString(),
);
}
String _trampolineAddress(ObjCBlock block) {
final func = block.protocolTrampoline!.func;
final type = NativeFunc(
func.functionType,
).getCType(context, writeArgumentNames: false);
final ffiPrefix = context.libs.prefix(ffiImport);
return '$ffiPrefix.Native.addressOf<$type>(${func.name}).cast()';
}
@override
BindingString? toObjCBindingString(Writer w) {
if (generateAsStub) return null;
final libraryId = context.objCBuiltInFunctions.libraryId;
final mainString =
'''
Protocol* _${libraryId}_$originalName(void) { return @protocol($originalName); }
''';
return BindingString(
type: BindingStringType.objcProtocol,
string: mainString,
);
}
@override
String getCType(Context context) =>
PointerType(objCObjectType).getCType(context);
@override
String getDartType(Context context) =>
isObjCImport ? '${context.libs.prefix(objcPkgImport)}.$name' : name;
@override
String getNativeType({String varName = ''}) => 'id $varName';
@override
String getObjCBlockSignatureType(Context context) => getDartType(context);
@override
bool get sameFfiDartAndCType => true;
@override
bool get sameDartAndCType => false;
@override
bool get sameDartAndFfiDartType => false;
@override
String convertDartTypeToFfiDartType(
Context context,
String value, {
required bool objCRetain,
required bool objCAutorelease,
}) => ObjCInterface.generateGetId(value, objCRetain, objCAutorelease);
@override
String convertFfiDartTypeToDartType(
Context context,
String value, {
required bool objCRetain,
String? objCEnclosingClass,
}) => ObjCInterface.generateConstructor(
getDartType(context),
value,
objCRetain,
);
@override
String? generateRetain(String value) =>
'(__bridge id)(__bridge_retained void*)($value)';
bool _isSuperProtocolOf(ObjCProtocol protocol) {
if (protocol == this) return true;
for (final superProtocol in protocol.superProtocols) {
if (_isSuperProtocolOf(superProtocol)) return true;
}
return false;
}
@override
bool isSupertypeOf(Type other) {
other = other.typealiasType;
if (other is ObjCProtocol) {
return _isSuperProtocolOf(other);
} else if (other is ObjCInterface) {
for (ObjCInterface? t = other; t != null; t = t.superType) {
for (final protocol in t.protocols) {
if (_isSuperProtocolOf(protocol)) return true;
}
}
}
return false;
}
@override
String toString() => originalName;
@override
void visit(Visitation visitation) => visitation.visitObjCProtocol(this);
// Set typeGraphOnly to true to skip iterating methods and other children, and
// just iterate the DAG of interfaces, categories, and protocols. This is
// useful for visitors that need to ensure super types are visited first.
@override
void visitChildren(Visitor visitor, {bool typeGraphOnly = false}) {
if (!typeGraphOnly) {
super.visitChildren(visitor);
visitor.visit(_protocolPointer);
visitor.visit(_conformsTo);
visitor.visit(_conformsToMsgSend);
visitMethods(visitor);
visitor.visit(ffiImport);
visitor.visit(objcPkgImport);
}
visitor.visitAll(superProtocols);
}
}