-
Notifications
You must be signed in to change notification settings - Fork 176
Doppio Developer Guide
So you want to hack on DoppioJVM: maybe to integrate it into your project more closely, or to add support for JVM features that are currently busted, or whatever your heart desires. Start here to save yourself a lot of head-scratching.
Contents:
Native methods allow doppio to run Javascript code from Java classes. They're critical to the execution of the JVM, but they can also be used to provide web functionality to your Java programs. For example, you might write a native method to pop up an alert message, or manipulate DOM elements, or anything else you can do with Javascript.
First, identify the native method that you want to write:
is it in the Java Class Library somewhere, or perhaps in some Java code you wrote?
For this example, we'll assume we wrote the file classes/util/Test.java like so:
package classes.util;
class Test {
static native int addTwo(int x);
public static void main(String[] args) {
System.out.println(addTwo(5));
}
}
DoppioJVM ships with a utility called doppioh, which we liken to the official javah. Simply point it to a compiled class file classes/util/Test.class to generate a native method stub like so:
./doppioh -classpath . -d natives classes/util/Test
The above command will create the file natives/classes_util_Test.js with a stub for the native method addTwo. If you want to write your native methods in TypeScript, you can do that too:
./doppioh -classpath . -ts -d natives classes/util/Test
That command will create natives/classes_util_Test.ts, and a JVMTypes.d.ts containing type information for all of the JVM types it needs. It will depend on the doppiojvm NPM module, which the TypeScript compiler will automatically grab type information from if you have it installed as a dependency.
If you want to specify a custom path to the doppiojvm module, simply pass in -doppiojvm-path path/to/doppiojvm.ts.
You can also generate native method stubs for an entire package, e.g. java.lang:
./doppioh -classpath vendor/java_home/classes -d natives java.lang
... which will define the file natives/java_lang.js.
ProTip: Make sure you perform a grunt release-cli so you have a doppioh shortcut in your doppio directory.
Next, you'll need to write your native method implementation. Here's the stub generated by doppioh:
registerNatives({
'classes/util/Test': {
'addTwo(I)I': function(thread, arg0) {
thread.throwNewException('Ljava/lang/UnsatisfiedLinkError;', 'Native method not implemented.');
}
}
});
Let's implement that! Our method will add 2 to the specified integer, which is fairly straightforward:
registerNatives({
'classes/util/Test': {
'addTwo(I)I': function(thread, arg0) {
// The |0 ensures that we return a 32-bit integer quantity. :)
return (arg0 + 2)|0;
}
}
});
Now that you have a new native defined, you'll need to make sure that DoppioJVM can find it at runtime. With the Node frontend, specify the directories with the -Xnative-classpath option, delimited by colons (:), much like the regular classpath. The browser frontend has a similar configuration option.
It's easy to write a DoppioJVM native method that invokes asynchronous browser functionality:
- Put the DoppioJVM thread invoking your native method into the
ASYNC_WAITINGstate. - Perform the desired operation.
- When your operation completes, invoke the thread's
asyncReturnfunction with the function's return value (if any), which will resume the thread with the specified return value.
Here's an example that 'sleeps' the thread for 50 milliseconds:
// Native methods can reference DoppioJVM modules from the DoppioJVM global variable.
var ThreadStatus = DoppioJVM.VM.Enums.ThreadStatus;
registerNatives({
'classes/util/Test': {
'sleep50()V': function(thread) {
// This informs the thread to ignore the return value from this JavaScript function.
thread.setStatus(ThreadStatus.ASYNC_WAITING);
// Sleep for 50 milliseconds.
setTimeout(function() {
// Wake up the thread by returning from this method.
thread.asyncReturn();
}, 50);
}
}
});
While this DoppioJVM thread is waiting for your sleep operation to complete, other DoppioJVM threads may run.
If the above instructions aren't enough to get your native running, try looking at other native method implementations in src/natives/*.ts.
These contain examples of how to do all sorts of tricky things, like getting/setting fields on objects,
converting between JVM and Javascript strings, working with long integers, and more.
As a full-featured JVM, DoppioJVM requires a number of files available at runtime to run properly. This guide will show you how to configure your environment to run DoppioJVM on your webpage.
Follow the instructions in README.md to clone and build a release version of DoppioJVM. You will need the following files from the build/release directory when you deploy DoppioJVM:
-
./doppio.js: The actual fully-built doppio library. -
./src/natives/*.js: DoppioJVM's native methods. It dynamically loads these from the file system at runtime, much likeclassfiles. -
./vendor/java_home: DoppioJVM's Java Home, which contains important classes and data files for the JVM to function.
DoppioJVM requires the in-browser filesystem, BrowserFS. Include it on your webpage before doppio.js, and ensure that you have the files described above included somewhere in the file system (most likely, you will set up an XmlHttpRequest file system that pulls in files via downloads). You can download the latest version here.
For example, you might do the following to set up temporary storage at /tmp, system files at /sys, and writable storage at /home:
<script type="text/javascript" src="browserfs.min.js"></script>
<script type="text/javascript">
// Wrap in a closure; don't pollute the global namespace.
(function() {
var mfs = new BrowserFS.FileSystem.MountableFileSystem(),
fs = BrowserFS.BFSRequire('fs');
BrowserFS.initialize(mfs);
// Temporary storage.
mfs.mount('/tmp', new BrowserFS.FileSystem.InMemory());
mfs.mount('/home', new BrowserFS.FileSystem.LocalStorage());
// The first argument is the relative URL to your listings file generated by the BrowserFS XHR
// listings tool. In this example, the URL is <thiswebpage>/browser/listings.json
mfs.mount('/sys', new BrowserFS.FileSystem.XmlHttpRequest('browser/listings.json', ''));
})();
</script>
BrowserFS also supports a wide variety of browser-local storage technologies, including HTML5 and IndexedDB, and cloud storage like Dropbox. DoppioJVM is able to read files, classes, and native methods from any of these storage mediums through BrowserFS.
First, make sure you include doppio.js on your webpage:
<script type="text/javascript" src="doppio.js"></script>
When you want to invoke the JVM, you can do so through a command-line style interface, or through a more traditional JavaScript interface. We'll describe the JavaScript interface first.
new doppio.JVM({
// Paths to the Java Class Library (JCL), e.g. [/sys/vendor/java_home/classes]
bootstrapClasspath: string[],
// Non-JCL paths on the class path, e.g. [/path/to/my/classes]
// These can be JAR files.
classpath: string[],
// Path to JAVA_HOME, e.g. /sys/vendor/java_home
javaHomePath: string,
// Path where we can extract JAR files, e.g. /tmp
extractionPath: string,
// Paths where native methods are located, e.g. [/sys/src/natives, /path/to/my/natives]
nativeClasspath: string[],
// [Optional] True if you want Java assertions enabled, false otherwise.
assertionsEnabled: boolean,
// [Optional] A map containing any JVM properties you want to set.
properties: {[name: string]: string},
// A location where the JVM can store temporary files. Defaults to /tmp.
// Must be valid for programs that write to temporary files!
tmpDir: string
}, function(err, jvmObject) {
// Called once initialization completes. Now you can call whatever class or JAR file you want!
jvmObject.runClass('classes.mypackage.MyClass', ['argument1', 'argument2'], function(exitCode) {
if (exitCode === 0) {
// Execution terminated successfully
} else {
// Execution failed. :(
}
});
// If you'd rather run a JAR file, you can do that, too! Put the JAR file as the only item in the
// classpath, and then:
jvmObject.runJar(['argument1, 'argument2'], function(exitCode) {
// etc.
});
});
The command-line style interface emulates how you might invoke Java on the command line. For example, the command line java -classpath classes classes.mypackage.MyClass 43 would look like the following:
doppio.javaCli.java(
// Arguments to the 'java' command.
['-classpath', 'classes', 'classes.mypackage.MyClass', '43'],
// Essential JVM information.
{
// Path to the Java Class Library, e.g. [/sys/vendor/java_home/classes]
bootstrapClasspath: string[],
// Path to Java home, e.g. /sys/vendor/java_home
javaHomePath: string,
// Path in the file system to extract zip files, e.g. /tmp
extractionPath: string,
// Location of native methods, e.g. [/sys/src/natives].
nativeClasspath: string[],
// True if you want Java assertions enabled, false otherwise.
assertionsEnabled: boolean,
// Path to use for temporary files. Defaults to /tmp.
tmpDir: string
}, function(exitCode) {
if (exitCode === 0) {
// Class finished executing successfully.
} else {
// Execution failed. :(
}
}, function(jvmObject) {
// [Optional callback] Called once the JVM is instantiated.
});
Your webpage can easily hook into the JVM's standard out, standard error, and standard input streams:
process.stdout.on('data', function(data) {
// data is a Node Buffer, which BrowserFS implements in the browser.
// http://nodejs.org/api/buffer.html
alert("Received the following output: " + data.toString());
});
process.stderr.on('data', function(data) {
// data is a Node Buffer, which BrowserFS implements in the browser.
// http://nodejs.org/api/buffer.html
alert("Received the following error: " + data.toString());
});
process.stdin.on('_read', function() {
// Something is looking for stdin input.
// You can write to stdin *at any time* to provide input. Thus, you can asynchronously
// prompt the user for data on the webpage, and then write to stdin once input is provided.
process.stdin.write(new Buffer(prompt("Input?")));
});
The JVM will print error messages to standard error (e.g. fatal exceptions), so you might want to hook into that stream for debugging purposes. Note that the data you receive on these streams is not neatly broken up, so if you redirect stderr to console.error, you could have many single character messages! You will want to introduce buffering to the nearest line before forwarding it to console.error.