Skip to content

Conversation

@mikelle-rogers
Copy link
Member

Create a trigger and new trace type for the GenericPInvokeCalli stub.

@mikelle-rogers mikelle-rogers self-assigned this Dec 13, 2024
@ghost ghost added the needs-area-label An area label is needed to ensure this gets routed to the appropriate area owners label Dec 13, 2024
UNINSTALL_UNWIND_AND_CONTINUE_HANDLER;
UNINSTALL_MANAGED_EXCEPTION_DISPATCHER;

if (g_genericPInvokeCalliHelperTraceActiveCount > 0)
Copy link
Member Author

Choose a reason for hiding this comment

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

In the case where the IL Stub is already made, it does not go down this path even though the trigger is enabled. I need to figure out how to disable the trigger in that scenario.

Copy link
Member Author

@mikelle-rogers mikelle-rogers Dec 13, 2024

Choose a reason for hiding this comment

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

When the ILStub is made, the path of execution is through JIT_ReversePInvokeEnterRare2 and into TraceCall. Eventually, a managed patch is set and then DisableAll is called, so the GenericPInvokeCalli trigger will be disabled with that call.

Copy link
Member

Choose a reason for hiding this comment

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

Is there some state we can assert here or there?

In the case where the IL Stub is already made

What is this path then? How is this path different from the other, with respect to the IL Stub state?

Copy link
Member Author

Choose a reason for hiding this comment

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

After returning from the exception that was the step into, we eventually make it to HCIMPL1_RAW(void, JIT_ReversePInvokeEnter, ReversePInvokeFrame* frame), which is some kind of jitHelper. This calls into JIT_ReversePInvokeEnterRare2, which calls into TraceCall. We then go into DispatchTraceCall, which calls TriggerTraceCall because we are working with a LEAF_MOST_FRAME. We go into an if statement with TraceStub, FollowTrace and PatchTrace. TraceStub decides we are managed code and followTrace agrees and a managed trace patch is set in PatchTrace. All three functions (TS, FT and PT) returned true, so inside of TriggerTraceCall, we DisableAll() which disables all triggers.

Copy link
Member

Choose a reason for hiding this comment

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

I'm a bit confused by the description here. In the code above it looks like we are creating an ILStub that facilitates a call from managed code to native code (a pinvoke). However in your description you say the code is executing JIT_ReversePInvokeEnter which is code I'd expect to run when a native function is calling a managed function, the reverse of a pinvoke. I'd like to understand better what does the test case you are running look like and what methods are calling one another. My worry is that you might have a test-case that is relying on the reverse pinvoke to be successful, but some other test case that doesn't include the reverse pinvoke might fail.

Copy link
Member Author

Choose a reason for hiding this comment

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

This is the test case I am using:

[UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] 
static int DoubleInt(int i) => i * 2;
[UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] 
static float MultiplyInt (float a, float b) => (a * b);
static unsafe void Function()
{
    var fnPtr = (delegate* unmanaged[Cdecl]<int,int>) &DoubleInt;
    var fnPtr2 = (delegate* unmanaged[Cdecl]<float,float, float>) &MultiplyInt;
    fnPtr(21); // step into me
    Console.WriteLine("We are in the middle");
    fnPtr2(1.5f, 2.5f); //
    fnPtr(34); // step into me
    Console.WriteLine("We made it");
}
Function();

The case discussed above happens when we step into the following line: fnPtr(34);

Copy link
Member

Choose a reason for hiding this comment

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

@AaronRobinsonMSFT - does it match your understanding that a calli to managed method marked UnmanagedCallersOnly would first do a pinvoke followed by a reverse p/invoke? That sounds plausible to me but I've never looked into the details of the implementation.

@mikelle-rogers - I'd suggest trying an alternate test case where you write the native function in a separate dll using C++ and call that. I'd expect you will find that the reverse pinvoke part disappears. @AaronRobinsonMSFT - do you know of any good examples of this? I poked around a little bit but of the few examples I saw it was always passing a managed callback to native code rather than passing a native callback to managed code.

Copy link
Member

@AaronRobinsonMSFT AaronRobinsonMSFT Jan 6, 2025

Choose a reason for hiding this comment

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

UnmanagedCallersOnly would first do a pinvoke followed by a reverse p/invoke?

@noahfalk When you say "do a pinvoke", what does that mean in this case? Meaning, what part of the machinery are you referring to as pinvoke?

Copy link
Member

@noahfalk noahfalk Jan 7, 2025

Choose a reason for hiding this comment

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

Sorry, let me try to be more precise :) By 'Do a pinvoke' I was refering to the calli executing down the code path commented here GetILStubForCalli(...), followed by invoking the ILStub that is generated in this method. PInvoke may not have been accurate terminology for that part. Does that make more sense?

@mikelle-rogers mikelle-rogers marked this pull request as ready for review December 13, 2024 23:57
@mikelle-rogers mikelle-rogers requested review from a team, noahfalk and tommcdon December 13, 2024 23:58
@mikelle-rogers mikelle-rogers added area-VM-coreclr and removed needs-area-label An area label is needed to ensure this gets routed to the appropriate area owners labels Dec 14, 2024
@dotnet-policy-service
Copy link
Contributor

Tagging subscribers to this area: @tommcdon
See info in area-owners.md if you want to be subscribed.

TraceDestination trace;
FramePointer fp = LEAF_MOST_FRAME;
trace.InitForStub(target);
g_pEEInterface->FollowTrace(&trace);
Copy link
Member

Choose a reason for hiding this comment

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

Should we check the return value here and conditionally execute PatchTrace?

Copy link
Member Author

Choose a reason for hiding this comment

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

In this case, FollowTrace should always return True. Should we assert that that is the case? Then, if there is an issue (or a case that I do not know about) we will find it easily.

trace.InitForStub(target);
g_pEEInterface->FollowTrace(&trace);
//fStopInUnmanaged only matters for TRACE_UNMANAGED
PatchTrace(&trace, fp, /*fStopInUnmanaged*/false);
Copy link
Member

Choose a reason for hiding this comment

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

Same comment as above, check the return value? If it fails, should we avoid calling DisableGenericPInvokeCalli?

Copy link
Member Author

Choose a reason for hiding this comment

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

The same goes for this one. PatchTrace should return true always, so maybe we assert that it has? And if it hasn't then that possible provides us with an easy issue to track?

Copy link
Member

@tommcdon tommcdon left a comment

Choose a reason for hiding this comment

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

Left a couple comments for your review, but otherwise LGTM!

FramePointer fp = LEAF_MOST_FRAME;
trace.InitForStub(target);
bool hasTraceType = g_pEEInterface->FollowTrace(&trace);
//fStopInUnmanaged only matters for TRACE_UNMANAGED
Copy link
Member

Choose a reason for hiding this comment

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

Nit: it seems a little odd to have an unrelated assert in between the comment and the line it is commenting on

Suggested change
//fStopInUnmanaged only matters for TRACE_UNMANAGED
_ASSERTE(hasTraceType);
//fStopInUnmanaged only matters for TRACE_UNMANAGED
bool setPatch = PatchTrace(&trace, fp, /*fStopInUnmanaged*/false);

@stephentoub
Copy link
Member

@tommcdon, what are the next steps for this PR? Thanks.

@tommcdon
Copy link
Member

@tommcdon, what are the next steps for this PR? Thanks.

@stephentoub I believe there was some testing that needs to done and if that looked good this was very close to ready. @noahfalk did you have any other concerns?

@noahfalk
Copy link
Member

@noahfalk did you have any other concerns?

Based on #110677 (comment) it wasn't clear that we've gotten calls into the correct runtime code paths. Mikelle's explanation sounded like the current code relied on a reverse p/invoke to occur during the operation but I wouldn't expect all scenarios involving calli to contain a reverse p/invoke. Testing a call to a pure C++ dll may reveal changes are needed.

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.

5 participants