blob: ab51ff139f722ae42850e71abba4bb4a0d19d005 [file] [log] [blame]
// Copyright (c) 2022, 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 'package:ffigen/src/code_generator.dart';
import 'package:logging/logging.dart';
import 'binding_string.dart';
import 'utils.dart';
import 'writer.dart';
// Class methods defined on NSObject that we don't want to copy to child objects
// by default.
const _excludedNSObjectClassMethods = {
'allocWithZone:',
'class',
'conformsToProtocol:',
'copyWithZone:',
'debugDescription',
'description',
'hash',
'initialize',
'instanceMethodForSelector:',
'instanceMethodSignatureForSelector:',
'instancesRespondToSelector:',
'isSubclassOfClass:',
'load',
'mutableCopyWithZone:',
'poseAsClass:',
'resolveClassMethod:',
'resolveInstanceMethod:',
'setVersion:',
'superclass',
'version',
};
final _logger = Logger('ffigen.code_generator.objc_interface');
class ObjCInterface extends BindingType {
ObjCInterface? superType;
final methods = <String, ObjCMethod>{};
bool filled = false;
final String lookupName;
final ObjCBuiltInFunctions builtInFunctions;
late final ObjCInternalGlobal _classObject;
late final ObjCInternalGlobal _isKindOfClass;
late final ObjCMsgSendFunc _isKindOfClassMsgSend;
ObjCInterface({
super.usr,
required String super.originalName,
String? name,
String? lookupName,
super.dartDoc,
required this.builtInFunctions,
}) : lookupName = lookupName ?? originalName,
super(
name: name ?? originalName,
) {
builtInFunctions.registerInterface(this);
}
bool get isNSString => originalName == "NSString";
bool get isNSData => originalName == "NSData";
@override
BindingString toBindingString(Writer w) {
String paramsToString(List<ObjCMethodParam> params,
{required bool isStatic}) {
final List<String> stringParams = [];
stringParams.addAll(
params.map((p) => '${_getConvertedType(p.type, w, name)} ${p.name}'));
return '(${stringParams.join(", ")})';
}
final s = StringBuffer();
if (dartDoc != null) {
s.write(makeDartDoc(dartDoc!));
}
final uniqueNamer = UniqueNamer({name, 'pointer'});
final rawObjType = PointerType(objCObjectType).getCType(w);
final wrapObjType = ObjCBuiltInFunctions.objectBase.gen(w);
final superTypeIsInPkgObjc = superType == null;
// Class declaration.
s.write('''
class $name extends ${superType?.name ?? wrapObjType} {
$name._($rawObjType pointer,
{bool retain = false, bool release = false}) :
${superTypeIsInPkgObjc ? 'super(pointer, ' : 'super._(pointer,'}
retain: retain, release: release);
/// Returns a [$name] that points to the same underlying object as [other].
static $name castFrom<T extends $wrapObjType>(T other) {
return $name._(other.pointer, retain: true, release: true);
}
/// Returns a [$name] that wraps the given raw object pointer.
static $name castFromPointer($rawObjType other,
{bool retain = false, bool release = false}) {
return $name._(other, retain: retain, release: release);
}
/// Returns whether [obj] is an instance of [$name].
static bool isInstance($wrapObjType obj) {
return ${_isKindOfClassMsgSend.invoke(
w,
'obj.pointer',
_isKindOfClass.name,
[_classObject.name],
)};
}
''');
if (isNSString) {
ObjCBuiltInFunctions.generateNSStringUtils(w, s);
}
// Methods.
for (final m in methods.values) {
final methodName = m._getDartMethodName(uniqueNamer);
final isStatic = m.isClass;
final isStret = m.msgSend!.isStret;
var returnType = m.returnType;
var params = m.params;
if (isStret) {
params = [ObjCMethodParam(PointerType(returnType), 'stret'), ...params];
returnType = voidType;
}
// The method declaration.
if (m.dartDoc != null) {
s.write(makeDartDoc(m.dartDoc!));
}
s.write(' ');
if (isStatic) {
s.write('static ');
s.write(_getConvertedType(returnType, w, name));
switch (m.kind) {
case ObjCMethodKind.method:
// static returnType methodName(...)
s.write(' $methodName');
break;
case ObjCMethodKind.propertyGetter:
// static returnType getMethodName()
s.write(' get');
s.write(methodName[0].toUpperCase() + methodName.substring(1));
break;
case ObjCMethodKind.propertySetter:
// static void setMethodName(...)
s.write(' set');
s.write(methodName[0].toUpperCase() + methodName.substring(1));
break;
}
s.write(paramsToString(params, isStatic: true));
} else {
if (superType?.methods[m.originalName]?.sameAs(m) ?? false) {
s.write('@override\n ');
}
switch (m.kind) {
case ObjCMethodKind.method:
// returnType methodName(...)
s.write(_getConvertedType(returnType, w, name));
s.write(' $methodName');
s.write(paramsToString(params, isStatic: false));
break;
case ObjCMethodKind.propertyGetter:
s.write(_getConvertedType(returnType, w, name));
if (isStret) {
// void getMethodName(Pointer<returnType> stret)
s.write(' get');
s.write(methodName[0].toUpperCase() + methodName.substring(1));
s.write(paramsToString(params, isStatic: false));
} else {
// returnType get methodName
s.write(' get $methodName');
}
break;
case ObjCMethodKind.propertySetter:
// set methodName(...)
s.write(' set $methodName');
s.write(paramsToString(params, isStatic: false));
break;
}
}
s.write(' {\n');
// Implementation.
final convertReturn = m.kind != ObjCMethodKind.propertySetter &&
_needsConverting(returnType);
if (returnType != voidType) {
s.write(' ${convertReturn ? 'final _ret = ' : 'return '}');
}
s.write(m.msgSend!.invoke(
w,
isStatic ? _classObject.name : 'this.pointer',
m.selObject!.name,
m.params.map((p) => p.type
.convertDartTypeToFfiDartType(w, p.name, objCRetain: false)),
structRetPtr: 'stret'));
s.write(';\n');
if (convertReturn) {
final result = returnType.convertFfiDartTypeToDartType(
w,
'_ret',
objCRetain: !m.isOwnedReturn,
objCEnclosingClass: name,
);
s.write(' return $result;');
}
s.write(' }\n\n');
}
s.write('}\n\n');
if (isNSString) {
ObjCBuiltInFunctions.generateStringUtils(w, s);
}
return BindingString(
type: BindingStringType.objcInterface, string: s.toString());
}
@override
void addDependencies(Set<Binding> dependencies) {
if (dependencies.contains(this)) return;
dependencies.add(this);
builtInFunctions.addDependencies(dependencies);
_classObject = ObjCInternalGlobal('_class_$originalName',
(Writer w) => '${ObjCBuiltInFunctions.getClass.gen(w)}("$lookupName")')
..addDependencies(dependencies);
_isKindOfClass = builtInFunctions.getSelObject('isKindOfClass:');
_isKindOfClassMsgSend = builtInFunctions.getMsgSendFunc(
BooleanType(), [ObjCMethodParam(PointerType(objCObjectType), 'clazz')]);
if (isNSString) {
_addNSStringMethods();
}
if (isNSData) {
_addNSDataMethods();
}
if (superType != null) {
superType!.addDependencies(dependencies);
_copyMethodsFromSuperType();
_fixNullabilityOfOverriddenMethods();
}
for (final m in methods.values) {
m.addDependencies(dependencies, builtInFunctions);
}
}
void _copyMethodsFromSuperType() {
// 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.
for (final m in superType!.methods.values) {
if (m.isClass &&
!_excludedNSObjectClassMethods.contains(m.originalName)) {
addMethod(m);
} else if (_isInstanceType(m.returnType)) {
addMethod(m);
}
}
}
void _fixNullabilityOfOverriddenMethods() {
// ObjC ignores nullability when deciding if an override for an inherited
// method is valid. But in Dart it's invalid to override a method and change
// it's return type from non-null to nullable, or its arg type from nullable
// to non-null. So in these cases we have to make the non-null type
// nullable, to avoid Dart compile errors.
var superType_ = superType;
while (superType_ != null) {
for (final method in methods.values) {
final superMethod = superType_.methods[method.originalName];
if (superMethod != null && !superMethod.isClass && !method.isClass) {
if (superMethod.returnType.typealiasType is! ObjCNullable &&
method.returnType.typealiasType is ObjCNullable) {
superMethod.returnType = ObjCNullable(superMethod.returnType);
}
final numArgs = method.params.length < superMethod.params.length
? method.params.length
: superMethod.params.length;
for (int i = 0; i < numArgs; ++i) {
final param = method.params[i];
final superParam = superMethod.params[i];
if (superParam.type.typealiasType is ObjCNullable &&
param.type.typealiasType is! ObjCNullable) {
param.type = ObjCNullable(param.type);
}
}
}
}
superType_ = superType_.superType;
}
}
static bool _isInstanceType(Type type) {
if (type is ObjCInstanceType) return true;
final baseType = type.typealiasType;
return baseType is ObjCNullable && baseType.child is ObjCInstanceType;
}
void addMethod(ObjCMethod method) {
final oldMethod = methods[method.originalName];
if (oldMethod != null) {
// 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 (method.isProperty && !oldMethod.isProperty) {
// Fallthrough.
} else if (!method.isProperty && oldMethod.isProperty) {
// Don't override, but also skip the same method check below.
return;
} else {
// Check duplicate is the same method.
if (!method.sameAs(oldMethod)) {
_logger.severe('Duplicate methods with different signatures: '
'$originalName.${method.originalName}');
}
return;
}
}
methods[method.originalName] = method;
}
void _addNSStringMethods() {
addMethod(ObjCMethod(
originalName: 'stringWithCharacters:length:',
kind: ObjCMethodKind.method,
isClass: true,
returnType: this,
params_: [
ObjCMethodParam(PointerType(wCharType), 'characters'),
ObjCMethodParam(unsignedIntType, 'length'),
],
));
addMethod(ObjCMethod(
originalName: 'dataUsingEncoding:',
kind: ObjCMethodKind.method,
isClass: false,
returnType: builtInFunctions.nsData,
params_: [
ObjCMethodParam(unsignedIntType, 'encoding'),
],
));
addMethod(ObjCMethod(
originalName: 'length',
kind: ObjCMethodKind.propertyGetter,
isClass: false,
returnType: unsignedIntType,
params_: [],
));
}
void _addNSDataMethods() {
addMethod(ObjCMethod(
originalName: 'bytes',
kind: ObjCMethodKind.propertyGetter,
isClass: false,
returnType: PointerType(voidType),
params_: [],
));
}
@override
String getCType(Writer w) => PointerType(objCObjectType).getCType(w);
@override
String getDartType(Writer w) => name;
@override
bool get sameFfiDartAndCType => true;
@override
bool get sameDartAndCType => false;
@override
bool get sameDartAndFfiDartType => false;
@override
String convertDartTypeToFfiDartType(
Writer w,
String value, {
required bool objCRetain,
}) =>
ObjCInterface.generateGetId(value, objCRetain);
static String generateGetId(String value, bool objCRetain) =>
objCRetain ? '$value.retainAndReturnPointer()' : '$value.pointer';
@override
String convertFfiDartTypeToDartType(
Writer w,
String value, {
required bool objCRetain,
String? objCEnclosingClass,
}) =>
ObjCInterface.generateConstructor(name, value, objCRetain);
static String generateConstructor(
String className,
String value,
bool objCRetain,
) {
final ownershipFlags = 'retain: $objCRetain, release: true';
return '$className._($value, $ownershipFlags)';
}
// Utils for converting between the internal types passed to native code, and
// the external types visible to the user. For example, ObjCInterfaces are
// passed to native as Pointer<ObjCObject>, but the user sees the Dart wrapper
// class. These methods need to be kept in sync.
bool _needsConverting(Type type) =>
type is ObjCInstanceType ||
type.typealiasType is ObjCInterface ||
type.typealiasType is ObjCBlock ||
type.typealiasType is ObjCObjectPointer ||
type.typealiasType is ObjCNullable;
String _getConvertedType(Type type, Writer w, String enclosingClass) {
if (type is ObjCInstanceType) return enclosingClass;
final baseType = type.typealiasType;
if (baseType is ObjCNullable && baseType.child is ObjCInstanceType) {
return '$enclosingClass?';
}
return type.getDartType(w);
}
}
enum ObjCMethodKind {
method,
propertyGetter,
propertySetter,
}
class ObjCProperty {
final String originalName;
String? dartName;
ObjCProperty(this.originalName);
}
class ObjCMethod {
final String? dartDoc;
final String originalName;
final ObjCProperty? property;
Type returnType;
final List<ObjCMethodParam> params;
final ObjCMethodKind kind;
final bool isClass;
bool returnsRetained = false;
ObjCInternalGlobal? selObject;
ObjCMsgSendFunc? msgSend;
ObjCMethod({
required this.originalName,
this.property,
this.dartDoc,
required this.kind,
required this.isClass,
required this.returnType,
List<ObjCMethodParam>? params_,
}) : params = params_ ?? [];
bool get isProperty =>
kind == ObjCMethodKind.propertyGetter ||
kind == ObjCMethodKind.propertySetter;
void addDependencies(
Set<Binding> dependencies, ObjCBuiltInFunctions builtInFunctions) {
returnType.addDependencies(dependencies);
for (final p in params) {
p.type.addDependencies(dependencies);
}
selObject ??= builtInFunctions.getSelObject(originalName)
..addDependencies(dependencies);
msgSend ??= builtInFunctions.getMsgSendFunc(returnType, params)
..addDependencies(dependencies);
}
String _getDartMethodName(UniqueNamer uniqueNamer) {
if (property != null) {
// A getter and a setter are allowed to have the same name, so we can't
// just run the name through uniqueNamer. Instead they need to share
// the dartName, which is run through uniqueNamer.
if (property!.dartName == null) {
property!.dartName = uniqueNamer.makeUnique(property!.originalName);
}
return property!.dartName!;
}
// Objective C methods can look like:
// foo
// foo:
// foo:someArgName:
// So replace all ':' with '_'.
return uniqueNamer.makeUnique(originalName.replaceAll(":", "_"));
}
bool sameAs(ObjCMethod other) {
if (originalName != other.originalName) return false;
if (kind != other.kind) return false;
if (isClass != other.isClass) return false;
// msgSend is deduped by signature, so this check covers the signature.
return msgSend == other.msgSend;
}
static final _copyRegExp = RegExp('[cC]opy');
bool get isOwnedReturn =>
returnsRetained ||
originalName.startsWith('new') ||
originalName.startsWith('alloc') ||
originalName.contains(_copyRegExp);
@override
String toString() => '$returnType $originalName(${params.join(', ')})';
}
class ObjCMethodParam {
Type type;
final String name;
ObjCMethodParam(this.type, this.name);
@override
String toString() => '$type $name';
}