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.
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