Using Haxe macros to instantiate a class with parameters
Asked Answered
E

2

6

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?

Embody answered 27/1, 2018 at 12:4 Comment(0)
L
6

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);
Lovely answered 27/1, 2018 at 12:44 Comment(1)
This is really helpful, my life making macros will be easier now! Sometimes i think "i get it" but not really, it's hard to learn all about haxe macros. Thanks!Pragmatist
E
5

If you prefer adding fields by fields.push({...}) instead of using class reification, you can trigger type inference by using null as a type in FVar(null, pool):

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 pool = macro new exp.Pool($p{path});
    fields.push({
        name: "_pool",
        access: [APrivate, AStatic],
        pos: Context.currentPos(),
        kind: FVar(null, pool)
    });

    return fields;
}

This is using @gama11 trick for path. This generates the exact same code as @gama11 answer (and can be checked in the same way).

Eisler answered 27/1, 2018 at 14:31 Comment(1)
I didn't know how to do class reification, i think it's a better approach instead of building fields manually for people like me with a basic knowledge of macros, but i will remember your tips. Thanks!Pragmatist

© 2022 - 2024 — McMap. All rights reserved.