Wednesday, June 2, 2010

Kahlua - the J2SE goodies

In this post, Lua will generally refer to the language specification, whereas Kahlua will refer to this actual implementation so don't be confused when I talk about Lua scripts running under Kahlua.

The goodies
Kahlua has a couple of useful extensions that makes your life as a developer easier when integrating Kahlua into your applications. Some of them depend on each other, so I'll go through them in dependency order to make the reading more digestible.

Converting objects
Since Lua only has a few built in types (String, Double, Boolean, KahluaTable) and Java has many many more types, it's sometimes useful to automatically convert types when going back and forth between Lua and Java.

For instance, if Java were to send an Integer object to Kahlua, that object would just be an opaque reference, without any operations. If we instead convert it to a Double, Kahlua can work with it without any problems. This is the rationale for introducing the first J2SE goodie - the KahluaConverterManager.

This is a class that can help you convert types back and forth between Lua and Kahlua. By itself, it can't convert anything at all, but you can register actual converters with it. For convenience, a couple of generic converters are bundled with Kahlua and you can also write custom converters if you need to.

The converter manager has two conversion methods: fromLuaToJava and fromJavaToLua.
Converting from Lua to Java takes an object and a Java class.
The converter manager then uses the type of the object and the Java class to determine which converter best matches the conversion. If nothing matches it returns null instead.

Converting from Java to Lua is different - it doesn't have a target type, since Lua is dynamically typed. Instead the converter just looks at the Java type to choose converter. If no suitable converter is found, it returns the object as it is.

Here are the already existing converters:

The number converter
Registering the number converter allows you to automatically convert Lua numbers (which are implemented as Double) to the numeric types in Java (int, long, char, byte, short, float, double).

It also works the other way around, converting the numeric types in Java to Double.

The table converter
This one also converts both ways. KahluaTable instances can be converted both to List and Map, and Lists and Maps can be converted back to KahluaTable. It also converts maps recursively, so be careful to avoid converting cyclic structures.

The enum converter
You can also convert enums to their string representations (enum.name()) and back, which means that you can
call Java methods that takes an enum just by passing in a string that matches one of the enums.

Calling Lua from Java
LuaCaller.protectedCall is a utility method that simplifies the usage of thread.pcall. Instead of returning a raw array where the first return value always is true or false, it returns an abstraction that hides the details: the LuaReturn class.

LuaReturn has the methods isSuccess(), getErrorMessage(), getStacktrace(), getJavaException() which makes it easier to use than extracting result[0] (and 1, 2, 3, respectively).

LuaCaller.protectedCall additionally applies the installed converters to the inputs before the call. It doesn't apply it to the return values, because it doesn't know which types it wants.

Calling Java from Lua
Be prepared - here comes the most advanced, the most useful and simply best feature of the J2SE goodies.

The LuaJavaClassExposer is a class that eliminates the need to manually define JavaFunction implementations for things that you want to expose to Lua.

Imaging writing a simple game, which is scriptable with Kahlua, and you want to be able to query a game units health from the Lua script.
Without the exposer, you'd have to write something like this:

public class GetUnitHealth implements JavaFunction {
public int call(LuaCallFrame callFrame, int nArguments) {
if (nArguments < 1) {
throw new IllegalArgumentException("Expected at least one argument");
}
Object arg = callFrame.get(0);
if (arg == null || !(arg instanceof String)) {
throw new IllegalArgumentException("Expected a string argument but got " + arg);
}
String s = (String) arg;
Unit unit = unitService.getUnit(s);
callFrame.push(unit.getHealth());
return 1;
}
}

Using the exposer instead, you can write something like this:

public class UnitFunctions {
@LuaMethod
public int getUnitHealth(String unitName) {
Unit unit = unitService.getUnit(unitName);
return unit.getHealth();
}
}

It removes a lot of the boilerplate right? The exposer takes care of all the conversions and argument matchings for us!

Instead, you can write an ordinary method in any class, and let the exposer expose that class. One JavaFunction per method will be created, and the method will be available in Lua. The generated JavaFunction will automatically use the installed converters which makes it really easy to integrate the Java and Lua code.

There are basically two different styles available when exposing:

Exposing like java
The first style is exposing things to work almost the same as in Java.
You simply call exposer.exposeLikeJava(yourClass, staticBase).
After that, a Lua script may call obj:yourMethod(arg1, arg2, ...) if obj is of type yourClass and yourMethod is a method in yourClass.

You may be wondering what staticBase means (and if not, you're not paying enough attention). A method are only available for the object if it's a regular java method.
Static methods and constructors can't be used this way. Instead, Kahlua uses the staticBase argument, which is a KahluaTable, to fill up with methods that don't belong to specific objects. If you expose the class java.util.Date, then the constructors will be available as:
staticBase.java.util.Date.new
and static methods such as parse are available as:
staticBase.java.util.Date.parse

If the class name is unique enough, it is also available directly from the staticBase root, such as staticBase.Date

These static methods and functions are what I call functions instead of methods.
Instead of invoking them with obj:method, do it like this:

local unixTimeStamp = 123
local dateObject = staticBase.java.util.Date.new(unixTimeStamp)
local dateObject = staticBase.Date.new(unixTimeStamp)
local NewDate = staticBase.Date.new
local dateObject = NewDate(unixTimeStamp)

Any of those would work.

You can also use exposer.exposeLikeJavaRecursively(yourClass) which works the sam but additionally it also exposes all other classes that yourClass interacts with in any way. This can be useful if you want to avoid manually setting up a lot of exposing, but don't use it when dealing with untrusted code.

Annotation based exposing
For untrusted code, or simply when you need more custom configuration, consider using the annotation based exposing instead.

Use one of these methods to expose a class or objects by using annotations:
exposer.exposeGlobalFunctions(object) and exposer.exposeClass(class),

Exposing global function takes an actual Java objects and scans it for method annotations. All methods which are marked as global will be exported to Lua as functions, implicitly bound to the object. This means that you should not use the obj:method() notation of invoking them, just use method().

Exposing a class instead creates Lua methods for objects of the class, and also exposes static methods as function, if they're marked as global.

The available annotations are: @LuaMethod and @LuaConstructor.

@LuaMethod has two optional parameters. "name" gives you the option to expose
the method with a different name. If this is left out, it uses the method name. The second parameter is "global" which can be true or false. True means that the method will be exposed as a function, and false means that it will be an object method. False is the default.

@LuaConstructor similarly takes a "name" parameter.
Note that it doesn't take a "global" , since constructors are never bound to an object; they are always implicitly global.

Overloading
Java supports method overloading based on parameter types, but since functions are first class objects in Lua, it can't support overloading. However, the Kahlua exposer can detect overloaded Java methods and create a limited form av overloading by creating a function that can call any of the overloaded Java methods.
Kahlua then uses the parameter types to try to invoke the method that best matches the supplied arguments.

Here are some concrete usage examples:

public class Vector {
private final double x, y, z;
@LuaConstructor(name = "newvector")
public Vector(double x, double y, double z) {
this.x = x;
this.y = y;
this.z = z;
}

@LuaMethod(name = "length")
public double length() {
return Math.sqrt(x*x + y*y + z*z);
}
}


If this class were exposed by exposer.exposeClass(Vector.class), then Lua scripts could use it like this:

local v = newvector(3, 4, 0)
local len = v:length()
assert(len == 5)

The other method is mostly useful for service-type features:

public class NetworkService {
private final Pinger pinger;

@LuaMethod(name = "ping", global = true)
public int ping(String host) {
return pinger.ping(host);
}
}
exposer.exposeGlobalFunctions(new NetworkService());

Used as:

local pingTime = ping("localhost")
That's it for this time! I could probably go on more in depth with the advanced features in the exposer, but this should already be enough for you to digest.

Time for you get started with embedding Kahlua in your Java application, and let me know how it works out! Feedback is always appreciated.

No comments:

Post a Comment