Skip to main content
GameDev.net gamedev.net
🔒 Locked 🤖 Godot

Java scripting in Java (not JavaScript)

Started by SteveTaylor Jan 17, 2006 at 3:34 AM 4 replies 1.2k views
Original Post
SteveTaylor
SteveTaylor
I'm making an MMORPG and have decided to make it with Java. I also decided I wanted to use Java as the scripting language, by having classes load dynamically. I worked on this problem for a few days. I ended up writing a custom ClassLoader because the existing ones wouldn't reload classes that had changed. I ran into all sorts of problems, such as getting ClassCastException if I loaded a class and cast it to itself! This is because each class loader has its own heirarchy (well, almost). The solution was to make each script (which is implemented as a dynamically loaded class) implement the same interface. Then I had the custom ClassLoader always load the dynamic class (script) from file, and always delegate the interface loading to the parent ClassLoader (there is a ClassLoader heirarchy). Then I was able to cast the dynamically loaded class to the interface and call its methods through that interface. While the program is running, I can change the class, recompile it and reload it, and have the main program create a new object from it and execute its methods. Exciting stuff. This gives me the ability to use Java as a scripting language in a Java program, which of course has some benefits. Here's the code: MainProgram.java
import java.io.*;
import java.lang.reflect.*;

class MainProgram
{
	public static void main(String[] args) throws IOException, ClassNotFoundException, InstantiationException, IllegalAccessException
	{
		BufferedReader stdin = new BufferedReader(new InputStreamReader(System.in));
		String command;
		do
		{
			System.out.println("Enter a command. 'P' will invoke DynInterface.printVersion(). Blank line will end.");
			command = stdin.readLine();
			if (command.toLowerCase().equals("p"))
			{
				ClassLoader loader = new FlexibleClassLoader(new CustomDelegationFilter());
				Class dynClass = loader.loadClass("DynClass");
				DynInterface dynObject = (DynInterface)(dynClass.newInstance());
				dynObject.printVersion();
			}
		}
		while (command.length() > 0);
	}
}


class FlexibleClassLoader extends ClassLoader
{
	private DelegationFilter delegate;
	
	
	// If this constructor is used, no delegation filter is specified. This constructor
	// therefore creates a delegation filter that causes this class loader to implement a
	// load-first delegate-last policy for all non-SDK classes.
	public FlexibleClassLoader()
	{
		delegate = new DelegationFilter()
		{
			// Nothing specified to be always delegated.
			public boolean alwaysDelegateClass(String className)
			{
				return false;
			}
			
			// Only SDK classes are predelegated. Saves time and ClassCastExceptions.
			public boolean preDelegateClass(String className)
			{
				return isSdkClass(className);
			}
			
			// All other classes not found can be delegated.
			public boolean postDelegateClass(String className)
			{
				return true;
			}
		};
	}
	
	
	// Assign a custom delegation filter to this class loader.
	public FlexibleClassLoader(DelegationFilter delegate)
	{
		this.delegate = delegate;
	}
	
	
	protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException
	{
		if (delegate.alwaysDelegateClass(name))
		{
			System.out.println("Class " + name + " is being auto-delegated to the parent loader.");
			return getParent().loadClass(name);
		}
		else if (delegate.preDelegateClass(name))
		{
			try
			{
				System.out.println("Class " + name + " is being predelegated to the parent loader.");
				return getParent().loadClass(name);
			}
			catch (ClassNotFoundException exception)
			{
				System.out.println("Predelegated class not found! Attempting to load class " + name + " from file.");
				return loadClassFromFile(name, resolve);
			}
		}
		else
		{
			try
			{
				System.out.println("Class " + name + " is loading from file.");
				return loadClassFromFile(name, resolve);
			}
			catch (ClassNotFoundException exception)
			{
				System.out.println("Class " + name + " is being postdelegated to the parent loader.");
				if (delegate.postDelegateClass(name))
				{
					return getParent().loadClass(name);
				}
				else
				{
					throw exception;
				}
			}
		}
	}
	
	
	// Load a class from file
	protected Class<?> loadClassFromFile(String name, boolean resolve) throws ClassNotFoundException
	{
		try
		{
			// Open the class file for reading.
			File classFile = new File(name + ".class");
			FileInputStream stream;
			try
			{
				stream = new FileInputStream(classFile);
			}
			catch (FileNotFoundException exception)
			{
				throw new ClassNotFoundException(name, exception);
			}

			// Read the class file's raw bytes into an array.
			byte[] rawClassData;
			try
			{
				rawClassData = new byte[(int)classFile.length()];
				stream.read(rawClassData);
				stream.close();
			}
			catch (IOException exception)
			{
				throw new ClassNotFoundException(name, exception);
			}

			// Convert the array into a class.
			Class<?> loadedClass;
			try
			{
				loadedClass = defineClass(name, rawClassData, 0, rawClassData.length);
			}
			catch (ClassFormatError exception)
			{
				throw new ClassNotFoundException(name, exception);
			}

			// Resolve the class if requested by the caller.
			if (resolve)
			{
				resolveClass(loadedClass);
			}

			System.out.println("Loaded class " + name + " from file successfully.");
			return loadedClass;
		}
		catch (ClassNotFoundException exception)
		{
			// Delegate class loading to the parent.
			System.out.println("Could not load class " + name + ". Propogating exception.");
			throw exception;
		}
	}


	// Load a class without resolving (the default behaviour according to the SDK docs).
	public Class<?> loadClass(String name) throws ClassNotFoundException
	{
		return loadClass(name, false);
	}
}


abstract class DelegationFilter
{
	// Returns true if the specified class will be loaded ONLY by the parent loader.
	public abstract boolean alwaysDelegateClass(String className);
	
	// Returns true if the specified class will be attempted to be loaded by the
	// parent loader before trying to custom load it.
	public abstract boolean preDelegateClass(String className);
	
	// Returns true if the specified class will be loaded by the parent loader
	// after unsuccessfully trying to custom load it.
	public abstract boolean postDelegateClass(String className);
	
	// Is the className from the SDK?
	protected boolean isSdkClass(String className)
	{
		return className.startsWith("java.") || className.startsWith("javax.") || className.startsWith("org.");
	}
}


class CustomDelegationFilter extends DelegationFilter
{
	public boolean alwaysDelegateClass(String className)
	{
		if (className.equals("DynInterface"))
		{
			return true;
		}
		else
		{
			return false;
		}
	}
	public boolean preDelegateClass(String className)
	{
		if (isSdkClass(className) || className.equals("DynInterface"))
		{
			return true;
		}
		else
		{
			return false;
		}
	}
	
	// If we haven't found what we're looking for, there's nothing left to do.
	public boolean postDelegateClass(String className)
	{
		return false;
	}
}


DynInterface.java
public interface DynInterface
{
	public void printVersion();
}


DynClass.java
/* This is a version of the DynClass class */
public class DynClass implements DynInterface
{
	public DynClass()
	{
	}

	private int version = 1;

	public void printVersion()
	{
		System.out.println(this.toString());
	}

	public String toString()
	{
		return "This is DynClass version " + version;
	}
}


Here is some sample i/o:
$java MainProgram
Enter a command. 'P' will invoke DynInterface.printVersion(). Blank line will end.
p
Class DynClass is loading from file.
Class DynInterface is being auto-delegated to the parent loader.
Class java.lang.Object is being predelegated to the parent loader.
Loaded class DynClass from file successfully.
Class java.lang.System is being predelegated to the parent loader.
Class java.lang.StringBuilder is being predelegated to the parent loader.
Class java.io.PrintStream is being predelegated to the parent loader.
This is DynClass version 1
Enter a command. 'P' will invoke DynInterface.printVersion(). Blank line will end.
Then I modify and recompile DynClass.java...
p
Class DynClass is loading from file.
Class DynInterface is being auto-delegated to the parent loader.
Class java.lang.Object is being predelegated to the parent loader.
Loaded class DynClass from file successfully.
Class java.lang.System is being predelegated to the parent loader.
Class java.lang.StringBuilder is being predelegated to the parent loader.
Class java.io.PrintStream is being predelegated to the parent loader.
This is DynClass version 2
Enter a command. 'P' will invoke DynInterface.printVersion(). Blank line will end.


[Edited by - SteveTaylor on January 17, 2006 9:51:46 AM]
Monkeyget
Monkeyget
It's interesting and in fact some c++ game use java as a scripting language (i don't remember the name but there is at least a plane simulation and some kind of rpg).
The problem i see with your technic is how can i edit a script? To edit a script i need to modify the content of it(duh) but the problem is...how do i compile the updated code? i can't just change the code, i must also recompile the thing and THAT'S annoying because it's not a script anymore but a piece of "executable"!
As a user of your engine, in order to recompile the code i need to use an ide or use some kind of .bat(launch javac) or whatever. That means that in order to modify a script i need to have the jdk...and since most user only have a jre they can't modify the script unless they download some kind of heavy framework.

I know that it's possible but not easy to compile a piece of code programmatically, it would remove the burden of having to recompile manually.
Another solution would be to interpret the .java code (no .class anymore). I know there is a project somewhere on the internet that allows that kind of thing, it's useful because it's far more flexible.
SteveTaylor
SteveTaylor
Quote:
The problem i see with your technic is how can i edit a script? To edit a script i need to modify the content of it(duh) but the problem is...how do i compile the updated code? i can't just change the code, i must also recompile the thing and THAT'S annoying because it's not a script anymore but a piece of "executable"!
As a user of your engine, in order to recompile the code i need to use an ide or use some kind of .bat(launch javac) or whatever. That means that in order to modify a script i need to have the jdk...and since most user only have a jre they can't modify the script unless they download some kind of heavy framework.

Hi Monkeyget. They're all valid points. First of all, the scripting FAQs mention that the line between scripting languages and regular programming languages is blurred. I'll simply make the statement that I'm calling it scripting because it does the same job. If you were using Lua scripts in C++, you could dynamically load those without restarting the whole program. You couldn't do that in C++ with C++ classes. The other point I want to make is that this is for my MMORPG, which I will be personally responsible for modding. My requirements never involved the general public modding my game. So as you can see, using dynamically loaded classes instead of a language that's officially called a scripting language makes sense here.

Thanks for you input.
mrkaizen999
mrkaizen999

i found intresting art about class loader:
http://www.javaworld.com/javaworld/jw-03-2000/jw-03-classload.html#resources

in this article there is described method very similiar to yours but i was thiniking about that cast exception i do not think it is from class loading hierarhy, have you tried in another way for example:

1.load class as you desribed in that code in first qute
2.do not cast it to your class itself but use a reflect mechanism like for example this:

loadedClass = defineClass(name, rawClassData, 0, rawClassData.length);
resolveClass(loadedClass);

// for now everything should be allright, generally no exceptions should ocur
// as you write this kind of code will generate cast exception right?
// - mySrciptClass - is for wxample class script you wanted dynamicly load
// mySrciptClass = (myScriptClass) c ; am i right?
// maybe instead interface etc you should try use reflection for example like

Method m = c.getMethod( "printVersion", ... );
m.invoke(...); // printVersion invoked

----------------------------

have you tried in this way?

i was thinking about how sun implemented this becouse if some class is loaded,
all subclasses must be preloaded, those subclases are loaded using classloader, but class loader return Class type ( not a for example myScriptClass), i am saing that java knows about what is a name etc of this class but classloader returns class, so you tried to cast class to itself becouse everyone will try to implement it in this way (i'm too) but i think that sun is making it in diffrent way, maybe it is made by native code, becouse in some way it mast make code like this mySrciptClass(c), have anyone tried to solve this kind of problem or how sun is making it
mrkaizen999
mrkaizen999

now i have founded how to make it , it is decribed in this article:

http://www.javaworld.com/javaworld/jw-10-2003/jw-1003-beanshell-p3.html

you just need to make simple class (not interface methods) with abstract methods so there will not occur cast exception, well it is similiar to interfaces but it works, check this article it is very usful in making scripting language even with on fly compilation

Topic Locked

This topic has been locked by a moderator. New replies are not allowed.

Sign in to reply to this topic.