diff --git a/src/Engine/ProtoCore/FFI/CLRObjectMarshaler.cs b/src/Engine/ProtoCore/FFI/CLRObjectMarshaler.cs
index d9bc0b883a2..23a1d55e5e1 100644
--- a/src/Engine/ProtoCore/FFI/CLRObjectMarshaler.cs
+++ b/src/Engine/ProtoCore/FFI/CLRObjectMarshaler.cs
@@ -5,6 +5,7 @@
using System.IO;
using System.Linq;
using System.Reflection;
+using System.Runtime.CompilerServices;
using System.Xml;
using Autodesk.DesignScript.Interfaces;
using DesignScript.Builtin;
@@ -1394,8 +1395,17 @@ void core_Dispose(ProtoCore.RuntimeCore sender)
}
///
- /// This class compares two CLR objects. It is used in CLRObjectMap to
- /// avoid hash collision.
+ /// This class compares two CLR objects using reference equality. It is used in CLRObjectMap
+ /// to map CLR object instances to their marshaled StackValue representations. Uses
+ /// to get the object's identity hash code, which
+ /// ensures well-distributed hash codes even when objects have value-based hash codes that
+ /// collide (e.g., Point objects with identical coordinates).
+ ///
+ /// Note: The hash code computation is intentionally aligned with the reference
+ /// equality behavior used by . This ensures consistent
+ /// semantics between equality comparison and hash code generation, which is a requirement
+ /// for proper implementation.
+ ///
///
public class ReferenceEqualityComparer: IEqualityComparer
{
@@ -1406,7 +1416,7 @@ bool IEqualityComparer.Equals(object x, object y)
public int GetHashCode(object obj)
{
- return obj.GetHashCode();
+ return RuntimeHelpers.GetHashCode(obj);
}
}
diff --git a/test/Engine/ProtoTest/FFITests/ReferenceEqualityComparerTests.cs b/test/Engine/ProtoTest/FFITests/ReferenceEqualityComparerTests.cs
new file mode 100644
index 00000000000..c904e0906f4
--- /dev/null
+++ b/test/Engine/ProtoTest/FFITests/ReferenceEqualityComparerTests.cs
@@ -0,0 +1,140 @@
+using System;
+using System.Collections.Generic;
+using System.Runtime.CompilerServices;
+using NUnit.Framework;
+using ReferenceEqualityComparer = ProtoFFI.ReferenceEqualityComparer;
+
+namespace ProtoFFITests
+{
+ [TestFixture]
+ public class ReferenceEqualityComparerTests
+ {
+ ///
+ /// Test class that simulates geometry objects with value-based hash codes
+ /// (similar to Point objects with identical coordinates)
+ ///
+ private class TestPoint
+ {
+ public double X { get; set; }
+ public double Y { get; set; }
+ public double Z { get; set; }
+
+ private const double Epsilon = 1e-10;
+
+ public TestPoint(double x, double y, double z)
+ {
+ X = x;
+ Y = y;
+ Z = z;
+ }
+
+ // Value-based hash code (like Point.ComputeHashCode in LibG)
+ public override int GetHashCode()
+ {
+ int hash = 17;
+ hash = hash * 23 + X.GetHashCode();
+ hash = hash * 23 + Y.GetHashCode();
+ hash = hash * 23 + Z.GetHashCode();
+ return hash;
+ }
+
+ public override bool Equals(object obj)
+ {
+ if (obj == null || GetType() != obj.GetType())
+ return false;
+
+ var other = (TestPoint)obj;
+ return Math.Abs(X - other.X) < Epsilon
+ && Math.Abs(Y - other.Y) < Epsilon
+ && Math.Abs(Z - other.Z) < Epsilon;
+ }
+ }
+
+ [Test]
+ [Category("UnitTests")]
+ public void ProducesDifferentHashCodesForDifferentInstances()
+ {
+ // Arrange: Create multiple objects with identical values but different instances
+ var point1 = new TestPoint(0, 0, 0);
+ var point2 = new TestPoint(0, 0, 0);
+ var point3 = new TestPoint(0, 0, 0);
+
+ var comparer = new ReferenceEqualityComparer();
+
+ // Act: Get hash codes using ReferenceEqualityComparer
+ int hash1 = comparer.GetHashCode(point1);
+ int hash2 = comparer.GetHashCode(point2);
+ int hash3 = comparer.GetHashCode(point3);
+
+ // Assert: Different instances should produce different hash codes
+ // (even though they have identical values)
+ Assert.AreNotEqual(hash1, hash2, "Different instances should produce different hash codes");
+ Assert.AreNotEqual(hash1, hash3, "Different instances should produce different hash codes");
+ Assert.AreNotEqual(hash2, hash3, "Different instances should produce different hash codes");
+ }
+
+ [Test]
+ [Category("UnitTests")]
+ public void UsesIdentityHashCode()
+ {
+ // Arrange
+ var point = new TestPoint(1, 2, 3);
+ var comparer = new ReferenceEqualityComparer();
+
+ // Act
+ int comparerHash = comparer.GetHashCode(point);
+ int identityHash = RuntimeHelpers.GetHashCode(point);
+
+ // Assert: ReferenceEqualityComparer should use RuntimeHelpers.GetHashCode
+ Assert.AreEqual(identityHash, comparerHash,
+ "ReferenceEqualityComparer should use RuntimeHelpers.GetHashCode for identity-based hashing");
+ }
+
+ [Test]
+ [Category("UnitTests")]
+ public void DictionaryLookupPerformance_NoCollisions()
+ {
+ // Arrange: Create dictionary using ReferenceEqualityComparer
+ var dictionary = new Dictionary(new ReferenceEqualityComparer());
+
+ // Create objects with identical values but different instances
+ const int count = 20;
+ var objects = new TestPoint[count];
+ for (int i = 0; i < count; i++)
+ {
+ objects[i] = new TestPoint(0, 0, 0); // All have identical coordinates
+ dictionary[objects[i]] = $"Value_{i}";
+ }
+
+ // Act & Assert: All lookups should succeed and be fast (O(1))
+ for (int i = 0; i < count; i++)
+ {
+ Assert.IsTrue(dictionary.TryGetValue(objects[i], out string value),
+ $"Lookup should succeed for object at index {i}");
+ Assert.AreEqual($"Value_{i}", value,
+ $"Retrieved value should match for object at index {i}");
+ }
+
+ // Verify that objects with same values but different instances are treated as different
+ var newPoint = new TestPoint(0, 0, 0);
+ Assert.IsFalse(dictionary.ContainsKey(newPoint),
+ "New instance with same values should not be found (reference equality)");
+ }
+
+ [Test]
+ [Category("UnitTests")]
+ public void ReferenceEqualitySemantics()
+ {
+ // Arrange
+ var point1 = new TestPoint(1, 2, 3);
+ var point2 = new TestPoint(1, 2, 3); // Same values, different instance
+ IEqualityComparer comparer = new ReferenceEqualityComparer();
+
+ // Act & Assert: Reference equality should be used, not value equality
+ Assert.IsFalse(comparer.Equals(point1, point2),
+ "Different instances should not be equal (reference equality)");
+ Assert.IsTrue(comparer.Equals(point1, point1),
+ "Same instance should be equal to itself");
+ }
+ }
+}