Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Runtime.ExceptionServices;
using System.Runtime.Versioning;

namespace System.Runtime.InteropServices.ObjectiveC
Expand Down Expand Up @@ -39,6 +41,16 @@ private static partial IntPtr CreateReferenceTrackingHandleInternal(
out int memInSizeT,
out IntPtr mem);

[StackTraceHidden]
internal static void ThrowPendingExceptionObject()
{
Exception? ex = System.StubHelpers.StubHelpers.GetPendingExceptionObject();
if (ex is not null)
{
ExceptionDispatchInfo.Throw(ex);
}
}

[UnmanagedCallersOnly]
internal static unsafe void* InvokeUnhandledExceptionPropagation(Exception* pExceptionArg, IntPtr methodDesc, IntPtr* pContext, Exception* pException)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
using ILCompiler.ReadyToRun;
using ILCompiler.Reflection.ReadyToRun;
using Internal.TypeSystem.Ecma;
using Internal.TypeSystem.Interop;
using ILCompiler.ReadyToRun.TypeSystem;

namespace ILCompiler
Expand Down Expand Up @@ -701,6 +702,7 @@ public void PrepareForCompilationRetry(MethodWithGCInfo methodToBeRecompiled, IE
private ManualResetEventSlim _compilationSessionComplete = new ManualResetEventSlim();
private bool _hasCreatedCompilationThreads = false;
private bool _hasAddedAsyncReferences = false;
private bool _hasAddedObjectiveCMarshalReferences = false;

protected override void ComputeDependencyNodeDependencies(List<DependencyNodeCore<NodeFactory>> obj)
{
Expand Down Expand Up @@ -737,6 +739,9 @@ protected override void ComputeDependencyNodeDependencies(List<DependencyNodeCor
if (method.IsAsyncCall() && shouldBeCompiled)
AddNecessaryAsyncReferences(method);

if (method.IsPInvoke && shouldBeCompiled && MarshalHelpers.ShouldCheckForPendingException(method.Context.Target, method.GetPInvokeMethodMetadata()))
AddNecessaryObjectiveCMarshalReferences(method);

if (method.IsCompilerGeneratedILBodyForAsync() && shouldBeCompiled)
EnsureAsyncThunkTokensAreAvailable(method);

Expand Down Expand Up @@ -1043,6 +1048,24 @@ private void AddNecessaryAsyncReferences(MethodDesc method)
_hasAddedAsyncReferences = true;
}

private void AddNecessaryObjectiveCMarshalReferences(MethodDesc method)
{
if (_hasAddedObjectiveCMarshalReferences || TypeSystemContext.SystemModule is null)
return;

MetadataType objectiveCMarshalType = TypeSystemContext.SystemModule.GetType(
"System.Runtime.InteropServices.ObjectiveC"u8,
"ObjectiveCMarshal"u8,
throwIfNotFound: false);
if (objectiveCMarshalType is null)
return;

MethodDesc throwPendingExceptionObject = objectiveCMarshalType.GetKnownMethod("ThrowPendingExceptionObject"u8, null);
var moduleForNewReferences = ((EcmaMethod)method.GetPrimaryMethodDesc().GetTypicalMethodDefinition()).Module;
_tokenManager.EnsureDefTokensAreAvailable([objectiveCMarshalType, throwPendingExceptionObject], moduleForNewReferences, false);
_hasAddedObjectiveCMarshalReferences = true;
}

public ISymbolNode GetFieldRvaData(FieldDesc field)
{
if (!CompilationModuleGroup.ContainsType(field.OwningType.GetTypeDefinition()))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,29 @@ private void EmitPInvokeCall(PInvokeILCodeStreams ilCodeStreams)

callsiteSetupCodeStream.Emit(ILOpcode.call, emitter.NewToken(rawTargetMethod));

if (MarshalHelpers.ShouldCheckForPendingException(context.Target, _importMetadata))
{
MetadataType objcMarshalType = context.SystemModule.GetKnownType(
"System.Runtime.InteropServices.ObjectiveC"u8, "ObjectiveCMarshal"u8);

// Spill the native return value across the pending-exception helper call.
ILLocalVariable nativeReturnLocal = (ILLocalVariable)(-1);
bool hasNativeReturn = !nativeReturnType.IsVoid;
if (hasNativeReturn)
{
nativeReturnLocal = emitter.NewLocal(nativeReturnType);
callsiteSetupCodeStream.EmitStLoc(nativeReturnLocal);
}

callsiteSetupCodeStream.Emit(ILOpcode.call, emitter.NewToken(
objcMarshalType.GetKnownMethod("ThrowPendingExceptionObject"u8, null)));
Comment on lines +61 to +74

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO for me: Look into this

@BrzVlad BrzVlad May 18, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kotlarmilos What is the purpose of this ? Did you check that it actually produces an error ? It is completely normal for a call to be made while the IL stack is not empty. I don't believe copilot's analysis makes any sense.


if (hasNativeReturn)
{
callsiteSetupCodeStream.EmitLdLoc(nativeReturnLocal);
}
}
Comment thread
kotlarmilos marked this conversation as resolved.
Comment thread
kotlarmilos marked this conversation as resolved.
Comment thread
kotlarmilos marked this conversation as resolved.
Comment thread
kotlarmilos marked this conversation as resolved.

static PInvokeTargetNativeMethod AllocateTargetNativeMethod(MethodDesc targetMethod, MethodSignature nativeSigArg)
{
var contextMethods = s_contexts.GetOrCreateValue(targetMethod.Context);
Expand All @@ -72,9 +95,6 @@ private MethodIL EmitIL()
if (!_importMetadata.Flags.PreserveSig)
throw new NotSupportedException();

if (MarshalHelpers.ShouldCheckForPendingException(_targetMethod.Context.Target, _importMetadata))
throw new NotSupportedException();

if (_targetMethod.IsUnmanagedCallersOnly)
throw new NotSupportedException();

Expand Down Expand Up @@ -142,7 +162,9 @@ public sealed class PInvokeILStubMethodIL : ILStubMethodIL

public PInvokeILStubMethodIL(ILStubMethodIL methodIL) : base(methodIL)
{
IsMarshallingRequired = Marshaller.IsMarshallingRequired(methodIL.OwningMethod);
MethodDesc method = methodIL.OwningMethod;
IsMarshallingRequired = Marshaller.IsMarshallingRequired(method)
|| MarshalHelpers.ShouldCheckForPendingException(method.Context.Target, method.GetPInvokeMethodMetadata());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -117,9 +117,6 @@ public static bool IsMarshallingRequired(MethodDesc targetMethod)
if (!flags.PreserveSig)
return true;

if (MarshalHelpers.ShouldCheckForPendingException(targetMethod.Context.Target, metadata))
return true;

var marshallers = GetMarshallersForMethod(targetMethod);
for (int i = 0; i < marshallers.Length; i++)
{
Expand Down
1 change: 1 addition & 0 deletions src/coreclr/vm/corelib.h
Original file line number Diff line number Diff line change
Expand Up @@ -457,6 +457,7 @@ DEFINE_FIELD_U(_buckets, GCHandleSetObject, _buckets)

#ifdef FEATURE_OBJCMARSHAL
DEFINE_CLASS(OBJCMARSHAL, ObjectiveC, ObjectiveCMarshal)
DEFINE_METHOD(OBJCMARSHAL, THROW_PENDING_EXCEPTION_OBJECT, ThrowPendingExceptionObject, SM_RetVoid)
DEFINE_METHOD(OBJCMARSHAL, INVOKEUNHANDLEDEXCEPTIONPROPAGATION, InvokeUnhandledExceptionPropagation, SM_PtrException_IntPtr_PtrIntPtr_PtrException_RetVoidPtr)
#endif // FEATURE_OBJCMARSHAL

Expand Down
108 changes: 108 additions & 0 deletions src/tests/readytorun/ObjCPInvokeR2R/ObjCPInvokeR2R.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.ObjectiveC;

// Validates that blittable objc_msgSend P/Invoke stubs are precompiled into the R2R image
// (via the crossgen2 --map output) and that the emitted stub actually executes the
// pending-exception path at runtime.
public static unsafe class ObjCPInvokeR2RTest
{
[DllImport("/usr/lib/libobjc.dylib", EntryPoint = "objc_msgSend")]
private static extern IntPtr objc_msgSend(IntPtr receiver, IntPtr selector);

[DllImport("/usr/lib/libobjc.dylib", EntryPoint = "objc_msgSend")]
private static extern IntPtr objc_msgSend_2(IntPtr receiver, IntPtr selector, IntPtr arg1);

[DllImport("/usr/lib/libobjc.dylib", EntryPoint = "objc_msgSend_stret")]
private static extern void objc_msgSend_stret(IntPtr receiver, IntPtr selector);

private sealed class PendingException : Exception
{
public PendingException(string message) : base(message) { }
}

[UnmanagedCallersOnly]
private static IntPtr MsgSendCallback(IntPtr inst, IntPtr sel)
{
ObjectiveCMarshal.SetMessageSendPendingException(new PendingException(nameof(MsgSendCallback)));
return IntPtr.Zero;
}

public static int Main()
{
if (!ValidateMapFile())
return 1;

if (!ValidatePendingExceptionPropagates())
return 1;

Console.WriteLine("PASSED: ObjC P/Invoke stubs are precompiled and the pending-exception path executes.");
return 100;
}

private static bool ValidateMapFile()
{
string mapFile = Path.ChangeExtension(Assembly.GetExecutingAssembly().Location, "map");
if (!File.Exists(mapFile))
{
Console.WriteLine($"FAILED: Map file not found at {mapFile}");
return false;
}

// Only MethodWithGCInfo entries prove the stub was compiled into the image.
string[] compiledStubs = File.ReadAllLines(mapFile)
.Where(l => l.Contains("objc_msgSend") && l.Contains("MethodWithGCInfo"))
.ToArray();

string[] expectedStubs = new[]
{
"__objc_msgSend ",
"__objc_msgSend_2 ",
"__objc_msgSend_stret ",
};

bool allFound = true;
foreach (string expected in expectedStubs)
{
bool found = compiledStubs.Any(l => l.Contains(expected));
Console.WriteLine($" {(found ? "OK" : "MISSING")}: {expected.Trim()}");
if (!found)
allFound = false;
}

if (!allFound)
Console.WriteLine("FAILED: Not all objc_msgSend P/Invoke stubs were precompiled into the R2R image.");

return allFound;
}

private static bool ValidatePendingExceptionPropagates()
{
IntPtr callback = (IntPtr)(delegate* unmanaged<IntPtr, IntPtr, IntPtr>)&MsgSendCallback;
ObjectiveCMarshal.SetMessageSendCallback(MessageSendFunction.MsgSend, callback);

Check failure on line 89 in src/tests/readytorun/ObjCPInvokeR2R/ObjCPInvokeR2R.cs

View check run for this annotation

Azure Pipelines / runtime (Build coreclr Common Pri0 Test Build AnyOS AnyCPU checked)

src/tests/readytorun/ObjCPInvokeR2R/ObjCPInvokeR2R.cs#L89

src/tests/readytorun/ObjCPInvokeR2R/ObjCPInvokeR2R.cs(89,50): error CS0103: The name 'MessageSendFunction' does not exist in the current context [/__w/1/s/src/tests/readytorun/ObjCPInvokeR2R/ObjCPInvokeR2R.csproj]

Check failure on line 89 in src/tests/readytorun/ObjCPInvokeR2R/ObjCPInvokeR2R.cs

View check run for this annotation

Azure Pipelines / runtime

src/tests/readytorun/ObjCPInvokeR2R/ObjCPInvokeR2R.cs#L89

src/tests/readytorun/ObjCPInvokeR2R/ObjCPInvokeR2R.cs(89,50): error CS0103: The name 'MessageSendFunction' does not exist in the current context [/__w/1/s/src/tests/readytorun/ObjCPInvokeR2R/ObjCPInvokeR2R.csproj]

try
{
objc_msgSend(IntPtr.Zero, IntPtr.Zero);
}
catch (PendingException ex) when (ex.Message == nameof(MsgSendCallback))
{
return true;
}
catch (Exception ex)
{
Console.WriteLine($"FAILED: unexpected exception from objc_msgSend: {ex.GetType()} - {ex.Message}");
return false;
}

Console.WriteLine("FAILED: objc_msgSend returned without throwing the pending exception.");
return false;
}
}
62 changes: 62 additions & 0 deletions src/tests/readytorun/ObjCPInvokeR2R/ObjCPInvokeR2R.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<RequiresProcessIsolation>true</RequiresProcessIsolation>
<ReferenceXUnitWrapperGenerator>false</ReferenceXUnitWrapperGenerator>
<CrossGenTest>false</CrossGenTest>
<!-- https://github.com/dotnet/runtime/issues/73138 -->
<IlasmRoundTripIncompatible>true</IlasmRoundTripIncompatible>
<!-- Only run on Apple platforms where ShouldCheckForPendingException recognizes objc_msgSend -->
<CLRTestTargetUnsupported Condition="'$(TargetsOSX)' != 'true'">true</CLRTestTargetUnsupported>
<!-- Skip when sanitizers are enabled — crossgen2 invocation requires non-sanitized jitinterface -->
<CLRTestTargetUnsupported Condition="'$(EnableNativeSanitizers)' != ''">true</CLRTestTargetUnsupported>
</PropertyGroup>
<ItemGroup>
<Compile Include="ObjCPInvokeR2R.cs" />
Comment thread
kotlarmilos marked this conversation as resolved.
</ItemGroup>
<ItemGroup>
<ProjectReference Include="$(TestLibraryProjectPath)" />
</ItemGroup>
<PropertyGroup>
<CLRTestBatchPreCommands><![CDATA[
$(CLRTestBatchPreCommands)

REM ObjC P/Invoke R2R test is macOS-only — batch commands are a no-op placeholder
echo ObjC P/Invoke R2R test is not supported on Windows
exit /b 0
]]></CLRTestBatchPreCommands>
<CLRTestBashPreCommands><![CDATA[
$(CLRTestBashPreCommands)

# Suppress some DOTNET variables for the duration of Crossgen2 execution
export -n DOTNET_GCName DOTNET_GCStress DOTNET_HeapVerify DOTNET_ReadyToRun

mkdir -p IL_DLLS

if [ ! -f IL_DLLS/ObjCPInvokeR2R.dll ]
then
cp ObjCPInvokeR2R.dll IL_DLLS/ObjCPInvokeR2R.dll
fi
if [ ! -f IL_DLLS/ObjCPInvokeR2R.dll ]
then
echo Failed to copy ObjCPInvokeR2R.dll to IL_DLLS
exit 1
fi

"$CORE_ROOT"/crossgen2/crossgen2 --map --inputbubble -r:"$CORE_ROOT"/*.dll -o:ObjCPInvokeR2R.dll IL_DLLS/ObjCPInvokeR2R.dll

__cgExitCode=$?
if [ $__cgExitCode -ne 0 ]
then
echo Crossgen2 failed with exitcode: $__cgExitCode
exit 1
fi
if [ ! -f ObjCPInvokeR2R.map ]
then
echo FAILED: crossgen2 did not produce map file
exit 1
fi

export DOTNET_GCName DOTNET_GCStress DOTNET_HeapVerify DOTNET_ReadyToRun
]]></CLRTestBashPreCommands>
</PropertyGroup>
</Project>
Loading