Haxe advanced conditional compilation
September 2nd, 2012 | Published in Uncategorized
Conditional Compilation (AKA preprocessor macros) in Haxe is pretty useful but limited to boolean values. We can make use of macros to use compiler flag of non-boolean type.
The traditional way
Usually we can define a flag mydebug in hxml:
-D mydebug
And use it in Haxe code in this way:
#if mydebug trace("some debug trace") #end |
If we want to conditionally include browser support, we need to use a scheme of flags.
-D supportIE8 #define we support IE8 and above
#if (supportIE6 || supportIE7) //assume 6 is the min version trace("some code for IE7"); #end |
As shown above we need to specify every version before 7 for a piece of code that is for IE7.
It is problematic because for now we at most put #if (supportIE6 || supportIE7 || supportIE8 || supportIE9 || supportIE10 || supportIE11) for a piece of IE11-specific code. But we don’t know if IE will have version 99 and definitely we do not care IE users do not want to write #if (supportIE6 || supportIE7 || … || supportIE99) for a piece of IE99-specific code.
The macros way
With the help of macros, we will be allowed to code something like:
-js bin/Test.js
-main Test
-cp src
--macro CompilationOption.set('supportIE', 8)
class Test { public static function main() { if (CompilationOption.exists("supportIE") && CompilationOption.get("supportIE") <= 7) { trace("some code for IE7"); } } } |
The JavaScript output of the main function:
Test.main = function() { console.log("some code for IE7"); } |
And here is my CompilationOption class:
#if macro import haxe.macro.Context; import haxe.macro.Expr; #end /** * CompilationOption allows us to use compile-time variables for advanced conditional compilation. */ class CompilationOption { #if macro /** * Internal storage of the options. */ static var storage = new Hash<Dynamic>(); #end /** * Set `key` to `value`. * * For simplicity `value` can only be constant Bool/Int/Float/String or null. * Array and structures are also possible, check: * http://haxe.org/manual/macros#constant-arguments * * Set `force` to true in order to override an option. * But be careful overriding will influenced by compilation order which may be hard to predict. */ @:overload(function (key:String, value:Bool, ?force:Bool = false):Void{}) @:overload(function (key:String, value:Int, ?force:Bool = false):Void{}) @:overload(function (key:String, value:Float, ?force:Bool = false):Void{}) @:overload(function (key:String, value:String, ?force:Bool = false):Void{}) @:macro static public function set(key:String, value:Void, ?force:Bool = false) { if (!force && storage.exists(key)) throw key + " has already been set to " + storage.get(key); storage.set(key, value); return macro {}; //an empty block, which means nothing } /** * Return the option as a constant. */ @:macro static public function get(key:String):Expr { return Context.makeExpr(storage.get(key), Context.currentPos()); } /** * Tell if `key` was set. */ @:macro static public function exists(key:String):ExprOf<Bool> { return Context.makeExpr(storage.exists(key), Context.currentPos()); } /** * Removes an option. Returns true if there was such option. */ @:macro static public function remove(key:String):ExprOf<Bool> { return Context.makeExpr(storage.remove(key), Context.currentPos()); } /** * Dump the options as an object with keys as fields. * eg. trace(CompilationOption.dump()); */ @:macro static public function dump():ExprOf<Dynamic> { var obj = {}; for (key in storage.keys()) { Reflect.setField(obj, key, storage.get(key)); } return Context.makeExpr(obj, Context.currentPos()); } } |
Alternative
One alternative to the above is to have a macro that define all the supportIE7…supportIE11 when supportIE7 is passed.
The source code can now be:
#if supportIE7 trace("some code for IE7"); #end |
It is very similar to how the Haxe compiler handles Haxe version currently (haxe_208, haxe_209, haxe_210 are defined when using Haxe 2.10).
Improvement
Generally it is better to integrate the macro into specific class when needed, instead of using the CompilationOption class above. Because:
Firstly, the value type is unspecified. Although it is still typed after one specified a value, but user may have to guess or look it up before using it.
Secondly, the key(name) of an option is a String, and we don’t like “stringly typed code” because again people have to look up or remember.
The below API will be much better:
-js bin/Test.js -main Test -cp src --macro SupportIE.equalsOrAbove(7)
class Test { public static function main() { if (SupportIE.minVersion() <= 7) { trace("some code for IE7"); } } } |