Moving an Xtend generator into its own plugin

One of the nice things that you get when starting an Xtext project is an Xtend based generator that is automatically invoked when you save an Xtext model file. The Xtend generator for your language resides in the .generator subpackage of your language. The problem with that is that it is usually no good idea to have the generator bundled with your language. It is a completely seperate feature which is reasonable to put it in an own plugin. Further, the DSL plugins must not depend on the generator plugin, the dependency must be vice versa. This article describes the steps that need to be done for this.

As a reference take the sources from project Spray. There you can find an concrete example where the described steps have been applied.

Create the generator plugins

Create two additional plugins: One for the runtime part of the generator, one for the UI contributions.

Generator runtime plugin

Move everything from the .generator subpackage of your DSL runtime project to the generator plugin. (In Spray this is org.eclipselabs.spray.generator.graphiti). The dependencies are the same as in the DSL runtime project, but add the DSL runtime plugin as additional dependency (see this MANIFEST.MF as reference). Don’t forget to add an xtend-gen source folder to the project.

Guice module for runtime plugin

Create a Module class extending AbstractGenericModule in the runtime plugin. In this module at least the IGenerator implementation class must be bound.

public class GraphitiGeneratorModule extends AbstractGenericModule {
    public Class<? extends org.eclipse.xtext.generator.IGenerator> bindIGenerator() {
        return SprayGenerator.class;
    }
    ...
}

Generator UI plugin

The UI plugin must basically bind the JavaProjectBasedBuilderParticipant in its own module and register it through the org.eclipse.xtext.builder.participant extension point. These steps are required:

UI Guice Module

Create a module class extending AbstractGenericModule and bind JavaProjectBasedBuilderParticipant:

public class GraphitiGeneratorUIModule extends AbstractGenericModule {
    private final AbstractUIPlugin plugin;
    
    public GraphitiGeneratorUIModule (AbstractUIPlugin plugin) {
        this.plugin = plugin;
    }

    @Override
    public void configure(Binder binder) {
        super.configure(binder);
        binder.bind(AbstractUIPlugin.class).toInstance(plugin);
        binder.bind(IDialogSettings.class).toInstance(plugin.getDialogSettings());
    }
    /**
     * Bind the JavaProjectBasedBuilderParticipant in order to invoke the generator during the build.
     */
    public Class<? extends org.eclipse.xtext.builder.IXtextBuilderParticipant> bindIXtextBuilderParticipant() {
        return org.eclipse.xtext.builder.JavaProjectBasedBuilderParticipant.class;
    }

    ...
}

Activator class

Create a class Activator which creates a Guice injector from the modules:

  1. DSL runtime module
  2. org.eclipse.xtext.ui.shared.SharedStateModule
  3. DSL UI module
  4. Generator runtime module
  5. Generator UI module
public class Activator extends AbstractUIPlugin {
	private Injector injector;
	private static Activator INSTANCE;

	public Injector getInjector() {
		return injector;
	}
	
	@Override
	public void start(BundleContext context) throws Exception {
		super.start(context);
		INSTANCE = this;
		try {
		    injector = Guice.createInjector(Modules2.mixin(new SprayRuntimeModule(), new SharedStateModule(), new SprayUiModule(this), new GraphitiRuntimeModule(), new GraphitiGeneratorModule(), new GraphitiGeneratorUIModule(this)));
		} catch (Exception e) {
			Logger.getLogger(getClass()).error(e.getMessage(), e);
			throw e;
		}
	}
	
	@Override
	public void stop(BundleContext context) throws Exception {
		injector = null;
		super.stop(context);
	}
	
	public static Activator getInstance() {
		return INSTANCE;
	}
	
}

Open the manifest editor. On the “Overview” page enter the Activator class name. Also check the options “Activate this plug-in when one of its classes is loaded” and “This plug-in is a singleton”.

ExecutableExtensionFactory

Create a class ExecutableExtensionFactory. Since this class won’t be public API it is a good approach to put it into an internal package.

public class ExecutableExtensionFactory extends AbstractGuiceAwareExecutableExtensionFactory {

	@Override
	protected Bundle getBundle() {
		return Activator.getInstance().getBundle();
	}
	
	@Override
	protected Injector getInjector() {
		return Activator.getInstance().getInjector();
	}
	
}

plugin.xml

Add a plugin.xml with the following content:

<?xml version="1.0" encoding="UTF-8"?>
<?eclipse version="3.4"?>
<plugin>
   <extension
         point="org.eclipse.xtext.builder.participant">
      <participant
            class="org.eclipselabs.spray.generator.graphiti.ui.internal.ExecutableExtensionFactory:org.eclipse.xtext.builder.IXtextBuilderParticipant">
      </participant>
   </extension>
</plugin>

Check that the ExecutableExtensionFactory class name matches yours.

Remove the GeneratorFragment

Open the .mwe2 workflow of your DSL project. Remove the entry for the GeneratorFragment:

// Code generator
fragment = generator.GeneratorFragment {
  generateJavaMain = false 
  generateMwe = false
  generatorStub = true 
}

Now regenerate the DSL.

Add binding for IWorkspaceRoot

The generator fragment contributes a binding for IWorkspaceRoot to the UI Module of your DSL, which is required by the builder participant. Therefore this binding must be added manually to your UI Module.

public class SprayUiModule extends AbstractSprayUiModule {
    ...
    public org.eclipse.core.resources.IWorkspaceRoot bindIWorkspaceRootToInstance() {
        return org.eclipse.core.resources.ResourcesPlugin.getWorkspace().getRoot();
    }
}

Result

As a result your DSL plugins should have no dependencies on your generator plugin. When you save a model file in your Eclipse instance with the deployed plugins the code generator should be invoked. The pattern described here would also allow to create multiple generator plugins for the same DSL which are invoked independently when building the project. Each of them registers its own builder participant and invokes its own generator.

About these ads

9 thoughts on “Moving an Xtend generator into its own plugin

  1. Pingback: Moving an Xtend generator into its own plugin | Eclipse | Syngu

  2. Works very well!

    One important thing took me a while to find out: if you provide a project wizard, then you have to do the Guice binding manually in your DSK UI Module. THis is usually done by org.eclipse.xtext.generator.generator.GeneratorFragment, but we don’t use it anymore :)

    public org.eclipse.core.resources.IWorkspaceRoot bindIWorkspaceRootToInstance() {
    return org.eclipse.core.resources.ResourcesPlugin.getWorkspace()
    .getRoot();
    }

    Otherwise the project creation wizard would fail…

  3. Hi Karsten,

    with your approach you are creating two injectors, which means that singletons will exist twice. Also the instances configured in EMFs global registry will be different from what your injector creates. This is dangerous and I recommend not to do it.
    You could fix it by changing the UI’s activator to obtain the injector from your generator’s activator.

    Why do you think that the compiler does not belong to the language implementation?

    Sven

    • I remember that I’ve heard somewhere that each plugin should create its own injector. And again, this would require that a language plugin depends on a generator plugin. Is this really required? I don’t hope so.

      It should potentially be possible to have a language be agnostic to the generators. A generator is a kind of application of a language, and there might be multiple of them, which should be completely independent. For example, you could have plugins producing code for different platforms. It would depend on the existence of these plugins which code is produced, or even all of them. If you would ship all this within your language plugin, it just grows uncontrolled just be the need to implement generators. It must be possible to build different generator plugins for the same language without the need to blow up the language plugin itself. For me, it just feels wrong.

    • Hi,

      I do not want to chime in here unprofessionally, but i think Karsten is right.
      I have seen projects which create a normal EMF ecore model. They use this model to create an xText DSL for it.
      (no generate grammar there so xText will only be the UI)
      And after that use for example Acceleo to generate the code.
      So i think this is a valid usage and therefore the DSL(Ui) should be separated out from the actual generator…

      But this is only my 2 cents.

      Regards,
      attila

  4. Thnaks for this blog post which helped me extract the generator. But there was one last step missing from the tutorial that cost me much time was (don’t know if this is obvious since I’m a beginner). If you add your new ExecutableExtensionFactory from generator.ui plugin via the plugin.xml to the “org.eclipse.xtext.builder.participant” Extension Point, you have ro remove the corresponding entry in the “original” ui plugin (Plugin.xml) to prevent an additional BuilderParticipant from beeing added. This additional BuilderParticipant deleted all files generated by my generator as it ran as the last participant in the Build. The result was a generated directory structure under the output folder but no generated files at all.

  5. Thanks for the article. Two problems I have found is that

    1) The Guice injector in the Activator class is referencing GraphitiRuntimeModule (“new GraphitiRuntimeModule()”), but GraphitiRuntimeModule was not mentioned in the article and is not necessary.

    2) As Patrick Lehmann noted, I had to remove org.eclipse.xtext.builder.participant Extension Point from plugin.xml in the original DSL UI plugin to make it work.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s