Advanced Macro Features

Rest Arguments

If you want your macro to take a variable number of arguments, you can use Array<Expr> as the last "catch-all" argument :

@:macro static function foo( e : Array<Expr> ) : Expr {
//    ....
}

Better Completion with Macros

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 the ExprOf type that will specify which type to show when doing completion :

@:macro static function isqrt( a : ExprOf<Int> ) : ExprOf<Float> {
    ....
}

Please note that this only a tip : it will not enforce making the expression into the given type.

This is useful in particular to implement using macros.

Member Macros Methods

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;
    }
}
// ...
#if !macro
var a = new A();
trace( a.getThis() );
#end

Please note that if you try to call getThis() inside the macro context, you will get an error since it becomes static in that case.

Macros + Using

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 the ''ExprOf' type to specify that you will only accept expressions of the given type :

import haxe.macro.Expr;
// ...
@:macro static function encodeBase64( e : ExprOf<String> ) : Expr {
//    ....
}

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 takes 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.

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 performance into account. Here's a few tips for improving macro performance :

  • 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 IDEs 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 --times parameter. It will give you time measurements for the different parts of the compiling process. Additionally you can use -D macro_times that 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 of course affects 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 produces code based on other files that are not part of the compilation process, you can use haxe.macro.Context.registerModuleDependency to inform the compiler that if 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 xxx if it gets added twice)
  • callbacks such as Context.onGenerate are 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 --macro command line parameter)

Any other unexpected side effects can be reported on the Haxe google group for discussions.

version #19359, modified 2013-05-13 05:01:15 by jason