Skip to content

Commit 5abe9e0

Browse files
authored
[pigeon]fix "as Any" workaround due to nested optional (#3658)
## The problem This PR fixes a weird casting behavior discussed [here](#3545 (comment)): ``` private func nilOrValue<T>(_ value: Any?) -> T? { if value is NSNull { return nil } return (value as Any) as! T? // <- HERE } ``` Without this intermediate `as Any` cast, [these 3 tests](https://github.com/flutter/packages/blob/5662a7e5799c723f76e9589a75c9d9310e2ba8c1/packages/pigeon/platform_tests/test_plugin/example/ios/RunnerTests/RunnerTests.swift#L10-L29) would crash with `SIGABRT: Could not cast value of type 'Swift.Optional<Any>' (0x7ff865e84b38) to 'Swift.String' (0x7ff865e7de08).` ## Investigation The crash happens because `value` here is actually of type `Any??` (nested optional!). When it crashes, the debugger simply shows `value` is `nil`. But if we print in `lldb`, the `value` here is actually an inner `Optional.none` case nested by an outer `Optional.some` case. ### Why does `Any??` crash Since outer case is `some`, it fails to force cast to `T?` (e.g. `String?`) due to type mismatch. ### How did we end up with `Any??` It's related to the signature of these 3 functions: - `func toList() -> [Any?]` - `func fromList(args: [Any])` - `func nilOrValue<T>(_ value: Any?) -> T?` Firstly `toList` returns `nil` (of type `Any?`) as the first element of array. Then the type gets coerced as an `Any` type in `fromList`. Then because `nilOrValue` takes `Any?`, this `nil` value gets wrapped by an `Optional.some`. Hence the nested optional. ## Workarounds ### Workaround 1: `as Any` This is the current code [in this PR](https://github.com/flutter/packages/pull/3545/files#r1155061282). When casting `Optional.some(nil) as Any`, it erases the outer Optional, so no problem casting to `T?`. ### Workaround 2: Handle with nested optional directly: ``` private func nilOrValue<T>(_ value: Any?) -> T? { if value is NSNull { return nil } // `if let` deals with "outer some" case and then erase the outer Optional if let val = value { // here returns "outer some + inner some" or "outer some + inner none" return val as! T? } // here returns "outer none" return nil } ``` A similar version of this was also [attempted in this PR](https://github.com/flutter/packages/pull/3545/files/241f0e31e32917f5501dab11f81ab0fbf064687f#diff-bfdb6a91beb03a906435e77e0168117f3f3977ee4d6f8bcaa1724156ae4dc27cR647-R650). It just that we did not know why that worked previously, and now we know! ### Workaround 3 Casting value to nested optional (`T??`), then stripe the outer optional ``` private func nilOrValue<T>(_ value: Any?) -> T? { if value is NSNull { return nil } return (value as! T??) ?? nil } ``` ## Solutions These above workarounds handle nested optionals. However, **a real solution should prevent nested optionals from happening in the first place**, since they are so tricky. ### Solution 1 (This PR) The nested optional happens when we do cast from `Any?` to `Any` and then wrapped into `Any?`. (Refer to "How did we end up with Any??" section). So the easiest way is just to use `func fromList(args: [Any?])` to match the types of `func toList` and `func nilOrValue`. ### Solution 2 Solution 2 is the opposite - avoid using `Any?` as much as possible. Drawbacks compare to Solution 1: a. When inter-op with ObjC, `nullable id` is exported as `Any?`. So we can't 100% prevent `Any?` usage. Though this can be addressed by immediately cast it to `Any`. b. Losing of semantic meaning of `Any?` that it <s>can</s> must be optional. The hidden/implicit optional **is** the culprit here in the first place. c. While this solution fixes the nested optional issue, it does not address all issues related to implicit optional. For example: https://github.com/flutter/packages/blob/c53db71f496b436e48629a8f3e4152c48e63cd66/packages/pigeon/platform_tests/test_plugin/ios/Classes/CoreTests.gen.swift#L563-L564 This is supposed to crash if `args[0]` is `nil`. However, the crash is silenced because `as! [Any]` will make `args[0]` an implicit optional! The correct codegen should instead be: ``` let args = message as! [Any?] let anObjectArg = args[0]! ``` ### Solution 3 Just remove `as Any` and update the test. The nested optional won't happen in production code, because ObjC `NSArray` contains `NSNull` rather than `nil` when exporting to Swift. We can simply fix [the tests](https://github.com/flutter/packages/blob/5662a7e5799c723f76e9589a75c9d9310e2ba8c1/packages/pigeon/platform_tests/test_plugin/example/ios/RunnerTests/RunnerTests.swift#L10-L29) by replacing `nil`s with `NSNull`s. However, if we were to re-write engine's codec to Swift, it's actually better practice to use `nil` and not `NSNull` in the array. ## Additional TODO We would've caught this earlier if this were an error rather than warning in our unit test. ![Screenshot 2023-04-06 at 4 15 07 PM](https://user-images.githubusercontent.com/41930132/230510477-1505f830-2fc5-4a4d-858f-e658729fa7bf.png) *List which issues are fixed by this PR. You must list at least one issue.* #3545 (comment) *If you had to change anything in the [flutter/tests] repo, include a link to the migration guide as per the [breaking change policy].*
1 parent c90dd98 commit 5abe9e0

File tree

9 files changed

+193
-188
lines changed

9 files changed

+193
-188
lines changed

packages/pigeon/CHANGELOG.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
1-
## NEXT
1+
## 10.0.0
22

3+
* [swift] Avoids using `Any` to represent `Optional` in Swift.
4+
* [swift] **Breaking Change** A raw `List` (without generic type argument) in Dart will be
5+
translated into `[Any?]` (rather than `[Any]`) in Swift.
6+
* [swift] **Breaking Change** A raw `Map` (without generic type argument) in Dart will be
7+
translated into `[AnyHashable:Any?]` (rather than `[AnyHashable:Any]`) in Swift.
38
* Adds an example application that uses Pigeon directly, rather than in a plugin.
49

510
## 9.2.5

packages/pigeon/lib/generator_tools.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import 'ast.dart';
1111
/// The current version of pigeon.
1212
///
1313
/// This must match the version in pubspec.yaml.
14-
const String pigeonVersion = '9.2.5';
14+
const String pigeonVersion = '10.0.0';
1515

1616
/// Read all the content from [stdin] to a String.
1717
String readStdin() {

packages/pigeon/lib/swift_generator.dart

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ import FlutterMacOS
169169
Set<String> customEnumNames,
170170
) {
171171
final String className = klass.name;
172-
indent.write('static func fromList(_ list: [Any]) -> $className? ');
172+
indent.write('static func fromList(_ list: [Any?]) -> $className? ');
173173

174174
indent.addScoped('{', '}', () {
175175
enumerate(getFieldsInSerializationOrder(klass),
@@ -431,7 +431,7 @@ import FlutterMacOS
431431
indent.addScoped('{ $messageVarName, reply in', '}', () {
432432
final List<String> methodArgument = <String>[];
433433
if (components.arguments.isNotEmpty) {
434-
indent.writeln('let args = message as! [Any]');
434+
indent.writeln('let args = message as! [Any?]');
435435
enumerate(components.arguments,
436436
(int index, _SwiftFunctionArgument arg) {
437437
final String argName =
@@ -524,7 +524,7 @@ import FlutterMacOS
524524
indent.writeln('case ${customClass.enumeration}:');
525525
indent.nest(1, () {
526526
indent.writeln(
527-
'return ${customClass.name}.fromList(self.readValue() as! [Any])');
527+
'return ${customClass.name}.fromList(self.readValue() as! [Any?])');
528528
});
529529
}
530530
indent.writeln('default:');
@@ -605,8 +605,7 @@ import FlutterMacOS
605605
'nullable enums require special code that this helper does not supply');
606606
return '${_swiftTypeForDartType(type)}(rawValue: $value as! Int)!';
607607
} else if (type.baseName == 'Object') {
608-
// Special-cased to avoid warnings about using 'as' with Any.
609-
return value;
608+
return value + (type.isNullable ? '' : '!');
610609
} else if (type.baseName == 'int') {
611610
if (type.isNullable) {
612611
// Nullable ints need to check for NSNull, and Int32 before casting can be done safely.
@@ -628,7 +627,7 @@ import FlutterMacOS
628627
if (listEncodedClassNames != null &&
629628
listEncodedClassNames.contains(type.baseName)) {
630629
indent.writeln('var $variableName: $fieldType? = nil');
631-
indent.write('if let ${variableName}List = $value as! [Any]? ');
630+
indent.write('if let ${variableName}List = $value as! [Any?]? ');
632631
indent.addScoped('{', '}', () {
633632
indent.writeln(
634633
'$variableName = $fieldType.fromList(${variableName}List)');
@@ -652,7 +651,7 @@ import FlutterMacOS
652651
if (listEncodedClassNames != null &&
653652
listEncodedClassNames.contains(type.baseName)) {
654653
indent.writeln(
655-
'let $variableName = $fieldType.fromList($value as! [Any])!');
654+
'let $variableName = $fieldType.fromList($value as! [Any?])!');
656655
} else {
657656
indent.writeln(
658657
'let $variableName = ${castForceUnwrap(value, type, root)}');
@@ -695,7 +694,7 @@ import FlutterMacOS
695694
696695
private func nilOrValue<T>(_ value: Any?) -> T? {
697696
if value is NSNull { return nil }
698-
return (value as Any) as! T?
697+
return value as! T?
699698
}''');
700699
}
701700

@@ -739,9 +738,9 @@ String _flattenTypeArguments(List<TypeDeclaration> args) {
739738
String _swiftTypeForBuiltinGenericDartType(TypeDeclaration type) {
740739
if (type.typeArguments.isEmpty) {
741740
if (type.baseName == 'List') {
742-
return '[Any]';
741+
return '[Any?]';
743742
} else if (type.baseName == 'Map') {
744-
return '[AnyHashable: Any]';
743+
return '[AnyHashable: Any?]';
745744
} else {
746745
return 'Any';
747746
}

packages/pigeon/platform_tests/test_plugin/example/ios/RunnerTests/EchoBinaryMessenger.swift

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,14 @@ class EchoBinaryMessenger: NSObject, FlutterBinaryMessenger {
2828

2929
guard
3030
let args = self.codec.decode(message) as? [Any?],
31-
let firstArg: Any? = nilOrValue(args.first)
31+
let firstArg = args.first,
32+
let castedFirstArg: Any? = nilOrValue(firstArg)
3233
else {
3334
callback(self.defaultReturn.flatMap { self.codec.encode($0) })
3435
return
3536
}
3637

37-
callback(self.codec.encode(firstArg))
38+
callback(self.codec.encode(castedFirstArg))
3839
}
3940

4041
func setMessageHandlerOnChannel(

packages/pigeon/platform_tests/test_plugin/example/ios/RunnerTests/PrimitiveTests.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ class MockPrimitiveHostApi: PrimitiveHostApi {
1111
func aBool(value: Bool) -> Bool { value }
1212
func aString(value: String) -> String { value }
1313
func aDouble(value: Double) -> Double { value }
14-
func aMap(value: [AnyHashable: Any]) -> [AnyHashable: Any] { value }
15-
func aList(value: [Any]) -> [Any] { value }
14+
func aMap(value: [AnyHashable: Any?]) -> [AnyHashable: Any?] { value }
15+
func aList(value: [Any?]) -> [Any?] { value }
1616
func anInt32List(value: FlutterStandardTypedData) -> FlutterStandardTypedData { value }
1717
func aBoolList(value: [Bool?]) -> [Bool?] { value }
1818
func aStringIntMap(value: [String?: Int64?]) -> [String?: Int64?] { value }

0 commit comments

Comments
 (0)