Search code examples
javapythonpython-2.7pylucenejcc

PyLucene JCC: implement a Java interface in python and receive Java thread callbacks through it


I'm playing around with my new toy, JCC 2.21, and am having trouble implementing callbacks in a python script. I have wrapped the following simple Java thread API and am calling it from python 2.7 (CPython), but when I call the JccTest.addJccTestListener(JccTestListener) method, the JVM reports a null argument.

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;

public class JccTest implements Runnable {

    private final Object listenersLock = new Object();
    private final List<JccTestListener> listeners = new ArrayList<JccTestListener>();
    private final AtomicBoolean running = new AtomicBoolean(false);
    private final AtomicBoolean finished = new AtomicBoolean(false);

    public void start() {
        if (running.compareAndSet(false, true)) {            
            new Thread(this).start();
        }
    }

    public void stop() {
        finished.set(true);
    }

    public void addJccTestListener(JccTestListener l) {
        if (l == null) {
            throw new IllegalArgumentException("argument must be non-null");
        }
        synchronized (listenersLock) {
            listeners.add(l);
        }
    }

    public void removeJccTestListener(JccTestListener l) {
        synchronized (listenersLock) {
            listeners.remove(l);
        }
    }

    @Override
    public void run() {     
        System.out.println("Start");

        while (!finished.get()) {
            System.out.println("Notifiying listeners");
            synchronized (listenersLock) {
                for (JccTestListener l : listeners) {
                    System.out.println("Notifiying " + String.valueOf(l));
                    l.message("I'm giving you a message!");
                }
            }
            System.out.println("Sleeping");
            try {
                Thread.sleep(5000);
            } catch (InterruptedException ex) {
                continue;
            }
        }

        running.set(false);
        System.out.println("Stop");
    }

    public static void main(String[] args) throws InterruptedException {
        JccTest test = new JccTest();
        test.addJccTestListener(new JccTestListener() {

            @Override
            public void message(String msg) {
                // called from another thread
                System.out.println(msg);
            }
        });
        test.start();
        Thread.sleep(10000);
        test.stop();
    }
}

public interface JccTestListener {
    public void message(String msg);
}

Generated wrapper with:

python -m jcc --jar jcc-test.jar --python jcc_test --build --install

And then executed this script (equivalent to the main method of JccTest):

import jcc_test
import time, sys

jcc_test.initVM(jcc_test.CLASSPATH)

test = jcc_test.JccTest()


class MyListener(jcc_test.JccTestListener):
    def __init__(self):
        pass

    def message(self, msg):
        print msg

test.addJccTestListener(MyListener())
test.start()
time.sleep(10)
test.stop()

sys.exit(0)

Which results in:

"python.exe" jcc_test_test.py
Traceback (most recent call last):
  File "jcc_test_test.py", line 16, in <module>
    test.addJccTestListener(MyListener())
jcc_test.JavaError: java.lang.IllegalArgumentException: argument must be non-null
    Java stacktrace:
java.lang.IllegalArgumentException: argument must be non-null
    at com.example.jcc.JccTest.addJccTestListener(JccTest.java:32)

Besides the null listener instance, is doing something like this even possible with CPython? I've read that in its implementation only one thread may execute the python script at a time, which might (?) be a problem for me. Doing something like this with Jython was trivial.

I'm rather new to python so please be gentle.


Solution

  • Figured it out. You need to define a pythonic extension for a java class to make this work. The detailed procedure is described in JCC documentation (Writing Java class extensions in Python) and is rather simple.

    First, code a class that implements your interface and add some magic markers that are recognized by JCC and affect what the wrapper generator will generate.

    public class JccTestListenerImpl implements JccTestListener {
    
        // jcc specific
        private long pythonObject;
    
        public JccTestListenerImpl() {}
    
        @Override
        public void message(String msg) {
            messageImpl(msg);
        }
    
        // jcc specific
        public void pythonExtension(long pythonObject) {
            this.pythonObject = pythonObject;
        }
    
        // jcc specific
        public long pythonExtension() {
            return this.pythonObject;
        }
    
        // jcc specific
        @Override
        public void finalize() throws Throwable {
            pythonDecRef();
        }
    
        // jcc specific
        public native void pythonDecRef();
    
        public native void messageImpl(String msg);
    
    }
    

    The markers are denoted by my comments and must appear verbatim in any class that is to be extended in python. My implementation delegates the interface method to a native implementation method, which will be extended in python.

    Then generate the wrapper as usual:

    python -m jcc --jar jcc-test.jar --python jcc_test --build --install
    

    And finally make a python extension for the new class:

    import jcc_test
    import time, sys
    
    jvm = jcc_test.initVM(jcc_test.CLASSPATH)
    
    test = jcc_test.JccTest()
    
    
    class MyListener(jcc_test.JccTestListenerImpl):
        ## if you define a constructor here make sure to invoke super constructor
        #def __init__(self):
        #    super(MyListener, self).__init__()
        #    pass
    
        def messageImpl(self, msg):
            print msg
    
    
    listener = MyListener()
    test.addJccTestListener(listener)
    test.start()
    time.sleep(10)
    test.stop()
    
    sys.exit(0)
    

    This now works as expected with callbacks coming in.

    "python.exe" jcc_test_test.py
    Start
    Notifiying listeners
    Notifiying com.example.jcc.JccTestListenerImpl@4b67cf4d
    I'm giving you a message!
    Sleeping
    Notifiying listeners
    Notifiying com.example.jcc.JccTestListenerImpl@4b67cf4d
    I'm giving you a message!
    Sleeping
    
    Process finished with exit code 0