HXP Projects

If you look closely at the OpenFL “docs” page, you’ll notice it lists two options under “project files.” The first of the two is the XML format you know and love, or at least know, but what’s up with the second?

Before I answer that question, I have some questions for you to answer.

Is your project file getting out of hand? Do you wish the XML format supported more advanced string manipulation? Do you find yourself typing out multiple <set> tags to simulate the “or” operator? Have you ever tried and failed to copy a directory with the <template> tag?

If you answered “yes” to any of these questions, HXP files may be right for you. Consult your physician today!

So anyway, what are HXP files? The short answer is, they’re HX files with an extra P.

The P stands for “Project,” so the long answer is, they’re project files which let you use Haxe instead of XML. You can write functions, manipulate strings, declare variables, and even access Haxe’s standard library.

Let’s try it out!

Ok, find a sample project that uses an HXP file, and… oh. There aren’t any, are there?

Fine, we’ll use DisplayingABitmap. I’ve created an HXP version of the project file, available here. Save it as project.hxp, delete project.xml, and you’re ready to give it a try.

Lime recognizes the file format, so just run lime test neko and it’ll find the new project file. It should show the OpenFL logo in the middle of the screen.

Congratulations! Through no fault of your own, you have gotten an HXP project file to work!

Converting your XML file to HXP

Running a sample project is all well and good, but what about that much larger project file you want to convert? Or perhaps that large file that you don’t want to convert because it will be so much work, but you have to convert anyway? Sometimes it’s the latter.

To make your job easier, I recommend copying over these functions. With those in place, here’s a conversion guide:

- <set name="godMode" />
+ defines.set("godMode", 1);
+ //Or if you only need it for conditionals:
+ var godMode:Bool = true;

- <setenv name="DARK_AND_STORMY_NIGHT" />
+ setenv("DARK_AND_STORMY_NIGHT", "1");
+ //That second value must be a string and cannot be skipped.

- <setenv name="TRUE" value="FALSE" />
+ setenv("TRUE", "FALSE");
+ //Please do not do this.

- <haxedef name="Robert'); DROP TABLE Students;--" />
+ haxedefs["Robert'); DROP TABLE Students;--"] = 1;
+ //Please do not do this either.

- <haxedef name="dump=pretty" />
+ haxedefs["dump"] = "pretty";

- <haxeflag name="-dce" value="std" />
+ haxeflags.push("-dce std");

- <section if="flash"> </section>
+ if(target == Platform.FLASH) { }

- <section if="godMode" unless="mobile"> </section>
+ if(defines.exists("godMode") && platformType != PlatformType.MOBILE) { }

- <ndll name="regexp" />
+ ndll("regexp");

- <assets path="assets/text" rename="text" include="*.txt|*xml" exclude="*.rtf" />
+ includeAssets("assets/text", "text", ["*.txt", "*.xml"], ["*.rtf"]);

- <certificate path="correcthorse.keystore" password="batterystaple" alias="AzureDiamond" alias-password="hunter2" />
+ certificate = new Keystore("correcthorse.keystore", "batterystaple", "AzureDiamond", "hunter2");

- <dependency name="GooglePlayServices" path="pathtoGooglePlayServices" />
+ dependency("GooglePlayServices", "pathtoGooglePlayServices");

- <dependency name="GameKit.framework" />
+ dependency("GameKit.framework");

- <dependency path="/Library/Frameworks/LameKit.framework" />
+ dependencyByPath("/Library/Frameworks/LameKit.framework");

- <include path="industrial/application.xml" />
+ merge(new ProjectXMLParser("industrial/application.xml", defines));

- <template path="templates/CureForCancer.java" rename="src/com/jir/research/CureForCancer.java" />
+ templateFile("templates/CureForCancer.java", "src/com/jir/research/CureForCancer.java");

- <template path="templates/" rename="src/com/jir/research/" />
+ templatePaths.push("templates/");

Those are most of the existing tags, but if I missed one, you can always refer to the source.

Template directories

I also tossed in a new feature: template directories that actually do what you expect them to do.

If you’ve ever tried passing a folder to <template path="x">, you’ve probably been very disappointed. While Lime is quite good at taking a single file, processing it as a template, and putting it where you want, it absolutely refuses to do that with directories.To be specific, if you specify a directory, Lime expects to find a folder structure exactly matching its own template directory. A subdirectory for each target, and a very specific arrangement of files within those subdirectories.I guess that can be useful, but it really isn’t what you’d expect. If you can copy individual files into place…

<template path="templates/SourceFile0.java" rename="src/com/example/SourceFile0.java" />
<template path="templates/SourceFile1.java" rename="src/com/example/SourceFile1.java" />
<template path="templates/SourceFile2.java" rename="src/com/example/SourceFile2.java" />

…Why can’t you do it in bulk?

<template path="templates/" rename="src/com/example/" />

I honestly don’t know why XML projects work that way, but I do know you can make it work in HXP!

templateDirectory("templates/", "src/com/example/");

One step at a time

If this is too much work to do all at once, then keep your old project file around, and import it into the new one:

merge(new ProjectXMLParser("old_application.xml", defines));

With that done, you’re free to move items over one at a time. Just be warned that some features, such as the <template> tag for folders, may quit working correctly. If this happens, focus on converting them first.

OpenFL Template Basics: Overriding AndroidManifest.xml

What do you do if you need to change AndroidManifest.xml, and OpenFL’s customization options aren’t enough? Sure you can add permissions using <config:android permission="android.permission.WHATEVER" />, and change the app title, and so on…

But what if you want to support multiple screen sizes? You need to include a <supports-screens> tag, or Google may hide your app from tablet users! This is yet another of the billions of scenarios that OpenFL fails to account for. (Sheesh, OpenFL dev team, get a move on.)

What you need is full control of the app’s AndroidManifest.xml. You can edit Lime’s copy, but that’ll change it for every project, and it’ll get reverted when you update Lime. You can edit the copy in Export/android, but that’ll get reverted the very next time you compile. You can make an extension, but that’s overkill.

Creating Your Own Manifest File

First things first. Before you can overwrite AndroidManifest.xml, you need to define what exactly you want to overwrite it with.

Conveniently, Lime has a whole folder of template files you can copy from. In this example, we’re replacing an Android template, so we look inside templates/android. A quick search turns up the file inside templates/android/template/app/src/main. (The “app/src/main” part will be important later; remember it.)

Copy AndroidManifest.xml into your project. I usually make a “templates” folder for files like this, but if you have your own way to organize it, that’s fine too. Open up your copy of the file and add the following somewhere between <manifest> and </manifest>:

<supports-screens
    android:anydensity="true"
    android:smallscreens="true"
    android:normalscreens="true"
    android:largescreens="true"
    android:xlargescreens="true" />

Using Your Manifest File

Finally! Enough preparation, it’s time to impose our will upon Lime’s build process. No longer shall we be confined to normalScreens, we shall reach out and conquer anyDensity!

What’s that? “Get on with it”? Oh fine…

Type this in your project.xml file:

<template path="templates/AndroidManifest.xml" rename="app/src/main/AndroidManifest.xml" />

Yep, that’s it. path tells it where (in your Haxe project) to get the file from, and rename tells it where (in your Android project) to put it. In this case, it goes in app/src/main.

Which Files Can be Overridden?

To answer this question, first compile your project for Android, and open Export/android/bin (or bin/android/bin). Take a good look – everything the light touches is your kingdom. By which I mean you can override the files in that folder.

However, you need to make sure put your file in the same folder as the file you’re overriding. GameActivity.java is located in app/src/main/java/org/haxe/lime, so you’d put this in project.xml:

<template path="templates/GameActivity.java" rename="app/src/main/java/org/haxe/lime/GameActivity.java" />

OpenFL Extensions

For the most part, OpenFL does an excellent job of providing you with the features you need in a platform-independent manner. It tells you the screen size, loads your assets, and even maps Android’s back button to the escape key.

Unfortunately, OpenFL’s dev team can’t think of everything. Maybe you want to adjust the screen brightness on Android. Or the music volume. Maybe you want access to the camera on iOS. Or maybe you need to integrate a custom library used internally at your company, which OpenFL could not possibly have integrated for you.

For simplicity, I’ll be using screen brightness as an example. Setting this can be done in only 1-3 lines of code on both iOS and Android. The catch is, neither of those examples are in Haxe, and there’s no way to convert them to Haxe. If only you’d written the app “normally” rather than using OpenFL, you could just copy-paste those few lines of code, and you’d be done! But no, you wanted luxuries like cross-platform compilation, and now you have to somehow use Haxe code to invoke functions in Objective-C and Java.

Fun Fact

Did you know, when you compile for Android, OpenFL creates a complete, self-contained Android project, and then tells the Android SDK to compile that? And when compiling for iOS, it creates an Xcode project, and then has Xcode do the remaining work?

You can see for yourself by checking your output folder (probably Export/). Dig into Export/android/bin, and you’ll find all the files and folders you’d expect to find in a normal Android project. If you felt bold enough, you could make changes and recompile with `gradlew assembleRelease`. But be warned – your changes will be overwritten the next time you compile with OpenFL.

The same applies to iOS – after compiling, you can check out bin/ios/bin to see the project that OpenFL created. You could try modifying this too, but again, OpenFL is going to revert your changes. There has to be a better way.

Creating an Extension

The OpenFL team is well aware of this problem, and in their infinite wisdom they created the “extension” feature. Also in their infinite wisdom, they include almost no documentation.

Extensions are basically mini-projects consisting of native (or Java) code, as well as Haxe bindings. Once you include the extension in your project, these bindings allow you to run the native (or Java) code. Let’s look at an example.

Start by running the following:

$ lime create extension SetBrightness

(I’m calling it “SetBrightness” because that’s the only thing I want it to do.)

Open up the folder Lime made, and you’ll find several files. Here’s what they’re for:

  • haxelib.json – Allows you to submit this extension to Haxelib.
  • include.xml – Like project.xml in your main project, this tells OpenFL what to do with all the other files here.
  • SetBrightness.hx – Haxe bindings go here.
  • dependencies/
    • android/ – An Android dependency project, to be compiled by the Android build tools. Template syntax is available for all files in this folder.
      • build.gradle – A project file for this Android library, roughly equivalent to include.xml.
      • src/ – Despite the name, you can’t just put source files in here. They actually go in a child folder.
        • main/
          • AndroidManifest.xml – The manifest file for your Android library. If your extension requires permissions, this is the place to put them.
          • java/ – Java source files go here.
            • org/haxe/extension/
              • SetBrightness.java – The recommended place for your Java code. It comes with useful callbacks for monitoring the activity lifecycle, or you can ignore all that and write static functions.
  • ndll/ – Doesn’t exist yet, but compiled C++ binaries will go here after you rebuild.
  • project/ – The root folder for your C++ project.
    • Build.xml – Build file for your C++ project. Only files named here (or included from those named here) will be compiled.
    • common/ – C++ source files (but not header files) go here.
      • ExternalInterface.cpp – Registers your C++ functions, allowing SetBrightness.hx to access them.
      • SetBrightness.cpp – Put C++ code here, based on the sample code that starts here.
    • include/ – C++ header files go here.
      • Utils.h – Header file for SetBrightness.cpp. Functions must be declared here in order for ExternalInterface.cpp to access them.
    • obj/ – Doesn’t exist yet, but intermediate C++ files will go here when you rebuild. Make sure to exclude this from version control.

Writing Code for Android

Click through all the folders under dependencies/ until you reach SetBrightness.java. Add the following code:

public static void setBrightness(float brightness) {
    WindowManager.LayoutParams layout = Extension.mainActivity.getWindow().getAttributes();
    layout.screenBrightness = brightness;
    Extension.mainActivity.getWindow().setAttributes(layout);
}

That’s all well and good, but how do you call this function? The answer… is JNI. Dramatic thunder crashes Actually, it’s not that bad if you’re only dealing with one function. Climb your way back to the root SetBrightness/ folder, and add this to SetBrightness.hx:

#if android
public static function setBrightness(brightness:Float):Void {
    setbrightness_set_brightness_jni(brightness);
}
private static var setbrightness_set_brightness_jni = JNI.createStaticMethod("org.haxe.extension.SetBrightness", "setBrightness", "(F)V");
#end

That’s still a little much. Fortunately, shoe[box] came up with an easier way. Start by including the “inthebox-macros” library in your project, change the package in SetBrightness.hx to org.haxe.extension, and add @:build(ShortCuts.mirrors()) just before the class declaration. Now the code above can be replaced with this:

#if android
@JNI public static function setBrightness(brightness:Float):Void;
#end

All that’s left is to include the extension in your project (see below), and you can call SetBrightness.setBrightness(0.8); from Haxe.

Debugging

When you try to use this extension on Android, you’ll run into a few errors. First, a compile error:

Error: Source path "[path]/SetBrightness/ndll/Android/libsetbrightness-v7.so" does not exist

This happens because you aren’t compiling an ndll for Android, but by default Lime expects you to. To fix the error, go into include.xml and replace <ndll name="SetBrightness" /> with <ndll name="SetBrightness" unless="android" />.

Next, you’ll get a runtime error:

android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.

The only thing that matters about this error message is that it contains the word “thread.” When developing an OpenFL extension, all thread-related errors have the same solution.

On Android, some tasks have to be done on the main thread. When you use JNI, it runs on a thread other than the main one. Fortunately, Extension.callbackHandler.post() lets you get back to the main thread.

This function takes a Runnable object, so you’ll have to create one of those. Take all the code in your function, and put it inside the run() function:

public static void setBrightness(float brightness) {
    Extension.callbackHandler.post(new Runnable() {
        @Override public void run() {
            WindowManager.LayoutParams layout = Extension.mainActivity.getWindow().getAttributes();
            layout.screenBrightness = brightness;
            Extension.mainActivity.getWindow().setAttributes(layout);
        }
    });
}

(Remember, you only need to do this if you get a thread-related error. Usually, it isn’t worth the trouble.)

And that’s it for Android. On to iOS!

Writing Code for iOS

You’ll notice that the extension is set up for C++ code, but to access system properties like brightness, you need to use Objective-C code. Fortunately, this part’s easy: just change the .cpp file extensions to .mm. You’ll also need to update their names in Build.xml. And because Objective-C is specific to iOS, I suggest disabling them for everything else.

<compilerflag value="-Iinclude" if="iphone" />
<file name="common/ExternalInterface.mm" if="iphone" />
<file name="common/SetBrightness.mm" if="iphone" />

If you need C++ code on other platforms, just disable the C++ files on iOS:

<file name="common/ExternalInterface.cpp" unless="iphone" />
<file name="common/SetBrightness.cpp" unless="iphone" />

Now to write some actual Objective-C code! Put this in SetBrightness.mm:

void setBrightness(float brightness) {
    [[UIScreen mainScreen] setBrightness:brightness];
}

Now update Utils.h:

void setBrightness(float brightness);

Don’t forget to update ExternalInterface.mm:

static void setbrightness_setBrightness (value brightness) {
    setBrightness(val_float(brightness));
}
DEFINE_PRIM (setbrightness_set_brightness, 1);

Last but not least, create the Haxe bindings in SetBrightness.hx:

#if ios
public static function setBrightness(brightness:Float):Void {
    setbrightness_set_brightness(brightness);
}
private static var setbrightness_set_brightness = Lib.load ("setbrightness", "setbrightness_set_brightness", 1);
#end

Phew! That was a lot of updating.

You know, it feels like you shouldn’t need to do all that by hand. That’s why I and a few others wrote utility libraries. inthebox-macros can generate the Haxe code, and my “extension boilerplate” utility can generate the intermediate C++ files.

Time to compile!

$ lime rebuild . ios

Including the Extension

Almost done! All that’s left is to include it in your project.

Probably the best way to do this is by registering it as a Haxelib. I’m not saying you have to submit it to the public repository (though that does work). Instead, you can create a local Haxelib:

$ haxelib dev SetBrightness path/to/SetBrightness

The benefit of making it a Haxelib is, it’s easy to copy it to other machines

<haxelib name="SetBrightness" />