Skip to main content

Enactor Script Interfaces

One of the most useful features of Enactor Script is its ability to script Java interfaces. This lets a script act as an event handler, listener, or any other component expected by a Java API - and because the scripted object behaves like a normal Java object to the caller, it plugs directly into existing applications without any special handling on their part.

Anonymous Inner-Class Style

The most explicit way to implement an interface is with standard Java anonymous inner class syntax:

buttonHandler = new ActionListener() {
actionPerformed( event ) {
print(event);
}
};

button = new JButton();
button.addActionListener( buttonHandler );

This creates an object that implements ActionListener and assigns it to buttonHandler. That object contains the scripted method actionPerformed(), which is what actually runs when the interface method is invoked.

button here is a standard Swing component - it has no idea that invoking buttonHandler.actionPerformed() will run Enactor Script under the hood. More generally, a scripted interface implementation works by matching: when a Java method is invoked on a script that implements an interface, Enactor Script looks for a scripted method with a matching name and argument types, invokes it, and passes back any return value. Because everything runs in the same JVM, you can freely pass live Java objects as arguments and return values.

'this' References as Interface Types

The anonymous inner class style lets you explicitly build an object of a given interface type, just as in Java - but Enactor Script goes further. Any this type reference in a script - the current scope, or any scripted object - can automatically stand in for any interface type where one is expected. Enactor Script casts it to the correct type and handles the method delegation for you.

The button handler from above can be written more simply as a plain method in the current scope:

actionPerformed( event ) {
print( event );
}

button = new JButton("Foo!");
button.addActionListener( this );

Instead of wrapping actionPerformed() in its own scripted object, it is placed directly in the current context, and this is passed wherever the interface type is expected. When the button fires an ActionEvent, Enactor Script finds and runs the appropriately named method.

You do not have to define interface methods globally - you can scope them to any object, as described in Enactor Script Objects. The example below creates a scripted "message button" object that displays its own message when pressed, keeping both its handler method and its state together:

messageButton( message ) {
JButton button = new JButton("Press Me");
button.addActionListener( this );

actionPerformed( e ) {
print( message );
}
}

messageButton("Hey you!");
messageButton("Another message...");

Each call to messageButton() creates its own method context - its own local variables and its own instance of the ActionListener handler - so the two buttons act independently, each printing its own message.

Interface Types and Casting

Enactor Script usually casts a scripted object to the required interface type automatically, but you can also do it explicitly:

actionPerformed( event ) {
print( event );
}

button.addActionListener(
(ActionListener)this ); // added cast

The two forms behave the same, but the explicit cast happens immediately, at the point of the cast, rather than being deferred until Enactor Script tries to match the argument against a method signature.

This matters when a this reference leaves the script before Enactor Script has a chance to see how it will be used - for example, when placing a scripted object into a Map or List typed to hold Object, or when a scripted object is returned from an embedding application's eval() call or fetched as a variable. In those cases, an explicit cast lets you fix the type before the reference leaves the script.

Dummy Adapters and Incomplete Interfaces

In Java it is common to write a "dummy" adapter for an interface with many methods - a class that stubs out every method so a subclass only has to override the ones it actually cares about. Enactor Script does not need this: a script only has to implement the interface methods it expects to be called. Calling an interface method that wasn't implemented raises java.lang.reflect.UndeclaredThrowableException - an artefact of the dynamic proxy mechanism used to implement scripted interfaces. Its getCause() reveals the underlying Enactor Script evaluation error explaining that no matching method was found.

The invoke() Meta-Method

For interfaces with a lot of methods, Enactor Script offers a shortcut: define a special invoke( name, args ) method in any scripted context, and it will be called for any interface method that wasn't implemented directly.

mouseHandler = new MouseListener() {
mousePressed( event ) {
print("mouse button pressed");
}

invoke( method, args ) {
print("Undefined method of MouseListener interface invoked: "
+ name +", with args: "+args
);
}
};

Here, only mousePressed() is implemented directly; the other four MouseListener methods all fall through to invoke(), which simply reports the method name and arguments.

This is also useful for exploring an unfamiliar API - for example, printing every method the SAX ContentHandler interface calls while parsing an XML document:

import javax.xml.parsers.*;
import org.xml.sax.InputSource;

factory = SAXParserFactory.newInstance();
saxParser = factory.newSAXParser();
parser = saxParser.getXMLReader();
parser.setContentHandler( this );

invoke( name, args ) {
print( name );
}

parser.parse( new InputSource(args[0]) );

invoke( name, args ) also works outside of an interface implementation - placed in your own scope or the global scope, it catches any unknown method call, which is handy for implementing your own "virtual" commands:

invoke(name,args) { print("Command: "+name+" invoked!"); }
noSuchMethod(); // prints "Command: noSuchMethod() invoked!"

Threads and the Runnable Interface

A this type reference can implement java.lang.Runnable directly, so a scripted object with a run() method can be handed straight to a Thread:

foo() {
run() {
// do work...
}
return this;
}

foo = foo();
// Start two threads on foo.run()
new Thread( foo ).start();
new Thread( foo ).start();

Enactor Script is thread-safe internally, so scripts that avoid the usual thread-safety pitfalls - such as unsynchronized access to shared variables or objects - can safely run multi-threaded.

Limitations

On modern JVMs, Enactor Script can script any Java interface. Only under very old JVMs lacking dynamic proxy support was scripting limited to a fixed, statically implemented set of core AWT and Swing interfaces.