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] Empty generated Dart classes aren't serializable #160501

Open
feduke-nukem opened this issue Dec 18, 2024 · 7 comments · May be fixed by flutter/packages#8337
Open

[pigeon] Empty generated Dart classes aren't serializable #160501

feduke-nukem opened this issue Dec 18, 2024 · 7 comments · May be fixed by flutter/packages#8337
Labels
found in release: 3.27 Found to occur in 3.27 found in release: 3.28 Found to occur in 3.28 has reproducible steps The issue has been confirmed reproducible and is ready to work on p: pigeon related to pigeon messaging codegen tool P2 Important issues not at the top of the work list package flutter/packages repository. See also p: labels. team-ecosystem Owned by Ecosystem team triaged-ecosystem Triaged by Ecosystem team

Comments

@feduke-nukem
Copy link
Contributor

feduke-nukem commented Dec 18, 2024

Steps to reproduce

  • create pigeon/messages.dart:

  • Define sealed class A

  • Define B which extends A with some fields

  • Define C which extends A without any fields

  • run flutter pub run pigeon --input ./pigeon/messages.dart

  • get generated file src/messages.g.dart

Expected results

C.decode/C.encode is generated and we got no errors

Actual results

C.decode/C.encode was not generated, however B.decode/B.encode was.

// Autogenerated from Pigeon (v22.7.0), do not edit directly.
// See also: https://pub.dev/packages/pigeon
// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import, no_leading_underscores_for_local_identifiers

import 'dart:async';
import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List;

import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer;
import 'package:flutter/services.dart';

PlatformException _createConnectionError(String channelName) {
  return PlatformException(
    code: 'channel-error',
    message: 'Unable to establish connection on channel: "$channelName".',
  );
}

sealed class A {
}

class B extends A {
  B({
    this.number,
    this.message,
  });

  int? number;

  String? message;

  Object encode() {
    return <Object?>[
      number,
      message,
    ];
  }

  static B decode(Object result) {
    result as List<Object?>;
    return B(
      number: result[0] as int?,
      message: result[1] as String?,
    );
  }
}

class C extends A { <----- no decode/encode
}


class _PigeonCodec extends StandardMessageCodec {
  const _PigeonCodec();
  @override
  void writeValue(WriteBuffer buffer, Object? value) {
    if (value is int) {
      buffer.putUint8(4);
      buffer.putInt64(value);
    }    else if (value is B) {
      buffer.putUint8(129);
      writeValue(buffer, value.encode());
    }    else if (value is C) {
      buffer.putUint8(130);
      writeValue(buffer, value.encode()); <----- not generated, error
    } else {
      super.writeValue(buffer, value);
    }
  }

  @override
  Object? readValueOfType(int type, ReadBuffer buffer) {
    switch (type) {
      case 129: 
        return B.decode(readValue(buffer)!);
      case 130: 
        return C.decode(readValue(buffer)!); <----- not generated, error
      default:
        return super.readValueOfType(type, buffer);
    }
  }
}

class SomeHostApi {
  /// Constructor for [SomeHostApi].  The [binaryMessenger] named argument is
  /// available for dependency injection.  If it is left null, the default
  /// BinaryMessenger will be used which routes to the host platform.
  SomeHostApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''})
      : pigeonVar_binaryMessenger = binaryMessenger,
        pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : '';
  final BinaryMessenger? pigeonVar_binaryMessenger;

  static const MessageCodec<Object?> pigeonChannelCodec = _PigeonCodec();

  final String pigeonVar_messageChannelSuffix;

  Future<C> getC(C c) async {
    final String pigeonVar_channelName = 'dev.flutter.pigeon.pigeon_sealed_plugin_example.SomeHostApi.getC$pigeonVar_messageChannelSuffix';
    final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
      pigeonVar_channelName,
      pigeonChannelCodec,
      binaryMessenger: pigeonVar_binaryMessenger,
    );
    final List<Object?>? pigeonVar_replyList =
        await pigeonVar_channel.send(<Object?>[c]) as List<Object?>?;
    if (pigeonVar_replyList == null) {
      throw _createConnectionError(pigeonVar_channelName);
    } else if (pigeonVar_replyList.length > 1) {
      throw PlatformException(
        code: pigeonVar_replyList[0]! as String,
        message: pigeonVar_replyList[1] as String?,
        details: pigeonVar_replyList[2],
      );
    } else if (pigeonVar_replyList[0] == null) {
      throw PlatformException(
        code: 'null-error',
        message: 'Host platform returned null value for non-null return value.',
      );
    } else {
      return (pigeonVar_replyList[0] as C?)!;
    }
  }

  Future<A> getA(A a) async {
    final String pigeonVar_channelName = 'dev.flutter.pigeon.pigeon_sealed_plugin_example.SomeHostApi.getA$pigeonVar_messageChannelSuffix';
    final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
      pigeonVar_channelName,
      pigeonChannelCodec,
      binaryMessenger: pigeonVar_binaryMessenger,
    );
    final List<Object?>? pigeonVar_replyList =
        await pigeonVar_channel.send(<Object?>[a]) as List<Object?>?;
    if (pigeonVar_replyList == null) {
      throw _createConnectionError(pigeonVar_channelName);
    } else if (pigeonVar_replyList.length > 1) {
      throw PlatformException(
        code: pigeonVar_replyList[0]! as String,
        message: pigeonVar_replyList[1] as String?,
        details: pigeonVar_replyList[2],
      );
    } else if (pigeonVar_replyList[0] == null) {
      throw PlatformException(
        code: 'null-error',
        message: 'Host platform returned null value for non-null return value.',
      );
    } else {
      return (pigeonVar_replyList[0] as A?)!;
    }
  }
}

class SomeFlutterApi {
  /// Constructor for [SomeFlutterApi].  The [binaryMessenger] named argument is
  /// available for dependency injection.  If it is left null, the default
  /// BinaryMessenger will be used which routes to the host platform.
  SomeFlutterApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''})
      : pigeonVar_binaryMessenger = binaryMessenger,
        pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : '';
  final BinaryMessenger? pigeonVar_binaryMessenger;

  static const MessageCodec<Object?> pigeonChannelCodec = _PigeonCodec();

  final String pigeonVar_messageChannelSuffix;

  Future<C> getC(C c) async {
    final String pigeonVar_channelName = 'dev.flutter.pigeon.pigeon_sealed_plugin_example.SomeFlutterApi.getC$pigeonVar_messageChannelSuffix';
    final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
      pigeonVar_channelName,
      pigeonChannelCodec,
      binaryMessenger: pigeonVar_binaryMessenger,
    );
    final List<Object?>? pigeonVar_replyList =
        await pigeonVar_channel.send(<Object?>[c]) as List<Object?>?;
    if (pigeonVar_replyList == null) {
      throw _createConnectionError(pigeonVar_channelName);
    } else if (pigeonVar_replyList.length > 1) {
      throw PlatformException(
        code: pigeonVar_replyList[0]! as String,
        message: pigeonVar_replyList[1] as String?,
        details: pigeonVar_replyList[2],
      );
    } else if (pigeonVar_replyList[0] == null) {
      throw PlatformException(
        code: 'null-error',
        message: 'Host platform returned null value for non-null return value.',
      );
    } else {
      return (pigeonVar_replyList[0] as C?)!;
    }
  }

  Future<A> getA(A a) async {
    final String pigeonVar_channelName = 'dev.flutter.pigeon.pigeon_sealed_plugin_example.SomeFlutterApi.getA$pigeonVar_messageChannelSuffix';
    final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
      pigeonVar_channelName,
      pigeonChannelCodec,
      binaryMessenger: pigeonVar_binaryMessenger,
    );
    final List<Object?>? pigeonVar_replyList =
        await pigeonVar_channel.send(<Object?>[a]) as List<Object?>?;
    if (pigeonVar_replyList == null) {
      throw _createConnectionError(pigeonVar_channelName);
    } else if (pigeonVar_replyList.length > 1) {
      throw PlatformException(
        code: pigeonVar_replyList[0]! as String,
        message: pigeonVar_replyList[1] as String?,
        details: pigeonVar_replyList[2],
      );
    } else if (pigeonVar_replyList[0] == null) {
      throw PlatformException(
        code: 'null-error',
        message: 'Host platform returned null value for non-null return value.',
      );
    } else {
      return (pigeonVar_replyList[0] as A?)!;
    }
  }
}

Code sample

Code sample
import 'package:pigeon/pigeon.dart';

// #docregion config
@ConfigurePigeon(
  PigeonOptions(
    dartOut: 'lib/src/messages.g.dart',
    dartOptions: DartOptions(),
    kotlinOut:
        'android/src/main/kotlin/com/pigeonsealedexample/pigeon_sealed_plugin/Messages.g.kt',
    kotlinOptions:
        KotlinOptions(package: 'com.pigeonsealedexample.somepackage'),
    swiftOut: 'ios/Classes/Messages.g.swift',
    swiftOptions: SwiftOptions(),
    dartPackageName: 'pigeon_sealed_plugin_example',
  ),
)
sealed class A {
  A();
}

class B extends A {
  final int? number;
  final String? message;

  B({
    required this.number,
    this.message,
  });
}

class C extends A {
  C();
}

@HostApi()
abstract class SomeHostApi {
  C getC(C c);

  A getA(A a);
}

@HostApi()
abstract class SomeFlutterApi {
  C getC(C c);

  A getA(A a);
}

Screenshots or Video

Screenshots / Video demonstration

[Upload media here]

Logs

Logs
Deprecated. Use `dart run` instead.
Building package executable... 
Built pigeon:pigeon.

Flutter Doctor output

Doctor output
Doctor summary (to see all details, run flutter doctor -v):
[!] Flutter (Channel master, 3.26.0-1.0.pre.168, on macOS 14.5 23F79 darwin-arm64, locale en-RU)
    ! Warning: `dart` on your path resolves to /opt/homebrew/Cellar/dart/3.5.2/libexec/bin/dart, which is not inside your current Flutter SDK checkout at /Users/feduke-nukem/Desktop/git-projects/flutter/flutter. Consider adding
      /Users/feduke-nukem/Desktop/git-projects/flutter/flutter/bin to the front of your path.
    ! Upstream repository https://github.com/feduke-nukem/flutter is not a standard remote.
      Set environment variable "FLUTTER_GIT_URL" to https://github.com/feduke-nukem/flutter to dismiss this error.
[!] Android toolchain - develop for Android devices (Android SDK version 34.0.0)
    ! Some Android licenses not accepted. To resolve this, run: flutter doctor --android-licenses
[✓] Xcode - develop for iOS and macOS (Xcode 15.4)
[✓] Chrome - develop for the web
[✓] Android Studio (version 2023.3)
[✓] VS Code (version 1.96.0)
[✓] Connected device (6 available)
    ! Error: Browsing on the local area network for iPhone (Анна). Ensure the device is unlocked and attached with a cable or associated with the same local area network as this Mac.
      The device must be opted into Developer Mode to connect wirelessly. (code -27)
[✓] Network resources
@feduke-nukem

This comment was marked as off-topic.

@feduke-nukem
Copy link
Contributor Author

Relates to #153995

@stuartmorgan
Copy link
Contributor

Please do not ping individual people when filling issues; we have a triage process.

@feduke-nukem

This comment was marked as off-topic.

@darshankawar darshankawar added the in triage Presently being triaged by the triage team label Dec 19, 2024
@darshankawar
Copy link
Member

I was able to replicate the reported behavior, wherein the generated messages.g.dart file shows below:

Screenshot 2024-12-19 at 12 23 54 PM Screenshot 2024-12-19 at 12 24 12 PM
stable : 3.27.1
master : 3.28.0-1.0.pre.87

@darshankawar darshankawar added package flutter/packages repository. See also p: labels. has reproducible steps The issue has been confirmed reproducible and is ready to work on p: pigeon related to pigeon messaging codegen tool found in release: 3.27 Found to occur in 3.27 found in release: 3.28 Found to occur in 3.28 team-ecosystem Owned by Ecosystem team and removed in triage Presently being triaged by the triage team labels Dec 19, 2024
@tarrinneal
Copy link

This is actually a result of the class having no fields.

Just for my understanding and to guide future work, what is the use case of having the empty class?

@feduke-nukem
Copy link
Contributor Author

feduke-nukem commented Dec 20, 2024

This is actually a result of the class having no fields.

Just for my understanding and to guide future work, what is the use case of having the empty class?

Well, first of all, Kotlin and Swift do not have such problem:

sealed class A 
/** Generated class from Pigeon that represents data sent in messages. */
data class B (
  val number: Long? = null,
  val message: String? = null
) : A()
 {
  companion object {
    fun fromList(pigeonVar_list: List<Any?>): B {
      val number = pigeonVar_list[0] as Long?
      val message = pigeonVar_list[1] as String?
      return B(number, message)
    }
  }
  fun toList(): List<Any?> {
    return listOf(
      number,
      message,
    )
  }
}

/** Generated class from Pigeon that represents data sent in messages. */
data class C (
) : A()
 {
  companion object {
    fun fromList(pigeonVar_list: List<Any?>): C {
      return C()
    }
  }
  fun toList(): List<Any?> {
    return listOf(
    )
  }
}
private open class MessagesPigeonCodec : StandardMessageCodec() {
  override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? {
    return when (type) {
      129.toByte() -> {
        return (readValue(buffer) as? List<Any?>)?.let {
          B.fromList(it)
        }
      }
      130.toByte() -> {
        return (readValue(buffer) as? List<Any?>)?.let {
          C.fromList(it)
        }
      }
      else -> super.readValueOfType(type, buffer)
    }
  }
  override fun writeValue(stream: ByteArrayOutputStream, value: Any?)   {
    when (value) {
      is B -> {
        stream.write(129)
        writeValue(stream, value.toList())
      }
      is C -> {
        stream.write(130)
        writeValue(stream, value.toList())
      }
      else -> super.writeValue(stream, value)
    }
  }
}
protocol A {

}

/// Generated class from Pigeon that represents data sent in messages.
struct B: A {
  var number: Int64? = nil
  var message: String? = nil


  // swift-format-ignore: AlwaysUseLowerCamelCase
  static func fromList(_ pigeonVar_list: [Any?]) -> B? {
    let number: Int64? = nilOrValue(pigeonVar_list[0])
    let message: String? = nilOrValue(pigeonVar_list[1])

    return B(
      number: number,
      message: message
    )
  }
  func toList() -> [Any?] {
    return [
      number,
      message,
    ]
  }
}

/// Generated class from Pigeon that represents data sent in messages.
struct C: A {


  // swift-format-ignore: AlwaysUseLowerCamelCase
  static func fromList(_ pigeonVar_list: [Any?]) -> C? {

    return C(
    )
  }
  func toList() -> [Any?] {
    return [
    ]
  }
}

Secondly, the typical use case for sealed classes are events/states so let's image we are implementing custom video player. I would definitely have (especially with EventChannel support) something like:

sealed class VideoPlayerEvent {}

class VideoPlayerStarted extends VideoPlayerEvent {}

class VideoPlayerPaused extends VideoPlayerEvent {}

class VideoPlayerStopped extends VideoPlayerEvent {}

class VideoPlayerResumed extends VideoPlayerEvent {}

class VideoPlayerSeek extends VideoPlayerEvent {
  final int positionUs;

  VideoPlayerSeek({required this.positionUs});
}

As you can see - I don't need properties/fields for all of these classes.

In my opinion, described behaviour in this issue is unexpected and should not limit user for specific implementations. Even if I want all my classes to have no fields - it should work as it works for Kotlin/Swift.

If it is expected behaviour - Pigeon should throw exception with verbose description of a reason why user shouldn't generate empty classes (can't imagine such reason)

@stuartmorgan stuartmorgan added P2 Important issues not at the top of the work list triaged-ecosystem Triaged by Ecosystem team labels Dec 22, 2024
@stuartmorgan stuartmorgan changed the title [pigeon] No encode/decode generated dart class (22.7.0 version, sealed class) [pigeon] Empty generated Dart classes aren't serializable Dec 22, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
found in release: 3.27 Found to occur in 3.27 found in release: 3.28 Found to occur in 3.28 has reproducible steps The issue has been confirmed reproducible and is ready to work on p: pigeon related to pigeon messaging codegen tool P2 Important issues not at the top of the work list package flutter/packages repository. See also p: labels. team-ecosystem Owned by Ecosystem team triaged-ecosystem Triaged by Ecosystem team
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants