Skip to content

Enable R2R precompilation of objc_msgSend P/Invoke stubs#124770

Draft
davidnguyen-tech wants to merge 6 commits intodotnet:mainfrom
davidnguyen-tech:feature/r2r-objc-pinvoke-stubs
Draft

Enable R2R precompilation of objc_msgSend P/Invoke stubs#124770
davidnguyen-tech wants to merge 6 commits intodotnet:mainfrom
davidnguyen-tech:feature/r2r-objc-pinvoke-stubs

Conversation

@davidnguyen-tech
Copy link
Member

@davidnguyen-tech davidnguyen-tech commented Feb 23, 2026

Problem

TODO: Add measurements on a sample app

The R2R PInvokeILEmitter previously threw NotSupportedException for any P/Invoke requiring an ObjC pending exception check (objc_msgSend calls), forcing runtime IL stub generation every time.

On JIT-less platforms like iOS with CoreCLR, these stubs fall back to the interpreter, with unnecessary performance costs.

Solution

Change R2R P/Invoke IL Emitter to emit a call to ObjectiveCMarshal.ThrowPendingExceptionObject() after the native call, mirroring the NativeAOT PInvokeILEmitter:

if (MarshalHelpers.ShouldCheckForPendingException(context.Target, _pInvokeMetadata))
{
MetadataType lazyHelperType = context.SystemModule.GetKnownType("System.Runtime.InteropServices.ObjectiveC"u8, "ObjectiveCMarshal"u8);
callsiteSetupCodeStream.Emit(ILOpcode.call, emitter.NewToken(lazyHelperType
.GetKnownMethod("ThrowPendingExceptionObject"u8, null)));

For blittable ObjC P/Invokes, the entire stub is now precompiled into the R2R image.

davidnguyen-tech and others added 5 commits February 20, 2026 12:57
The R2R PInvokeILEmitter previously threw NotSupportedException for any
P/Invoke requiring an ObjC pending exception check (objc_msgSend family),
forcing runtime IL stub generation. On JIT-less platforms like iOS with
CoreCLR, these stubs fall back to the interpreter.

This change teaches the R2R emitter to emit a call to
ObjectiveCMarshal.ThrowPendingExceptionObject() after the native call,
mirroring the common/NativeAOT PInvokeILEmitter. For blittable ObjC
P/Invokes, the entire stub is now precompiled into the R2R image.

The ThrowPendingExceptionObject method was previously only available in
NativeAOT's System.Private.CoreLib. This change adds it to CoreCLR's
ObjectiveCMarshal.CoreCLR.cs as well, delegating to the existing
StubHelpers.GetPendingExceptionObject() infrastructure.

Changes:
- ObjectiveCMarshal.CoreCLR.cs: Add ThrowPendingExceptionObject()
- PInvokeILEmitter.cs: Remove NotSupportedException for ObjC; emit
  ThrowPendingExceptionObject call after the native call
- Marshaller.ReadyToRun.cs: Add IsObjCMessageSendPInvoke helper
- GeneratesPInvoke: Return true for ObjC P/Invokes
- CreateValueFromKey: Catch RequiresRuntimeJitException for ObjC
  P/Invokes with non-blittable params
- CorInfoImpl: Remove assert that assumed GeneratesPInvoke is false
  when stubIL is null

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Remove Marshaller.IsObjCMessageSendPInvoke wrapper, call
  MarshalHelpers.ShouldCheckForPendingException directly in GeneratesPInvoke
- Clarify GeneratesPInvoke comment: IsMarshallingRequired returns true for
  ObjC P/Invokes because they need an IL stub, not actual type marshalling
- Add comment to empty catch block explaining JIT fallback behavior

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings February 23, 2026 22:47
@davidnguyen-tech davidnguyen-tech self-assigned this Feb 23, 2026
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Enables ReadyToRun (R2R) precompilation of Objective-C objc_msgSend-family P/Invoke stubs by emitting a managed pending-exception check after the native call, avoiding a runtime-stub/interpreter fallback on JIT-less Apple platforms.

Changes:

  • Add ObjectiveCMarshal.ThrowPendingExceptionObject() in CoreCLR and expose it to the runtime binder.
  • Update the R2R PInvokeILEmitter to emit a post-call pending-exception check instead of rejecting ObjC P/Invokes.
  • Adjust R2R compilation policy and IL caching behavior to allow ObjC P/Invokes to be attempted and to gracefully fall back when unsupported.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated no comments.

Show a summary per file
File Description
src/coreclr/vm/corelib.h Adds CoreLib binder entry for ObjectiveCMarshal.ThrowPendingExceptionObject.
src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs Simplifies P/Invoke stub IL retrieval logic now that unsupported cases are handled/cached in ILCache.
src/coreclr/tools/aot/ILCompiler.ReadyToRun/IL/Stubs/PInvokeILEmitter.cs Emits ThrowPendingExceptionObject after ObjC native calls; removes ObjC-specific NotSupportedException.
src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/ReadyToRunCompilationModuleGroupBase.cs Allows GeneratesPInvoke for ObjC msgSend P/Invokes even when marshalling is otherwise considered required.
src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/ReadyToRunCodegenCompilation.cs Caches “requires runtime JIT” decisions for unsupported P/Invoke stub emission via RequiresRuntimeJitException catch.
src/coreclr/System.Private.CoreLib/src/System/Runtime/InteropServices/ObjectiveCMarshal.CoreCLR.cs Implements ThrowPendingExceptionObject() using pending-exception storage and ExceptionDispatchInfo.Throw.

@davidnguyen-tech davidnguyen-tech changed the title Enable R2R precompilation of ObjC objc_msgSend P/Invoke stubs Enable R2R precompilation of objc_msgSend P/Invoke stubs Feb 23, 2026
@dotnet-policy-service
Copy link
Contributor

Tagging subscribers to 'os-ios': @vitek-karas, @kotlarmilos, @steveisok, @akoeplinger
See info in area-owners.md if you want to be subscribed.

if (!Marshaller.IsMarshallingRequired(method))
return true;

if (MarshalHelpers.ShouldCheckForPendingException(method.Context.Target, method.GetPInvokeMethodMetadata()))
Copy link
Member

Choose a reason for hiding this comment

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

As we discussed yesterday, this check here looks wrong. This check is already done as part of Marshaller.IsMarshallingRequired and I believe it should be removed from there. In your case, this method is returning true for pinvokes with non-blittable types that require check for pending exception. It should be returning false instead and I believe this is the reason you are forcefully catching the requires jit exception above. If the checks here would be correct, you shouldn't need to do that.

@BrzVlad
Copy link
Member

BrzVlad commented Feb 24, 2026

Were there also any interpreted pinvokes with non-blittable types ? In case we might need additional fixes, related to the original plan of using LibraryImport.

@AaronRobinsonMSFT
Copy link
Member

Do we need to re-enable or create a new sets of tests for this? We have tests for these APIs, src/tests/Interop/ObjectiveC/ObjectiveCMarshalAPI, but I don't know if we've disabled them for R2R (it doesn't look like it), so are we missing them during R2R testing somehow?

/cc @jkoritzinsky

@jkoritzinsky
Copy link
Member

Do we need to re-enable or create a new sets of tests for this? We have tests for these APIs, src/tests/Interop/ObjectiveC/ObjectiveCMarshalAPI, but I don't know if we've disabled them for R2R (it doesn't look like it), so are we missing them during R2R testing somehow?

The tests there are falling back to JIT/Interpreter for these stubs (which is correct behavior). We could add testing to validate that the methods were actually generated (src/tests/readytorun/tests/test.cs and the mainv1.csproj project file have examples of the logic (pass the --map option when running cg2 and then validate the expected entries exist as part of running the test).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants