blob: 31cb46f9b1fa9a258e02fda162c1cbc5ae0f9c68 [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 'dart:collection';
import '../context.dart';
import '../header_parser/sub_parsers/api_availability.dart';
import '../visitor/ast.dart';
import 'func.dart';
import 'imports.dart';
import 'native_type.dart';
import 'objc_block.dart';
import 'objc_built_in_functions.dart';
import 'objc_interface.dart';
import 'objc_nullable.dart';
import 'pointer.dart';
import 'scope.dart';
import 'type.dart';
import 'typealias.dart';
import 'utils.dart';
import 'writer.dart';
mixin ObjCMethods {
Map<String, ObjCMethod> _methods = <String, ObjCMethod>{};
List<String> _order = <String>[];
Scope? methodNameScope;
Iterable<ObjCMethod> get methods =>
_order.map((key) => _methods[key]).nonNulls;
ObjCMethod? getSimilarMethod(ObjCMethod method) => _methods[method.key];
String get originalName;
String get name;
Context get context;
void addMethod(ObjCMethod? method) {
if (method == null) return;
final oldMethod = getSimilarMethod(method);
if (oldMethod == null) {
_methods[method.key] = method;
_order.add(method.key);
} else if (_shouldReplaceMethod(oldMethod, method)) {
_methods[method.key] = method;
}
}
void visitMethods(Visitor visitor) {
visitor.visitAll(methods);
}
bool _shouldReplaceMethod(ObjCMethod oldMethod, ObjCMethod newMethod) {
// 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
// important information, and is a plain old instanceMethod. So if the
// existing method is an instanceMethod, and the new one is a property,
// override it.
if (newMethod.isProperty && !oldMethod.isProperty) {
return true;
} else if (!newMethod.isProperty && oldMethod.isProperty) {
// Don't override, but also skip the same method check below.
return false;
}
// If one of the methods is optional, and the other is required, keep the
// required one.
if (newMethod.isOptional && !oldMethod.isOptional) {
return false;
} else if (!newMethod.isOptional && oldMethod.isOptional) {
return true;
}
// Check the duplicate is the same method.
if (!newMethod.sameAs(oldMethod)) {
context.logger.severe(
'Duplicate methods with different signatures: '
'$originalName.${newMethod.originalName}',
);
return true;
}
// There's a bug in some Apple APIs where an init method that should return
// instancetype has a duplicate definition that instead returns id. In that
// case, use the one that returns instancetype. Note that since instancetype
// is an alias of id, the sameAs check above passes.
if (ObjCBuiltInFunctions.isInstanceType(newMethod.returnType) &&
!ObjCBuiltInFunctions.isInstanceType(oldMethod.returnType)) {
return true;
} else if (!ObjCBuiltInFunctions.isInstanceType(newMethod.returnType) &&
ObjCBuiltInFunctions.isInstanceType(oldMethod.returnType)) {
return false;
}
return true;
}
void sortMethods() => _order.sort();
void filterMethods(bool Function(ObjCMethod method) predicate) {
final newOrder = <String>[];
final newMethods = <String, ObjCMethod>{};
for (final key in _order) {
final method = _methods[key];
if (method != null && predicate(method)) {
newMethods[key] = method;
newOrder.add(key);
}
}
_order = newOrder;
_methods = newMethods;
}
String generateMethodBindings(Writer w, ObjCInterface target) =>
_generateMethods(w, target, null);
String generateStaticMethodBindings(Writer w, ObjCInterface target) =>
_generateMethods(w, target, true);
String generateInstanceMethodBindings(Writer w, BindingType target) =>
_generateMethods(w, target, false);
String _generateMethods(Writer w, BindingType target, bool? staticMethods) {
return [
for (final m in methods)
if (staticMethods == null || staticMethods == m.isClassMethod)
m.generateBindings(w, target),
].join('\n');
}
}
enum ObjCMethodKind { method, propertyGetter, propertySetter }
enum ObjCMethodOwnership { retained, notRetained, autoreleased }
// In ObjC, the name of a method affects its ref counting semantics. See
// https://clang.llvm.org/docs/AutomaticReferenceCounting.html#method-families
enum ObjCMethodFamily {
alloc('alloc', returnsRetained: true, consumesSelf: false),
init('init', returnsRetained: true, consumesSelf: true),
new_('new', returnsRetained: true, consumesSelf: false),
copy('copy', returnsRetained: true, consumesSelf: false),
mutableCopy('mutableCopy', returnsRetained: true, consumesSelf: false);
final String name;
final bool returnsRetained;
final bool consumesSelf;
const ObjCMethodFamily(
this.name, {
required this.returnsRetained,
required this.consumesSelf,
});
static ObjCMethodFamily? parse(String methodName) {
final name = methodName.substring(_findFamilyStart(methodName));
for (final family in ObjCMethodFamily.values) {
if (_matchesFamily(name, family.name)) return family;
}
return null;
}
static int _findFamilyStart(String methodName) {
for (var i = 0; i < methodName.length; ++i) {
if (methodName[i] != '_') return i;
}
return methodName.length;
}
static final _lowerCase = RegExp('[a-z]');
static bool _matchesFamily(String name, String familyName) {
if (!name.startsWith(familyName)) return false;
final tail = name.substring(familyName.length);
return !tail.startsWith(_lowerCase);
}
}
class ObjCMethod extends AstNode with HasLocalScope {
final Context context;
final String? dartDoc;
final String originalName;
Symbol symbol;
final String originalProtocolMethodName;
Type returnType;
final List<Parameter> _params;
ObjCMethodKind kind;
final bool isClassMethod;
final bool isOptional;
final ObjCMethodOwnership? ownershipAttribute;
final ObjCMethodFamily? family;
final ApiAvailability apiAvailability;
final bool consumesSelfAttribute;
ObjCInternalGlobal selObject;
ObjCMsgSendFunc? msgSend;
ObjCBlock? protocolBlock;
Symbol? protocolMethodName;
@override
void visitChildren(Visitor visitor, {bool omitMethodName = false}) {
super.visitChildren(visitor);
if (!omitMethodName) {
visitor.visit(symbol);
visitor.visit(protocolMethodName);
}
visitor.visit(returnType);
visitor.visitAll(_params);
visitor.visit(selObject);
visitor.visit(msgSend);
visitor.visit(protocolBlock);
visitor.visit(ffiImport);
visitor.visit(ffiPkgImport);
visitor.visit(objcPkgImport);
}
@override
void visit(Visitation visitation) => visitation.visitObjCMethod(this);
ObjCMethod.withSymbol({
required this.context,
required this.originalName,
required this.symbol,
required String protocolMethodName,
this.dartDoc,
required this.kind,
required this.isClassMethod,
required this.isOptional,
required this.returnType,
required this.family,
required this.apiAvailability,
required List<Parameter> params,
required this.ownershipAttribute,
required this.consumesSelfAttribute,
}) : originalProtocolMethodName = protocolMethodName.replaceAll(':', '_'),
_params = params,
selObject = context.objCBuiltInFunctions.getSelObject(originalName);
factory ObjCMethod({
required Context context,
required String originalName,
required String name,
String? dartDoc,
required ObjCMethodKind kind,
required bool isClassMethod,
required bool isOptional,
required Type returnType,
required ObjCMethodFamily? family,
required ApiAvailability apiAvailability,
required List<Parameter> params,
required ObjCMethodOwnership? ownershipAttribute,
required bool consumesSelfAttribute,
}) {
final protocolMethodName = name;
// Split the name at the ':'. The first chunk is the name of the method, and
// the rest of the chunks are named parameters. Eg NSString's
// - compare:options:range:
// method becomes
// NSComparisonResult compare(NSString string,
// {required NSStringCompareOptions options, required NSRange range})
final chunks = name.split(':');
// Details:
// - The first chunk is always the new Dart method name.
// - The rest of the chunks correspond to the params, so there's always
// one more chunk than the number of params.
// - The correspondence between the chunks and the params is non-trivial:
// - The ObjC name always ends with a ':' unless there are no ':' at all.
// - The first param is an ordinary param, not a named param.
final correctNumParams = chunks.length == params.length + 1;
final lastChunkIsEmpty = chunks.length == 1 || chunks.last.isEmpty;
if (correctNumParams && lastChunkIsEmpty) {
// Take the first chunk as the name, ignore the last chunk, and map the
// rest to each of the params after the first.
name = chunks[0];
for (var i = 1; i < params.length; ++i) {
params[i].symbol = Symbol(chunks[i], SymbolKind.field);
}
} else {
// There are a few methods that don't obey these rules, eg due to variadic
// parameters. Most of these are omitted from the bindings as they're not
// supported yet. But as a fallback, just replace all the ':' in the name
// with '_', like we do for protocol methods.
name = name.replaceAll(':', '_');
}
return ObjCMethod.withSymbol(
context: context,
originalName: originalName,
symbol: Symbol(name, SymbolKind.method),
protocolMethodName: protocolMethodName,
dartDoc: dartDoc,
kind: kind,
isClassMethod: isClassMethod,
isOptional: isOptional,
returnType: returnType,
family: family,
apiAvailability: apiAvailability,
params: params,
ownershipAttribute: ownershipAttribute,
consumesSelfAttribute: consumesSelfAttribute,
);
}
String get name => symbol.name;
Iterable<Parameter> get params => _params;
bool get isProperty =>
kind == ObjCMethodKind.propertyGetter ||
kind == ObjCMethodKind.propertySetter;
bool get isRequired => !isOptional;
bool get isInstanceMethod => !isClassMethod;
void fillMsgSend() {
msgSend ??= context.objCBuiltInFunctions.getMsgSendFunc(
returnType,
_params,
);
}
void fillProtocolBlock() {
protocolBlock ??= ObjCBlock(
context,
returnType: returnType,
params: [
// First arg of the protocol block is a void pointer that we ignore.
Parameter(name: '_', type: PointerType(voidType), objCConsumed: false),
..._params,
],
returnsRetained: returnsRetained,
)..fillProtocolTrampoline();
protocolMethodName ??= symbol.oldName == originalProtocolMethodName
? symbol
: Symbol(originalProtocolMethodName, SymbolKind.method);
}
bool sameAs(ObjCMethod other) {
if (originalName != other.originalName) return false;
if (kind != other.kind) return false;
if (isClassMethod != other.isClassMethod) return false;
if (isOptional != other.isOptional) return false;
// msgSend is deduped by signature, so this check covers the signature.
return msgSend == other.msgSend;
}
bool get returnsRetained {
if (ownershipAttribute != null) {
return ownershipAttribute == ObjCMethodOwnership.retained;
}
return family?.returnsRetained ?? false;
}
bool get consumesSelf =>
consumesSelfAttribute || (family?.consumesSelf ?? false);
Iterable<Type> get childTypes sync* {
yield returnType;
for (final p in _params) {
yield p.type;
}
}
// Key used to dedupe methods in [ObjCMethods]. ObjC is similar to Dart in
// that it doesn't have method overloading, so the [originalName] is mostly
// sufficient as the key. But unlike Dart, ObjC can have static methods and
// instance methods with the same name, so we have to include staticness in
// the key. We order instance methods before static methods alphabetically.
String get key => '${isClassMethod ? 'S' : 'I'} $originalName';
@override
String toString() =>
'${isOptional ? '@optional ' : ''}$returnType '
'$originalName(${_params.join(', ')})';
bool get returnsInstanceType {
if (returnType is ObjCInstanceType) return true;
final baseType = returnType.typealiasType;
if (baseType is ObjCNullable && baseType.child is ObjCInstanceType) {
return true;
}
return false;
}
// Whether this method returns an `NSError` out param.
//
// In ObjC, the pattern for returning an error is to return it as an out
// param. Specifically, the last param is named error, and is a NSError**.
// The same pattern is used when translating Swift methods that throw into
// ObjC methods. We don't need to handle subtypes of NSError, as there are
// no known APIs that return subtypes of NSError (in fact the Swift bridging
// relies on the type always being exactly NSError).
bool get returnsNSErrorByOutParam {
final p = _params.lastOrNull;
if (p == null) return false;
if (!_errorOutParamNames.contains(p.originalName)) return false;
final t = p.type;
if (t is! PointerType) return false;
var c = t.child;
if (c is ObjCNullable) c = c.child;
if (c is! ObjCInterface) return false;
return ObjCBuiltInFunctions.isNSError(c.originalName);
}
static const _errorOutParamNames = {'error', 'outError'};
String _getConvertedReturnType(Context context, String instanceType) {
if (returnType is ObjCInstanceType) return instanceType;
final baseType = returnType.typealiasType;
if (baseType is ObjCNullable && baseType.child is ObjCInstanceType) {
return '$instanceType?';
}
return returnType.getDartType(context);
}
static String _paramToStr(Context context, Parameter p) =>
'${p.type.getDartType(context)} ${p.name}';
static String _paramToNamed(Context context, Parameter p) =>
'${p.isNullable ? '' : 'required '}${_paramToStr(context, p)}';
static String _joinParamStr(Context context, List<Parameter> params) {
if (params.isEmpty) return '';
if (params.length == 1) return _paramToStr(context, params.first);
final named = params
.sublist(1)
.map((p) => _paramToNamed(context, p))
.join(',');
return '${_paramToStr(context, params.first)}, {$named}';
}
String generateBindings(Writer w, BindingType target) {
// Class methods are only supported for ObjCInterface targets.
assert(target is ObjCInterface || !isClassMethod);
final context = w.context;
final methodName = symbol.name;
final upperName = methodName[0].toUpperCase() + methodName.substring(1);
final throwNSError = returnsNSErrorByOutParam;
final params = throwNSError
? _params.sublist(0, _params.length - 1)
: _params;
const retVar = '\$ret';
const ptrVar = '\$ptr';
const finalizableVar = '\$finalizable';
const errVar = '\$err';
final s = StringBuffer();
final targetType = target.getDartType(context);
final returnTypeStr = _getConvertedReturnType(context, targetType);
final paramStr = _joinParamStr(context, params);
// The method declaration.
s.write('\n ${makeDartDoc(dartDoc)} ');
late String targetStr;
if (isClassMethod) {
targetStr = (target as ObjCInterface).classObject.name;
switch (kind) {
case ObjCMethodKind.method:
s.write('static $returnTypeStr $methodName($paramStr)');
break;
case ObjCMethodKind.propertyGetter:
s.write('static $returnTypeStr get$upperName($paramStr)');
break;
case ObjCMethodKind.propertySetter:
s.write('static $returnTypeStr set$upperName($paramStr)');
break;
}
} else {
targetStr = target.convertDartTypeToFfiDartType(
context,
'object\$',
objCRetain: consumesSelf,
objCAutorelease: false,
);
switch (kind) {
case ObjCMethodKind.method:
s.write('$returnTypeStr $methodName($paramStr)');
break;
case ObjCMethodKind.propertyGetter:
s.write('$returnTypeStr get $methodName');
break;
case ObjCMethodKind.propertySetter:
s.write('set $methodName($paramStr)');
break;
}
}
s.write(' {\n');
// Implementation.
final versionCheck = apiAvailability.runtimeCheck(
ObjCBuiltInFunctions.checkOsVersion.gen(context),
'${target.originalName}.$originalName',
);
if (versionCheck != null) {
s.write(' $versionCheck\n');
}
final sel = selObject.name;
if (isOptional) {
final responds = ObjCBuiltInFunctions.respondsToSelector.gen(context);
final unimplementedException = ObjCBuiltInFunctions
.unimplementedOptionalMethodException
.gen(context);
s.write('''
if (!$responds($targetStr, $sel)) {
throw $unimplementedException('${target.originalName}', '$originalName');
}
''');
}
final calloc = '${context.libs.prefix(ffiPkgImport)}.calloc';
if (throwNSError) {
final rawObjType = PointerType(objCObjectType).getCType(context);
s.write('''
final $errVar = $calloc<$rawObjType>();
try {
''');
}
final convertReturn =
kind != ObjCMethodKind.propertySetter &&
!returnType.sameDartAndFfiDartType;
final msgSendParams = [
for (final p in params)
p.type.convertDartTypeToFfiDartType(
context,
p.name,
objCRetain: p.objCConsumed,
objCAutorelease: false,
),
if (throwNSError) errVar,
];
if (msgSend!.isStret) {
assert(!convertReturn);
assert(!throwNSError);
final sizeOf = '${context.libs.prefix(ffiImport)}.sizeOf';
final uint8Type = NativeType(SupportedNativeType.uint8).getCType(context);
final invoke = msgSend!.invoke(
context,
targetStr,
sel,
msgSendParams,
structRetPtr: ptrVar,
);
s.write('''
final $ptrVar = $calloc<$returnTypeStr>();
$invoke;
final $finalizableVar = $ptrVar.cast<$uint8Type>().asTypedList(
$sizeOf<$returnTypeStr>(), finalizer: $calloc.nativeFree);
return ${context.libs.prefix(ffiImport)}.Struct.create<$returnTypeStr>(
$finalizableVar);
''');
} else {
final useReturn = returnType != voidType;
final useReturnVar = convertReturn || throwNSError;
if (useReturn) {
s.write(' ${useReturnVar ? 'final $retVar = ' : 'return '}');
}
s.write(msgSend!.invoke(context, targetStr, sel, msgSendParams));
s.write(';\n');
if (throwNSError) {
final nsErrorException = ObjCBuiltInFunctions.nsErrorException.gen(
context,
);
s.write(' $nsErrorException.checkErrorPointer($errVar.value);\n');
}
if (useReturnVar) {
final result = convertReturn
? returnType.convertFfiDartTypeToDartType(
context,
retVar,
objCRetain: !returnsRetained,
objCEnclosingClass: targetType,
)
: retVar;
s.write(' return $result;');
}
}
if (throwNSError) {
s.write('''
} finally {
$calloc.free($errVar);
}
''');
}
s.write('\n }\n');
return s.toString();
}
}