В предыдущем посте я начал вести блог о том, как Xtext 2.1 чрезвычайно облегчил мощную интеграцию с Java в вашем DSL. В этой статье я хотел бы продолжить экспериментировать с этим механизмом, и в частности я начну использовать Xbase , новую библиотеку языка выражений, которая позволяет интегрировать (Java-подобные) выражения в ваш DSL.
Если вы посмотрите на Пять простых шагов к языку JVM , вы увидите мощь Xbase! Более того, в Xtext 2.1 использование Xbase еще проще, поскольку, написав только AbstractModelInferrer, вы получите полную интеграцию с Java (путем сопоставления элементов вашей модели AST с концепциями Java) и генератор (который создаст код Java, соответствующий этому отображению). ). Таким образом, вам даже не понадобится генератор вообще.
Тем не менее, в этом посте я хотел бы проверить, как использовать только небольшую часть Xbase и при этом иметь контроль над частью генерации: в частности (для других проектов) я хотел бы сохранить контроль над генерацией для моей модель, полагаясь на генерацию Xbase для деталей Xbase. Таким образом, в этом посте я опишу:
- как интегрировать выражения Xbase (XExpression) в ваш DSL
- написать генератор для вашего DSL и повторно использовать XbaseCompiler для кода XExpressions
Следуя духу предыдущего поста , я не буду использовать пример Domainmodel, но что-то действительно простое, например пример Greeting, т. Е. Базовый DSL, который вы получите при создании проекта Xtext внутри Eclipse. В частности, вы можете захотеть просмотреть предыдущий пост перед прочтением этого поста (поскольку мы повторно используем некоторые из представленных здесь концепций).
Итак, прежде всего, создайте новый проект Xtext; Я назову этот проект org.xtext.example.helloxbase (и файлы будут иметь расширение helloxbase ). Теперь мы можем начать генерировать артефакты Xtext. С настройками по умолчанию вы также получите HelloXbaseGenerator.xtend , который мы сейчас генерируем для кода Java. Первая реализация генератора: для каждого элемента Greeting мы получим пакет и класс, названный в соответствии с функцией имени Greeting (упаковывать все строчные буквы и имя класса с заглавной буквой). Кроме того, сгенерированный метод main выведет соответствующее приветствие:
package org.xtext.example.helloxbase.generator import org.eclipse.emf.ecore.resource.Resource import org.eclipse.xtext.generator.IFileSystemAccess import org.eclipse.xtext.generator.IGenerator import org.xtext.example.helloxbase.helloXbase.Greeting import static extension org.eclipse.xtext.xtend2.lib.ResourceExtensions.* class HelloXbaseGenerator implements IGenerator { override void doGenerate(Resource resource, IFileSystemAccess fsa) { for(greeting: resource.allContentsIterable.filter(typeof(Greeting))) { fsa.generateFile( greeting.packageName + "/" + // package greeting.className + ".java", // class name greeting.compile) } } def compile(Greeting greeting) ''' package «greeting.packageName»; public class «greeting.className» { public static void main(String args[]) { System.out.println("Hello «greeting.name»"); } } ''' def className(Greeting greeting) { greeting.name.toFirstUpper } def packageName(Greeting greeting) { greeting.name.toLowerCase } }
Если вы уже знакомы с Xtend2 (или читали предыдущий пост), вы должны иметь представление о том, что делает генератор; давайте попробуем генератор: перезапустите ваш второй экземпляр eclipse и убедитесь, что в проекте, который вы создали с помощью файла helloxbase, у вас есть папка с именем src-gen , и эта папка настроена как исходная папка. Если вы наберете что-то вроде
Hello foo! Hello bar!
Вы должны увидеть некоторый код Java, сгенерированный в папке src-gen (согласно схеме, описанной выше).
Теперь идея состоит в том, чтобы обогатить приветствующий DSL HelloXbase некоторыми выражениями Xbase, поэтому мы изменим грамматику языка (HelloXbase.xtext) следующим образом (вы должны быть знакомы с функциями импорта Xtext):
grammar org.xtext.example.helloxbase.HelloXbase with org.eclipse.xtext.xbase.Xbase generate helloXbase "http://www.xtext.org/example/helloxbase/HelloXbase" Model: imports += Import* greetings+=Greeting*; Import: 'import' importedNamespace = QualifiedNameWithWildcard ; QualifiedNameWithWildcard: QualifiedName '.*'? ; Greeting: 'Hello' name=ID 'from' expression = XExpression '!' ;
Обратите внимание, что теперь мы получаем нашу грамматику из грамматики Xbase, поэтому теперь мы можем полагаться на правила грамматики Xbase; идея состоит в том, чтобы иметь возможность написать в нашем DSL что-то вроде
Попробуйте сами: восстановите артефакты Xtext и перезапустите экземпляр eclipse во время выполнения. Чтобы полностью насладиться синтаксисом Xbase, убедитесь, что в проекте, который вы создали в рабочей среде времени выполнения, у вас также есть org.eclipse.xtext.xbase.lib в качестве зависимости в MANIFEST.
Вы можете увидеть много интересных вещей здесь! В основном у вас есть весь мощный Java-подобный синтаксис выражений Xbase, доступ к статическим методам с использованием ::, замыканий и методов расширения ( nullOrEmpty из StringExtensions )! Все бесплатно!
Теперь, что случилось с нашим генератором? Это все еще вызывается автоматически при изменении ресурса? Боюсь, что нет: как только вы вывели свою грамматику из Xbase, в вашем проекте появился новый файл HelloXbaseJvmModelInferrer.xtend , поскольку в Xtext 2.1, как было указано в начале, стратегия генерации изменилась. В частности, в src-gen AbstractHelloXbaseRuntimeModule IGenerator теперь связан с org.eclipse.xtext.xbase.compiler.JvmModelGenerator ; так как мы хотим вернуть наш собственный генератор, мы просто переопределяем эту привязку в HelloXbaseRuntimeModule (той, которую нам разрешено изменять):
public class HelloXbaseRuntimeModule extends org.xtext.example.helloxbase.AbstractHelloXbaseRuntimeModule { /** * Avoids to use the default org.eclipse.xtext.xbase.compiler.JvmModelGenerator * when using xbase. * @see org.xtext.example.helloxbase.AbstractHelloXbaseRuntimeModule#bindIGenerator() */ @Override public Class<? extends IGenerator> bindIGenerator() { return HelloXbaseGenerator.class; } }
Если вы перезапустите свой экземпляр времени выполнения, вы увидите, что наш генератор вернулся!
Теперь мы хотим сгенерировать код Java, соответствующий Xbase XExpression; Затем мы просто повторно используем XbaseCompiler в нашем генераторе (что, как вы знаете, в Xtext означает «впрыскивать его»). В генераторе мы теперь будем использовать также ImportManager (который объясняется в предыдущем посте ) и начнем проводить некоторые эксперименты с XbaseCompiler:
package org.xtext.example.helloxbase.generator import com.google.inject.Inject import org.eclipse.emf.ecore.resource.Resource import org.eclipse.xtext.common.types.TypesFactory import org.eclipse.xtext.generator.IFileSystemAccess import org.eclipse.xtext.generator.IGenerator import org.eclipse.xtext.xbase.XExpression import org.eclipse.xtext.xbase.compiler.ImportManager import org.eclipse.xtext.xbase.compiler.StringBuilderBasedAppendable import org.eclipse.xtext.xbase.compiler.XbaseCompiler import org.xtext.example.helloxbase.helloXbase.Greeting import static extension org.eclipse.xtext.xtend2.lib.ResourceExtensions.* class HelloXbaseGenerator implements IGenerator { @Inject protected XbaseCompiler xbaseCompiler override void doGenerate(Resource resource, IFileSystemAccess fsa) { for(greeting: resource.allContentsIterable.filter(typeof(Greeting))) { fsa.generateFile( greeting.packageName + "/" + // package greeting.className + ".java", // class name greeting.compile) } } def getJvmType(Greeting greeting) { val declaredType = TypesFactory::eINSTANCE.createJvmGenericType declaredType.setSimpleName(greeting.className) declaredType.setPackageName(greeting.packageName) declaredType } def compile(Greeting greeting) ''' «val importManager = new ImportManager(true, getJvmType(greeting))» «val mainMethod = compile(greeting, importManager)» package «greeting.packageName»; «IF !importManager.imports.empty» «FOR i : importManager.imports» import «i»; «ENDFOR» «ENDIF» «mainMethod» ''' def compile(Greeting greeting, ImportManager importManager) ''' public class «greeting.className» { public static void main(String args[]) { «compile(greeting.expression, importManager)» System.out.println("Hello «greeting.name» from "); } } ''' def compile(XExpression xExpression, ImportManager importManager) { val result = new StringBuilderBasedAppendable(importManager) xbaseCompiler.toJavaStatement(xExpression, result, true) result } def className(Greeting greeting) { greeting.name.toFirstUpper } def packageName(Greeting greeting) { greeting.name.toLowerCase } }
Давайте сосредоточимся на этих частях:
def compile(Greeting greeting, ImportManager importManager) ''' public class «greeting.className» { public static void main(String args[]) { «compile(greeting.expression, importManager)» System.out.println("Hello «greeting.name» from "); } } ''' def compile(XExpression xExpression, ImportManager importManager) { val result = new StringBuilderBasedAppendable(importManager) xbaseCompiler.toJavaStatement(xExpression, result, true) result }
Мы попытались использовать toJavaStatement из XbaseCompiler, который кажется хорошим кандидатом на то, что мы хотим; в частности, передавая true в качестве последнего аргумента, мы говорим, что «результат» генерации должен быть присвоен переменной Java, чтобы мы могли ссылаться на нее; давайте посмотрим, что происходит в рабочей среде времени выполнения с этим поколением:
Эй, круто! XbaseCompiler генерирует все операторы Java, соответствующие исходному выражению XExpression (включая замыкания)! Теперь нам хотелось бы получить доступ только к последней сгенерированной переменной (в приведенных выше примерах _isNullOrEmpty и _apply соответственно), чтобы мы могли сами сгенерировать некоторый код Java с использованием этой переменной; но как мы можем знать его имя?
Ну, класс StringBuilderBasedAppendable не только содержит результат генерации; он также содержит карту со всеми промежуточными локальными переменными Java, сгенерированными компилятором. Каждая переменная на карте имеет в качестве ключа соответствующий объект Xbase. Таким образом, нам нужно знать только имя переменной, соответствующее нашему выражению XExpression, и все готово:
result.getName(greeting.expression)
Итак, финальный генератор выглядит так:
package org.xtext.example.helloxbase.generator import com.google.inject.Inject import org.eclipse.emf.ecore.resource.Resource import org.eclipse.xtext.common.types.TypesFactory import org.eclipse.xtext.generator.IFileSystemAccess import org.eclipse.xtext.generator.IGenerator import org.eclipse.xtext.xbase.XExpression import org.eclipse.xtext.xbase.compiler.ImportManager import org.eclipse.xtext.xbase.compiler.StringBuilderBasedAppendable import org.eclipse.xtext.xbase.compiler.XbaseCompiler import org.xtext.example.helloxbase.helloXbase.Greeting import static extension org.eclipse.xtext.xtend2.lib.ResourceExtensions.* class HelloXbaseGenerator implements IGenerator { @Inject protected XbaseCompiler xbaseCompiler override void doGenerate(Resource resource, IFileSystemAccess fsa) { for(greeting: resource.allContentsIterable.filter(typeof(Greeting))) { fsa.generateFile( greeting.packageName + "/" + // package greeting.className + ".java", // class name greeting.compile) } } def getJvmType(Greeting greeting) { val declaredType = TypesFactory::eINSTANCE.createJvmGenericType declaredType.setSimpleName(greeting.className) declaredType.setPackageName(greeting.packageName) declaredType } def compile(Greeting greeting) ''' «val importManager = new ImportManager(true, getJvmType(greeting))» «val mainMethod = compile(greeting, importManager)» package «greeting.packageName»; «IF !importManager.imports.empty» «FOR i : importManager.imports» import «i»; «ENDFOR» «ENDIF» «mainMethod» ''' def compile(Greeting greeting, ImportManager importManager) ''' «val result = compile(greeting.expression, importManager)» public class «greeting.className» { public static void main(String args[]) { «result» Object expression = «result.getName(greeting.expression)»; System.out.println("Hello «greeting.name» from " + expression.toString()); } } ''' def compile(XExpression xExpression, ImportManager importManager) { val result = new StringBuilderBasedAppendable(importManager) xbaseCompiler.toJavaStatement(xExpression, result, true) result } def className(Greeting greeting) { greeting.name.toFirstUpper } def packageName(Greeting greeting) { greeting.name.toLowerCase } }
И во время выполнения вы получите то, что хотели:
Вы можете найти источники для проекта helloxbase на
https://github.com/LorenzoBettini/Xtext2-experiments
Надеюсь, вы найдете этот пост полезным и следите за новыми сообщениями о Xtext ?