3

With Xtext and Xtend, I have written a DSL grammar as well as its associated code generator which creates a bunch of Java source files making up a Java application. I would like to provide my colleagues with a program which lets them select a file written in our Domain Specific language, runs the code generator to produce the Java source files of the actual Java application, compiles these files and launches the app. This process works fine on my development machine because I have everything installed, including the JDK. But providing my colleagues with this app would force them to install the JDK before using the app. So my question is: is it possible to embed the Java compiler in a distribution package? If not, do you see an other way to proceed? I am using Gradle and javapackager + Inno Setup to generate the distribution package embedding the JRE.

Update #1

 override public void afterGenerate(Resource input, IFileSystemAccess2 fsa, IGeneratorContext context)
 {
  // get the class loader
  val URL[] classLoaderUrls = #{new URL("file:jfxrt.jar")}
  val URLClassLoader cl = new URLClassLoader(classLoaderUrls)

  // set Java compiler options
  var CompilerOptions compilerOptions = new CompilerOptions
  compilerOptions.sourceLevel = ClassFileConstants.JDK1_8
  compilerOptions.complianceLevel = ClassFileConstants.JDK1_8
  val Map<String, String> options = new HashMap
  options.put(CompilerOptions.OPTION_ReportMissingSerialVersion, CompilerOptions.IGNORE)
  options.put(CompilerOptions.OPTION_ReportUnusedParameter, CompilerOptions.IGNORE)
  compilerOptions.set(options)

  val InMemoryJavaCompiler compiler = new InMemoryJavaCompiler(cl, compilerOptions)

  // compile all generated Java source files
  val result = compiler.compile(new JavaSource("xxxx/Bit.java", bitGen.javaSource.toString),
   new JavaSource("xxxx/BitNature.java", bitNatureGen.javaSource.toString),
   new JavaSource("xxxx/ImageMaker.java", imgMakerGen.javaSource.toString),
   new JavaSource("xxxx/MyApp.java", myAppGen.javaSource.toString))

  val URLClassLoader runcl = new URLClassLoader(classLoaderUrls, result.classLoader)

  val Class<?> mainClazz = runcl.loadClass("xxxx.MyApp")

  val Method mainMethod = mainClazz.getMethod("main", typeof(String[]))
  val String[] args = #["C:\\temp\\memory_a.yyy"]
  try
  {
   mainMethod.invoke(null, #[args])
  }
  catch (InvocationTargetException e)
  {
   e.getCause().printStackTrace()
  }
  catch (Exception e)
  {
   // generic exception handling
   e.printStackTrace();
  }
 }

Update #2

stepping through my code, I can see the following:

1 - this statement executes with no problem (mainClazz seems to be a valid reference)

val Class<?> mainClazz = result.classLoader.loadClass("xxxx.MyApp")

2 - mainMethod seems to be a valid reference too when I execute

val Method mainMethod = mainClazz.getMethod("main", typeof(String[]))

3 - things turn bad when I execute

mainMethod.invoke(null, #[args])

The issue arises in the JavaFX Application class, launch method on the following statement (ClassNotFoundException: xxxx.MyApp)

Class theClass = Class.forName(callingClassName, false,
                               Thread.currentThread().getContextClassLoader());
Georgie
  • 77
  • 6
  • Possible duplicate of [Can I provide runtime compiler access when running with JRE in Java 9+?](https://stackoverflow.com/questions/50628270/can-i-provide-runtime-compiler-access-when-running-with-jre-in-java-9) – Alan Birtles Jun 27 '18 at 15:01
  • you can directly call jdt compiler on java files (in memory too) you can have a look e.g. at `InMemoryJavaCompiler` in the xbase test api – Christian Dietrich Jun 27 '18 at 16:48
  • Thank you @Alan, I'm studying the Java 9 jlink solution as well as the InMemoryJavaCompiler one. – Georgie Jun 28 '18 at 12:27
  • Thank you @Christian following your suggestion, I'm now using InMemoryJavaCompiler within the afterGenerate method of my generator. Compilation is OK, but things turn bad when executing `val Method mainMethod = mainClazz.getMethod("main", typeof(String[]));` I get a ClassNotFoundException on the JavaFX Stage class. How can I fix this? Is there a way to set the classpath so that it includes JavaFX packages? – Georgie Jun 28 '18 at 12:54
  • @Christian by looking at the `Result` returned by `compiler.compile(...` I could see that the compiler silently produced a lot of errors. Can you please tell me how I can set the classpath of the InMemoryJavaCompiler? – Georgie Jun 28 '18 at 14:53
  • you can use a urlclassloader and populate it. or you add the stuff to your own classes classpath – Christian Dietrich Jun 28 '18 at 16:49
  • p.s. you can use eclipse java compiler ecj directly too. you dont have to use the inmemorycompiler if you dont like its api. see https://help.eclipse.org/neon/index.jsp?topic=%2Forg.eclipse.jdt.doc.user%2Ftasks%2Ftask-using_batch_compiler.htm – Christian Dietrich Jun 28 '18 at 16:53
  • not without any details – Christian Dietrich Jul 02 '18 at 09:06
  • Sorry @Christian, I fired a request before updating my message (please look at Update #1). Compilation is OK with your help on URLClassLoader. The issue I'm facing now is a ClassNotFoundException when executing `mainMethod.invoke`. Do you see anything wrong in my code? – Georgie Jul 02 '18 at 09:21
  • what classnotfoundexception? – Christian Dietrich Jul 02 '18 at 09:40
  • @Christian ClassNotFoundException on xxxx.MyApp – Georgie Jul 02 '18 at 09:53
  • which class is not found and which is the one you load? – Christian Dietrich Jul 02 '18 at 10:20
  • did you debug the classloaders? – Christian Dietrich Jul 02 '18 at 10:31
  • can you provide a minimal reproducing example? – Christian Dietrich Jul 02 '18 at 15:07
  • In what form? Do you want a zip of my Eclipse project after simplification (only for your eyes) or should I post on SO the content of the essential files? It's difficult to decide which ones are relevant to you... – Georgie Jul 02 '18 at 15:21
  • why do you use the context classloader? you know it is not the one what is used to load the main? the classloader of the code afterGenerate is the context classloader. that one does not know what class – Christian Dietrich Jul 02 '18 at 20:21

1 Answers1

2
package org.xtext.example.mydsl2.tests

import java.lang.reflect.Method
import java.net.URLClassLoader
import org.eclipse.xtext.util.JavaVersion
import org.eclipse.xtext.xbase.testing.InMemoryJavaCompiler
import org.eclipse.xtext.xbase.testing.JavaSource

class SampleXXX {

    def static void main(String[] args) {
        val urls = #[]
        val classLoader = new URLClassLoader(urls)
        val compiler = new InMemoryJavaCompiler(classLoader, JavaVersion.JAVA8)
        val result = compiler.compile(new JavaSource("demo/Demo.java", '''
        package demo;
        public class Demo {
            public static void main(String[] args) throws Exception {
                Class theClass = Class.forName("demo.Demo", false,
                    Thread.currentThread().getContextClassLoader());
            }
        }
        '''))
        val URLClassLoader runcl = new URLClassLoader(#[], result.classLoader)
        (new Thread() {

            override run() {
            val Class<?> mainClazz = runcl.loadClass("demo.Demo")
            val String[] args2 = #["C:\\temp\\memory_a.yyy"]
            val Method mainMethod = mainClazz.getMethod("main", typeof(String[]))
            mainMethod.invoke(null, #[args2])
            }

        }=>[
            contextClassLoader = runcl
        ]).start
    }

}
Christian Dietrich
  • 11,778
  • 4
  • 24
  • 32
  • the context class loader is actually used by the launch method of the JavaFX Application class. To be honest, I have never been comfortable with class loaders, but I understand the elegant way you have solved my problem. Thanks a lot! – Georgie Jul 03 '18 at 06:43