I talked to Corky about Sun’s new recommendation to put all GUI code on the event thread. He said he had tried that for a while with DrJava, but he ran into a lot of complications. The splash screen didn’t show up anymore, for example. We both also agreed that, regardless of what Sun says, most code isn’t written that way and is probably still ok. We want to generate as few error messages as possible, so I should probably still find a way to allow non-event threads to interact with Swing components as long as they have not been realized.
Fortunately, I’ve found there’s an easy way to tell if an AWT component has been realized: To make that happen, Component.addNotify
is called. There’s even a method that should be able to let me know exactly if that has already happened: Component.isDisplayable
. It should therefore be fairly easy to modify the thread checks to only complain about execution by a non-event thread if Component.isDisplayable
returns true
.
The question I’m trying to answer right now, though, is: Whose isDisplayable
do I call? In many cases, that should be obvious: If the method is in a subclass of java.awt.Component
, then it will be this.isDisplayable()
, but what if it’s not a subclass? Both javax.swing.table.TableModel
and javax.swing.tree.TreeModel
should only be executed by the event thread, yet they are model classes and therefore not subclasses of Component
.
Initially, I’ll probably treat them as always unsafe. Then there are some refinements that I can make:
- Treat them as safe until any GUI component has been realized.
- Treat them as safe until a GUI component of the correct class, i.e.
TableModel
orTreeModel
has been realized. - Treat them as safe until a specific instance of a GUI component has been realized.
The first two are relatively easy to do: The thread checker will just keep flags that are initially false, and once the first addNotify
of the specific class is called, the flag is set to true. The annotations can then be limited to be active only when that flag is true. This can be generalized to the notions of “until” and “after”, corresponding to the values of false and true of a flag.
Ideally, though, you’d like to tie a certain instance of a model class to a certain instance of a view component. Here’s a rough sketch:
The @OnlyRunBy
annotation is essentially unchanged, except that it can also be applied to fields, local variables, and parameters. The @ThreadDesc
annotation adds a when
member, which is an array of @Condition
annotations. @Condition
annotation itself looks like this:
public @interface Condition {
String until() default "";
String after() default "";
}
It allows the programmer to specify that a @ThreadDesc
annotation is either active until a certain named condition has been met, or that is only active after that has happened (only one should be specified at a time). Whether all @Conditions
declared in a @ThreadDesc
have to apply or just one, that is up for debate.
A condition specified by a name is set to true when an annotated GUI component is realized (or when any other method is called). This requires the @ConditionTrigger
annotation:
@Retention(RetentionPolicy.CLASS)
@Target({ElementType.FIELD,ElementType.LOCAL_VARIABLE,
ElementType.PARAMETER,ElementType.TYPE})
public @interface ConditionTrigger {
String name();
String method();
}
Now we could link together both the model and the view component by using both the @Condition
and the @ConditionTrigger
annotations:
@ConditionTrigger(name="some-unique-name",
method="addNotify()V")
private JTable _table;
@OnlyRunBy(@ThreadDesc(eventThread=true,
when=@Condition(after="some-unique-name")))
private MyTableModel _model;
It should be obvious that since these annotations can be applied to fields, local variables, and parameters and not only to types, methods and constructors, inserting the correct bytecode is a lot harder. In order to insert the checks in a method, I would have to know the annotations at all the usage sites.
I think at least for the @OnlyRunBy
checks I should go for call-site instrumentation, i.e. inserting the checks where the call to a method is made, instead of putting all the checks in the method that is called. The @ConditionTriggers
have to go into the method that is supposed to do the trigger, e.g. into the void JTable.addNotify()
method in the example above. Right now, I don’t see a way around making two passes over the classes. Another problem is that due to Sun’s incompetence (sorry, I can’t say it any other way) annotations on local variables do not get put in class files, so I won’t see them.
I’ll ask Corky how general I should make this. This is a few days or weeks away anyway.
Update
I just had the idea of showing how incompetent Sun was by taking their apt (Annotation Processing Tool) and writing a small processor that reads Java source and adds a new attribute to code attributes, describing annotations on local variables. Guess what? Not even apt is able to deal with annotations on local variables. Why in the world did Sun allow annotations where they can’t be used in any way at all? Please, if someone knows, I would love to know!
Now there’s a clumsy way around that… I introduce another annotation that can be applied to constructors and methods, i.e. anywhere where locals can appear, that links the name of the local variable to the annotations it should carry:
@Retention(RetentionPolicy.CLASS)
@Target({ElementType.CONSTRUCTOR,ElementType.METHOD})
@interface LocalVariable {
String name();
OnlyRunBy[] onlyRunBy() default {};
NotRunBy[] notRunBy() default {};
}
Now we can do the glorious deed of indirectly attaching a @OnlyRunBy
annotation to a local variable in a way that’s visible in the class file:
@LocalVariable(name="myModel", onlyRunBy=@OnlyRunBy(
@ThreadDesc(eventThread=true, when=
@Condition(after="some-unique-name"))))
public void run() {
...
MyTableModel myModel = new MyTableModel();
}
Thanks, Sun. Really pretty.