Jump to content
micovery

Java Extension for Arma 3 (jni.dll)

Recommended Posts

Hey All,

I am the developer of the persistence framework for Arma 2: Takistan Life Revolution mission (which would save data such as position, gear, clothes, etc).

For that mission, I developed a C extension that could invoke Java code. From the Java side, it would then persist the player's data points to xml files. Later we switched the storage from plain files, to a relational database.

Anyways, I thought it would be good to contribute back to the Arma community by sharing the source code of the C extension, and a couple of Java classes that can be used as a starting point for all sorts of fancy Java extensions.

Prerequisites


1. Visual C++ Runtime (http://www.microsoft.com/en-us/download/details.aspx?id=26999)

2. Java 7 Runtime Environment (JRE) (http://www.oracle.com/technetwork/java/javase/downloads/index.html)

Components


1. jni.dll - This dll is a C extension that uses the Java Native Interface (JNI) to instantiate a Java Virtual Machine (JVM), and invoke a method with the signature: "public static String run(String arg)"

2. jni.conf - This is a configuration file which is read by jni.dll to configure the Java Runtime Environment (JRE)

3. RVExtension.jar - This is a jar file containing the main class file which has the "run" that is invoked by the C extension.

Configuration


The jni.dll takes its configuration from one file named "jni.conf". This file must be located in the Arma 3 process working directory (i.e. C:\Program Files (x86)\Steam\steamapps\common\Arma 3).

The jni.conf configuration file requires that you have the following 3 variables defined:

1. JAVA_HOME

You must set this environment variable to the location where your JRE is installed. e.g.

JAVA_HOME=C:\Program Files (x86)\Java\jre7

2. CLASSPATH

This is the usual Java CLASSPATH variable. You can have one or more jars in the classpath, or plain directories with classes in them. e.g.

CLASSPATH=.\RVExtensionDemo.jar

3. MAINCLASS

You must set this to the fully qualified name (including package) of your main Java class. This class must contain a method with the following signature "public static String run(String arg)". e.g.

MAINCLASS=com.micovery.RVExtensionDemo

This class must be available through the CLASSPATH specified on #2.

Runtime Execution Flow


When the C extension is invoked from SQF, for example:

"jni" callExtension "Hello World";

It invokes the "run" method from the class you specified in your jni.conf, and passes the "Hello World" argument to it. Whatever string the "run" method returns, is then passed back to Arma 3, and thus available as the return value of the SQF call.

Source Code


You can view the source in the following repositories:

https://bitbucket.org/micovery/rvextensiondemo.eclipse/

https://bitbucket.org/micovery/jni.dll

I have put both Eclipse, and Visual Studio projects in there.

Demo Java Extension & Mission


I have put together a demo Java extension, and demo mission using the jni.dll extension.

The binaries for the C extension, and the jar file can be found in:

https://bitbucket.org/micovery/rvextensiondemo.mission

Instructions:

1. Place all three files: jni.dll, RVExtensionDemo.jar, and jni.conf in your Arma 3 directory.

2. Place the RVExtensionDemo.Statis.pbo in your MPMisisons directory

Decription of the demo:

This demo is using JAXB to unmarshall (deserialize) XML that is generated on the SQF side describing a method invocation. For example, the following XML:

<MI>
 <M>date</M>
 <AL>
   <A>yyyy/MM/dd HH:mm:ss</A>
 </AL>
</MI>

Describes a method invocation (<MI>), for the "date" method (<M>), with the arguments list (<AL>), containing one argument (<A>) with value of "yyyy/MM/dd HH:mm:ss".

From SQF, the call would look like this:

"jni" callExtension "<MI><M>date</M><AL><A>yyyy/MM/dd HH:mm:ss</A></AL></MI>"

This instructs the "run" method on the demo Java extension to invoke the method with the following signature "String date(String arg)".

On the Java side, after unmarshaling the XML, the actual invocation is done using reflection.

Debugging and Troubleshooting the C Extension


The jni.dll extension creates a log file called "jni.log" in the same location where the "jni.conf" file is located. The log file contains detailed information about the work being done by the jni.dll itself.

1. It logs all the variables loaded from jni.conf, and will tell you if you are missing a required variable.

2. It logs if there are errors when instantiating the JVM, or if there are errors finding the MAINCLASS, or if the MAINCLASS does not contain the prescribed "run" method.

3. It logs trace information for every call made to the jni.dll showing the input and output. (This is useful for development, but not good for production)

Debugging and Troubleshooting the Java code


There is an optional variable you can set in "jni.conf" called JDWP_ADDRESS. This variable allows you to set the "-Xrunjdwp" initialization argument for the JVM.

For example:

JDWP_ADDRESS=50050

Would set the following initialization argument for the JVM

-Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=50050

Which would allow you to remotely attach a Java debugger to the process.

Summary/Reflection


I am just providing the basic building blocks. Hopefully the Java developers within the Arma community can see the potential for doing more interesting things with this.

I will be releasing later on the full source code for the stats persistence Java extension that drove me to write this in the first place. It uses OpenJPA to interact with any relational database you want.

Cheers,

micovery

Edited by micovery
  • Like 1

Share this post


Link to post
Share on other sites

I man,

Nice Job. However do you know that BIS have already done this in take on Helicopters with all of the SQF commands available?

Now sure we really don't know their plan with ArmA 3.

Anyway. I am personnaly interested to do the reverse process, let say an external Java program that could reach an ArmA 3 addon through JNI Java -> C.

Have you done some try that way ?

Thanks

Edited by Major_Shepard

Share this post


Link to post
Share on other sites
I man,

Nice Job. However do you know that BIS have already done this in take on Helicopters with all of the SQF commands available?

Thanks

I am aware. I wrote this originally because I needed it for Arma 2, which did not have any official Java support. It looks to be the same for Arma 3 so far.

I man,

Anyway. I am personnaly interested to do the reverse process, let say an external Java program that could reach an ArmA 3 addon through JNI Java -> C.

Thanks

A.

With the existing functionality, it would have to be asynchronous ... using events, and callbacks.

There would have to be an event loop on the SQF side, that would periodically call Java to pop events from a queue and perform the needed SQF calls.

Then SQF would have to call Java to put back the response on another queue, for the Java side to pickup.

B.

JNI can go both ways, you can invoke C code from Java side as well. BIS could provide us with a way to directly invoke the SQF commands from C itself. Then wrapping those calls with JNI becomes trivial, and you can directly call those methods from Java.

Share this post


Link to post
Share on other sites

Thanks for this great extension micovery! :)

I was about to do something like this myself but this is even better I think! Thanks again for your work.

Quick question: do you think it is good practise to spawn a thread on the SQF side and calling the extention from there, if the java side is blocking the thread for some time? Or better do a threaded solution on the java side and do a "callback" to the SQF script (which would be harder if you need the return value in the SQF code).

I am still wondering why there is not much going on in this thread, really sad (we will never know if there will ne an official Java API or not I guess)?

Anyway you should provide a simple config option to disable logging of info messages, since you have said it yourself: "not good for production". ;)

I will inform you if I come up with something useful and share it here.

Share this post


Link to post
Share on other sites

Works nicely and I can easily modify already Java part output (note:this requires 32bit version of java, else won't work)

Now I'm totally clueless about SQF, so at first steps can I easily push XYZ of player location to Java side via some engine function? (kind of "IsMoving" and "HasStopped" style event's or just raw location updates)

Share this post


Link to post
Share on other sites

yeah I also noticed you need a 32-bit version of the JVM, might be because ArmA is running in 32-but and it's a native call to the JNI it has to be a 32-bit JVM to work properly.

About the SQF stuff, there are no move events as far as I know, you can just use a infinite loop or something and get the position with "position player" or getPosATL player" or whatever you need. depends what you wanna do with the positions and how often you need to refresh them? :D

Share this post


Link to post
Share on other sites
yeah I also noticed you need a 32-bit version of the JVM, might be because ArmA is running in 32-but and it's a native call to the JNI it has to be a 32-bit JVM to work properly.

About the SQF stuff, there are no move events as far as I know, you can just use a infinite loop or something and get the position with "position player" or getPosATL player" or whatever you need. depends what you wanna do with the positions and how often you need to refresh them? :D

Often as possible :P ... I had some ideas to start first testing with simple RabbitMQ messagebus queue and from that live web map via websocket.

Maybe later check if it's possible to spawn units and synchronize unit positions with bit logic and pub/sub channels :)

Share this post


Link to post
Share on other sites

Bit testing and I got java, rabbitmq message bus and html5 websocket map working.

init.sqf

sleep 0.5;

private["_h"];
_h = [] execVM "jni.sqf";
waitUntil {scriptDone _h};


[] spawn {
while {true} do {
	_pos = getPos player;
	_x = _pos select 0;
	_y = _pos select 1;
	_z = _pos select 2;
	["getPos" , _x , _y , _z , direction player ] call invoke_java_method;
	sleep 0.5;
};
};

and java part is simple (BussObject just simple json wrapper object)

	public String getPos(String _x, String _y, String _z, String _h) {
	try {
		BussObject obj = new BussObject();
		obj.x = Double.parseDouble(_x);
		obj.y = Double.parseDouble(_y);
		obj.z = Double.parseDouble(_z);
		obj.h = Double.parseDouble(_h);
		if ( channel.isOpen() ) {
			channel.basicPublish(EXCHANGE_NAME, "arma", null, gson.toJson(obj).getBytes() );
		} 
		return "update " + gson.toJson(obj);
	} catch ( Exception e ) {
		return e.getMessage();
	}
}

Note. When building JAR, need to select extract required libs to JAR as else it won't work.

Edit: looks like this ... https://dl.dropboxusercontent.com/u/1402817/arma3websocket.jpg

Edited by Salakka

Share this post


Link to post
Share on other sites

Note. When building JAR, need to select extract required libs to JAR as else it won't work.

You can actually just set up your classpath properly to achieve this. Message me if you need to know how to do this.

Would the author of htis extension mind adding a variable to be able to turn off the logging to the jni.log file?

Share this post


Link to post
Share on other sites

Please sign in to comment

You will be able to leave a comment after signing in



Sign In Now

×