Macros
Some languages features such as C #define enable the user to define syntax shortcuts. They are useful to perform some pseudo-code-generation, but at the same time allow to modify the syntax of the language, making the code unreadable for other developers.
Haxe macro system allow powerful compile-time code-generation without modifying the Haxe syntax.
Macro functions
A function can be defined as a macro function by using the @:macro Metadata :
import haxe.macro.Expr; class MyMacro { @:macro public static function getDate() { var date = Date.now().toString(); var pos = haxe.macro.Context.currentPos(); return { expr : EConst(CString(date)), pos : pos }; } }
Each macro function must return an expression which correspond to the block of code that will replace the macro call. Please note that unlike inline functions you can do actual code generation inside the macro.
For example, calling MyMacro.getDate() will generate the constant string containing the compilation Date and time.
The expression itself is an enum that is declared in the haxe.macro.Expr package
Context
The haxe.macro.Context API can be used inside macros in order to query some contextual informations about the code at the place the macro is being called.
Read Context API documentation for more detailed informations.
Parameters
Macro can take one or several expression parameters, that can be manipulated in many different ways. Here's for example a macro that will repeat n times the same expression :
import haxe.macro.Expr; import haxe.macro.Context; class Repeat { @:macro public static function times( n : Expr, expr : Expr ) : Expr { // check that our first expression is a constant Int switch( n.expr ) { case EConst(c): switch( c ) { case CInt(n): // create a { } block that execute n times 'expr' var block = new Array(); for( i in 0...Std.parseInt(n) ) block.push(expr); return { expr : EBlock(block), pos : Context.currentPos() } default: } default: } Context.error("Should be an integer",n.pos); return null; } }
And here's an example on how to use it :
class Test { static function main() { var x = 0; Repeat.times(5,x++); trace(x); // will display 5 } }
If a macro has two Expr parameters, you will be able to call it with two and only two expressions. If you want your macro to take a variable number of parameters, you must accept a single Array<Expr> parameter :
@:macro static function foo( e : Array<Expr> ) : Expr { // .... }
Constant Parameters
New in Haxe 2.08
When you want your macro to accept some constant value, you can simply use the constant type instead of Expr :
@:macro static function doSomething( v : Int, name : String ) : Expr { // .... }
Please note that in that case, only immediate constant values can be passed to the macro.
Constant values are defined as :
null- int, string, float and bool constants values
- anonymous objects of constant values
- arrays of constant values
Better Completion with Macros
New in Haxe 2.08
Since most of the macros will take Expr arguments and return Expr, these parameters types will be displayed as Dynamic when using compiler-based autocompletion.
In order to improve this, you can use ExprRequire type that will specify which type to show when doing completion :
@:macro static function isqrt( a : ExprRequire<Int> ) : ExprRequire<Float> { .... }
Member Macros Methods
New in Haxe 2.08
You can have member methods that are @:macro : in the macro subsystem, these methods will be static and will have an extra first parameter that is the expression of the this object :
class A { public function new() { } @:macro public function getThis( ethis : Expr ) { return ethis; } } // ... var a = new A(); trace( a.getThis() );
Macros + Using
New in Haxe 2.08
It is possible to use "using" mixin together with macros. However, if you want your macro to be only applicable to a given type, you need to use ExprRequire type to specify that you will only accept expressions of the given type :
import haxe.macro.Expr; // ... @:macro static function encodeBase64( e : ExprRequire<String> ) : Expr { // .... }
Note : As of current SVN, ExprRequire constraint is only supported for the first parameter in the case the macro is called with "using" mixin.
Macro classes
You can also define a class as being a macro class :
@:macro class MyMacro { .... }
This will turn all static methods of this class into macro-functions. Please note that this prevent you from having helper static functions since they will be macros as well, and not normal functions.
Macros functions and macro classes are not compiled as part of your code : their code is only compiled inside the macro emulator. As a consequence, you can use macro code to perform some encryption without revealing your algorithm or secret key.
You can also include/exclude some code depending if you are running into the macro emulator, by using #if macro conditional compilation. For example :
class MyMacro { #if macro // ... utilities functions ... #end @:macro public static function foo() // .... }
Runtime system
Macros are executed directly into the Haxe compiler by using a neko platform emulator. Most of the neko package API are supported inside macros and should behave the same as they do when running inside the NekoVM.
This means that you have full file I/O access inside your macro, which allow you to read/write external files and output source code depending on some other files content.
Macro-in-macro
It is possible to call a macro while evaluating another macro. However, this second macro evaluation will be delayed until it is actually executed. As a result, a macro called inside another macro will always be typed as returning Dynamic, since we don't know at compile time what expression it will produce.
End-of-compilation generation
In order to create per-class data that take into account the fields and/or inheritance, you can use haxe.macro.Context.onGenerate to register a callback that will run once your project is entirely compiled, prior to being generated for the target platform.
At this point, you can perform post-compilation checks and display errors if some requirements are not matched, and you can also add some class/enum/field metadata that will get compiled in your final source/binary. Use the add and remove methods of Metadata API.
Building types
You can also generate and manipulate types declarations content with macros, see Building Types with Macros
Compiler configuration
Macros can be used during the compilation process in order to generate custom code, but they can also be used to perform some pre-compilation tasks, see macros compiler configuration
Benchmarking / Optimization
Since macro needs to run for every compilation, it is sometimes necessary to take performances into account. Here's a few tips for improving macro performances :
- use a cache file : if you're doing complex processing based on external files, create a cache file and check if your external files have been modified before doing the processing again.
- disable processing in completion : since many IDE rely on the Haxe compiler for handling the autocompletion, sometimes macro execution will make autocompletion slower. You can disable some parts of the macro processing when running in completion mode, this can be checked with
haxe.macro.Context.defined('display') - benchmarking : in order to measure time spent in your macros, you can use
haxe --timesparameter. It will give you time measurements for the different parts of the compiling process. Additionally you can use-D macrotimesthat will give you time details on time spent in each macro method you're calling. Time measurements will also popup in completion mode, so you can check in your IDE what impact macros might have on your completion speed.
Impact of Compiler Cache on Macros
Starting with Haxe 2.09, it is possible to run a compilation that will keep the modules in cache unless they have been modified or require being recompiled.
This affects of course macros as well, here's a few tips on how to deal with it :
- macros-in-macros : due to implementation difficulties, modules containing macros-in-macros are not cached and will then be recompiled everytime
- if your macro produce code based on other files that are not part of the compilation process, you can use
haxe.macro.Context.registerModuleDependencyto inform the compiler that each that file is changed the corresponding module needs to be recompiled - all modules loaded while executing a macro will add a dependency between the original module in which the macro is called and the target module
- if you are doing some class side effects such as adding metadata, make sure that it's not already applied (you might get errors such as
Duplicate metadata xxxif it gets added twice) - callbacks such as
Context.onGenerateare not persistent between compilations unless they are registered again manually - you can add a specific macro action to be made in case a given cached module is reused in later compilations by using
Context.registerModuleReuseCall: please note that the macro call is a string that can only contain constant data (it is executed in a similar way as--macrocommand line parameter)
Any other unexpected side effects can be reported on the Haxe google group for discussions.