Enable R2R precompilation of blittable ObjC P/Invokes#124770
Enable R2R precompilation of blittable ObjC P/Invokes#124770davidnguyen-tech wants to merge 10 commits intodotnet:mainfrom
Conversation
There was a problem hiding this comment.
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
PInvokeILEmitterto 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. |
|
Tagging subscribers to 'os-ios': @vitek-karas, @kotlarmilos, @steveisok, @akoeplinger |
| if (!Marshaller.IsMarshallingRequired(method)) | ||
| return true; | ||
|
|
||
| if (MarshalHelpers.ShouldCheckForPendingException(method.Context.Target, method.GetPInvokeMethodMetadata())) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
This approach would newly enable inlining of blittable P/Invokes that require a Pending Exception check. I'm researching the consequences of this decision - it might require additional changes to JIT. It's because JIT inlines a P/Invoke when Marshaller.MarshallingIsRequired return false (below is the proof for completeness):
- JIT calls
pInvokeMarshalingRequired:runtime/src/coreclr/jit/importercalls.cpp
Line 6856 in e7daed6
-
There was a problem hiding this comment.
The current approach with at first glance unnecessary duplicate if (MarshalHelpers.ShouldCheckForPendingException actually prevents JIT from inlining the ObjectiveC P/Invoke, while making sure that the IL Emitter emits the Pending Exception check.
There was a problem hiding this comment.
Synced offline with @jkoritzinsky: there is an option to implement inlining the Pending Exception check through a JIT helper. I will try to pursue this approach to see if it's cleaner.
|
Were there also any interpreted pinvokes with non-blittable types ? In case we might need additional fixes, related to the original plan of using |
|
Do we need to re-enable or create a new sets of tests for this? We have tests for these APIs, /cc @jkoritzinsky |
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 ( |
Analyzes reviewer feedback from BrzVlad, AaronRobinsonMSFT, and jkoritzinsky. Includes recommended code changes and test coverage plan. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ning findings - Add LibraryImport vs DllImport analysis explaining why switching macios wouldn't solve the R2R precompilation issue - Add P/Invoke inlining call chain trace showing CorInfoImpl.ReadyToRun.cs path (not VM dllimport.cpp) for R2R marshalling decisions - Document graceful fallback behavior for unsupported marshalling types - Confirm BrzVlad's approach is both cleaner and more efficient Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 8 out of 8 changed files in this pull request and generated 4 comments.
You can also share your feedback on Copilot code review. Take the survey.
| if (MarshalHelpers.ShouldCheckForPendingException(context.Target, _importMetadata)) | ||
| { | ||
| MetadataType objcMarshalType = context.SystemModule.GetKnownType( | ||
| "System.Runtime.InteropServices.ObjectiveC"u8, "ObjectiveCMarshal"u8); | ||
| callsiteSetupCodeStream.Emit(ILOpcode.call, emitter.NewToken( | ||
| objcMarshalType.GetKnownMethod("ThrowPendingExceptionObject"u8, null))); | ||
| } |
| if (MarshalHelpers.ShouldCheckForPendingException(method.Context.Target, method.GetPInvokeMethodMetadata())) | ||
| return true; | ||
|
|
| catch (RequiresRuntimeJitException) | ||
| { | ||
| // The P/Invoke IL emitter will throw for known unsupported scenarios. | ||
| // We don't propagate the exception and keep methodIL as null - this causes a fall back to JIT during runtime. |
pr-124770-plan.md
Outdated
| # PR #124770 — Final Review Assessment | ||
|
|
||
| ## Summary | ||
|
|
||
| Analysis of 3 reviewer concerns on PR #124770 ("Enable R2R precompilation of objc_msgSend P/Invoke stubs"), validated against the actual code. | ||
|
|
||
| ### Historical Context | ||
|
|
||
| The `ShouldCheckForPendingException` check was originally added to `IsMarshallingRequired` by Aaron Robinson in commit 4a782d58ac4 (PR #52849, May 2021). At that time, R2R had **no ability** to emit the pending exception check at all. The check served as a two-layer safety net: | ||
| 1. `IsMarshallingRequired` returns true → `GeneratesPInvoke` returns false → R2R won't try | ||
| 2. Even if it did try, `EmitIL()` threw `NotSupportedException` → caught in CorInfoImpl | ||
|
|
||
| Now that R2R **can** emit the check (via this PR's `ThrowPendingExceptionObject`), the safety net is no longer needed for ObjC P/Invokes — but the original design explains why it was there. | ||
|
|
||
| --- | ||
|
|
||
| ## Concern 1: Logic Error in `GeneratesPInvoke` (BrzVlad — inline review) | ||
|
|
||
| ### BrzVlad's Claim | ||
|
|
||
| > "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." | ||
|
|
||
| ### Verdict: ✅ BrzVlad is correct — the root cause is a conflation of concerns |
61aa0b6 to
825890f
Compare
825890f to
bef6edf
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.
You can also share your feedback on Copilot code review. Take the survey.
| if (MarshalHelpers.ShouldCheckForPendingException(context.Target, _importMetadata)) | ||
| { | ||
| MetadataType objcMarshalType = context.SystemModule.GetKnownType( | ||
| "System.Runtime.InteropServices.ObjectiveC"u8, "ObjectiveCMarshal"u8); | ||
| callsiteSetupCodeStream.Emit(ILOpcode.call, emitter.NewToken( | ||
| objcMarshalType.GetKnownMethod("ThrowPendingExceptionObject"u8, null))); | ||
| } |
Add a CoreLib helper that retrieves and throws any pending Objective-C exception. This will be called from R2R-precompiled P/Invoke IL stubs after objc_msgSend calls, mirroring the existing NativeAOT approach. Uses ExceptionDispatchInfo.Throw to preserve the original stack trace and is marked [StackTraceHidden] to keep the throw site clean. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Instead of throwing NotSupportedException for ObjC P/Invokes (which blocked R2R precompilation entirely), emit a call to ObjectiveCMarshal.ThrowPendingExceptionObject() after the native call in the IL stub. This mirrors the NativeAOT PInvokeILEmitter. The generated stub IL for a blittable objc_msgSend now looks like: marshal args → call objc_msgSend → call ThrowPendingExceptionObject → ret On iOS (no JIT), this eliminates 82 interpreter fallbacks at startup for a dotnet new maui app. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Remove ShouldCheckForPendingException from Marshaller.IsMarshallingRequired — ObjC pending exceptions are a platform concern, not a marshalling concern. return false for all ObjC P/Invokes, blocking R2R stub generation entirely. Add the check to PInvokeILStubMethodIL.IsMarshallingRequired instead. This property is read by pInvokeMarshalingRequired (the JIT-EE interface) to decide whether the JIT should use the precompiled stub or inline a raw native call. Without this check, the JIT would inline blittable ObjC P/Invokes with GTF_CALL_UNMANAGED — a raw call with no pending exception handling — silently skipping the ThrowPendingExceptionObject call that the stub contains. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
bef6edf to
e04bc85
Compare
- Document new 3-commit structure and PInvokeILStubMethodIL mechanism - Mark BrzVlad's recommendations as implemented (GeneratesPInvoke reverted, ShouldCheckForPendingException removed from IsMarshallingRequired) - Add open concerns: code size, P/Invoke frame correctness, non-void IL stack risk, JIT helper alternative under investigation - Test coverage still flagged as missing (merge blocker) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Analyzes reviewer feedback from BrzVlad, AaronRobinsonMSFT, and jkoritzinsky. Includes recommended code changes and test coverage plan. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ning findings - Add LibraryImport vs DllImport analysis explaining why switching macios wouldn't solve the R2R precompilation issue - Add P/Invoke inlining call chain trace showing CorInfoImpl.ReadyToRun.cs path (not VM dllimport.cpp) for R2R marshalling decisions - Document graceful fallback behavior for unsupported marshalling types - Confirm BrzVlad's approach is both cleaner and more efficient Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Document new 3-commit structure and PInvokeILStubMethodIL mechanism - Mark BrzVlad's recommendations as implemented (GeneratesPInvoke reverted, ShouldCheckForPendingException removed from IsMarshallingRequired) - Add open concerns: code size, P/Invoke frame correctness, non-void IL stack risk, JIT helper alternative under investigation - Test coverage still flagged as missing (merge blocker) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds a ReadyToRun test that validates objc_msgSend P/Invoke stubs are precompiled into the R2R image using crossgen2's --map output. The test: - Declares blittable objc_msgSend DllImport methods - Compiles them with crossgen2 --map - At runtime, reads the .map file and asserts P/Invoke stub entries exist - Only runs on macOS (CLRTestTargetUnsupported for non-Apple platforms) Addresses reviewer feedback from jkoritzinsky and AaronRobinsonMSFT requesting test coverage for the R2R ObjC P/Invoke precompilation feature. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Validates that blittable objc_msgSend P/Invoke stubs are precompiled into the ReadyToRun image by checking the crossgen2 --map output for MethodWithGCInfo entries. Key details: - Uses /usr/lib/libobjc.dylib (exact path matched by ShouldCheckForPendingException) - Checks specifically for MethodWithGCInfo entries (not just any map reference) - Requires --inputbubble so crossgen2 can resolve ThrowPendingExceptionObject tokens - macOS-only (CLRTestTargetUnsupported for non-OSX) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Promote regression test verification to a mandatory section in copilot-instructions.md with explicit guidance on using baseline build artifacts for negative verification - Add ReadyToRun/crossgen2 test gotchas section covering --inputbubble, map entry semantics, and MarshalHelpers.cs string matching - Add cross-component build warning (crossgen2 + CoreLib changes) - Add design doc discoverability instruction - Strengthen CoreLib rebuild guidance to explain libs.pretest necessity - Add Step 5 (mandatory negative verification) to jit-regression-test skill - Create src/tests/readytorun/README.md documenting test patterns, map validation rules, version bubble constraints, and platform gating Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 11 out of 11 changed files in this pull request and generated 4 comments.
You can also share your feedback on Copilot code review. Take the survey.
|
|
||
| # Run a single test manually | ||
| export CORE_ROOT=$(pwd)/artifacts/tests/coreclr/<os>.<arch>.Release/Tests/Core_Root | ||
| cd artifacts/tests/coreclr/<os>.<arch>.Debug/readytorun/<TestName>/ |
| <CLRTestTargetUnsupported Condition="'$(EnableNativeSanitizers)' != ''">true</CLRTestTargetUnsupported> | ||
| </PropertyGroup> | ||
| <ItemGroup> | ||
| <Compile Include="ObjCPInvokeR2R.cs" /> |
| # PR #124770 — Final Review Assessment | ||
|
|
||
| ## Summary | ||
|
|
||
| Analysis of the original 3 reviewer concerns on PR #124770 ("Enable R2R precompilation of objc_msgSend P/Invoke stubs"), updated for the March 17 force-push that replaced the earlier escape-hatch/try-catch design with a cleaner 3-commit approach. These March 17 notes describe the PR's force-pushed GitHub state, even if the local checkout still contains the earlier revision. | ||
|
|
||
| ### Historical Context | ||
|
|
||
| The `ShouldCheckForPendingException` check was originally added to `IsMarshallingRequired` by Aaron Robinson in commit 4a782d58ac4 (PR #52849, May 2021). At that time, R2R had **no ability** to emit the pending exception check at all. The check served as a two-layer safety net: | ||
| 1. `IsMarshallingRequired` returns true → `GeneratesPInvoke` returns false → R2R won't try | ||
| 2. Even if it did try, `EmitIL()` threw `NotSupportedException` → caught in CorInfoImpl | ||
|
|
||
| Now that R2R **can** emit the check (via this PR's `ThrowPendingExceptionObject`), the safety net is no longer needed for ObjC P/Invokes — but the original design explains why it was there. | ||
|
|
||
| --- | ||
|
|
||
| ## Concern 1: Logic Error in `GeneratesPInvoke` (BrzVlad — inline review) | ||
|
|
||
| ### BrzVlad's Claim | ||
|
|
||
| > "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." | ||
| ### Verdict: ✅ BrzVlad's core concern was correct, and the force-push implemented it | ||
|
|
||
| **Historical context:** the earlier PR revision conflated two concerns: | ||
| 1. "Does this P/Invoke need a pending exception check?" | ||
| 2. "Does this P/Invoke need real parameter/return marshalling?" | ||
|
|
||
| That conflation is what created the old `GeneratesPInvoke` escape hatch and the temporary `ReadyToRunCodegenCompilation.cs` try/catch workaround. | ||
|
|
||
| ### Update (March 17): implemented in the force-push | ||
|
|
||
| The restructured PR now: | ||
| 1. Removes `ShouldCheckForPendingException` from `Marshaller.IsMarshallingRequired` | ||
| 2. Reverts `GeneratesPInvoke` to `return !Marshaller.IsMarshallingRequired(method)` | ||
| 3. Adds the ObjC special-case to `PInvokeILStubMethodIL.IsMarshallingRequired` | ||
|
|
||
| That third bullet is the key extra insight beyond Vlad's original review comment: without it, the JIT could see a blittable ObjC signature and raw-inline/direct-call it, skipping `ThrowPendingExceptionObject()` entirely. | ||
|
|
||
| ### Current assessment | ||
|
|
||
| - The original layering issue is fixed. | ||
| - The removed pieces are the `GeneratesPInvoke` escape hatch, the `ReadyToRunCodegenCompilation.cs` try/catch, and the `CorInfoImpl.ReadyToRun.cs` cleanup. | ||
| - The remaining work is validating the new stub shape, code-size impact, P/Invoke frame correctness, and test coverage. | ||
|
|
||
| **Severity: ✅ Fixed** — the original logic issue is no longer an open blocker. | ||
|
|
||
| --- | ||
|
|
||
| ## Concern 2: Non-Blittable ObjC Scope (BrzVlad — general comment) | ||
|
|
||
| ### BrzVlad's Claim | ||
|
|
||
| > "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`." | ||
| # PR #124770 Analysis: Enable R2R precompilation of objc_msgSend P/Invoke stubs | ||
|
|
||
| ## PR Overview | ||
|
|
||
| - **Title**: Enable R2R precompilation of objc_msgSend P/Invoke stubs | ||
| - **Author**: davidnguyen-tech | ||
| - **Branch**: `feature/r2r-objc-pinvoke-stubs` → `main` | ||
| - **Status**: Draft, open | ||
| - **March 17 update**: The PR was force-pushed and restructured into 3 cleaner commits: | ||
| 1. `01a7fe190ad` — add `ThrowPendingExceptionObject()` plus the `corelib.h` binder entry | ||
| 2. `b41aa09deb6` — emit `ThrowPendingExceptionObject` in `PInvokeILEmitter.EmitPInvokeCall()` and remove the old ObjC blocker | ||
| 3. `e04bc858067` — remove `ShouldCheckForPendingException` from `Marshaller.IsMarshallingRequired` and move the direct-call suppression to `PInvokeILStubMethodIL.IsMarshallingRequired` | ||
| - **Historical note**: The earlier 6-file snapshot below is still useful context, but sections 4-6 are now historical because the force-push removed that part of the design. | ||
| - **Scope note**: The March 17 updates in this document describe the PR as force-pushed on GitHub, even if the local checkout you are reading alongside this note still reflects the older pre-force-push revision. | ||
|
|
||
| ## Problem Statement | ||
|
|
||
| On iOS with CoreCLR, the JIT is unavailable. ReadyToRun (R2R) precompiles code ahead of time, but ObjC `objc_msgSend` P/Invoke stubs were explicitly blocked from R2R compilation — the `PInvokeILEmitter` threw `NotSupportedException` for any P/Invoke that needed a pending exception check. This forced these calls to fall back to runtime JIT stub generation, which on JIT-less iOS means falling back to the interpreter, causing a performance penalty. | ||
|
|
||
| ## Key Concepts |
Add P/Invoke marshalling and R2R-specific review rules: - inputbubble requirement for cross-module CoreLib references - IsMarshallingRequired prevents JIT from inlining past R2R stubs - Separation of marshalling vs platform-specific stub requirements - DllImport detection string matching against MarshalHelpers.cs - R2R map validation (MethodWithGCInfo vs MethodFixupSignature) - R2R+NativeAOT P/Invoke emitter alignment checks - R2R tests need both map validation and runtime verification Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Problem
TODO: Add measurements on a sample app
Objective-C P/Invokes didn't get precompiled in R2R. On JIT-less platforms like iOS, they would fall back to interpreter with significant performance costs.
They are a common case worth improving - e.g. in dotnet/macios, almost all native calls from managed code go through Objective-C P/Invokes.
Solution
Enable R2R precompilation of blittable Objective-C P/Invoke stubs:
Emit
ThrowPendingExceptionObject()in the IL stub after the native call. This mirrors the NativeAOT PInvokeILEmitter.Remove
ShouldCheckForPendingExceptionfromMarshaller.IsMarshallingRequired— this blockedGeneratesPInvokefrom returningtruefor blittable ObjC P/Invokes.GeneratesPInvokeis called here to determine whether to emit IL for a P/Invoke.Add
ShouldCheckForPendingExceptiontoPInvokeILStubMethodIL.IsMarshallingRequired— this property is read bypInvokeMarshalingRequired, which the JIT uses to decide whether to inline a P/Invoke. When it returnsfalse, the JIT inlines a raw native call (GTF_CALL_UNMANAGED) with no ObjC-specific handling — the precompiled stub withThrowPendingExceptionObjectwould exist but never be called. Adding the check here ensurespInvokeMarshalingRequiredreturnstruefor ObjC P/Invokes, so the JIT uses the stub.Result
Startup of a benchmark app improved 1% with the cost of 0,6% increase in size.
Precompilation of blittable ObjC P/Invokes should also have a positive throughput improvement.