Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
82065a8
Improve efficiency in some methods
tarrinneal Feb 28, 2026
9861c00
analyze this
tarrinneal Feb 28, 2026
11daedd
format
tarrinneal Mar 2, 2026
e197fc0
less repeating, and less listing
tarrinneal Mar 3, 2026
cbf4096
allow individual class types to not have colliding hashes
tarrinneal Mar 3, 2026
935aa47
improve deep equals for non-collections
tarrinneal Mar 3, 2026
4e9541f
allowing for NaN or other cases where identical is better
tarrinneal Mar 3, 2026
f30b44c
more tests, add other languages, unify behavior across platforms
tarrinneal Mar 3, 2026
08a79f3
first pass at gobject
tarrinneal Mar 3, 2026
8cb51ac
Merge branch 'main' of https://github.com/flutter/packages into effic
tarrinneal Mar 3, 2026
ddc41c2
analyze
tarrinneal Mar 3, 2026
4ad3636
delete random extra files
tarrinneal Mar 3, 2026
0a08ef5
fix kotlin generator change that broke unit test
tarrinneal Mar 3, 2026
5855b23
fix broken gobject and cpp code
tarrinneal Mar 4, 2026
f99e64a
more bugs on languages I can't compile locally
tarrinneal Mar 4, 2026
101fb0f
last few issues I hope
tarrinneal Mar 4, 2026
7309d17
not being able to run things locally sucks
tarrinneal Mar 4, 2026
24d0f23
bigobj
tarrinneal Mar 4, 2026
21ad393
consolidate tests
tarrinneal Mar 4, 2026
94a58c8
revert project files
tarrinneal Mar 4, 2026
3fdd987
More consistency across languages
tarrinneal Mar 4, 2026
b920f50
revert -0.0 changes
tarrinneal Mar 4, 2026
7df481e
gen example
tarrinneal Mar 5, 2026
25021e7
Merge branch 'main' of https://github.com/flutter/packages into effic
tarrinneal Mar 5, 2026
4cbfdcd
memecmp
tarrinneal Mar 5, 2026
8c75a36
re-normalize -0.0 in hash methods to create consistent behavior acros…
tarrinneal Mar 5, 2026
655229c
finish tests
tarrinneal Mar 5, 2026
80d8bbc
lints and reverting of accidentally pushed files
tarrinneal Mar 5, 2026
a57dd6f
hopeful test fix
tarrinneal Mar 5, 2026
4ded545
delete file that shouldn't ever have existed
tarrinneal Mar 5, 2026
8698cb5
more tests and windows method rework
tarrinneal Mar 5, 2026
7355a96
bug fixes and more robust map==
tarrinneal Mar 5, 2026
45d8207
fix linix
tarrinneal Mar 5, 2026
97b8e8c
one last linux fix
tarrinneal Mar 5, 2026
38cffb2
nits and nats
tarrinneal Mar 6, 2026
a2a80b5
dentist has been busy today
tarrinneal Mar 11, 2026
b5b4506
Merge branch 'main' of https://github.com/flutter/packages into effic
tarrinneal Mar 11, 2026
1305a1d
fix std::map hash
tarrinneal Mar 11, 2026
a5ce9cc
even more better
tarrinneal Mar 11, 2026
6d92541
Merge branch 'main' of https://github.com/flutter/packages into effic
tarrinneal Mar 11, 2026
eba918b
revert dumb change
tarrinneal Mar 11, 2026
5097bdc
namespace
tarrinneal Mar 11, 2026
0a5adee
Triple checked methods
tarrinneal Mar 12, 2026
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
6 changes: 6 additions & 0 deletions packages/pigeon/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
## 26.3.0

* Optimizes and improves data class equality and hashing.
* Changes hashing and equality methods to behave consistently across platforms.
* Adds equality methods to previously unsupported languages.

## 26.2.3

* Produces a helpful error message when a method return type is missing or an
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,171 @@
import java.lang.annotation.Target;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;

/** Generated class from Pigeon. */
@SuppressWarnings({"unused", "unchecked", "CodeBlock2Expr", "RedundantSuppression", "serial"})
public class Messages {
static boolean pigeonDoubleEquals(double a, double b) {
// Normalize -0.0 to 0.0 and handle NaN equality.
return (a == 0.0 ? 0.0 : a) == (b == 0.0 ? 0.0 : b) || (Double.isNaN(a) && Double.isNaN(b));
}

static boolean pigeonFloatEquals(float a, float b) {
// Normalize -0.0 to 0.0 and handle NaN equality.
return (a == 0.0f ? 0.0f : a) == (b == 0.0f ? 0.0f : b) || (Float.isNaN(a) && Float.isNaN(b));
}

static int pigeonDoubleHashCode(double d) {
// Normalize -0.0 to 0.0 and handle NaN to ensure consistent hash codes.
if (d == 0.0) {
d = 0.0;
}
long bits = Double.doubleToLongBits(d);
return (int) (bits ^ (bits >>> 32));
}

static int pigeonFloatHashCode(float f) {
// Normalize -0.0 to 0.0 and handle NaN to ensure consistent hash codes.
if (f == 0.0f) {
f = 0.0f;
}
return Float.floatToIntBits(f);
}

static boolean pigeonDeepEquals(Object a, Object b) {
if (a == b) {
return true;
}
if (a == null || b == null) {
return false;
}
if (a instanceof byte[] && b instanceof byte[]) {
return Arrays.equals((byte[]) a, (byte[]) b);
}
if (a instanceof int[] && b instanceof int[]) {
return Arrays.equals((int[]) a, (int[]) b);
}
if (a instanceof long[] && b instanceof long[]) {
return Arrays.equals((long[]) a, (long[]) b);
}
if (a instanceof double[] && b instanceof double[]) {
double[] da = (double[]) a;
double[] db = (double[]) b;
if (da.length != db.length) {
return false;
}
for (int i = 0; i < da.length; i++) {
if (!pigeonDoubleEquals(da[i], db[i])) {
return false;
}
}
return true;
}
if (a instanceof List && b instanceof List) {
List<?> listA = (List<?>) a;
List<?> listB = (List<?>) b;
if (listA.size() != listB.size()) {
return false;
}
for (int i = 0; i < listA.size(); i++) {
if (!pigeonDeepEquals(listA.get(i), listB.get(i))) {
return false;
}
}
return true;
}
if (a instanceof Map && b instanceof Map) {
Map<?, ?> mapA = (Map<?, ?>) a;
Map<?, ?> mapB = (Map<?, ?>) b;
if (mapA.size() != mapB.size()) {
return false;
}
for (Map.Entry<?, ?> entryA : mapA.entrySet()) {
Object keyA = entryA.getKey();
Object valueA = entryA.getValue();
boolean found = false;
for (Map.Entry<?, ?> entryB : mapB.entrySet()) {
Object keyB = entryB.getKey();
if (pigeonDeepEquals(keyA, keyB)) {
Object valueB = entryB.getValue();
if (pigeonDeepEquals(valueA, valueB)) {
found = true;
break;
} else {
return false;
}
}
}
if (!found) {
return false;
}
}
return true;
}
if (a instanceof Double && b instanceof Double) {
return pigeonDoubleEquals((double) a, (double) b);
}
if (a instanceof Float && b instanceof Float) {
return pigeonFloatEquals((float) a, (float) b);
}
return a.equals(b);
}

static int pigeonDeepHashCode(Object value) {
if (value == null) {
return 0;
}
if (value instanceof byte[]) {
return Arrays.hashCode((byte[]) value);
}
if (value instanceof int[]) {
return Arrays.hashCode((int[]) value);
}
if (value instanceof long[]) {
return Arrays.hashCode((long[]) value);
}
if (value instanceof double[]) {
double[] da = (double[]) value;
int result = 1;
for (double d : da) {
result = 31 * result + pigeonDoubleHashCode(d);
}
return result;
}
if (value instanceof List) {
int result = 1;
for (Object item : (List<?>) value) {
result = 31 * result + pigeonDeepHashCode(item);
}
return result;
}
if (value instanceof Map) {
int result = 0;
for (Map.Entry<?, ?> entry : ((Map<?, ?>) value).entrySet()) {
result +=
((pigeonDeepHashCode(entry.getKey()) * 31) ^ pigeonDeepHashCode(entry.getValue()));
}
return result;
}
if (value instanceof Object[]) {
int result = 1;
for (Object item : (Object[]) value) {
result = 31 * result + pigeonDeepHashCode(item);
}
return result;
}
if (value instanceof Double) {
return pigeonDoubleHashCode((double) value);
}
if (value instanceof Float) {
return pigeonFloatHashCode((float) value);
}
return value.hashCode();
}

/** Error class for passing custom error details to Flutter via a thrown PlatformException. */
public static class FlutterError extends RuntimeException {
Expand Down Expand Up @@ -142,15 +299,16 @@ public boolean equals(Object o) {
return false;
}
MessageData that = (MessageData) o;
return Objects.equals(name, that.name)
&& Objects.equals(description, that.description)
&& code.equals(that.code)
&& data.equals(that.data);
return pigeonDeepEquals(name, that.name)
&& pigeonDeepEquals(description, that.description)
&& pigeonDeepEquals(code, that.code)
&& pigeonDeepEquals(data, that.data);
Copy link
Collaborator

Choose a reason for hiding this comment

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

Capturing for posterity, this has the same inefficiency as the previous iteration, where we are feeding known-type values through a big type-inspection chain instead of dispatching to specific-type equality checkers.

Not something we need to address now since we still mostly expect these to be used in tests where efficiency doesn't matter, but something to keep in mind later if it performance ever comes up.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This doesn't actually, any non-collections well return first before any type checking. Even collections are only checking for collection types.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You're probably thinking about the codec, which I intend to change so known types bypass the type checking soon(tm)

Copy link
Collaborator

Choose a reason for hiding this comment

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

This doesn't actually, any non-collections well return first before any type checking.

I'm not following. The first line in this statement is calling pigeonDeepEquals with two strings. We know at compile time they are strings. But we are calling pigeonDeepEquals for them which will do runtime checks for whether they are:

  • byte arrays
  • int arrays
  • long arrays
  • double arrays
  • Lists
  • Maps
  • Doubles
  • Floats

and only then, after all of that, check that the strings are equal.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

the first thing pigeon deep equals does is return if identical

Copy link
Collaborator

Choose a reason for hiding this comment

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

Right, but that's only pointer equality for most types. For two instances of "Foo", a == b at the top is false, only a.equals(b) at the very end is true, after all the type checks. Similarly, two Map instance variables that aren't actually the same map, but will evaluate to the same under the deep equality evaluation, will go through 5 pointless type checks first. Similarly with List. Similarly with Double and Float.

Basically, in any case where we actually need all this logic in the first place and can't just rely on == alone, we are doing a bunch of type checks.

Copy link
Contributor Author

@tarrinneal tarrinneal Mar 11, 2026

Choose a reason for hiding this comment

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

For two instances of "Foo", a == b at the top is false, only a.equals(b) at the very end is true

That is a detail I missed. I've resolved this in every language (that I can).

My future work for changing the codec calls to skip the type checking (this will be relevant once native interop lands) will change this to avoid type checking also.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I don't think the quick fix of moving the non-trivial equality check to the top is something we want to do, because it may be non-trivially expensive, and duplicative of specific checks. E.g., I'm almost positive this will compare non-equal strings twice in Obj-C instead of once, which is potentially more expensive than the thing I was describing in the initial comment.

The fix here (which again, doesn't need to be done now, I just want to capture it for later reference) isn't to change how pigeonDeepEquals works, it's to not generate calls to pigeonDeepEquals in the first place when we already know the type. Instead, all the specific checks should be helpers (pigeonMapDeepEquals, etc.), pigeonDeepEquals should be implemented using those helpers, and the generator for class equality should be calling the correct helper for each field instead of the generic helper.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That is true, reverted.

}

@Override
public int hashCode() {
return Objects.hash(name, description, code, data);
Object[] fields = new Object[] {getClass(), name, description, code, data};
return pigeonDeepHashCode(fields);
}

public static final class Builder {
Expand Down
Loading
Loading