Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[pigeon] Add errors for ProxyAPI callback methods and null instances when reading in a ProxyApiBaseCodec #8567

Merged
merged 18 commits into from
Feb 10, 2025

Conversation

bparrishMines
Copy link
Contributor

@bparrishMines bparrishMines commented Feb 4, 2025

  • Adds an error message when a ProxyAPI callback method that returns a non-null value is nullable. Returning a null value causes error on the platform side. This ensures the Dart side prevents this from happening.

  • Adds an error message in the ProxyApiBaseCodec when an instance could not be retrieved when reading a value.

Pre-launch Checklist

If you need help, consider asking for advice on the #hackers-new channel on Discord.

@github-actions github-actions bot removed the p: interactive_media_ads Plugin for IMA SDK label Feb 4, 2025
@bparrishMines bparrishMines changed the title improve error messaging [pigeon] Add errors for ProxyAPI callback methods and null instances when reading in ProxyApiBaseCodec Feb 4, 2025
@bparrishMines bparrishMines marked this pull request as ready for review February 5, 2025 17:50
@bparrishMines bparrishMines requested review from stuartmorgan and removed request for LouiseHsu and hellohuanlin February 5, 2025 17:50
@bparrishMines bparrishMines changed the title [pigeon] Add errors for ProxyAPI callback methods and null instances when reading in ProxyApiBaseCodec [pigeon] Add errors for ProxyAPI callback methods and null instances when reading in a ProxyApiBaseCodec Feb 5, 2025
@@ -3095,64 +3060,12 @@ abstract class PigeonApiProxyApiTestClass(
Result.success(Unit)
Copy link
Contributor

Choose a reason for hiding this comment

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

I just noticed this being different from the other paths; is this a latent bug? Shouldn't it be callback(Result(success(Unit))?

Copy link
Contributor

Choose a reason for hiding this comment

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

Looking at the whole method: does this really need to take a callback at all? It seems like it could just directly return Result<Unit>, which would have prevented both of these bugs.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It mainly takes a callback because the code that generates the MethodCall is reused from the FlutterApi generation. I think removing the callback would require me to write separate code for the MethodCall.

Copy link
Contributor

Choose a reason for hiding this comment

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

It's using _writeFlutterMethod, which is a thin wrapper around two functions, the declaration part of which is already parameterized as to whether or not it's async, so unless there are details I'm missing it should be as easy as just re-exposing isAsynchronous (as a default-true argument) in _writeFlutterMethod, and setting it to false for this call.

Copy link
Contributor Author

@bparrishMines bparrishMines Feb 7, 2025

Choose a reason for hiding this comment

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

_writeProxyApiNewInstanceMethod only uses _writeFlutterMethodMessageCall and not _writeFlutterMethod. And _writeFlutterMethodMessageCall always assumes that the Flutter method uses a completion. All methods in FlutterApis are async from the platform side.

Copy link
Contributor

Choose a reason for hiding this comment

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

I was looking at the Kotlin generator (I assumed the Swift generator had the same structure), where _writeProxyApiNewInstanceMethod is almost entirely a call to _writeFlutterMethod, so maybe this is easy for Kotlin but not Swift.

We can leave this for now though since the PR is still an improvement, even if it leaves the potential for a new version of this bug in the future.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Whoops I didn't realize this comment was made on a different file than the one below. But it looks like the _writeFlutterMethod in the Kotlin generator contains onWriteBody, which gives the method more flexibility than the swift one. I think the main problem is that the method parameters for the method signature is different than the parameters passed in the MethodCall. Basically _writeProxyApiNewInstanceMethod does

void method(MyClass newInstance) {
  // Check if a new dart instance needs be created and return if not.

  // Retrieve all the field values needed by the Dart instance. This part is not included in a typical Flutter method.

  // use _writeFlutterMethodMessageCall with all the field values from above
}

There's probably a better way to do this and make it clearer that this is happening.

@@ -2853,64 +2820,13 @@ final class PigeonApiProxyApiTestClass: PigeonApiProtocolProxyApiTestClass {
completion(.success(Void()))
return
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Same structural question for this whole method (I can't comment on the method declaration since it's outside of the change context): could this have a simple return instead of a completion, reducing the complexity?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I changed the code to use a if...else if...else, so it no longer needs a return.

Copy link
Contributor

Choose a reason for hiding this comment

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

The issue isn't the return, it's that (as the Kotlin bug demonstrated) having a function use an async callback is inherently more error-prone, because the compiler will enforce that you return exactly once, but callbacks don't. Since we don't need any async behavior here, the method would be safer if it used a return value instead of a callback.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, but technically the method is making a async call when it makes the MethodCall to create the Dart instance. However, the case that the MethodCall is replaced by the error, the method probably could just return a Result. Were you suggesting that the method should return a Result when its not actually making a method call?

Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not following where there's an async step in this method, looking at the code. The structure of pigeonNewInstance is:

func pigeonNewInstance(
    pigeonInstance: ProxyApiTestClass,
    completion: @escaping (Result<Void, ProxyApiTestsError>) -> Void
  ) {
    if pigeonRegistrar.ignoreCallsToDart {
      completion(...)
    } else if pigeonRegistrar.instanceManager.containsInstance(pigeonInstance as AnyObject) {
      completion(...)
    } else {
      completion(...)
    }
  }

In every branch, it's just synchronously calling completion. I don't see any reason (other than the logistics of the generator logic helper function discussed in the other thread) it couldn't be

pigeonNewInstance(pigeonInstance: ProxyApiTestClass) -> Result<Void, ProxyApiTestsError>

with every completion(...) replaced with a return ....

Copy link
Contributor Author

@bparrishMines bparrishMines Feb 10, 2025

Choose a reason for hiding this comment

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

For most classes, the final completion is done inside of a MethodCall:

  } else {
      let pigeonIdentifierArg = pigeonRegistrar.instanceManager.addHostCreatedInstance(
        pigeonInstance as AnyObject)
      let binaryMessenger = pigeonRegistrar.binaryMessenger
      let codec = pigeonRegistrar.codec
      let channelName: String =
        "dev.flutter.pigeon.pigeon_integration_tests.ProxyApiSuperClass.pigeon_newInstance"
      let channel = FlutterBasicMessageChannel(
        name: channelName, binaryMessenger: binaryMessenger, codec: codec)
      channel.sendMessage([pigeonIdentifierArg] as [Any?]) { response in
        guard let listResponse = response as? [Any?] else {
          completion(.failure(createConnectionError(withChannelName: channelName)))
          return
        }
        if listResponse.count > 1 {
          let code: String = listResponse[0] as! String
          let message: String? = nilOrValue(listResponse[1])
          let details: String? = nilOrValue(listResponse[2])
          completion(.failure(ProxyApiTestsError(code: code, message: message, details: details)))
        } else {
          completion(.success(()))
        }
      }
    }

Its based on whether the Dart class has required callback methods to be implemented. If yes, this final completion synchronously returns an error.

Copy link
Contributor

Choose a reason for hiding this comment

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

Oh, I see now. Sorry about that; I missed that there were two different forms of it.

Copy link
Contributor

@stuartmorgan stuartmorgan left a comment

Choose a reason for hiding this comment

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

LGTM with a nit about a stray return.

@@ -3089,70 +3054,17 @@ abstract class PigeonApiProxyApiTestClass(
callback(
Result.failure(
ProxyApiTestsError("ignore-calls-error", "Calls to Dart are being ignored.", "")))
} else if (pigeonRegistrar.instanceManager.containsInstance(pigeon_instanceArg)) {
callback(Result.success(Unit))
return
Copy link
Contributor

Choose a reason for hiding this comment

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

This return shouldn't be generated any more; with the change to the if/else if/else structure it's not needed, and it's inconsistent with the other branches.

() {
indent.writeln('Result.success(Unit)');
indent.writeln('callback(Result.success(Unit))');
indent.writeln('return');
Copy link
Contributor

Choose a reason for hiding this comment

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

I think this is corresponding generator line?

Copy link
Contributor

@tarrinneal tarrinneal left a comment

Choose a reason for hiding this comment

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

tiny nit, optional really

Comment on lines 1917 to 1930
'} ',
() {
indent.format(
'''
callback(
Result.failure(
$errorClassName("ignore-calls-error", "Calls to Dart are being ignored.", "")))
return''',
$errorClassName("ignore-calls-error", "Calls to Dart are being ignored.", "")))''',
);
},
addTrailingNewline: false,
);
indent.writeScoped(
'if (pigeonRegistrar.instanceManager.containsInstance(${classMemberNamePrefix}instanceArg)) {',
'}',
'else if (pigeonRegistrar.instanceManager.containsInstance(${classMemberNamePrefix}instanceArg)) {',
'} ',
Copy link
Contributor

Choose a reason for hiding this comment

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

Pretty minor nit, but I think it's better to put the spaces before the else rather than after the } since the scoped statement doesn't need it, but the else statement does.

@@ -2310,71 +2313,78 @@ private func nilOrValue<T>(_ value: Any?) -> T? {
indent.writeScoped('$methodSignature {', '}', () {
indent.writeScoped(
'if pigeonRegistrar.ignoreCallsToDart {',
'}',
'} ',
Copy link
Contributor

Choose a reason for hiding this comment

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

here too

Comment on lines 2331 to 2332
'else if pigeonRegistrar.instanceManager.containsInstance(pigeonInstance as AnyObject) {',
'} ',
Copy link
Contributor

Choose a reason for hiding this comment

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

here

@bparrishMines bparrishMines added the autosubmit Merge PR when tree becomes green via auto submit App label Feb 10, 2025
@auto-submit auto-submit bot merged commit 94ce998 into flutter:main Feb 10, 2025
82 checks passed
@bparrishMines bparrishMines deleted the pigeon_require_nonnull branch February 10, 2025 22:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
autosubmit Merge PR when tree becomes green via auto submit App p: pigeon platform-android platform-ios platform-macos
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants