Skip to content

Conversation

@janvorli
Copy link
Member

@janvorli janvorli commented May 7, 2025

This change adds support for making calls from the interpreter to JIT/AOT generated code. For each target method, it parses the signature and creates a list of hand written asm routines that transfer the arguments from the interpreter stack to the CPU registers / stack based on the native calling convention, call the target method and then places the return value to the interpreter stack. This list is cached in the MethodDescData so that for repeated calls to the same method, it doesn't need to be re-generated.

For example, let's say that interpreter needs to call a JIT/AOT generated method with the following signature on Windows x64:

long M(int a, int b, long c, double d)

The interpreter stack stores these arguments aligned to 8 byte slots like this:

Slot 0: a
Slot 1: b
Slot 2: c
Slot 3: d

The CallStubGenerator::GenerateCallStub would then generate the following list of routines:

Load_RCX_RDX_R8
Load_XMM3
<target method address>
  • The interpreter calls the CallJittedMethodRetI8 function, because the return type is long. The arguments passed to this function are the list of routines above, pointer to slot 0 mentioned above, pointer to the return value slot on the interpreter stack and the size of the stack arguments the target method uses (0 in this case, as all arguments are passed in registers).
  • The CallJittedMethodRetI8 calls the first routine in the list
  • The Load_RCX_RDX_R8 loads RCX from slot 0, RDX from slot 1, R8 from slot 2 and then jumps to the next routine, which is the Load_XMM3
  • The Load_XMM3 loads XMM3 from the slot 3 and jumps to the next routine, which is the target method
  • After the target method returns, the control flow gets back to the CallJittedMethodRetI8. That function stores the RAX that contains the result to the return value slot on the interpreter stack and returns back to the interpreter.

@janvorli janvorli added this to the 10.0.0 milestone May 7, 2025
@janvorli janvorli self-assigned this May 7, 2025
Copilot AI review requested due to automatic review settings May 7, 2025 18:42
@janvorli janvorli requested review from BrzVlad and kg as code owners May 7, 2025 18:42
@janvorli janvorli added NO-MERGE The PR is not ready for merge yet (see discussion for detailed reasons) NO-REVIEW Experimental/testing PR, do NOT review it area-CodeGen-Interpreter-coreclr labels May 7, 2025
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

This PR implements support for making calls from the interpreter to JIT/AOT generated code by generating and caching call stubs for target methods. The key changes include:

  • Adding new fields and methods in MethodDesc to store and manage call stub headers.
  • Implementing a new routine in interpexec.cpp to invoke compiled methods via generated call stubs.
  • Adding new assembly routines in AsmHelpers.asm and updating CMakeLists.txt to include the call stub generator sources.

Reviewed Changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
src/coreclr/vm/method.hpp Added call stub header field and new API for call stub management
src/coreclr/vm/method.cpp Implemented SetCallStubHeader and GetCallStubHeader methods
src/coreclr/vm/interpexec.cpp Added InvokeCompiledMethod and updated interpreter branch to handle JIT/AOT calls
src/coreclr/vm/callstubgenerator.h Introduced the definition for call stub generation
src/coreclr/vm/amd64/AsmHelpers.asm Added multiple assembly routines to move arguments and perform calls
src/coreclr/vm/CMakeLists.txt Updated build configuration to include call stub generator sources

EECodeInfo codeInfo((PCODE)targetIp);
if (!codeInfo.IsValid())
{
printf("Attempted to execute native code from interpreter.\n");
Copy link

Copilot AI May 7, 2025

Choose a reason for hiding this comment

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

Using printf and assert directly for error handling may not be robust in production. Consider replacing with structured error logging or exception handling to better manage execution in release builds.

Copilot uses AI. Check for mistakes.
@dotnet-policy-service
Copy link
Contributor

Tagging subscribers to this area: @BrzVlad, @janvorli, @kg
See info in area-owners.md if you want to be subscribed.

@janvorli janvorli mentioned this pull request May 7, 2025
66 tasks
@janvorli janvorli changed the title [WIP] Intepreter to JIT/AOT calls [WIP] Interpreter to JIT/AOT calls May 7, 2025
@kg
Copy link
Member

kg commented May 8, 2025

This was very easy to adapt for p/invoke, I like how you engineered it.

@janvorli janvorli force-pushed the interpreter-to-jit-calls branch from 5eb2f82 to d375289 Compare May 12, 2025 23:54
@janvorli janvorli removed NO-MERGE The PR is not ready for merge yet (see discussion for detailed reasons) NO-REVIEW Experimental/testing PR, do NOT review it labels May 13, 2025
@janvorli janvorli changed the title [WIP] Interpreter to JIT/AOT calls Interpreter to JIT/AOT calls May 13, 2025
@janvorli janvorli requested a review from jkotas May 13, 2025 22:29
@janvorli
Copy link
Member Author

The PR is now ready for review.

janvorli added 5 commits May 14, 2025 15:25
This change adds support for making calls from the interpreter to JIT/AOT generated code.
For each target method, it parses the signature and creates a list of
hand written asm routines that transfer the arguments from the
interpreter stack to the CPU registers / stack based on the native
calling convention, call the target method and then places the return
value to the interpreter stack. This list is cached in the
MethodDescData so that for repeated calls to the same method, it doesn't
need to be re-generated.
@janvorli janvorli force-pushed the interpreter-to-jit-calls branch from 9ac91be to ed393bc Compare May 14, 2025 14:47
Comment on lines +1131 to +1135
if (!codeInfo.IsValid())
{
EEPOLICY_HANDLE_FATAL_ERROR_WITH_MESSAGE(COR_E_EXECUTIONENGINE, W("Attempted to execute native code from interpreter"));
}
else if (codeInfo.GetCodeManager() != ExecutionManager::GetInterpreterCodeManager())
Copy link
Member

@jkotas jkotas May 14, 2025

Choose a reason for hiding this comment

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

Suggested change
if (!codeInfo.IsValid())
{
EEPOLICY_HANDLE_FATAL_ERROR_WITH_MESSAGE(COR_E_EXECUTIONENGINE, W("Attempted to execute native code from interpreter"));
}
else if (codeInfo.GetCodeManager() != ExecutionManager::GetInterpreterCodeManager())
if (!codeInfo.IsValid() || codeInfo.GetCodeManager() != ExecutionManager::GetInterpreterCodeManager())

I do not see why we need to block native code (FCalls?) here. Should this be like this?

(I agree with TODO that this needs to be faster.)

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 was initially a temporary check to make sure something unexpected doesn't leak in here. As the comment mentions, I want to get rid of the codeInfo construction here soon and move to tagged pointer for the interpreter code so that we don't need to burn time looking up the code ranges. Then this will go away.
As for FCalls, I have made a local change in a testing branch yesterday to make them work, but they need to be handled explicitly, as their GetNativeCode returns NULL, so I have used the TryGetMultiCallableAddrOfCode. Maybe we can use that function instead of the GetNativeCode for all calls, but I need to refresh my memory on the differences.

Copy link
Member

@jkotas jkotas left a comment

Choose a reason for hiding this comment

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

LGTM. Somebody from the interpreter v-team should sign-off as well.

@janvorli janvorli force-pushed the interpreter-to-jit-calls branch from cf3c9f8 to 552ca6f Compare May 14, 2025 23:12
}
else if (codeInfo.GetCodeManager() != ExecutionManager::GetInterpreterCodeManager())
{
MethodDesc *pMD = codeInfo.GetMethodDesc();
Copy link
Member

Choose a reason for hiding this comment

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

I think it would be great if we didn't have to work with MethodDesc at all for the fast invocation path. Also if these transition thunks would be shared per signature and not owned by each method independently, meaning the compiled targetIp would be passed explicitly. So a call pseudocode could look like:

obtain targetIp;
if (targetIp is not interp)
    callStubInvoke = check call site cache
    if (!callStubInvoke)
         build CallStubGenerator
         set callStubInvoke and write cache
    callStubInvoke->Invoke(targetIp, ...)
else
    interp call 

Copy link
Member Author

Choose a reason for hiding this comment

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

I have considered generating the thunk per (normalized) signature, but decided to leave it as a possible future optimization. I don't have a good idea yet on how to make normalized signature comparison and generate cache key for a signature that would be significantly faster than generating it per method. And the size of the thunk is very small, so I am not sure how much we would save space-wise. Anyways, it is still worth looking into at some point.

Copy link
Member

@BrzVlad BrzVlad left a comment

Choose a reason for hiding this comment

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

Nice

@janvorli janvorli merged commit 97424fe into dotnet:main May 19, 2025
99 of 101 checks passed
@janvorli janvorli deleted the interpreter-to-jit-calls branch May 19, 2025 12:29
@github-actions github-actions bot locked and limited conversation to collaborators Jun 19, 2025
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants