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] Kotlin implementation for ProxyApis #6371

Open
wants to merge 32 commits into
base: main
Choose a base branch
from

Conversation

bparrishMines
Copy link
Contributor

@bparrishMines bparrishMines commented Mar 21, 2024

Kotlin portion of flutter/flutter#134777

Pre-launch Checklist

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

@bparrishMines bparrishMines changed the title Pigeon kotlin split [pigeon] Initial kotlin implementation for ProxyApis Mar 22, 2024
@bparrishMines bparrishMines marked this pull request as ready for review March 22, 2024 19:23
Comment on lines 370 to 426
abstract class PigeonProxyApiBaseCodec(
val binaryMessenger: BinaryMessenger,
val instanceManager: PigeonInstanceManager
) : StandardMessageCodec() {
/**
* An implementation of [PigeonApiProxyApiTestClass] used to add a new Dart instance of
* `ProxyApiTestClass` to the Dart `InstanceManager`.
*/
abstract fun getPigeonApiProxyApiTestClass(): PigeonApiProxyApiTestClass

/**
* An implementation of [PigeonApiProxyApiSuperClass] used to add a new Dart instance of
* `ProxyApiSuperClass` to the Dart `InstanceManager`.
*/
abstract fun getPigeonApiProxyApiSuperClass(): PigeonApiProxyApiSuperClass

/**
* An implementation of [PigeonApiProxyApiInterface] used to add a new Dart instance of
* `ProxyApiInterface` to the Dart `InstanceManager`.
*/
abstract fun getPigeonApiProxyApiInterface(): PigeonApiProxyApiInterface

fun setUpMessageHandlers() {
PigeonApiProxyApiTestClass.setUpMessageHandlers(
binaryMessenger, getPigeonApiProxyApiTestClass())
PigeonApiProxyApiSuperClass.setUpMessageHandlers(
binaryMessenger, getPigeonApiProxyApiSuperClass())
}

override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? {
return when (type) {
128.toByte() -> {
return instanceManager.getInstance(
readValue(buffer).let { if (it is Int) it.toLong() else it as Long })
}
else -> super.readValueOfType(type, buffer)
}
}

override fun writeValue(stream: ByteArrayOutputStream, value: Any?) {
if (value is ProxyApiTestClass) {
getPigeonApiProxyApiTestClass().pigeon_newInstance(value) {}
} else if (value is com.example.test_plugin.ProxyApiSuperClass) {
getPigeonApiProxyApiSuperClass().pigeon_newInstance(value) {}
} else if (value is ProxyApiInterface) {
getPigeonApiProxyApiInterface().pigeon_newInstance(value) {}
}

when {
instanceManager.containsInstance(value) -> {
stream.write(128)
writeValue(stream, instanceManager.getIdentifierForStrongReference(value))
}
else -> super.writeValue(stream, value)
}
}
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@camsim99 FYI

@stuartmorgan @tarrinneal This is one of the places where the design changes from the implementation in the doc. I wanted to improve the scenario of when a method returns a ProxyApi or a list/map of a ProxyApi:

class Basket {
  Apple giveMeAnApple();
  List<Apple> giveMeApples();
}

Typically we solve this by manually handling adding a new instance when we implement the api:

class PigeonApiBasketImpl {
  Apple giveMeAnApple(Basket instance) {
    final Apple apple = instance.giveMeAnApple();
    if (!instanceManager.containsInstance(apple)) {
      instanceManager.addHostCreatedInstance(apple);
    }

    return apple;
  }

  List<Apple> giveMeApples(Basket instance) {
    final List<Apple> apples = instance.giveMeApples();
    for (Apple apple in apples) {
      if (!instanceManager.containsInstance(apple)) {
        instanceManager.addHostCreatedInstance(apple);
      }
    }

    return apples;
  }
}

This change makes the codec the central class that contains all the proxy api implementations, so the writeValue can handle creating new dart instances automatically by calling pigeon_newInstance. And pigeon_newInstance would handle the logic shown in the methods above.

This also:

  • Creates a registrar like class that ensures a user implements each proxy api.
  • Provides access to a central class with every api implementation which prevents the need for multiple instances of the same api implementation.

Copy link
Contributor

Choose a reason for hiding this comment

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

This may end up overlapping with some of the codec work I'm about to do as well. I think it's generally a good idea to create a central codec though. We may end up with multiple codecs implementing each other.

Copy link
Contributor

Choose a reason for hiding this comment

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

My main concern at this point is that the number of custom inputs is limited to around 120, so if there are a lot of proxy apis, enums, and or data classes, we may not be able to accommodate them all.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

As of right now, the generation of the ProxyApi codec is separate from Host/Flutter APIs codecs. Mainly because:

  1. ProxyApis don't support data classes
  2. ProxyApis support other ProxyApis in the codec and Host/Flutter APIs don't.

So they currently don't really share anything. If you create a central custom codec for all the APIs, then ProxyApis should only need one dedicated spot that uses the InstanceManager: https://github.com/flutter/packages/blob/main/packages/pigeon/platform_tests/shared_test_plugin_code/lib/src/generated/proxy_api_tests.gen.dart#L374.

Also, as a side note, since all data classes have an encode/decode could you create a base data class for all the data classes and only use one spot. I realized this wouldn't work since the reader wouldn't know what class to call this on.

@tarrinneal
Copy link
Contributor

Are you planning to add the full implementation and test suite to this pr?

@bparrishMines
Copy link
Contributor Author

Are you planning to add the full implementation and test suite to this pr?

I wasn't planning on doing that for this PR, but I can if you prefer the full implementation. This PR just adds the Kotlin ProxyApi utility classes, including the InstanceManager and the tests for it. And also the bare minimum ProxyApi class declaration.

@bparrishMines bparrishMines changed the title [pigeon] Initial kotlin implementation for ProxyApis [pigeon] Kotlin implementation for ProxyApis Apr 16, 2024
@bparrishMines
Copy link
Contributor Author

@tarrinneal I updated the PR to have the full implementation and tests. Its ready for a review.

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.

quick little glance through, I think most of this pr is good as is. Any suggestions I've made here are generally open for discussion if you want to keep them.

@@ -4,6 +4,7 @@

import 'package:collection/collection.dart' show ListEquality;
import 'package:meta/meta.dart';
import 'kotlin_generator.dart';
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't love this being imported to this file. Can we move the kotlin options into the normal kotlin options class?

@@ -307,6 +307,12 @@ const String seeAlsoWarning = 'See also: https://pub.dev/packages/pigeon';
/// parameters.
const String classNamePrefix = 'Pigeon';

/// Prefix for apis generated for ProxyApi.
Copy link
Contributor

Choose a reason for hiding this comment

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

nit APIs

@@ -494,6 +500,48 @@ Map<TypeDeclaration, List<int>> getReferencedTypes(
return references.map;
}

/// Find the [TypeDeclaration] that has the highest api requirement and its
Copy link
Contributor

Choose a reason for hiding this comment

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

API

Comment on lines +31 to +35
private val identifiers = java.util.WeakHashMap<Any, Long>()
private val weakInstances = HashMap<Long, java.lang.ref.WeakReference<Any>>()
private val strongInstances = HashMap<Long, Any>()
private val referenceQueue = java.lang.ref.ReferenceQueue<Any>()
private val weakReferencesToIdentifiers = HashMap<java.lang.ref.WeakReference<Any>, Long>()
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 sure you've already figured out this is the best way, but why is it that we need the java Weak version of these data structures? Is there no kotlin version that's usable?

Comment on lines +285 to +320
fun setUpMessageHandlers(binaryMessenger: BinaryMessenger, instanceManager: $instanceManagerClassName?) {
run {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "$removeStrongReferenceName", codec)
if (instanceManager != null) {
channel.setMessageHandler { message, reply ->
val identifier = message as Number
val wrapped: List<Any?> = try {
instanceManager.remove<Any?>(identifier.toLong())
listOf<Any?>(null)
} catch (exception: Throwable) {
wrapError(exception)
}
reply.reply(wrapped)
}
} else {
channel.setMessageHandler(null)
}
}
run {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "$clearName", codec)
if (instanceManager != null) {
channel.setMessageHandler { _, reply ->
val wrapped: List<Any?> = try {
instanceManager.clear()
listOf<Any?>(null)
} catch (exception: Throwable) {
wrapError(exception)
}
reply.reply(wrapped)
}
} else {
channel.setMessageHandler(null)
}
}
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

a lot of this code is essentially duplicated with the host api. I'd prefer if that code could be gened from the same code. to avoid future work duplication

() {
indent.writeln('val instanceManager: PigeonInstanceManager');
indent.format(
'private var _codec: StandardMessageCodec? = null\n'
Copy link
Contributor

Choose a reason for hiding this comment

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

This will need to change to use the new codec - once the codec pr lands of course

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
2 participants