Monday, February 28, 2011

Steering Eclipse progress dialogs from Smalltalk

My first post is dedicated to a blend of Smalltalk promises, resumable exceptions and Java change listeners that I found interesting enough to share. It might be useful to others who want to put an Eclipse SWT GUI on top of their Smalltalk application. Before delving into the details, I'll clarify why I wanted to do this first.

"To find bugs in Java programs" has always been my slightly provocative answer to "What are you using Smalltalk for?". I would then explain how Smalltalk-to-Java interconnection libraries such as Johan Brichau's JavaConnect enable launching an instance of Eclipse from within a Smalltalk image. They also enable invoking methods on the Java objects that make up the Eclipse IDE. This is illustrated by the following Smalltalk snippet which retrieves the class path a particular Eclipse project should be launched with:

classPath := WriteStream on: Core.String new.
(JavaWorld.org.eclipse.jdt.launching.JavaRuntime
  computeDefaultRuntimeClassPath_IJavaProject: p)
    asSmalltalkValue
      do: [:jstring | classPath write: jstring asSmalltalkValue]
      separatedBy: [classPath write: ':'].

JavaConnect automatically generates Smalltalk selectors for Java signatures. In the above snippet, static method JavaRuntime.computeDefaultRuntimeClassPath(IJavaProject) is invoked through the Smalltalk selector computeDefaultRuntimeClassPath_IJavaProject:. JavaConnect also creates Smalltalk proxies for Java objects that are returned from an invocation. These proxies can be decorated with plain Smalltalk methods. Smalltalk proxies for instances of java.lang.String are decorated with an asSmalltalkValue method that returns a corresponding instance of Smalltalk's ByteString class. It is called by the block in the above snippet. More example uses of JavaConnect can be found on the website.

Support for callbacks is one of the lesser known features of JavaConnect. By dynamically generating Java proxies for Smalltalk objects, Smalltalk objects can be passed to Java methods. Methods invoked on such a Java proxy are delegated to the original Smalltalk object. The following extension to BlockClosure therefore allows passing Smalltalk blocks to Java methods that expect a java.lang.Runnable:

BlockClosure>>run
  self value

This brings me back to the topic of finding bugs in Java programs. It has always been hard to convince Java programmers of the need to launch Smalltalk in order to use our bug finding tools. To overcome this adoption hurdle, Carlos Noguera and I have recently begun working on an Eclipse plugin for these tools. While we are still launching Eclipse from within Smalltalk, we intend to hide the Smalltalk IDE altogether from the Eclipse user. Users should only have to interact with our tools through the Eclipse plugin. To minimize the impact on our code base, I set out on a quest to substitute Eclipse progress bars for those of the Smalltalk IDE.

Most of our existing code relies on class Notice of VisualWorks Smalltalk for progress bars dialogs. The following code snippet shows a progress bar while printing a thousand numbers on the transcript:

Notice showProgress: 'Counting till 1000'
          complete: 1000
          while: [(1 to: 1000) do: [:i | Transcript show: i; cr.
                                               IncrementNotification raiseSignal]]

Note how the code advances the progress bar by raising an IncrementNotification (a subclass of Exception). Clearly, nothing would scare an Eclipse user away faster than several of the following dialogs popping up out of thin air:


Several approaches exist to displaying progress dialogs in Eclipse. The one that fits our requirements most entails implementing the abstract method run(IProgressMonitor) in a subclass of Job. Eclipse jobs are scheduled to run in the background of the IDE in separate threads. While running, a job notifies the IDE of its progress by invoking worked(int) on the IProgressMonitor that was given as an argument to its run(IProgressMonitor) method. If set as a user job, the job will be accompanied by a progress dialog.

Armed with this knowledge, I set out to implement EclipseProgressDialog as a suitable replacement for the Notice class. This would entail scheduling the Smalltalk block that is given as the third argument to EclipseProgressDialog class>>showProgress:complete:while: as an Eclipse job. However, a call to Notice class >>showProgress:complete:while: returns the value its third argument evaluates to. A call to Job.schedule(), in contrast, returns immediately without a result. I therefore needed the Smalltalk caller of EclipseProgressDialog class>>showProgress:complete:while: to wait until Eclipse had finished running the corresponding job. The following combination of Smalltalk promises, resumable exceptions and Java change listeners gets the "job" done:

EclipseProgressDialog>>showProgress: label complete: work while: block
  | job listener result promise |
  promise := Smalltalk.Promise new.
  job := self new_String: label asJavaValue
  IBaristaRunnableWithProgressAndStatus:
    [:m | 
     m beginTask_String: label asJavaValue int: work asJavaValue.
     result := [block value] on: Smalltalk.IncrementNotification
       do: 
        [:ex | 
          m worked_int: 1.
          ex resume].
     m done.
    JavaWorld.org.eclipse.core.runtime.Status get_OK_STATUS].
  listener := Smalltalk.EclipseJobListener new.
  listener doneBlock:
    [:event |
     Smalltalk.Transcript
       show: 'Completed job: ' , event getJob getName asSmalltalkValue; cr.
     promise value: result].
  job addJobChangeListener_IJobChangeListener: listener.
  job setUser_boolean: true.
  job schedule.
  ^promise value

The first line instantiates a Smalltalk promise. The last line asks the promise for its value. This will effectively block the caller of the method until the promise has received its value. The second line instantiates an Eclipse Job subclass. Its constructor expects a Java object of which the run(IProgressMonitor) method returns an IStatus Java object. I hand it a Smalltalk block instead and rely on JavaConnect to create a Java proxy for this block. The proxy will call back to Smalltalk and invoke method BlockClosure>>run:. Its implementation is analogous to method BlockClosure>>run above. As a result, the Eclipse job will execute the Smalltalk block when it is ran. This block sets the label of the Eclipse progress bar and executes the third argument of the method. Note that this argument will raise an IncrementNotification (a subclass of Exception) to signal that the progress bar has to advance (cf. the snippet that counts till 1000). I therefore invoke IProgressMonitor.worked(int) to advance the Eclipse progress bar whenever such an exception is raised. Next, I resume this exception such that the third argument of the method continues executing. To resolve the Smalltalk promise when the Eclipse job finishes, I register an instance of EclipseJobListener with the job. This Smalltalk class amounts to a default implementation of the IJobChangeListener interface of which individual methods can be replaced by Smalltalk blocks. Here, I replace the default implementation of IJobChangeListener.done(IJobChangeEvent) by a Smalltalk block that prints some info to the transcript and resolves the Smalltalk promise that was waiting for the result of the job.

Substituting EclipseProgressDialog for Notice in the snippet that counts till 1000 now results in the following dialog popping up:


Let's hope that this one will scare fewer Eclipse users away. The actual Eclipse plugin for our tools is not yet ready to be released, but I published the changes to JavaConnect that were required to get this going such that others can try it out (cf. the download section of the JavaConnect website).


No comments:

Post a Comment