Now I've been pondering and studying this since the conference, and I promised the other day I'd bundle my thoughts on such an implementation.
Base principle: it should feel natural to Java developers, because to get them on board, we'll have to save them from having to deal with C.
Bonus points if we can get the same glue working on Android + Desktop platforms - iOS being the outsider here (*)
1. A simple example
What would a simple external look like?
Code: Select all
package com.example.external;
import com.runrev.external.LCExternalCommand;
import com.runrev.external.LCExternalFunction;
import com.runrev.external.LCExternalLibrary;
public class ExampleExternal implements LCExternalLibrary {
@LCExternalCommand
public void exampleExternalCommand(String[] args) { ... }
@LCExternalFunction
public String exampleExternalFunction(String[] args) { ... }
}
- LCExternalLibrary is an interface to mark this class as a collection of external commands and functions
- LCExternalCommand is an annotation to mark this method as an external command
- LCExternalFunction is an annotation to mark this method as an external function
Code: Select all
on mouseUp
exampleExternalCommand "foo", "bar"
answer exampleExternalFunction("baz")
end mouseUp
2.1 Startup and Shutdown
Analogous to the iOS externals, we would also have optional annotations for the Startup and Shutdown of an external:
Code: Select all
package com.example.external;
import com.runrev.external.LCExternalCommand;
import com.runrev.external.LCExternalFunction;
import com.runrev.external.LCExternalLibrary;
import com.runrev.external.LCExternalShutdown;
import com.runrev.external.LCExternalStartup;
public class ExampleExternal implements LCExternalLibrary {
@LCExternalStartup
public void exampleExternalStartup(...) { ... }
@LCExternalShutdown
public void exampleExternalShutdown() { ... }
@LCExternalCommand
public void exampleExternalCommand(String[] args) { ... }
@LCExternalFunction
public String exampleExternalFunction(String[] args) { ... }
}
- LCExternalStartup is an annotation to mark this method as the one to call at startup of the external library
- LCExternalShutdown is an annotation to mark this method as the one to call at shutdown of the external library
To prevent name clashes while preserving the Java style of method naming, the LCExternalCommand and LCExternalFunction annotations would also have an 'alias' parameter to allow the engine to map external calls to the right methods
Code: Select all
package com.example.external;
import com.runrev.external.LCExternalCommand;
import com.runrev.external.LCExternalFunction;
import com.runrev.external.LCExternalLibrary;
import com.runrev.external.LCExternalInterface;
import com.runrev.external.LCExternalStartup;
public class ExampleExternal implements LCExternalLibrary {
@LCExternalStartup
public void exampleExternalStartup(LCExternalInterface ei) { ... }
@LCExternalCommand(alias="exampleExternalCommand")
public void externalCommand(String[] args) { ... }
@LCExternalFunction(alias="exampleExternalFunction")
public String externalFunction(String[] args) { ... }
}
Callbacks would be handled via an object passed into the 'startup' method which the object then stores and uses when necessary.
Note that we could also place all callback methods in a singleton, but this would make evolution of the API harder.
Code: Select all
package com.example.external;
import com.runrev.external.LCExternalCommand;
import com.runrev.external.LCExternalFunction;
import com.runrev.external.LCExternalLibrary;
import com.runrev.external.LCExternalInterface;
import com.runrev.external.LCExternalStartup;
public class ExampleExternal implements LCExternalLibrary {
@LCExternalStartup
public void exampleExternalStartup(LCExternalInterface ei) { ... }
@LCExternalCommand
public void exampleExternalCommand(String[] args) { ... }
@LCExternalFunction
public String exampleExternalFunction(String[] args) { ... }
}
- LCExternalInterface is an interface (not an implementation class) defining the provided callback methods
- com.runrev.android.AndroidExternalHost -> Android specific
- com.runrev.desktop.DesktopExternalHost -> Java generic for desktop
3.1 Callback methods
Ideally, we would have the best of both the classic desktop and iOS externals callbacks in one unified set of methods.
This is something that I haven't completely mulled over yet, but I do know we'll need Java-like names for them.
For instance, LCRunBlockOnMainThread would become LCExternalInterface.runOnMainThread(Runnable r) and thus an external could use it like this:
Code: Select all
package com.example.external;
import com.runrev.external.LCExternalCommand;
import com.runrev.external.LCExternalFunction;
import com.runrev.external.LCExternalLibrary;
import com.runrev.external.LCExternalInterface;
import com.runrev.external.LCExternalStartup;
import java.lang.Runnable;
public class ExampleExternal implements LCExternalLibrary {
LCExternalInterface externalInterface;
@LCExternalStartup
public void exampleExternalStartup(LCExternalInterface ei) {
this.externalInterface = ei;
}
@LCExternalCommand
public void exampleExternalCommand(String[] args) {
externalInterface.runOnMainThread(new Runnable() {
@Override
public void run() {
// whatever
}
});
}
@LCExternalFunction
public String exampleExternalFunction(String[] args) { ... }
}
There will probably also need to be a LCCallback interface for those cases where we need to shuffle context data back and forth.
That context data should probably take the form of a java.io.Serializable object.
3.2 LCContext and LCObject references
One of the nice features of he iOS externals API is the LCObjectRef weak handle that you can get back by calling LCContextMe and friends.
Definitely needed, though a Java makeover will have to happen for the API - to be discussed.
3.3 Variable setting and getting
One thing present in the Desktop externals API but missing in the iOS externals API is the ability to get and set variables.
We're going to need a consistent API which also covers arrays (both traditional and nested arrays, with conversion from/to java.util.Map<K,V> instances) and binary data (presumably using java.nio.ByteBuffer instances).
Again, this needs to be fleshed out.
4. Loading externals
Java 6 introduced the ServiceLoader class as a simple service-provider loading facility.
The idea is that you include a file in your .jar file that is read by the class loader and allows it to find at runtime the classes which implement a certain interface.
In our case, we'd have a single file
Code: Select all
META-INF/services/com.runrev.external.LCExternalInterface
Code: Select all
com.example.external.ExampleExternal
Code: Select all
com.quartam.livecode.android.ZeroConf
com.quartam.livecode.android.XslTransformer
We could provide an AnnotationProcessor which automatically populates this file, and any other files that the engine might need to make this work (e.g. the .lcidl file). The main point is that we should make life easy and let the annotations do the work.
5. Complicating factors on the desktop
Traditionally, Sun/Oracle has provided the 32-bit implementation for Windows, with 64-bit versions since Java 6.
In the Linux world, RedHat has been the driving force behind IcedTea, and everything is converging towards OpenJDK these days.
The OS X situation is more complicated:
Apple used to provide its own implementations of Java for OS X, tightly coupled to the operating system version - Technical Note 2110: Identifying Java on OS X provides a complete overview.
Oracle only provides 64-bit Java implementations for OS X 10.7 Lion and 10.8 Mountain Lion. There are no 32-bit implementations, and given that Oracle doesn't support 10.6 Snow Leopard, chances are good that 10.7 Lion will be dropped as soon as 10.9 <insert feline> comes out.
And the Mac App Store won't allow apps which rely on a built-in Java - you must bundle a JRE yourself.
So a lot depends on which Java version we want and which operating system versions we still want to support.
(*) boot note regarding iOS
Technically, there are no restrictions against running Java on iOS platforms (except that it ought to be AOT compilation instead of JIT)
Both Avian and RoboVM may be of interest for anyone wanting to take this route.
But in the current situation, it would definitely be out of scope for the mainstream LiveCode engine.
These are my thoughts - I realise they're incomplete and things have to be fleshed out.
So have at!
Jan Schenkel.