The stress of cross-platform Java
I am approaching year eight of managing an open source project known as Apktool. This project, as the blog suggests, is written in primarily Java with support on three major operating systems (Unix, Mac and Windows). I've made plenty of mistakes along this journey usually breaking a feature on a specific operating systems. This post is a few common problems I encounter when building cross-platform software.
Like most are probably aware, the best way to execute a
.jar file is to simply run.
java -jar path/to/file.jar
However, if you want to add arguments to Java and arguments to the program you are executing this slowly evolves to something like this:
java -jar -Xmx1024m path/to/file.jar -argument --argument value
This makes executing a program very tedious, so developers tend to move towards making helper files. These files have evolved over the years because you can't assume any of the following:
- Is this Windows?
- Is the character set UTF-8 compatible?
- Is it a CLI application?
- Is the user running cygwin on Windows?
After about 30 lines of code, you end up with something like this at the bottom:
exec java $javaOpts jar "$jarpath" "$@"
This is very helpful for users as they just need to drop this file into the directory where the project binary exists and it will do the rest. This changes our ugly program execution to this.
program -argument --argument value
Maintaining these operating specific helper files is not too bad, because once you solve a problem it might take years until another one crops up. This paired with the benefit the end user gets makes it a great enhancement to add.
As you build functionality into your application, chances are you might need to interact with the file system. The most common mistake is the difference between slashes as Windows continues to use the backward slash for separating directories.
C:\ = root location, usually, on Windows / = root location on Unix based systems
So imagine you write code to change to directory
/private/var/ to drop a temporary file. Next thing you know users on the Windows platform are looking at some cryptic error. This is easy enough to solve with a Java variable.
String path = File.separator + "private" + File.separator + "var"
The above snippet shows you how to create
/private/var in a manner that works on all platforms.
3rd party binaries
Sometimes Java is not the best language for every task. Perhaps the Java application you wrote actually leverages another non-Java tool. This can be done in a multitude of ways, but then you need binaries built for every respective system you intend to support.
This becomes difficult and fast due to the additional logic of architecture type on top of platform. For example in Apktool at one point we balanced the following:
- Windows 64 / 32 bit
- Unix 64 / 32 bit
- Mac 64 / 32 bit
This on top of the stress of missing dependencies taught me to build 3rd party binaries statically so all binaries contain every dependency they need internally. It isn't easy using a native binary with Java so I'd avoid this, if at all possible.
In Apktool, there is a situation where we create folders and files based on the naming of internal Java classes. Clever developers noticed they could create a class package named "COM5" and when done, Windows once again experienced a crazy problem.
From a relic of the DOS era, none of the following words can be used in a folder name without some tricks on Windows.
CON, PRN, AUX, CLOCK$, NUL COM0, COM1, COM2, COM3, COM4, COM5, COM6, COM7, COM8, COM9 LPT0, LPT1, LPT2, LPT3, LPT4, LPT5, LPT6, LPT7, LP
This forces developers to create a lookup table to store the files on the file system under one name, but remember the original correct value when referenced in the tool.
Max Command Length
In Apktool, we execute a 3rd party tool that requires a great deal of parameters. It seemed simple as we pass the parameters on and capture the result. This worked great on all platforms except Windows. Windows gave us quite a nasty error in return.
Caused by: java.io.IOException: CreateProcess error=206
We can cross check Microsoft documentation and find:
The filename or extension is too long.
Which means our command was too long. Lets take a look at command line limits in a few systems.
|Max Command Length||System|
|8,191 characters||Windows 10|
|32,000 characters||Cygwin (Windows 10)|
|262,144 characters||Mac OS X|
(these values isolated to my equipment, may be different for your own)
This means in order for our application to function perfectly, we must design it in a way that never executes a command that exceeds 8,191 characters. Perhaps this is running the 3rd party binary multiple times or rethinking the problem.
Max Folder Structure Length
Once again in Apktool, we have an interesting test case to test the possibility of all qualifiers together. This tests our decoder with the maximum possibility, which creates a directory named this:
Append in the entire project path and we get this:
Once again, Windows strikes with an annoying problem. Our limit on Windows for the entire directory structure is 255 characters. We already have 217 above, so cloning our project into a directory path over 38 characters will break Apktool.
Looking at a few systems again shows us the max is different among the pool of systems.
|Max Path Length||System|
|255 characters||Windows 10|
|4,096 characters||Mac OS X|
We quickly see that our application fills with complex operations for dealing with the ins and outs of every operating system. However, the benefit of writing Java and only requiring a JVM on each system prevents the stress of individually building and supporting binaries for each system. I hope a few random tidbits of what I encounter building cross-platform Java applications may help anyone reading.