jpype-project / jpype

JPype is cross language bridge to allow Python programs full access to Java class libraries.
http://www.jpype.org
Apache License 2.0
1.12k stars 182 forks source link

Intercept Java function calls #1122

Closed MrFizzyBubbs closed 1 year ago

MrFizzyBubbs commented 1 year ago

I would like to execute a function after every Java function call to monitor the state of my .jar file. I am able to achieve this with a tracer (see below) but this is not optimal for many reasons. Is there a better approach in jpype?

import jpype

kolmafia = jpype.JPackage("net").sourceforge.kolmafia

def tracer(frame, event, arg):
    if event == "call":
        # disable per-line events on the frame to improve performance
        frame.f_trace_lines = False
        return tracer
    elif event == "return":
        if not kolmafia.KoLmafia.permitsContinue():
            kolmafia.KoLmafia.forceContinue()
            raise RuntimeError(kolmafia.KoLmafia.getLastMessage())

sys.settrace(tracer)
Thrameos commented 1 year ago

There are no hooks in JPype that one could catch every Java call. However, you can monkey patch to create such a hook using the fact that all JPype calls go through the same private internal classes. If you are trying to intercept methods it would have to go through _JMethod while setting or getting any field goes through _JField.

Here is a short example:

import jpype
import jpype.imports
jpype.startJVM()

# Use a monkey patch of _jpype._JMethod where all Java methods calls go through
import _jpype                               # we need to access the private classes
jmethod = _jpype._JMethod        #  method calls all go through _JMethod
call = getattr(jmethod, "__call__") # first we have to move the old call method out of the way
setattr(jmethod, "_c", call)       

# define a user hook
def hook(self, *args):
    print("Pre")
    ret = self._c(*args)  # call the real Java implementation
    print("Post")
    return ret
setattr(jmethod, "__call__", hook)  # replace the call method with user hook

# Test it
import java  
mystr = java.lang.String("hello")
print(mystr.substring(3))

That is my best guess. Please note this will void the warrantee.

MrFizzyBubbs commented 1 year ago

Thank you for the prompt and detailed answer, the bit about all method calls routing through _jpype._JMethod is exactly what I was looking for. I appreciate all the work that you and others have done on JPype!

MrFizzyBubbs commented 1 year ago

For those who are interested in doing something similar, I found that I also needed to patch _JClass.__call__ in addition to _JMethod.__call__.