Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit b3ae801

Browse files
authoredApr 26, 2024··
Skip closure allocation in ObjectReferenceWithContext<T> (#1587)
* Skip closure allocation in ObjectReferenceWithContext<T> * Remove even more closure allocations in ObjectReferenceWithContext<T> (#1589) * Skip closures for 'Context.CallInContext' calls * Use function pointers instead of delegates * Move context lambda to FOH
1 parent bfaff2a commit b3ae801

File tree

4 files changed

+114
-30
lines changed

4 files changed

+114
-30
lines changed
 

‎src/WinRT.Runtime/AgileReference.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ protected virtual void Dispose(bool disposing)
9494
{
9595
Git.RevokeInterfaceFromGlobal(_cookie);
9696
}
97-
catch(ArgumentException)
97+
catch (ArgumentException)
9898
{
9999
// Revoking cookie from GIT table may fail if apartment is gone.
100100
}

‎src/WinRT.Runtime/Context.cs

+20-6
Original file line numberDiff line numberDiff line change
@@ -29,17 +29,31 @@ public static unsafe IntPtr GetContextCallback()
2929
// On any exception, calls onFail callback if any set.
3030
// If not set, exception is handled due to today we don't
3131
// have any scenario to propagate it from here.
32-
public unsafe static void CallInContext(IntPtr contextCallbackPtr, IntPtr contextToken, Action callback, Action onFailCallback)
32+
//
33+
// On modern .NET, we can use function pointers to avoid the
34+
// small binary size increase from all generated fields and
35+
// logic to cache delegates, since we don't need any of that.
36+
public unsafe static void CallInContext(
37+
IntPtr contextCallbackPtr,
38+
IntPtr contextToken,
39+
#if NET && CsWinRT_LANG_11_FEATURES
40+
delegate*<object, void> callback,
41+
delegate*<object, void> onFailCallback,
42+
#else
43+
Action<object> callback,
44+
Action<object> onFailCallback,
45+
#endif
46+
object state)
3347
{
3448
// Check if we are already on the same context, if so we do not need to switch.
3549
if(contextCallbackPtr == IntPtr.Zero || GetContextToken() == contextToken)
3650
{
37-
callback();
51+
callback(state);
3852
return;
3953
}
4054

4155
#if NET && CsWinRT_LANG_11_FEATURES
42-
IContextCallbackVftbl.ContextCallback(contextCallbackPtr, callback, onFailCallback);
56+
IContextCallbackVftbl.ContextCallback(contextCallbackPtr, callback, onFailCallback, state);
4357
#else
4458
ComCallData data = default;
4559
var contextCallback = new ABI.WinRT.Interop.IContextCallback(ObjectReference<ABI.WinRT.Interop.IContextCallback.Vftbl>.FromAbi(contextCallbackPtr));
@@ -48,13 +62,13 @@ public unsafe static void CallInContext(IntPtr contextCallbackPtr, IntPtr contex
4862
{
4963
contextCallback.ContextCallback(_ =>
5064
{
51-
callback();
65+
callback(state);
5266
return 0;
5367
}, &data, IID.IID_ICallbackWithNoReentrancyToApplicationSTA, 5);
5468
}
55-
catch(Exception)
69+
catch (Exception)
5670
{
57-
onFailCallback?.Invoke();
71+
onFailCallback?.Invoke(state);
5872
}
5973
#endif
6074
}

‎src/WinRT.Runtime/Interop/IContextCallback.cs

+20-12
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,14 @@ internal struct ComCallData
1717
public IntPtr pUserDefined;
1818
}
1919

20+
#if NET && CsWinRT_LANG_11_FEATURES
21+
internal unsafe struct CallbackData
22+
{
23+
public delegate*<object, void> Callback;
24+
public object State;
25+
}
26+
#endif
27+
2028
#if NET && CsWinRT_LANG_11_FEATURES
2129
internal unsafe struct IContextCallbackVftbl
2230
{
@@ -25,34 +33,31 @@ internal unsafe struct IContextCallbackVftbl
2533
private delegate* unmanaged[Stdcall]<IntPtr, IntPtr, ComCallData*, Guid*, int, IntPtr, int> ContextCallback_4;
2634
#pragma warning restore CS0649
2735

28-
public static void ContextCallback(IntPtr contextCallbackPtr, Action callback, Action onFailCallback)
36+
public static void ContextCallback(IntPtr contextCallbackPtr, delegate*<object, void> callback, delegate*<object, void> onFailCallback, object state)
2937
{
3038
ComCallData comCallData;
3139
comCallData.dwDispid = 0;
3240
comCallData.dwReserved = 0;
3341

34-
// Copy the callback into a local to make sure it really is a local that
35-
// gets marked as address taken, rather than something that could potentially
36-
// be inlined into the caller into some state machine or anything else not safe.
37-
Action callbackAddressTaken = callback;
42+
CallbackData callbackData;
43+
callbackData.Callback = callback;
44+
callbackData.State = state;
3845

3946
// We can just store a pointer to the callback to invoke in the context,
4047
// so we don't need to allocate another closure or anything. The callback
4148
// will be kept alive automatically, because 'comCallData' is address exposed.
4249
// We only do this if we can use C# 11, and if we're on modern .NET, to be safe.
4350
// In the callback below, we can then just retrieve the Action again to invoke it.
44-
comCallData.pUserDefined = (IntPtr)(void*)&callbackAddressTaken;
51+
comCallData.pUserDefined = (IntPtr)(void*)&callbackData;
4552

4653
[UnmanagedCallersOnly]
4754
static int InvokeCallback(ComCallData* comCallData)
4855
{
4956
try
5057
{
51-
// Dereference the pointer to Action and invoke it (see notes above).
52-
// Once again, the pointer is not to the Action object, but just to the
53-
// local *reference* to the object, which is pinned (as it's a local).
54-
// That means that there's no pinning to worry about either.
55-
((Action*)comCallData->pUserDefined)->Invoke();
58+
CallbackData* callbackData = (CallbackData*)comCallData->pUserDefined;
59+
60+
callbackData->Callback(callbackData->State);
5661

5762
return 0; // S_OK
5863
}
@@ -74,7 +79,10 @@ static int InvokeCallback(ComCallData* comCallData)
7479

7580
if (hresult < 0)
7681
{
77-
onFailCallback?.Invoke();
82+
if (onFailCallback is not null)
83+
{
84+
onFailCallback(state);
85+
}
7886
}
7987
}
8088
}

‎src/WinRT.Runtime/ObjectReference.cs

+73-11
Original file line numberDiff line numberDiff line change
@@ -698,17 +698,29 @@ private ConcurrentDictionary<IntPtr, IObjectReference> Make_CachedContext()
698698
private volatile bool _isAgileReferenceSet;
699699
private volatile AgileReference __agileReference;
700700
private AgileReference AgileReference => _isAgileReferenceSet ? __agileReference : Make_AgileReference();
701-
private AgileReference Make_AgileReference()
701+
private unsafe AgileReference Make_AgileReference()
702702
{
703-
Context.CallInContext(_contextCallbackPtr, _contextToken, InitAgileReference, null);
703+
Context.CallInContext(
704+
_contextCallbackPtr,
705+
_contextToken,
706+
#if NET && CsWinRT_LANG_11_FEATURES
707+
&InitAgileReference,
708+
#else
709+
InitAgileReference,
710+
#endif
711+
null,
712+
this);
704713

705714
// Set after CallInContext callback given callback can fail to occur.
706715
_isAgileReferenceSet = true;
716+
707717
return __agileReference;
708718

709-
void InitAgileReference()
719+
static void InitAgileReference(object state)
710720
{
711-
global::System.Threading.Interlocked.CompareExchange(ref __agileReference, new AgileReference(this), null);
721+
ObjectReferenceWithContext<T> @this = Unsafe.As<ObjectReferenceWithContext<T>>(state);
722+
723+
global::System.Threading.Interlocked.CompareExchange(ref @this.__agileReference, new AgileReference(@this), null);
712724
}
713725
}
714726

@@ -805,16 +817,32 @@ private ObjectReference<T> GetCurrentContext()
805817
// of ConcurrentDictionary<,> and transitively dependent types for every vtable type T, since it's not
806818
// something we actually need. Because the cache is private and we're the only ones using it, we can
807819
// just store the per-context agile references as IObjectReference values, and then cast them on return.
808-
IObjectReference objectReference = CachedContext.GetOrAdd(currentContext, CreateForCurrentContext);
820+
#if NET
821+
IObjectReference objectReference = CachedContext.GetOrAdd(currentContext, ContextCallbackHolder.Value, this);
822+
#else
823+
IObjectReference objectReference = CachedContext.GetOrAdd(currentContext, ptr => ContextCallbackHolder.Value(ptr, this));
824+
#endif
809825

810826
return Unsafe.As<ObjectReference<T>>(objectReference);
827+
}
828+
829+
private static class ContextCallbackHolder
830+
{
831+
// We have a single lambda expression in this type, so we can manually rewrite it to a 'static readonly'
832+
// field. This avoids the extra logic to lazily initialized it (it's already lazily initialized because
833+
// it's in a 'beforefieldinit' type which is only used when the lambda is actually needed), and also it
834+
// allows storing the entire delegate in the Frozen Object Heap (FOH) on modern runtimes.
835+
public static readonly Func<IntPtr, IObjectReference, IObjectReference> Value = CreateForCurrentContext;
811836

812837
#if NET
813838
[UnconditionalSuppressMessage("Trimming", "IL2087", Justification = "The '_iid' field is only empty when using annotated APIs not trim-safe.")]
814839
#endif
815-
IObjectReference CreateForCurrentContext(IntPtr _)
840+
private static IObjectReference CreateForCurrentContext(IntPtr _, IObjectReference state)
816841
{
817-
var agileReference = AgileReference;
842+
ObjectReferenceWithContext<T> @this = Unsafe.As<ObjectReferenceWithContext<T>>(state);
843+
844+
var agileReference = @this.AgileReference;
845+
818846
// We may fail to switch context and thereby not get an agile reference.
819847
// In these cases, fallback to using the current context.
820848
if (agileReference == null)
@@ -829,17 +857,17 @@ IObjectReference CreateForCurrentContext(IntPtr _)
829857
// going through a trim-unsafe constructor, which is explicitly not supported in this configuration.
830858
if (!RuntimeFeature.IsDynamicCodeCompiled)
831859
{
832-
return agileReference.Get<T>(_iid);
860+
return agileReference.Get<T>(@this._iid);
833861
}
834862
#endif
835863

836-
if (_iid == Guid.Empty)
864+
if (@this._iid == Guid.Empty)
837865
{
838866
return agileReference.Get<T>(GuidGenerator.GetIID(typeof(T)));
839867
}
840868
else
841869
{
842-
return agileReference.Get<T>(_iid);
870+
return agileReference.Get<T>(@this._iid);
843871
}
844872
}
845873
catch (Exception)
@@ -858,8 +886,42 @@ protected override unsafe void Release()
858886
CachedContext.Clear();
859887
}
860888

861-
Context.CallInContext(_contextCallbackPtr, _contextToken, base.Release, ReleaseWithoutContext);
889+
Context.CallInContext(
890+
_contextCallbackPtr,
891+
_contextToken,
892+
#if NET && CsWinRT_LANG_11_FEATURES
893+
&Release,
894+
&ReleaseWithoutContext,
895+
#else
896+
Release,
897+
ReleaseWithoutContext,
898+
#endif
899+
this);
900+
862901
Context.DisposeContextCallback(_contextCallbackPtr);
902+
903+
static void Release(object state)
904+
{
905+
ObjectReferenceWithContext<T> @this = Unsafe.As<ObjectReferenceWithContext<T>>(state);
906+
907+
@this.ReleaseFromBase();
908+
}
909+
910+
static void ReleaseWithoutContext(object state)
911+
{
912+
ObjectReferenceWithContext<T> @this = Unsafe.As<ObjectReferenceWithContext<T>>(state);
913+
914+
@this.ReleaseWithoutContext();
915+
}
916+
}
917+
918+
// Helper stub to invoke 'base.Release()' on a given 'ObjectReferenceWithContext<T>' input parameter.
919+
// We can't just do 'param.base.Release()' (or something like that), so the only way to specifically
920+
// invoke the base implementation of an overridden method on that object is to go through a helper
921+
// instance method invoked on it that just calls the base implementation of the method we want.
922+
private void ReleaseFromBase()
923+
{
924+
base.Release();
863925
}
864926

865927
public override ObjectReference<IUnknownVftbl> AsKnownPtr(IntPtr ptr)

0 commit comments

Comments
 (0)
Please sign in to comment.