From e55b55ba80428baf82989ad2feebbc9f2e261db8 Mon Sep 17 00:00:00 2001 From: Aaron R Robinson Date: Fri, 22 May 2026 15:59:38 -0700 Subject: [PATCH 1/2] Add ObjectiveCMarshal.GetOrCreateTaggedMemory API Adds ObjectiveCMarshal.GetOrCreateTaggedMemory(object) which returns the tagged memory span for an object without creating a GCHandle. This is a more efficient alternative when only the tagged memory is needed and no reference tracking handle is required. Implements the API across CoreCLR, NativeAOT, Mono, and the PlatformNotSupported stub. The CoreCLR native implementation shares logic with CreateReferenceTrackingHandle via a common helper. Fixes https://github.com/dotnet/runtime/issues/128476 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../ObjectiveCMarshal.CoreCLR.cs | 6 + .../ObjectiveCMarshal.NativeAot.cs | 11 +- src/coreclr/vm/interoplibinterface.h | 5 + src/coreclr/vm/interoplibinterface_objc.cpp | 103 +++++++++++++----- src/coreclr/vm/qcallentrypoints.cpp | 1 + .../ObjectiveCMarshal.PlatformNotSupported.cs | 3 + .../ObjectiveC/ObjectiveCMarshal.cs | 46 ++++++++ .../ref/System.Runtime.InteropServices.cs | 1 + .../InteropServices/ObjectiveCMarshal.Mono.cs | 5 + 9 files changed, 151 insertions(+), 30 deletions(-) diff --git a/src/coreclr/System.Private.CoreLib/src/System/Runtime/InteropServices/ObjectiveCMarshal.CoreCLR.cs b/src/coreclr/System.Private.CoreLib/src/System/Runtime/InteropServices/ObjectiveCMarshal.CoreCLR.cs index 101978434bd612..bf40d1b5e12a6d 100644 --- a/src/coreclr/System.Private.CoreLib/src/System/Runtime/InteropServices/ObjectiveCMarshal.CoreCLR.cs +++ b/src/coreclr/System.Private.CoreLib/src/System/Runtime/InteropServices/ObjectiveCMarshal.CoreCLR.cs @@ -39,6 +39,12 @@ private static partial IntPtr CreateReferenceTrackingHandleInternal( out int memInSizeT, out IntPtr mem); + [LibraryImport(RuntimeHelpers.QCall, EntryPoint = "ObjCMarshal_GetOrCreateTaggedMemory")] + private static partial void GetOrCreateTaggedMemoryInternal( + ObjectHandleOnStack obj, + out int memInSizeT, + out IntPtr mem); + [UnmanagedCallersOnly] internal static unsafe void* InvokeUnhandledExceptionPropagation(Exception* pExceptionArg, IntPtr methodDesc, IntPtr* pContext, Exception* pException) { diff --git a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Runtime/InteropServices/ObjectiveCMarshal.NativeAot.cs b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Runtime/InteropServices/ObjectiveCMarshal.NativeAot.cs index 621c62d87db7d9..4a194b76175727 100644 --- a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Runtime/InteropServices/ObjectiveCMarshal.NativeAot.cs +++ b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Runtime/InteropServices/ObjectiveCMarshal.NativeAot.cs @@ -125,6 +125,16 @@ private static IntPtr CreateReferenceTrackingHandleInternal( object obj, out int memInSizeT, out IntPtr mem) + { + // Rely on GetOrCreateTaggedMemoryInternal for state checking. + GetOrCreateTaggedMemoryInternal(obj, out memInSizeT, out mem); + return RuntimeImports.RhHandleAllocRefCounted(obj); + } + + private static void GetOrCreateTaggedMemoryInternal( + object obj, + out int memInSizeT, + out IntPtr mem) { if (!s_initialized) { @@ -139,7 +149,6 @@ private static IntPtr CreateReferenceTrackingHandleInternal( var trackerInfo = s_objects.GetOrAdd(obj, static o => new ObjcTrackingInformation()); trackerInfo.EnsureInitialized(obj); trackerInfo.GetTaggedMemory(out memInSizeT, out mem); - return RuntimeImports.RhHandleAllocRefCounted(obj); } internal class ObjcTrackingInformation diff --git a/src/coreclr/vm/interoplibinterface.h b/src/coreclr/vm/interoplibinterface.h index e7d8ea3e209fbb..41fba14b64b4d2 100644 --- a/src/coreclr/vm/interoplibinterface.h +++ b/src/coreclr/vm/interoplibinterface.h @@ -63,6 +63,11 @@ extern "C" void* QCALLTYPE ObjCMarshal_CreateReferenceTrackingHandle( _Out_ int* memInSizeT, _Outptr_ void** mem); +extern "C" void QCALLTYPE ObjCMarshal_GetOrCreateTaggedMemory( + _In_ QCall::ObjectHandleOnStack obj, + _Out_ int* memInSizeT, + _Outptr_ void** mem); + extern "C" BOOL QCALLTYPE ObjCMarshal_TrySetGlobalMessageSendCallback( _In_ ObjCMarshalNative::MessageSendFunction msgSendFunction, _In_ void* fptr); diff --git a/src/coreclr/vm/interoplibinterface_objc.cpp b/src/coreclr/vm/interoplibinterface_objc.cpp index 9fb23c76451ad1..f322990ac4b349 100644 --- a/src/coreclr/vm/interoplibinterface_objc.cpp +++ b/src/coreclr/vm/interoplibinterface_objc.cpp @@ -57,6 +57,62 @@ extern "C" BOOL QCALLTYPE ObjCMarshal_TryInitializeReferenceTracker( return success; } +namespace +{ + void* TaggedMemoryForObjectHelper( + _In_ QCall::ObjectHandleOnStack obj, + _Out_ size_t* memInSizeT, + _Out_opt_ OBJECTHANDLE* instHandle) + { + CONTRACTL + { + THROWS; + GC_TRIGGERS; + MODE_PREEMPTIVE; + PRECONDITION(CheckPointer(memInSizeT)); + } + CONTRACTL_END; + + // The reference tracking system must be initialized. + if (!g_ReferenceTrackerInitialized) + COMPlusThrow(kInvalidOperationException, W("InvalidOperation_ObjectiveCMarshalNotInitialized")); + + void* taggedMemoryLocal; + + // Switch to Cooperative mode since object references + // are being manipulated. + { + GCX_COOP(); + + struct + { + OBJECTREF objRef; + } gc; + gc.objRef = NULL; + GCPROTECT_BEGIN(gc); + + gc.objRef = obj.Get(); + + // The object's type must be marked appropriately and with a finalizer. + if (!gc.objRef->GetMethodTable()->IsTrackedReferenceWithFinalizer()) + COMPlusThrow(kInvalidOperationException, W("InvalidOperation_ObjectiveCTypeNoFinalizer")); + + // Initialize the syncblock for this instance. + SyncBlock* syncBlock = gc.objRef->GetSyncBlock(); + InteropSyncBlockInfo* interopInfo = syncBlock->GetInteropInfo(); + taggedMemoryLocal = interopInfo->AllocTaggedMemory(memInSizeT); + _ASSERTE(taggedMemoryLocal != NULL); + + if (instHandle != NULL) + *instHandle = GetAppDomain()->CreateTypedHandle(gc.objRef, HNDTYPE_REFCOUNTED); + + GCPROTECT_END(); + } + + return taggedMemoryLocal; + } +} + extern "C" void* QCALLTYPE ObjCMarshal_CreateReferenceTrackingHandle( _In_ QCall::ObjectHandleOnStack obj, _Out_ int* memInSizeT, @@ -72,44 +128,33 @@ extern "C" void* QCALLTYPE ObjCMarshal_CreateReferenceTrackingHandle( BEGIN_QCALL; - // The reference tracking system must be initialized. - if (!g_ReferenceTrackerInitialized) - COMPlusThrow(kInvalidOperationException, W("InvalidOperation_ObjectiveCMarshalNotInitialized")); - - // Switch to Cooperative mode since object references - // are being manipulated. - { - GCX_COOP(); - - struct - { - OBJECTREF objRef; - } gc; - gc.objRef = NULL; - GCPROTECT_BEGIN(gc); - - gc.objRef = obj.Get(); + taggedMemoryLocal = TaggedMemoryForObjectHelper(obj, &memInSizeTLocal, &instHandle); + END_QCALL; - // The object's type must be marked appropriately and with a finalizer. - if (!gc.objRef->GetMethodTable()->IsTrackedReferenceWithFinalizer()) - COMPlusThrow(kInvalidOperationException, W("InvalidOperation_ObjectiveCTypeNoFinalizer")); + *memInSizeT = (int)memInSizeTLocal; + *mem = taggedMemoryLocal; + return (void*)instHandle; +} - // Initialize the syncblock for this instance. - SyncBlock* syncBlock = gc.objRef->GetSyncBlock(); - InteropSyncBlockInfo* interopInfo = syncBlock->GetInteropInfo(); - taggedMemoryLocal = interopInfo->AllocTaggedMemory(&memInSizeTLocal); - _ASSERTE(taggedMemoryLocal != NULL); +extern "C" void QCALLTYPE ObjCMarshal_GetOrCreateTaggedMemory( + _In_ QCall::ObjectHandleOnStack obj, + _Out_ int* memInSizeT, + _Outptr_ void** mem) +{ + QCALL_CONTRACT; + _ASSERTE(memInSizeT != NULL); + _ASSERTE(mem != NULL); - instHandle = GetAppDomain()->CreateTypedHandle(gc.objRef, HNDTYPE_REFCOUNTED); + size_t memInSizeTLocal; + void* taggedMemoryLocal; - GCPROTECT_END(); - } + BEGIN_QCALL; + taggedMemoryLocal = TaggedMemoryForObjectHelper(obj, &memInSizeTLocal, NULL); END_QCALL; *memInSizeT = (int)memInSizeTLocal; *mem = taggedMemoryLocal; - return (void*)instHandle; } namespace diff --git a/src/coreclr/vm/qcallentrypoints.cpp b/src/coreclr/vm/qcallentrypoints.cpp index 1692140748aee8..a4f96915c06666 100644 --- a/src/coreclr/vm/qcallentrypoints.cpp +++ b/src/coreclr/vm/qcallentrypoints.cpp @@ -441,6 +441,7 @@ static const Entry s_QCall[] = DllImportEntry(ObjCMarshal_TrySetGlobalMessageSendCallback) DllImportEntry(ObjCMarshal_TryInitializeReferenceTracker) DllImportEntry(ObjCMarshal_CreateReferenceTrackingHandle) + DllImportEntry(ObjCMarshal_GetOrCreateTaggedMemory) #endif #if defined(FEATURE_JAVAMARSHAL) DllImportEntry(JavaMarshal_Initialize) diff --git a/src/libraries/System.Private.CoreLib/src/System/Runtime/InteropServices/ObjectiveC/ObjectiveCMarshal.PlatformNotSupported.cs b/src/libraries/System.Private.CoreLib/src/System/Runtime/InteropServices/ObjectiveC/ObjectiveCMarshal.PlatformNotSupported.cs index 133b2e7fe887eb..f9e46f3d291360 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Runtime/InteropServices/ObjectiveC/ObjectiveCMarshal.PlatformNotSupported.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Runtime/InteropServices/ObjectiveC/ObjectiveCMarshal.PlatformNotSupported.cs @@ -94,6 +94,9 @@ public static GCHandle CreateReferenceTrackingHandle( out Span taggedMemory) => throw new PlatformNotSupportedException(); + public static Span GetOrCreateTaggedMemory(object obj) + => throw new PlatformNotSupportedException(); + /// /// Objective-C msgSend function override options. /// diff --git a/src/libraries/System.Private.CoreLib/src/System/Runtime/InteropServices/ObjectiveC/ObjectiveCMarshal.cs b/src/libraries/System.Private.CoreLib/src/System/Runtime/InteropServices/ObjectiveC/ObjectiveCMarshal.cs index 812de71191cc93..1d38df07e87f5f 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Runtime/InteropServices/ObjectiveC/ObjectiveCMarshal.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Runtime/InteropServices/ObjectiveC/ObjectiveCMarshal.cs @@ -101,6 +101,8 @@ public static unsafe void Initialize( /// return a new handle each time but the same tagged memory will be returned. The /// tagged memory is only guaranteed to be zero initialized on the first call. /// + /// The tagged memory returned is the same as the memory returned from . + /// /// The caller is responsible for freeing the returned . /// public static GCHandle CreateReferenceTrackingHandle( @@ -126,6 +128,50 @@ public static GCHandle CreateReferenceTrackingHandle( return GCHandle.FromIntPtr(refCountHandle); } + /// + /// Request a pointer to memory tagged for the supplied object. + /// + /// The object whose tagged memory to return. + /// A pointer to memory tagged to the object. + /// Thrown if the ObjectiveCMarshal API has not been initialized. + /// + /// The function must be called prior to calling this function. + /// + /// The parameter must have a type in its hierarchy marked with + /// . + /// + /// The "Is Referenced" callback passed to + /// will be passed the memory returned from this function. + /// The memory it points at is defined by the length in the and + /// will be zeroed out. It will be available until is collected by the GC. + /// The returned memory can be used for any purpose by the caller of this function and usable + /// during the "Is Referenced" callback. + /// + /// Calling this function multiple times with the same will + /// return the same tagged memory. It is only guaranteed to be zero initialized on + /// the first call. + /// + /// The return value is the same as the tagged memory returned from . + /// + public static Span GetOrCreateTaggedMemory(object obj) + { + ArgumentNullException.ThrowIfNull(obj); + + GetOrCreateTaggedMemoryInternal( +#if NATIVEAOT + obj, +#else + ObjectHandleOnStack.Create(ref obj), +#endif + out int memInSizeT, + out IntPtr mem); + + unsafe + { + return new Span(mem.ToPointer(), memInSizeT); + } + } + /// /// Objective-C msgSend function override options. /// diff --git a/src/libraries/System.Runtime.InteropServices/ref/System.Runtime.InteropServices.cs b/src/libraries/System.Runtime.InteropServices/ref/System.Runtime.InteropServices.cs index 07ef66f0e6a62d..c3645cc1c5bd7d 100644 --- a/src/libraries/System.Runtime.InteropServices/ref/System.Runtime.InteropServices.cs +++ b/src/libraries/System.Runtime.InteropServices/ref/System.Runtime.InteropServices.cs @@ -2448,6 +2448,7 @@ public static unsafe void Initialize( public static GCHandle CreateReferenceTrackingHandle( object obj, out System.Span taggedMemory) => throw null; + public static System.Span GetOrCreateTaggedMemory(object obj) => throw null; public enum MessageSendFunction { MsgSend, diff --git a/src/mono/System.Private.CoreLib/src/System/Runtime/InteropServices/ObjectiveCMarshal.Mono.cs b/src/mono/System.Private.CoreLib/src/System/Runtime/InteropServices/ObjectiveCMarshal.Mono.cs index 0f4945b8e138c8..261f7b8ed4ad0f 100644 --- a/src/mono/System.Private.CoreLib/src/System/Runtime/InteropServices/ObjectiveCMarshal.Mono.cs +++ b/src/mono/System.Private.CoreLib/src/System/Runtime/InteropServices/ObjectiveCMarshal.Mono.cs @@ -33,5 +33,10 @@ private static IntPtr CreateReferenceTrackingHandleInternal( ObjectHandleOnStack obj, out int memInSizeT, out IntPtr mem) => throw new NotImplementedException(); + + private static void GetOrCreateTaggedMemoryInternal( + ObjectHandleOnStack obj, + out int memInSizeT, + out IntPtr mem) => throw new NotImplementedException(); } } From f0f8414cce3ed2a1b59653a1be26f144fae61118 Mon Sep 17 00:00:00 2001 From: Aaron R Robinson Date: Fri, 22 May 2026 16:24:08 -0700 Subject: [PATCH 2/2] Add tests for ObjectiveCMarshal.GetOrCreateTaggedMemory Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../ObjectiveCMarshalAPI/Program.cs | 96 ++++++++++++++++--- 1 file changed, 82 insertions(+), 14 deletions(-) diff --git a/src/tests/Interop/ObjectiveC/ObjectiveCMarshalAPI/Program.cs b/src/tests/Interop/ObjectiveC/ObjectiveCMarshalAPI/Program.cs index 55015c45cd16c9..9cee3bee913e45 100644 --- a/src/tests/Interop/ObjectiveC/ObjectiveCMarshalAPI/Program.cs +++ b/src/tests/Interop/ObjectiveC/ObjectiveCMarshalAPI/Program.cs @@ -39,9 +39,9 @@ public static extern unsafe void SetImports( public unsafe class Program { - static void Validate_ReferenceTrackingAPIs_InvalidArgs() + static void Validate_InvalidArgs() { - Console.WriteLine($"Running {nameof(Validate_ReferenceTrackingAPIs_InvalidArgs)}..."); + Console.WriteLine($"Running {nameof(Validate_InvalidArgs)}..."); delegate* unmanaged beginEndCallback; delegate* unmanaged isReferencedCallback; @@ -73,6 +73,11 @@ static void Validate_ReferenceTrackingAPIs_InvalidArgs() { ObjectiveCMarshal.CreateReferenceTrackingHandle(null , out _); }); + Assert.Throws( + () => + { + ObjectiveCMarshal.GetOrCreateTaggedMemory(null); + }); } // The expectation here is during reference tracking handle creation @@ -184,6 +189,25 @@ static void InitializeObjectiveCMarshal() ObjectiveCMarshal.Initialize(beginEndCallback, isReferencedCallback, trackedObjectEnteredFinalization, OnUnhandledExceptionPropagationHandler); } + static void Validate_PreInitialize_Scenario() + { + Console.WriteLine($"Running {nameof(Validate_PreInitialize_Scenario)}..."); + + // Attempting to create handle prior to initialization. + Assert.Throws( + () => + { + ObjectiveCMarshal.CreateReferenceTrackingHandle(new Base(), out _); + }); + + // Not initialized yet - should throw. + Assert.Throws( + () => + { + ObjectiveCMarshal.GetOrCreateTaggedMemory(new Base()); + }); + } + [MethodImpl(MethodImplOptions.NoInlining)] static GCHandle AllocAndTrackObject(uint count) where T : Base, new() { @@ -223,17 +247,6 @@ static unsafe void Validate_ReferenceTracking_Scenario() { Console.WriteLine($"Running {nameof(Validate_ReferenceTracking_Scenario)}..."); - var handles = new List(); - - // Attempting to create handle prior to initialization. - Assert.Throws( - () => - { - ObjectiveCMarshal.CreateReferenceTrackingHandle(new Base(), out _); - }); - - InitializeObjectiveCMarshal(); - // Type attributed but no finalizer. Assert.Throws( () => @@ -249,6 +262,8 @@ static unsafe void Validate_ReferenceTracking_Scenario() AllocUntrackedObject(); AllocUntrackedObject(); + var handles = new List(); + // Provide the minimum number of times the reference callback should run. // See IsRefCb() in NativeObjCMarshalTests.cpp for usage logic. const uint callbackCount = 3; @@ -426,6 +441,53 @@ static void _Validate_ExceptionPropagation() GC.KeepAlive(delThrowException); } + static unsafe void Validate_GetOrCreateTaggedMemory_Scenario() + { + Console.WriteLine($"Running {nameof(Validate_GetOrCreateTaggedMemory_Scenario)}..."); + + // Type attributed but no finalizer. + Assert.Throws( + () => + { + ObjectiveCMarshal.GetOrCreateTaggedMemory(new AttributedNoFinalizer()); + }); + + var obj = new Base(); + + // Validate length matches CreateReferenceTrackingHandle contract. + Span memFromGet = ObjectiveCMarshal.GetOrCreateTaggedMemory(obj); + Assert.Equal(2, memFromGet.Length); + + // Memory should be zero-initialized on first call. + Assert.Equal(IntPtr.Zero, memFromGet[0]); + Assert.Equal(IntPtr.Zero, memFromGet[1]); + + // Multiple calls return the same memory. + Span memFromGet2 = ObjectiveCMarshal.GetOrCreateTaggedMemory(obj); + fixed (void* p1 = memFromGet) + fixed (void* p2 = memFromGet2) + Assert.Equal((IntPtr)p1, (IntPtr)p2); + + // CreateReferenceTrackingHandle on the same object returns the same memory. + GCHandle h = ObjectiveCMarshal.CreateReferenceTrackingHandle(obj, out Span memFromCreate); + fixed (void* p1 = memFromGet) + fixed (void* p2 = memFromCreate) + Assert.Equal((IntPtr)p1, (IntPtr)p2); + h.Free(); + + // GetOrCreateTaggedMemory after CreateReferenceTrackingHandle also returns the same memory. + var obj2 = new Base(); + GCHandle h2 = ObjectiveCMarshal.CreateReferenceTrackingHandle(obj2, out Span memFromCreate2); + Span memFromGetAfter = ObjectiveCMarshal.GetOrCreateTaggedMemory(obj2); + fixed (void* p1 = memFromCreate2) + fixed (void* p2 = memFromGetAfter) + Assert.Equal((IntPtr)p1, (IntPtr)p2); + h2.Free(); + + GC.KeepAlive(obj); + GC.KeepAlive(obj2); + } + static void Validate_Initialize_FailsOnSecondAttempt() { Console.WriteLine($"Running {nameof(Validate_Initialize_FailsOnSecondAttempt)}..."); @@ -444,8 +506,14 @@ public static int TestEntryPoint() { try { - Validate_ReferenceTrackingAPIs_InvalidArgs(); + Validate_InvalidArgs(); + Validate_PreInitialize_Scenario(); + + InitializeObjectiveCMarshal(); + + Validate_GetOrCreateTaggedMemory_Scenario(); Validate_ReferenceTracking_Scenario(); + Validate_Initialize_FailsOnSecondAttempt(); } catch (Exception e)