10 min. reading time

Xtext derives a metamodel from the grammar file by default. For more complex languages it is often better to define the metamodel manually since it allows much more control over the AST, the abstract syntax tree. The metamodel has to be an EMF metamodel, which could be defined in Ecore or XcoreIn this article I will show you the setup to build the model project and how to integrate it into the project build.

The Xcore SDK allows to define the metamodel in a DSL which has some advantages against the definition in Ecore. However, using Xcore adds some complexity to the toolchain. Some of these implications are adressed by Holger Schill's presentation "Using Xcore models with Xtext". The presentation also covers the build integration aspect, but with Maven and with the "old" Xtext generator workflows.

Nowadays often Gradle is chosen as the build system to use. The new Xtext project wizard introduced with Xtext 2.9 allows to choose Gradle as the build system and produces the necessary build files for the projects. When using Xtext's web integration it is even mandatory at this time to use Gradle.

The problem now is that when an Xcore metamodel should be used, there is not much known on how to integrate Xcore in a Gradle build. At GitHub there is a xcore-gradle-example, but this only works with Gradle 2.7 and not with the current version 3.1. Further, it uses the old Xtext gradle plugin and not the new Xtext Builder Plugin.

This article shows an example which uses the following technology stack:

  • Xtext 2.10
  • Gradle 3.1
  • Xcore 1.3.1

All sources are available in our GitHub repository.

Project Creation

As a reference the default "Greeting DSL" is used which is produced by the Xtext project wizard. The following options where chosen on the Advanced Xtext Configuration page:

  • Web Integration
  • Generic IDE Support
  • Preferred Build System: Gradle

This creates the following projects:

org.eclipse.example.mydsl.parent
|
+--org.eclipse.example.mydsl
|
+--org.eclipse.example.mydsl.ide
|
+--org.eclipse.example.mydsl.web

For the parent project the latest gradle wrapper was added:

gradle wrapper --gradle-version 3.1

Model Project

The Xcore model should be defined in a separate subproject org.eclipse.example.mydsl.model. The new project is added as a subproject to the settings.gradle of the parent project:

include 'org.xtext.example.mydsl.model' include 'org.xtext.example.mydsl' ... >

It contains the Xcore model mydsl.xcore in the folder src. The declared metamodel is equivalent to the one that Xtext would generate from the grammar definition:

@Ecore(nsPrefix="mydsl",nsURI="http://www.xtext.org/example/mydsl/MyDsl")
@GenModel(
    bundleManifest="false",
    modelDirectory="org.xtext.example.mydsl.model/build/xcore/main", 
    complianceLevel="8.0"
)
package org.xtext.example.mydsl

class Model {
    contains Greeting[] greetings
}

class Greeting {
    String name
}

Note that the model directory is org.xtext.example.mydsl.model/build/xcore/main. This directory is added as a source folder to the project.

To build the project with Gradle the following build.gradle is used:

dependencies {
    compile "org.eclipse.xtext:org.eclipse.xtext:${xtextVersion}"
    compile "org.eclipse.xtext:org.eclipse.xtext.xbase:${xtextVersion}"
    compile 'org.eclipse.emf:org.eclipse.emf.ecore.xcore.lib:+'

    xtextLanguages 'org.eclipse.emf:org.eclipse.emf.ecore.xcore:+'
    xtextLanguages 'org.eclipse.emf:org.eclipse.emf.ecore.xcore.lib:+'
    xtextLanguages 'org.eclipse.emf:org.eclipse.emf.codegen.ecore:+'
    xtextLanguages 'org.eclipse.emf:org.eclipse.emf.codegen.ecore.xtext:+'
    xtextLanguages "org.eclipse.xtext:org.eclipse.xtext.ecore:${xtextVersion}"

}

sourceSets {
    main {
        resources {
            exclude '**/*.xcore'
        }
    }
}

xtext {
    version = "${xtextVersion}"
    languages {
        ecore {
            setup = 'org.eclipse.xtext.ecore.EcoreSupport'
        }
        codegen {
            setup = 'org.eclipse.emf.codegen.ecore.xtext.GenModelSupport'
        }
        xcore {
            setup = 'org.eclipse.emf.ecore.xcore.XcoreStandaloneSetup'
            generator.outlet.producesJava = true
        }
    }
}

The main part is the configuration of the xtext-builder-plugin. EcoreSupport and GenModelSupport have to be added as languages, since they are used by Xcore. For Xcore the producesJava flag has to be set, so that the produced Java classes are compiled and packaged in the resulting jar file.

In the sourceSets section .xcore files are excluded from resources. Without that setting Gradle would copy the .xcore file during the build and within Eclipse there would be unwanted duplication errors.

Runtime Project

In the DSL runtime project we have to make some changes in order to use the metamodel defined with Xcore.

build.gradle

First, a build dependency has to be added to the build.gradle file:

dependencies {
    compile project(':org.xtext.example.mydsl.model')
    ...
}

Next, two additional dependencies have to be added to the mwe2 configuration:

dependencies {
    mwe2 "org.eclipse.emf:org.eclipse.emf.mwe2.launch:2.8.3"
    mwe2 "org.eclipse.xtext:org.eclipse.xtext.xtext.generator:${xtextVersion}"
    // added for Xcore support
    mwe2 'org.eclipse.emf:org.eclipse.emf.ecore.xcore:+'
    mwe2 'org.eclipse.emf:org.eclipse.emf.codegen.ecore.xtext:+'
}

DSL Grammar

Since the grammar's metamodel should not be generated anymore the grammar definition MyDsl.xtext has to be changed slightly. Instead of declaring the Ecore package to generate, we import it:

grammar org.xtext.example.mydsl.MyDsl with org.eclipse.xtext.common.Terminals

import "http://www.xtext.org/example/mydsl/MyDsl"

Model:
    greetings+=Greeting*;

Greeting:
    'Hello' name=ID '!';

Xtext Generator Workflow

Also the generator workflow file GenerateMyDsl.mwe2 has to be changed to use the manually defined metamodel.

First, the model project org.xtext.example.mydsl.model is declared using a project mapping:

bean = org.eclipse.emf.mwe.utils.StandaloneSetup {
    projectMapping = {
        path = "${rootPath}/org.xtext.example.mydsl.model"
        projectName = "org.xtext.example.mydsl.model"
    }
}

This is required in order to reference the mydsl.xcore file later with a platform resource URI. Without this registration, the platform URI cannot be mapped. The mydsl.xcore file is now declared as a referencedResource for the MyDSL language configuration:

language = StandardLanguage {
    name = "org.xtext.example.mydsl.MyDsl"
    fileExtensions = "mydsl"

    referencedResource = "platform:/resource/org.xtext.example.mydsl.model/src/mydsl.xcore"
    ...
}

Standalone Setup

When not using the DSL in the context of an Eclipse plugin, the metamodel's EPackage has to be registered in the EPackage registry. For the case that the metamodel is automatically generated, Xtext takes care of that registration in the generated base class of the DSL's standalone setup class.

Since the metamodel is not automatically registered in the case that it defines manually, the standalone setup class MyDslStandaloneSetup has to be extended to do the registration process explicitly:

class MyDslStandaloneSetup extends MyDslStandaloneSetupGenerated {

    def static void doSetup() {
        new MyDslStandaloneSetup().createInjectorAndDoEMFRegistration()
    }

    override register(Injector injector) {
        if (!EPackage.Registry.INSTANCE.containsKey(MydslPackage.eNS_URI)) {
            EPackage.Registry.INSTANCE.put(MydslPackage.eNS_URI, MydslPackage.eINSTANCE);
        }
        super.register(injector)
    }
}

Overview

The following image shows the resulting project structure with the files mydsl.xcore, MyDsl.xtext and GenerateMyDsl.mwe2:

Conclusion

Using an Xcore metamodel for an Xtext grammar requires an additional subproject which holds the Xcore definition. The article showed the necessary setup to build the model project with Gradle and integrate it into the project build.

Some additional changes have to be done to the Xtext grammar to use the predefined metamodel and to integrate it into Xtext's generator workflow, but it's worth the effort especially when it comes to more complex languages.

The Gradle build configuration is a bit tricky, but taking this project as a reference it should be mostly copy/paste work.

Comments