using System; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using XFESystem.Collections; namespace XFESystem.Kernel; /// Actual guest gate for the interpreter's owned ephemeron storage. /// Host success is a fixture control only, not proof about the native collector. public static class NativeWeakCollectionsRegression { public static int ChecksPassed { get; private set; } public static int Failure { get; private set; } public static bool LiveValuePreserved { get; private set; } public static bool UpdatedValuePreserved { get; private set; } public static bool DeadKeyCycleCollected { get; private set; } public static bool DeadTablePruned { get; private set; } public static bool ReleasedScopeRejectsAccess { get; private set; } public static bool HandleCountRestored { get; private set; } public static bool NativeHandleCountChecked { get; private set; } public static ulong DependentHandlesBefore { get; private set; } public static ulong DependentHandlesAfter { get; private set; } public static bool Run(Func? dependentHandleCount = null) { Failure = ChecksPassed = 0; LiveValuePreserved = UpdatedValuePreserved = DeadKeyCycleCollected = DeadTablePruned = ReleasedScopeRejectsAccess = false; NativeHandleCountChecked = dependentHandleCount is not null; // Initialize the collection and ObjectDisposedException paths before // measuring native slots; every warmup handle is explicitly released. bool warmup = Warmup(); DependentHandlesBefore = dependentHandleCount?.Invoke() ?? 0; var live = CreateLive(); CollectTwice(); LiveValuePreserved = VerifyLive(live, 17); Record(LiveValuePreserved, 1); ReplaceValue(live); CollectTwice(); UpdatedValuePreserved = VerifyLive(live, 42) && live.Scope.Lifetime is { Entries: 1, Created: 1, Disposed: 0 }; Record(UpdatedValuePreserved, 2); live.WeakValue.Free(); live.Scope.Release(); GC.KeepAlive(live); var cycle = CreateDeadKeyCycle(); CollectTwice(); var cycleLifetime = cycle.Scope.Prune(); DeadKeyCycleCollected = cycle.WeakKey.Target is null && cycle.WeakValue.Target is null && cycleLifetime is { Tables: 1, Entries: 0, Created: 1, Disposed: 1 }; Record(DeadKeyCycleCollected, 3); cycle.WeakKey.Free(); cycle.WeakValue.Free(); cycle.Scope.Release(); GC.KeepAlive(cycle.Table); var abandoned = CreateDeadTable(); CollectTwice(); bool tableDied = abandoned.WeakTable.Target is null; var tableLifetime = abandoned.Scope.Prune(); CollectTwice(); DeadTablePruned = tableDied && abandoned.WeakValue.Target is null && tableLifetime is { Tables: 0, Entries: 0, Created: 1, Disposed: 1, TableHandlesCreated: 1, TableHandlesDisposed: 1 }; Record(DeadTablePruned, 4); abandoned.WeakTable.Free(); abandoned.WeakValue.Free(); abandoned.Scope.Release(); GC.KeepAlive(abandoned.Key); ReleasedScopeRejectsAccess = warmup && VerifyReleasedScope(); Record(ReleasedScopeRejectsAccess, 5); DependentHandlesAfter = dependentHandleCount?.Invoke() ?? 0; HandleCountRestored = !NativeHandleCountChecked || DependentHandlesAfter == DependentHandlesBefore; Record(HandleCountRestored, 6); return Failure == 0 && ChecksPassed == 6; } private static void Record(bool passed, int failure) { if (passed) ChecksPassed++; else if (Failure == 0) Failure = failure; } [MethodImpl(MethodImplOptions.NoInlining)] private static bool Warmup() { var scope = new WeakCollectionScope(); var table = new OwnedWeakTable(scope); object key = new(); table.Add(key, new Payload { Key = key, Value = 1 }); table.AddOrUpdate(key, new Payload { Key = key, Value = 2 }); scope.Prune(); scope.Release(); return RejectsDisposed(() => table.Remove(key)) && scope.Lifetime is { Entries: 0, Created: 1, Disposed: 1 }; } [MethodImpl(MethodImplOptions.NoInlining)] private static Live CreateLive() { var scope = new WeakCollectionScope(); var table = new OwnedWeakTable(scope); object key = new(); var value = new Payload { Key = key, Value = 17 }; table.Add(key, value); return new Live { Scope = scope, Table = table, Key = key, WeakValue = GCHandle.Alloc(value, GCHandleType.Weak) }; } [MethodImpl(MethodImplOptions.NoInlining)] private static bool VerifyLive(Live live, int expected) => live.Table.TryGetValue(live.Key, out var value) && value.Value == expected && ReferenceEquals(value.Key, live.Key) && ReferenceEquals(live.WeakValue.Target, value); [MethodImpl(MethodImplOptions.NoInlining)] private static void ReplaceValue(Live live) { var value = new Payload { Key = live.Key, Value = 42 }; live.Table.AddOrUpdate(live.Key, value); live.WeakValue.Free(); live.WeakValue = GCHandle.Alloc(value, GCHandleType.Weak); } [MethodImpl(MethodImplOptions.NoInlining)] private static DeadCycle CreateDeadKeyCycle() { var scope = new WeakCollectionScope(); var table = new OwnedWeakTable(scope); object key = new(); var value = new Payload { Key = key, Value = 23 }; table.Add(key, value); return new DeadCycle { Scope = scope, Table = table, WeakKey = GCHandle.Alloc(key, GCHandleType.Weak), WeakValue = GCHandle.Alloc(value, GCHandleType.Weak) }; } [MethodImpl(MethodImplOptions.NoInlining)] private static DeadTable CreateDeadTable() { var scope = new WeakCollectionScope(); var table = new OwnedWeakTable(scope); object key = new(); var value = new Payload { Key = key, Value = 29 }; table.Add(key, value); return new DeadTable { Scope = scope, Key = key, WeakTable = GCHandle.Alloc(table, GCHandleType.Weak), WeakValue = GCHandle.Alloc(value, GCHandleType.Weak) }; } [MethodImpl(MethodImplOptions.NoInlining)] private static bool VerifyReleasedScope() { var scope = new WeakCollectionScope(); var table = new OwnedWeakTable(scope); object key = new(); var value = new Payload { Key = key, Value = 31 }; table.Add(key, value); scope.Release(); scope.Release(); var before = scope.Lifetime; bool rejected = RejectsDisposed(() => table.Add(key, value)) & RejectsDisposed(() => table.AddOrUpdate(key, value)) & RejectsDisposed(() => table.TryGetValue(key, out _)) & RejectsDisposed(() => table.Remove(key)) & RejectsDisposed(() => { _ = new OwnedWeakTable(scope); }); var after = scope.Lifetime; return rejected && before == after && after is { Tables: 0, Entries: 0, Created: 1, Disposed: 1, TableHandlesCreated: 1, TableHandlesDisposed: 1 }; } [MethodImpl(MethodImplOptions.NoInlining)] private static bool RejectsDisposed(Action action) { try { action(); } catch (ObjectDisposedException) { return true; } return false; } [MethodImpl(MethodImplOptions.NoInlining)] private static void CollectTwice() { GC.Collect(); GC.Collect(); } private sealed class Payload { public object Key = null!; public int Value; } private sealed class Live { public WeakCollectionScope Scope = null!; public OwnedWeakTable Table = null!; public object Key = null!; public GCHandle WeakValue; } private sealed class DeadCycle { public WeakCollectionScope Scope = null!; public OwnedWeakTable Table = null!; public GCHandle WeakKey, WeakValue; } private sealed class DeadTable { public WeakCollectionScope Scope = null!; public object Key = null!; public GCHandle WeakTable, WeakValue; } }