Skip to content
This repository has been archived by the owner on Jan 28, 2024. It is now read-only.

Handle ObjC nullable annotations #624

Merged
merged 9 commits into from
Sep 28, 2023
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
1 change: 1 addition & 0 deletions lib/src/code_generator.dart
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export 'code_generator/native_type.dart';
export 'code_generator/objc_block.dart';
export 'code_generator/objc_built_in_functions.dart';
export 'code_generator/objc_interface.dart';
export 'code_generator/objc_nullable.dart';
export 'code_generator/pointer.dart';
export 'code_generator/struct.dart';
export 'code_generator/type.dart';
Expand Down
2 changes: 1 addition & 1 deletion lib/src/code_generator/objc_built_in_functions.dart
Original file line number Diff line number Diff line change
Expand Up @@ -326,7 +326,7 @@ class $name implements ${w.ffiLibraryPrefix}.Finalizable {
String toString() {
final data = dataUsingEncoding_(
0x94000100 /* NSUTF16LittleEndianStringEncoding */);
return data.bytes.cast<${w.ffiPkgLibraryPrefix}.Utf16>().toDartString(
return data!.bytes.cast<${w.ffiPkgLibraryPrefix}.Utf16>().toDartString(
length: length);
}
''');
Expand Down
107 changes: 69 additions & 38 deletions lib/src/code_generator/objc_interface.dart
Original file line number Diff line number Diff line change
Expand Up @@ -76,10 +76,8 @@ class ObjCInterface extends BindingType {
if (isStatic) {
stringParams.add('${w.className} _lib');
}
stringParams.addAll(params.map((p) =>
(_getConvertedType(p.type, w, name) +
(p.isNullable ? "? " : " ") +
p.name)));
stringParams.addAll(
params.map((p) => '${_getConvertedType(p.type, w, name)} ${p.name}'));
return '(${stringParams.join(", ")})';
}

Expand Down Expand Up @@ -146,8 +144,7 @@ class $name extends ${superType?.name ?? '_ObjCWrapper'} {
s.write(' ');
if (isStatic) {
s.write('static ');
s.write(
_getConvertedReturnType(returnType, w, name, m.isNullableReturn));
s.write(_getConvertedType(returnType, w, name));

switch (m.kind) {
case ObjCMethodKind.method:
Expand All @@ -173,14 +170,12 @@ class $name extends ${superType?.name ?? '_ObjCWrapper'} {
switch (m.kind) {
case ObjCMethodKind.method:
// returnType methodName(...)
s.write(_getConvertedReturnType(
returnType, w, name, m.isNullableReturn));
s.write(_getConvertedType(returnType, w, name));
s.write(' $methodName');
s.write(paramsToString(params, isStatic: false));
break;
case ObjCMethodKind.propertyGetter:
s.write(_getConvertedReturnType(
returnType, w, name, m.isNullableReturn));
s.write(_getConvertedType(returnType, w, name));
if (isStret) {
// void getMethodName(Pointer<returnType> stret, NativeLibrary _lib)
s.write(' get');
Expand Down Expand Up @@ -219,8 +214,8 @@ class $name extends ${superType?.name ?? '_ObjCWrapper'} {
}
s.write(');\n');
if (convertReturn) {
final result = _doReturnConversion(returnType, '_ret', name, '_lib',
m.isNullableReturn, m.isOwnedReturn);
final result = _doReturnConversion(
returnType, '_ret', name, '_lib', m.isOwnedReturn);
s.write(' return $result;');
}

Expand Down Expand Up @@ -263,6 +258,7 @@ class $name extends ${superType?.name ?? '_ObjCWrapper'} {
if (superType != null) {
superType!.addDependencies(dependencies);
_copyMethodsFromSuperType();
_fixNullabilityOfOverriddenMethods();
}

for (final m in methods.values) {
Expand All @@ -280,12 +276,48 @@ class $name extends ${superType?.name ?? '_ObjCWrapper'} {
if (m.isClass &&
!_excludedNSObjectClassMethods.contains(m.originalName)) {
addMethod(m);
} else if (m.returnType is ObjCInstanceType) {
} 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) =>
type is ObjCInstanceType ||
(type is ObjCNullable && type.child is ObjCInstanceType);

void addMethod(ObjCMethod method) {
final oldMethod = methods[method.originalName];
if (oldMethod != null) {
Expand Down Expand Up @@ -365,39 +397,36 @@ class $name extends ${superType?.name ?? '_ObjCWrapper'} {
type is ObjCInterface ||
type is ObjCBlock ||
type is ObjCObjectPointer ||
type is ObjCInstanceType;
type is ObjCInstanceType ||
type is ObjCNullable;

String _getConvertedType(Type type, Writer w, String enclosingClass) {
if (type is ObjCInstanceType) return enclosingClass;
return type.getDartType(w);
}

String _getConvertedReturnType(
Type type, Writer w, String enclosingClass, bool isNullableReturn) {
final result = _getConvertedType(type, w, enclosingClass);
if (isNullableReturn) {
return "$result?";
if (type is ObjCNullable && type.child is ObjCInstanceType) {
return '$enclosingClass?';
}
return result;
return type.getDartType(w);
}

String _doArgConversion(ObjCMethodParam arg) {
if (arg.type is ObjCInterface ||
if (arg.type is ObjCNullable) {
return '${arg.name}?._id ?? ffi.nullptr';
} else if (arg.type is ObjCInterface ||
arg.type is ObjCObjectPointer ||
arg.type is ObjCInstanceType ||
arg.type is ObjCBlock) {
if (arg.isNullable) {
return '${arg.name}?._id ?? ffi.nullptr';
} else {
return '${arg.name}._id';
}
return '${arg.name}._id';
}
return arg.name;
}

String _doReturnConversion(Type type, String value, String enclosingClass,
String library, bool isNullable, bool isOwnedReturn) {
final prefix = isNullable ? '$value.address == 0 ? null : ' : '';
String library, bool isOwnedReturn) {
var prefix = '';
if (type is ObjCNullable) {
prefix = '$value.address == 0 ? null : ';
type = type.child;
}
final ownerFlags = 'retain: ${!isOwnedReturn}, release: true';
if (type is ObjCInterface) {
return '$prefix${type.name}._($value, $library, $ownerFlags)';
Expand Down Expand Up @@ -432,8 +461,7 @@ class ObjCMethod {
final String? dartDoc;
final String originalName;
final ObjCProperty? property;
final Type returnType;
final bool isNullableReturn;
Type returnType;
final List<ObjCMethodParam> params;
final ObjCMethodKind kind;
final bool isClass;
Expand All @@ -448,7 +476,6 @@ class ObjCMethod {
required this.kind,
required this.isClass,
required this.returnType,
this.isNullableReturn = false,
List<ObjCMethodParam>? params_,
}) : params = params_ ?? [];

Expand Down Expand Up @@ -488,7 +515,6 @@ class ObjCMethod {

bool sameAs(ObjCMethod other) {
if (originalName != other.originalName) return false;
if (isNullableReturn != other.isNullableReturn) 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.
Expand All @@ -501,11 +527,16 @@ class ObjCMethod {
originalName.startsWith('new') ||
originalName.startsWith('alloc') ||
originalName.contains(_copyRegExp);

@override
String toString() => '$returnType $originalName(${params.join(', ')})';
}

class ObjCMethodParam {
final Type type;
Type type;
final String name;
final bool isNullable;
ObjCMethodParam(this.type, this.name, {this.isNullable = false});
ObjCMethodParam(this.type, this.name);

@override
String toString() => '$type $name';
}
46 changes: 46 additions & 0 deletions lib/src/code_generator/objc_nullable.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// Copyright (c) 2023, 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 'writer.dart';

/// An ObjC type annotated with nullable. Eg:
/// +(nullable NSObject*) methodWithNullableResult;
class ObjCNullable extends Type {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice, much cleaner!

Type child;

ObjCNullable(this.child) {
assert(isSupported(child));
}

static bool isSupported(Type type) =>
type is ObjCInterface ||
type is ObjCBlock ||
type is ObjCObjectPointer ||
type is ObjCInstanceType;

@override
void addDependencies(Set<Binding> dependencies) {
child.addDependencies(dependencies);
}

@override
Type get baseType => child.baseType;

@override
String getCType(Writer w) => child.getCType(w);

@override
String getFfiDartType(Writer w) => child.getFfiDartType(w);

@override
String getDartType(Writer w) => '${child.getDartType(w)}?';

@override
String toString() => '$child?';

@override
String cacheKey() => '${child.cacheKey()}?';
}
25 changes: 21 additions & 4 deletions lib/src/header_parser/clang_bindings/clang_bindings.dart
Original file line number Diff line number Diff line change
Expand Up @@ -915,6 +915,23 @@ class Clang {
late final _clang_Type_getAlignOf =
_clang_Type_getAlignOfPtr.asFunction<int Function(CXType)>();

/// Return the type that was modified by this attributed type.
///
/// If the type is not an attributed type, an invalid type is returned.
CXType clang_Type_getModifiedType(
CXType T,
) {
return _clang_Type_getModifiedType(
T,
);
}

late final _clang_Type_getModifiedTypePtr =
_lookup<ffi.NativeFunction<CXType Function(CXType)>>(
'clang_Type_getModifiedType');
late final _clang_Type_getModifiedType =
_clang_Type_getModifiedTypePtr.asFunction<CXType Function(CXType)>();

/// Determine whether the given cursor represents an anonymous
/// tag or namespace
int clang_Cursor_isAnonymous(
Expand Down Expand Up @@ -2623,10 +2640,10 @@ abstract class CXChildVisitResult {
///
/// The visitor should return one of the \c CXChildVisitResult values
/// to direct clang_visitCursorChildren().
typedef CXCursorVisitor = ffi.Pointer<
ffi.NativeFunction<
ffi.Int32 Function(
CXCursor cursor, CXCursor parent, CXClientData client_data)>>;
typedef CXCursorVisitor
= ffi.Pointer<ffi.NativeFunction<CXCursorVisitor_function>>;
typedef CXCursorVisitor_function = ffi.Int32 Function(
CXCursor cursor, CXCursor parent, CXClientData client_data);

/// Opaque pointer representing client data that will be passed through
/// to various callbacks and visitors.
Expand Down
4 changes: 3 additions & 1 deletion lib/src/header_parser/parser.dart
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,9 @@ List<Binding> parseToBindings() {
0,
clang_types.CXTranslationUnit_Flags.CXTranslationUnit_SkipFunctionBodies |
clang_types.CXTranslationUnit_Flags
.CXTranslationUnit_DetailedPreprocessingRecord,
.CXTranslationUnit_DetailedPreprocessingRecord |
clang_types
.CXTranslationUnit_Flags.CXTranslationUnit_IncludeAttributedTypes,
);

if (tu == nullptr) {
Expand Down
24 changes: 2 additions & 22 deletions lib/src/header_parser/sub_parsers/objcinterfacedecl_parser.dart
Original file line number Diff line number Diff line change
Expand Up @@ -175,9 +175,6 @@ void _parseProperty(clang_types.CXCursor cursor) {
final isReadOnly = propertyAttributes &
clang_types.CXObjCPropertyAttrKind.CXObjCPropertyAttr_readonly >
0;
// TODO(#334): Use the nullable attribute to decide this.
final isNullable =
cursor.type().kind == clang_types.CXTypeKind.CXType_ObjCObjectPointer;

final property = ObjCProperty(fieldName);

Expand All @@ -193,7 +190,6 @@ void _parseProperty(clang_types.CXCursor cursor) {
kind: ObjCMethodKind.propertyGetter,
isClass: isClass,
returnType: fieldType,
isNullableReturn: isNullable,
);
itf.addMethod(getter);

Expand All @@ -208,8 +204,7 @@ void _parseProperty(clang_types.CXCursor cursor) {
kind: ObjCMethodKind.propertySetter,
isClass: isClass,
returnType: NativeType(SupportedNativeType.Void));
setter.params
.add(ObjCMethodParam(fieldType, 'value', isNullable: isNullable));
setter.params.add(ObjCMethodParam(fieldType, 'value'));
itf.addMethod(setter);
}
}
Expand Down Expand Up @@ -264,22 +259,7 @@ int _parseMethodVisitor(clang_types.CXCursor cursor,
}

void _parseMethodParam(clang_types.CXCursor cursor) {
/*
TODO(#334): Change this to use:

clang.clang_Type_getNullability(cursor.type()) ==
clang_types.CXTypeNullabilityKind.CXTypeNullability_Nullable;

NOTE: This will only work with the

clang_types
.CXTranslationUnit_Flags.CXTranslationUnit_IncludeAttributedTypes

option set.
*/
final parsed = _methodStack.top;
final isNullable =
cursor.type().kind == clang_types.CXTypeKind.CXType_ObjCObjectPointer;
final name = cursor.spelling();
final type = cursor.type().toCodeGenType();
if (type.isIncompleteCompound) {
Expand All @@ -291,7 +271,7 @@ void _parseMethodParam(clang_types.CXCursor cursor) {
}
_logger.fine(
' >> Parameter: $type $name ${cursor.completeStringRepr()}');
parsed.method.params.add(ObjCMethodParam(type, name, isNullable: isNullable));
parsed.method.params.add(ObjCMethodParam(type, name));
}

void _markMethodReturnsRetained(clang_types.CXCursor cursor) {
Expand Down
Loading