|
|
||
Tom Ball's BlogSeptember 2006 ArchivesHacking javacPosted by tball on September 16, 2006 at 03:55 PM | Permalink | Comments (20)hack (hăk) n., A non-obvious solution to an interesting problem. This definition is on the front of a tee-shirt I have from O'Reilly Media to promote their Hack Series, which includes one of my favorite books, Swing Hacks by fellow java.net bloggers Joshua Marinacci and Chris Adamson. The reason I like the Hack Series is that even for subjects you know fairly well, these books describe interesting solutions which I hadn't realized. It's that constant discovery of "non-obvious solutions" which has kept me so interested in programming over the years. Take compilers, for example: they just compile source code into object code, right? Hacking them is generally frowned upon because as Ken Thompson discused in his ACM award acceptance speech, Reflections on Trusting Trust, they are a great place to hide a trojan horse. But if the compiler is designed to function as a tool library, as javac is, much more interesting (and benign) hacks are possible. Mustang (excuse me, Java 6) has three related API which all Java tool hackers should check out: JSR-199: JavaTM Compiler API, JSR 269: Pluggable Annotation Processing API, and the Tree API (com.sun.source.tree and com.sun.source.util). The Compiler API is deceptively simple: it gives you the ability to programmatically invoke javac (actually, any tool that implements the Tool interface such that ServiceLoader can find it) and compile one or more source files to class files. That's nice if you are writing an appserver container, perhaps, but not very hack-inspiring, right? As Robin Williams said in the Disney cartoon Aladdin (I have small kids), "Wrong! But thanks for playing." The first hack leverages JSR-199's DiagnosticListener interface, which lets you listen to the errors and warnings created by the compiler. NetBeans uses this technique to display errors while you are editing Java sources; compilation is run regularly in the background but only the diagnostic events are used. JSR-199 improves on the old trick of parsing error strings with Diagnostic instances, which provide accurate source position information, and locale-independent error IDs with locale-specific error text. Still bored? How about creating your own scripting language? Rather than write a complicated intepreter, write a (hopefully) simpler compiler which outputs Java source files. Then use the Compiler API with a custom ClassLoader to dynamically load these classes on-the-fly, as if they were interpreted. Think the process is slow? The Jackpot rule language parser generates Java sources and uses this hack to compile them (look in $HOME/.jackpot). We used to conditionally compile scripts only if they had changed, but found that javac is so fast that caching didn't make a difference (we just keep the files for troubleshooting). If you don't want to write anything to disk, a related hack involves implementing the JavaFileManager interface to use memory instead of files -- javac doesn't care about files since it only uses streams supplied by whatever JavaFileManager implementation you provide. JSR-269 is officially the "Pluggable Annotation Processing API", and while it does that very well it also enables lots of other hacks. A general way to think of JSR-269 is that it gives you access to all of the types and elements (symbols) in any set of source files, not just its annotations. Its javax.lang.model.util package has easily extendable visitor and scanner base classes which can be used by tools to inspect projects at the semantic level. One group of tools which can use this hack include error checkers that validate design rules, such as that any class which extends Object.equals() also extends Object.hashCode(). One advantage of using JSR-269 types and elements rather than your own parser for these sorts of tests is that while you can infer many properties from a parse tree, the javac semantic model knows the correct properties. If you want to dig deeper than types and elements, the Tree API makes this sort of hacking simpler. Because the Tree API is not defined by a JSR, the JSR-199 and JSR-269 APIs cannot directly refer to it. So if you just look at those API, it seems impossible to inspect class members beyond their declarations. The semi-secret hack here is that javac's javax.tools.JavaCompiler implementation returns a CompilationTask instance which is also an instance of com.sun.source.util.JavacTask. This class is much more interesting to tool hackers, since it give you access to the parse trees and all JSR-269 information, plus control over javac's execution. Don't need to generate class files? Just invoke JavacTask.analyze() instead of JavacTask.generate(). This class also provides access to the Types and Elements utility classes, which make hacking even easier. So if you know you are using javac as the tool provider, you can just cast the task instance to JavacTask and have fun. These API provide a read-only model of Java source code, so it is difficult to modify source code programmatically (you can overright the files, of course, but comments and formattting get blown away). For hacking source files without angry mobs coming after you, you'll want the Jackpot API, which extends these API to provide model transforming and formatted source rewriting. These four API work together to define a toolkit to create just about any Java language-aware tool. What sort of "non-obvious solutions" can you create with them?Subversion Just Works in NetBeansPosted by tball on September 06, 2006 at 12:05 PM | Permalink | Comments (2)It was exciting to read that the JDK sources are now available as a java.net project (admittedly, my life is pretty low-key). Igor Kushnirskiy's blog on accessing the sources using Subversion was very useful, so this looked like a decent trial of the new Subversion support recently added to the NetBeans 6.0 daily builds. I was a little scared between trying pre-alpha software and reading all the comments to Igor's blog about repository access problems. Of course, those worries didn't stop me from running new software without reading any sort of documentation or doing a backup! I am a developer, after all. I pulled up the Subversion menu for the first time, found "Checkout..." which displayed a non-threatening dialog, which I filled in using the information from Igor's blog:
The first surprise was clicking the Proxy button and finding it already had my proxy server address from CVS (yes, tools should share user data, but they usually don't). Hitting the Next button, I found the tool was smart enough to see that my repository path included the folder I wanted, and initialized the Repository Folder(s) field with that path. How refreshing to have a tool just do the right thing instead of throwing up an "invalid repository URL" error dialog. I then entered my java.net user name and password:
I clicked the Finish button with my eyes squinted in case my laptop's drive exploded, but ... it ... just ... worked: an output window opened, my drive started churning (switching to a shell window showed that directories and files were being created as they were supposed to), and in a few minutes the whole JDK source was checked out.
Kudos to the NetBeans Subversion team for such a great first impression! I'm sold -- when can we move the NetBeans source from CVS to Subversion?
Munge: Swing's Secret PreprocessorPosted by tball on September 05, 2006 at 01:57 PM | Permalink | Comments (7)This may seem like ancient history now, but when Swing was first developed the team was sucked into a maelstrom of technical and corporate controversy. The biggest areas of contention were:
This problem would be a no-brainer in C/C++: use its preprocessor. But back then one of Java's perceived advantages was that it didn't have a preprocessor, and it would have been politically-incorrect for the JDK team to then create one. A more pragmatic reason was that source was distributed and needed to be compilable without other tools, plus have full comments (cpp strips them). Another reason is that to meet our "three-month" release target, we usually skipping running make and instead ran the Java interpreter with the -checksource flag, which automatically recompiled as needed (I really miss that feature). The other big issue was what Swing's package names should be: com.sun.swing.*, java.swing.*, javax.swing.*, some.other.wacky.name.*, etc. It sounds a bit silly today, but like a lot of engineering discussions this seemed like a life-and-death issue to many developers. People really wanted Swing in core but weren't will to wait for 1.2 to release (smart move, in hindsight ;-), and were leery about any name with "Sun" in it. Even the final names, javax.swing.*, were controversial because they didn't seem "core enough" to address everyone's concerns. Since Swing was releasing every week at that point, we wound up changing the package names several times. So a combination preprocessor and string translator, Munge, was created to address these two problems (source). Since its requirements were that it be small, fast and require no maintenance (it wasn't a product, after all), it purposely has no features that weren't needed by the team. It won't have your favorite cpp or sed feature, but what it did, it did quickly and correctly. It wasn't open-source then, but if you wanted a feature you were handed the source and told to have fun. Munge used an approach which is now fairly common: embed the preprocessor directives in comments so the source is still Java and can be compiled without modification. This means picking a "preferred" target, which in our case was the Java 1.1 API since we first shipped Swing for that platform. For example (from Java 1.2 SwingUtilities source), support for the Java 1.2 security API is conditionally referenced, so that only the "a doo run run" line (original comment) is executed by default:
final static void doPrivileged(final Runnable doRun) {
/*if[JDK1.2]
java.security.AccessController.doPrivileged(
new java.security.PrivilegedAction() {
public Object run() {
doRun.run();
return null;
}
}
);
else[JDK1.2]*/
doRun.run(); // ... "a doo run run".
/*end[JDK1.2]*/
}
The mystery of those gaps in the old Swing source files is now solved: Munge stripped out either the 1.1- or 1.2-specific code, leaving those gaps so the line numbers would still match in stack traces.
So why call it Munge? The main reason was to discourage developers from overusing it, so I picked a word that suggested that the tool might do nasty things to your source files (plus I'm an old Zork fan). It doesn't hurt your source files, however, and can't if you use version control (which you should). Munge was used to build Swing until the 1.1-compatible version was finally retired, and was quietly used by other JDK teams for similarly reasons. Now that the JDK team doesn't include Munge in its build any more, hopefully other developers find it useful. | ||
|
|