private static int ILStubCache_GCTransition_NoGCTransition(int expected) { // This test uses a callback marked UnmanagedCallersOnly as a way to verify that // SuppressGCTransition is taken into account when caching IL stubs. // It calls functions with the same signature, differing only in SuppressGCTransition. // When calling an UnmanagedCallersOnly method, the runtime validates that the GC is in // pre-emptive mode. If not, it throws a fatal error that cannot be caught and crashes. Console.WriteLine($"{nameof(ILStubCache_GCTransition_NoGCTransition)} ({expected}) ..."); int n; void *cb = (delegate * unmanaged[Cdecl] < int, int >) & ReturnInt; // Call function that does not have SuppressGCTransition SuppressGCTransitionNative.InvokeCallbackVoidPtr_Inline_GCTransition(cb, &n); Assert.AreEqual(expected++, n); // Call function with same (blittable) signature, but with SuppressGCTransition. // IL stub should not be re-used, GC transition not should occur, and callback invocation should fail. SuppressGCTransitionNative.InvokeCallbackVoidPtr_Inline_NoGCTransition(cb, &n); Assert.AreEqual(expected++, n); // Call function that does not have SuppressGCTransition SuppressGCTransitionNative.InvokeCallbackVoidPtr_NoInline_GCTransition(cb, &n); Assert.AreEqual(expected++, n); // Call function with same (non-blittable) signature, but with SuppressGCTransition // IL stub should not be re-used, GC transition not should occur, and callback invocation should fail. expected = n + 1; SuppressGCTransitionNative.InvokeCallbackVoidPtr_NoInline_NoGCTransition(cb, &n); Assert.AreEqual(expected++, n); return(n + 1); }