Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Can Jitex still intercept methods that have already been JIT-compiled? #85

Open
InCerryGit opened this issue Jul 23, 2023 · 9 comments
Open
Assignees
Labels
feature New feature

Comments

@InCerryGit
Copy link

Hi!
This is a great project and work. I have a question about Jitex: can it be used for methods that have already been JIT-compiled? Because many times, we may not know in advance which methods need to be intercepted; it's only during the program's runtime that we discover the methods that need to be intercepted, and by that time, they may have already been JIT-compiled. I would like to know if Jitex can be applied in such a scenario. If it is currently not supported, are there any plans to implement this feature in the future?

Thank you for reading!

@Hitmasu
Copy link
Owner

Hitmasu commented Jul 23, 2023

Hi!

Thanks a lot!

Yes, Jitex can intercept methods already compiled by JIT. You can use: MethodHelper.ForceRecompile to help you. This method force JIT compile method again, making possible to Jitex intercept.

See this example intercepting a method Sum:

using Jitex;

var result = MyMath.Sum(1, 1);
Console.WriteLine(result);

JitexManager.MethodResolver += context =>
{
    if (context.Method.Name == "Sum")
        context.InterceptCall();
};

JitexManager.Interceptor += async context =>
{
    if (context.Method.Name == "Sum")
    {
        Console.WriteLine("Method sum intercepted");
        context.SetReturnValue(-1);
    }
};

//Will not be intercepted, because was already compiled.
result = MyMath.Sum(10, 10);
Console.WriteLine(result);

class MyMath
{
    public static int Sum(int a, int b)
    {
        Console.WriteLine("Sum called");
        return a + b;
    }
}

Output:

Sum called
2
Sum called
20

Calling MethodHelper.ForceRecompile:

using System.Reflection;
using Jitex;
using Jitex.Utils;

var result = MyMath.Sum(1, 1);
Console.WriteLine(result);

JitexManager.MethodResolver += context =>
{
    if (context.Method.Name == "Sum")
        context.InterceptCall();
};

JitexManager.Interceptor += async context =>
{
    if (context.Method.Name == "Sum")
    {
        Console.WriteLine("Method sum intercepted");
        context.SetReturnValue(-1);
    }
};

var sumMethod = typeof(MyMath).GetMethod("Sum", (BindingFlags)(-1));

//Force jit compile method again
MethodHelper.ForceRecompile(sumMethod);

result = MyMath.Sum(10, 10);
Console.WriteLine(result);

class MyMath
{
    public static int Sum(int a, int b)
    {
        Console.WriteLine("Sum called");
        return a + b;
    }
}

Output:

Sum called
2
Method sum intercepted
-1

If your method is a R2R, you can try:

MethodHelper.DisableReadyToRun(sumMethod)
MethodHelper.ForceRecompile(sumMethod)

Tell me if worked.

@InCerryGit
Copy link
Author

Thanks a lot, I'll try it out and let you know the result

@InCerryGit
Copy link
Author

Yes, it worked, at first I was using .NET Core 3.1 version but it reported error Recompile method is only supported on .NET 5 or above.. Then I switched to .NET 6.0 and it worked. But it seems that now it doesn't support .NET 7.0 and throws the following exception:

Fatal error. internal CLR error.(0x80131506)
   at Jitex.JIT.CorInfo.CEEInfo.ConstructStringLiteral(IntPtr, IntPtr, Int32, IntPtr)
   at Jitex.JIT.ManagedJit.ConstructStringLiteral(IntPtr, IntPtr, Int32, IntPtr)
   at Jitex.JIT.ManagedJit.CompileMethod(IntPtr, IntPtr, IntPtr, UInt32, IntPtr ByRef, Int32 ByRef)
   at Jitex.JitexManager.get_IsEnabled()
   at Jitex.JitexManager.AddMethodResolver(MethodResolverHandler)
   JitexManager.add_MethodResolver(MethodResolverHandler) at Jitex.
   at Program.<Main>$(System.String[])

Are there any plans for .NET Core 2.1/3.1 support for the Recompile method in the future, and will Jitex support .NET 7.0?

@Hitmasu
Copy link
Owner

Hitmasu commented Jul 24, 2023

Thanks!

For .NET Core 2.1/3.1, when I developed that feature, I couldn't make it work with .NET Core; it only works with .NET 5 or above. That's the reason it doesn't work on those versions. I will try to make it compatible with these versions again.

Regarding .NET 7, yes, I can make it work on .NET 7. Honestly, Jitex development was paused on .NET 6 because no one was using Jitex.

I'll work to add support for .NET 7 this week. It will probably take 2 weeks to be completed. After that, I will try to enable ForceRecompile on versions below .NET 5.

@Hitmasu Hitmasu self-assigned this Jul 24, 2023
@Hitmasu Hitmasu added the feature New feature label Jul 24, 2023
@InCerryGit
Copy link
Author

I am highly anticipating the support of .NET 7.0, and the release of .NET 8.0 in just a few months' time. I recognize that the development of Jitex is a formidable and challenging undertaking, and many people have yet to fully realize its value. The ability to non-invasively modify any method's IL is truly remarkable, and there is no other project that can achieve this functionality. I am currently planning to utilize Jitex for a project of my own, as it perfectly satisfies my requirements.

@InCerryGit
Copy link
Author

I have another concern, and I am unsure if this is a problem with my usage or something else, or if I should open a new issue. This is a test project for .NET 6.0, and I am attempting to retrieve the result of DateTime.Now. However, it seems that Console.WriteLine($"After interceptor! Result:{context.GetReturnValue()}") is never executed.

using System.Reflection;
using Jitex;
using Jitex.Utils;

Console.WriteLine(DateTime.Now);
var nowMethod = typeof(DateTime).GetProperty("Now", (BindingFlags)(-1)).GetGetMethod();

JitexManager.MethodResolver += context =>
{
    Console.WriteLine(context.Method.Name);
    if (context.Method.Name == nowMethod.Name)
    {
        Console.WriteLine("MethodResolver");
        context.InterceptCall();   
    }
};

JitexManager.Interceptor += async context =>
{
    if (context.Method.Name == nowMethod.Name)
    {
        Console.WriteLine("Before interceptor! ");
        await context.ContinueAsync();
>>>>   Console.WriteLine($"After interceptor! Result:{context.GetReturnValue()}"); <<<<
    }
};



if (MethodHelper.IsReadyToRun(nowMethod))
{
    MethodHelper.DisableReadyToRun(nowMethod);
}

//Force jit compile method again
MethodHelper.ForceRecompile(nowMethod);

Console.WriteLine(DateTime.Now);

The output of the project appears as follows:

2023/7/25 8:32:21
MethodResolver
Before interceptor!
2023/7/25 8:32:21

I am not sure whether the issue lies with my usage or with something else. Thank you for reading!

@Hitmasu
Copy link
Owner

Hitmasu commented Jul 25, 2023

Yes, it's a bug.

There are multiple 'ret' instructions in the body from DateTime.Now:

[0] = {Instruction} call System.DateTime get_UtcNow()
[1] = {Instruction} stloc.0 
[2] = {Instruction} ldloc.0 
[3] = {Instruction} ldloca.s 2
[4] = {Instruction} call System.TimeSpan GetDateTimeNowUtcOffsetFromUtc(System.DateTime, Boolean ByRef)
[5] = {Instruction} stloc.s 4
[6] = {Instruction} ldloca.s 4
[7] = {Instruction} call Int64 get_Ticks()
[8] = {Instruction} stloc.1 
[9] = {Instruction} ldloca.s 0
[10] = {Instruction} call Int64 get_Ticks()
[11] = {Instruction} ldloc.1 
[12] = {Instruction} add 
[13] = {Instruction} stloc.3 
[14] = {Instruction} ldloc.3 
[15] = {Instruction} ldc.i8 3155378975999999999
[16] = {Instruction} bgt.un.s 37
[17] = {Instruction} ldloc.2 
[18] = {Instruction} brtrue.s 17
[19] = {Instruction} ldloc.3 
[20] = {Instruction} ldc.i8 -9223372036854775808
[21] = {Instruction} or 
[22] = {Instruction} newobj Void .ctor(UInt64)
[23] = {Instruction} ret   <------------------------------------- HERE
[24] = {Instruction} ldloc.3 
[25] = {Instruction} ldc.i8 -4611686018427387904
[26] = {Instruction} or 
[27] = {Instruction} newobj Void .ctor(UInt64)
[28] = {Instruction} ret <------------------------------------- AND HERE
[29] = {Instruction} ldloc.3 
[30] = {Instruction} ldc.i4.0 
[31] = {Instruction} conv.i8 
[32] = {Instruction} blt.s 11
[33] = {Instruction} ldc.i8 -6067993060854775809
[34] = {Instruction} br.s 9
[35] = {Instruction} ldc.i8 -9223372036854775808
[36] = {Instruction} newobj Void .ctor(UInt64)
[37] = {Instruction} ret  <------------------------------- WE JUST EXPECTED THIS RET

Currently, Jitex only expects one 'ret' instruction and just replace the last, not covering all paths.

I'll open a new issue for that.

@InCerryGit
Copy link
Author

InCerryGit commented Jul 25, 2023

Okay, thank you very much for your time. There is another scenario, if the target method throws an exception, the After interceptor will not be executed. I want to use Jitex to implement AOP programming, so I need this.

I just read through the source code and noticed that your implementation of the Intercepter aspect is different from other AOP frameworks. Is it because of support for async/await? If we want to make the program more robust, we might need to modify the target method like this:

public int Sum(int n1, int n2)
{

    try
    {
        CallContext context = new CallContext(methodHandle, Pointer.Box((void*)this), Pointer.Box((void*)n1),Pointer.Box((void*)n2));
        CallManager callManager = new CallManager(context);
        callManager.CallInteceptorsAsync();        
    }
    catch (System.Exception ex)
    {
        // log exception
    }

    try
    {
        if(context.ProceedCall){
        int result = n1+n2;
        context.SetResult(Pointer.Box((void*)result);
    }
    }
    catch (System.Exception ex)
    {
        // log exception
        context.SetException(ex);
    }

    callManager.ReleaseTask();

    if(context.Exception != null)
        throw context.Exception;

    return context.GetResult<int>();
}

I have seen the implementation of Datadog before, and this is how they did it:

Rewrite the target method body with the calltarget implementation. (This is function is triggered by the ReJIT
handler) Resulting code structure:

- Add locals for TReturn (if non-void method), CallTargetState, CallTargetReturn/CallTargetReturn<TReturn>,
Exception
- Initialize locals

try
{
  try
  {
    try
    {
      - Invoke BeginMethod with object instance (or null if static method) and original method arguments
      - Store result into CallTargetState local
    }
    catch when exception is not Datadog.Trace.ClrProfiler.CallTarget.CallTargetBubbleUpException
    {
      - Invoke LogException(Exception)
    }

    - Execute original method instructions
      * All RET instructions are replaced with a LEAVE_S. If non-void method, the value on the stack is first stored
      in the TReturn local.
  }
  catch (Exception)
  {
    - Store exception into Exception local
    - throw
  }
}
finally
{
  try
  {
    - Invoke EndMethod with object instance (or null if static method), TReturn local (if non-void method),
    CallTargetState local, and Exception local
    - Store result into CallTargetReturn/CallTargetReturn<TReturn> local
    - If non-void method, store CallTargetReturn<TReturn>.GetReturnValue() into TReturn local
  }
  catch when exception is not Datadog.Trace.ClrProfiler.CallTarget.CallTargetBubbleUpException
  {
    - Invoke LogException(Exception)
  }
}

- If non-void method, load TReturn local
- RET

I will also try to see if I can solve this problem later, but I am not very familiar with MSIL. If you have time to do all of this, that would be great.

Thank you for reading!

@Hitmasu
Copy link
Owner

Hitmasu commented Jul 25, 2023

That's a good approach using exceptions. We can try to implement that in the near future.

I just read through the source code and noticed that your implementation of the Intercepter aspect is different from other AOP frameworks. Is it because of support for async/await?

I'm not familiar with how other frameworks implement Interceptors, as they require a lot of work (interfaces, virtual methods, ...) to intercept methods, and I never dug too deep to understand how they work. Perhaps we can explore new approaches in the future to simplify and enhance our implementation.

There is another scenario, if the target method throws an exception, the After interceptor will not be executed.

I'll open a new issue for this bug.

Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature New feature
Projects
None yet
Development

No branches or pull requests

2 participants