Dynamically loading .java source files

Greetings and thanks for passing by!

I´m working on a game prototype that needs to be extensible by my game designer and I´ve found out that Beanshell is too slow for my needs. The game is a rule-based simulation in a 9600 cell grid. That´s why I got to use the JavaCompiler API to compile and instantiate a class specified by a .java source file.

On Windows (and building from Sublime Text 2 via the Processing Plugin), I got it working. On OSX though, it throws the following exception:

    java.lang.IllegalAccessError:
        tried to access class TSBRPG$TileStyle from class TestStyle_Script
        at TestStyle_Script.initStyle(TestStyle_Script.java from InMemoryJavaFileObject:14)

The external file I want to load is this (TestStyle_Script.java)

    import processing.core.*;
    
    public class TestStyle_Script
    {
        TSBRPG game;
        
        public TestStyle_Script(TSBRPG game)
        {
            this.game = game;
        }
        
        public void initStyle(TSBRPG.TileStyle style)
        {
            style.col = this.game.color(0, 128, 0);
        }
        
        public void update(TSBRPG.Tile t)
        {
            t.moveTile(0, 1);
        }
    };

This is the tab/inner class that both uses the DynamicClass loader and is the TSBRPG$TileStyle that can´t be accesed by the external class:

        class TileStyle
    {
        String id;
        DynamicClass script;
        color col;
        
        TileStyle(String id)
        {
            this.id = id;
            this.col = color(255, 0, 255);
            this.script = new DynamicClass(APPLET, this.id + "_Script", dataPath("scripts/tiles/" + this.id + "_Script.java"));
            
            Class[] classParams = new Class[1];
            classParams[0] = TileStyle.class;
            this.script.prepareMethodCall("initStyle", classParams);
            
            Object[] objParams = new Object[1];
            objParams[0] = this;
            this.script.callMethod(objParams);
            
            classParams[0] = TSBRPG.Tile.class;
            this.script.prepareMethodCall("update", classParams);
        }
        
        void update(Tile t)
        {
            Object[] objParams = new Object[1];
            objParams[0] = t;
            this.script.callMethod(objParams);
        }
    };

This is the DynamicClass.java tab I put together from various sources:

    import java.io.File;
    import java.io.IOException;
    import java.lang.reflect.Method;
    import java.net.MalformedURLException;
    import java.net.URI;
    import java.net.URL;
    import java.net.URLClassLoader;
    import java.util.Arrays;
    import java.util.Locale;
 
    import javax.tools.Diagnostic;
    import javax.tools.DiagnosticListener;
    import javax.tools.JavaCompiler;
    import javax.tools.JavaFileObject;
    import javax.tools.SimpleJavaFileObject;
    import javax.tools.StandardJavaFileManager;
    import javax.tools.ToolProvider;

    import processing.core.*;

    public class DynamicClass
    {
    private PApplet applet;
    private String classOutputFolder;
    private String className;
    private Class realClass;
    private Object instance;
    private Method method;

    public DynamicClass(PApplet applet, String className, String filename)
    {
        this.applet = applet;
        this.className = className;
        this.classOutputFolder = this.applet.sketchPath("build-tmp");
        JavaFileObject file = getJavaFileObject(filename);
        Iterable<? extends JavaFileObject> files = Arrays.asList(file);
        compile(files);
        loadClassAndInstantiate();
    }   
 
    public class InMemoryJavaFileObject extends SimpleJavaFileObject
    {
        private String contents = null;
 
        public InMemoryJavaFileObject(String className, String contents) throws Exception
        {
            super(URI.create("string:///" + className.replace('.', '/') + Kind.SOURCE.extension), Kind.SOURCE);
            this.contents = contents;
        }
 
        public CharSequence getCharContent(boolean ignoreEncodingErrors)
                throws IOException
        {
            return contents;
        }
    }
 
    private JavaFileObject getJavaFileObject(String filename)
    {
        String[] lines = this.applet.loadStrings(filename);
        StringBuilder builder = new StringBuilder();

        for (String line : lines)
        {
            if (builder.length() > 0)
            {
                builder.append("\n");
            }
            builder.append(line);
        }

        String contents = builder.toString();

        JavaFileObject so = null;
        try
        {
            so = new InMemoryJavaFileObject(this.className, contents);
        }
        catch (Exception exception)
        {
            exception.printStackTrace();
        }
        return so;
    }
 
    /** compile your files by JavaCompiler */
    public void compile(Iterable<? extends JavaFileObject> files)
    {
        //get system compiler:
        JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
 
        // for compilation diagnostic message processing on compilation WARNING/ERROR
        MyDiagnosticListener c = new MyDiagnosticListener();
        StandardJavaFileManager fileManager = compiler.getStandardFileManager(c,
                                                                              Locale.ENGLISH,
                                                                              null);
        //specify classes output folder
        Iterable options = Arrays.asList("-d", classOutputFolder);
        JavaCompiler.CompilationTask task = compiler.getTask(null, fileManager,
                                                             c, options, null,
                                                             files);
        Boolean result = task.call();
        if (result == false)
        {
            System.out.println("Error compiling " + this.className);
        }
    }
 
    /** run class from the compiled byte code file by URLClassloader */
    public void loadClassAndInstantiate()
    {
        // Create a File object on the root of the directory
        // containing the class file
        File file = new File(this.classOutputFolder);
 
        try
        {
            // Convert File to a URL
            URL url = file.toURL(); // file:/classes/demo
            URL[] urls = new URL[] { url };
 
            // Create a new class loader with the directory
            ClassLoader loader = new URLClassLoader(urls, ClassLoader.getSystemClassLoader());
            
            realClass = loader.loadClass(this.className);
            this.instance = realClass.getConstructor(TSBRPG.class).newInstance(this.applet);
        }
        catch (MalformedURLException e)
        {
        }
        catch (ClassNotFoundException e)
        {
        }
        catch (Exception ex)
        {
            ex.printStackTrace();
        }
    }

    public void prepareMethodCall(String methodName, Class[] methodParams)
    {
        try
        {
            this.method = realClass.getDeclaredMethod(methodName, methodParams);
        }
        catch (NoSuchMethodException e)
        {
            e.printStackTrace();
        }
    }

    public void callMethod(Object[] params)
    {
        try
        {
            method.invoke(instance, params);
        }
        catch (Exception ex)
        {
            ex.printStackTrace();
        }
    }

    public class MyDiagnosticListener implements DiagnosticListener
    {
        public void report(Diagnostic<? extends JavaFileObject> diagnostic)
        { 
            System.out.println("Line Number->" + diagnostic.getLineNumber());
            System.out.println("code->" + diagnostic.getCode());
            System.out.println("Message->"
                               + diagnostic.getMessage(Locale.ENGLISH));
            System.out.println("Source->" + diagnostic.getSource());
            System.out.println(" ");
        }
    }
    };

Answers

  • As you can see on the DynamicClass::loadClassAndInstantiate method, I'm making the URLClassLoader a child of the system one (which is the solution I found that made it work on Windows).

  • (As a side note use <pre lang="processing">...your code...</pre> not <code>tags)

  • Thanks a lot fjen! My OCD was killing me :D Any ideas about my issue?

  • Can I do a little bump here? I think that, if working, this could make for a powerful tool for all advanced users.

  • Did you try public class TileStyle ? Also, you have some unnecessary use of this in there and you don't need semicolons after class definitions. I assume you know Javascript. (My OCD.)

  • edited October 2013

    Hi rbrauer, thanks for posting! =D I'll try to define it as public like you say, but I thought that all inner classes in a sketch were public by default. In any case, my OSX testing rig is at work so I'll give feedback in 14ish hours. About the unnecesary "this", it's part of my coding style, to differentiate local vars from class properties. The semicolons after class definitions are there by inertia. I code in some other languages that require this (C++ being the main one right now). Also, I didn't understand your Javascript reference. I'm scripting directly in Java, not in Javascript (which is too slow for my needs).

  • edited October 2013

    ... but I thought that all inner classes in a sketch were public by default.

    It's just that inner and static nested classes can't hide themselves from their top-class and vice-versa. :-B
    So it makes anything other than public useless.

    However, access levels matter when a class tries to access nested classes from another class.
    This time, those keywords do count! @-)

    Also, know that Processing's own pre-processor stamps public in everything w/o an explicit access level.
    But for any other IDE, we don't have such luxury! [-X

  • Heh, what do you know: Putting every referenced property and method as public did the trick. Strange that it worked as it is on Windows though, especially since only in that platform I use an external IDE.

    Well GoToLoop, I wish I could flag you for being awesome =D I got to show the prototype to my team and they loved it.

    Thank you!

    Oni

Sign In or Register to comment.