Search code examples
macroshaxe

Using Haxe macros to instantiate a class with parameters


I'm trying to make some dark magic with macros in Haxe, I have a class named Entity and I want to add a pool with the static and private modifiers:

Pool.hx:

package exp;

class Pool<T> {
    public function new(clazz:Class<T>) {
        
    }
}

Entity.hx:

package exp;

@:build(exp.PoolBuilder.build())
class Entity {
    public function new(){}
}

PoolBuilder.hx:

package exp;

import haxe.macro.Context;
import haxe.macro.Expr;
import haxe.macro.Type;

class PoolBuilder {
    static public macro function build() : Array<Field> {
        var fields = Context.getBuildFields();
        var clazz = Context.getLocalClass();

        var typePath = { name:"Pool", pack:["exp"], params: [TPType(TPath({name: "Entity", pack: ["exp"]}))] }
        var pool = macro new $typePath(/* clazz? */);
        fields.push({
            name: "_pool",
            access: [APrivate, AStatic],
            pos: Context.currentPos(),
            kind: FVar(macro: exp.Pool, pool)
        });

        return fields;
    }
}

I have a problem with the typePath params, and passing a Class<T> as an argument to the constructor. The compiler display this error:

exp/Entity.hx:3: characters 1-7 : Invalid number of type parameters for exp.Pool

exp/Entity.hx:4: lines 4-6 : Defined in this class

Does anybody know how to solve it?


Solution

  • Building fields manually like that is somewhat tedious - I'd recommend to use class reification instead, where you can express the field as regular Haxe code:

    macro class {
        static var _pool = new Pool(/* clazz */);
    }
    

    This bypasses the "Invalid number of type parameters" issue entirely - just let type inference do the trick and omit the type parameter in new Pool().

    The argument for the constructor call is of course variable, so we still need to use some expression reification. exp.Entity is a field expression, so we have to use $p{}. We can construct the type path needed for it by combining clazz.pack and clazz.name:

    class PoolBuilder {
        static public macro function build():Array<Field> {
            var fields = Context.getBuildFields();
            var clazz = Context.getLocalClass().get();
            var path = clazz.pack.concat([clazz.name]); // ["exp", "Entity"]
    
            var extraFields = (macro class {
                static var _pool = new Pool($p{path});
            }).fields;
            return fields.concat(extraFields);
        }
    }
    

    This generates the following code (as can be seen in exp/Entity.dump with -D dump=pretty):

    static var _pool:exp.Pool<exp.Entity> = new exp.Pool(exp.Entity);