Creating Macros

135 views
Skip to first unread message

Dinko Pavicic

unread,
Mar 19, 2016, 9:06:01 AM3/19/16
to Haxe
Hi all,
I'm trying to fiddle with macros to build Maps filled with asset data but I totally suck at it.

I have a JSON with asset data grouped by the locale and also by the build target ( within the each locale ), something like this:

assets: {

  en: {
    html: [
      {
         name: "textureA",
         type: "texture",
         resource: "some_url_to_a_texture.png"
         isLoaded: false
      },
      {
         name: "meshA",
         type: "mesh",
         resource: "some_url_to_a_texture.fbx"
         isLoaded: false
      }
    ],
    ios: [
      {
         name: "textureA",
         type: "texture",
         resource: "some_url_to_a_texture_ios.png"
         isLoaded: false
      },
      {
         name: "meshA",
         type: "mesh",
         resource: "some_url_to_a_texture_ios.fbx"
         isLoaded: false
      }
    ],
    android: [
      {
         name: "textureA",
         type: "texture",
         resource: "some_url_to_a_texture_android.png"
         isLoaded: false
      },
      {
         name: "meshA",
         type: "mesh",
         resource: "some_url_to_a_texture_android.fbx"
         isLoaded: false
      }
    ]
  },
  fr: {
    html: [
      {
         name: "textureA",
         type: "texture",
         resource: "some_url_to_a_texture.png"
         isLoaded: false
      },
      {
         name: "meshA",
         type: "mesh",
         resource: "some_url_to_a_texture.fbx"
         isLoaded: false
      }
    ],
    ios: [
      {
         name: "textureA",
         type: "texture",
         resource: "some_url_to_a_texture_ios.png"
         isLoaded: false
      },
      {
         name: "meshA",
         type: "mesh",
         resource: "some_url_to_a_texture_ios.fbx"
         isLoaded: false
      }
    ],
    android: [
      {
         name: "textureA",
         type: "texture",
         resource: "some_url_to_a_texture_android.png"
         isLoaded: false
      },
      {
         name: "meshA",
         type: "mesh",
         resource: "some_url_to_a_texture_android.fbx"
         isLoaded: false
      }
    ]
  }

}


I have a class like this where I would like to create and populate Map(s) with asset data via macros

class AssetService
{
    /** Map with structure something like this:

     [ localeName =>  [
       assetName => asset, 
       assetName => asset,
       assetName => asset,
       ],
       localeName =>  [
       assetName => asset, 
       assetName => asset,
       assetName => asset,
       ]
     ]
    */
    private var _assets:Map<String, Map<String, Asset>>;
}


And a class for the assets, something like this
class Asset
{
  public var name:String;
  public var locale:String;
  public var resource:String;
  public var type:String;
}





My macro starts something like this ... It successfully load that json and parses it into an object but I really don't know how to create Maps with macros and set them to appropriate fields:

#if macro
class AssetMetadataBuilder
{

macro public static function build()
{
trace( "------- ASSET COMPILER ------- " );
trace("");
switch ( Context.getType("test.AssetService" ))
{
case TInst(cl,_):
var cl = cl.get();
var meta = cl.meta;

// Get assets config ... loads the assets.json file and parses it
var assetsData = AssetMetadataBuilder.loadAssetConfig();

                                
                               
                                ..... need to create and populate Maps with macros here ... I'm a dumbass



I tried to find similar stuff on the group but didn't have any luck. If anybody have any pointers about this I would be really grateful.
Many thanks in advance!

Dinko

Dinko Pavicic

unread,
Mar 19, 2016, 9:10:54 AM3/19/16
to Haxe
I messed up the post subject, it should be 'creating Maps with macros'. 
Sorry for that

Yoni Gueta

unread,
Mar 19, 2016, 1:28:24 PM3/19/16
to Haxe

i think this can do what your'e looking for:

in your example, just replace your classes with typedefs and feed it to CompileTime.parseJsonFile("myJson.json")

Dinko Pavicic

unread,
Mar 20, 2016, 6:30:34 PM3/20/16
to Haxe
Thanks, not exactly what I'm looking for but it's nice for macro learning purposes.

Domagoj Štrekelj

unread,
Mar 21, 2016, 12:14:22 PM3/21/16
to Haxe
Hello,

I recently attempted something similar myself, though not with maps. There were some pitfalls, but nothing I couldn't overcome with some help from the good people frequenting the Haxe IRC channel.

What follows is a brief explanation of Haxe macro programming as I understand it. Feel free to move straight on to the code sample if you're not new to macros!

Haxe macros are an interesting exercise in meta-programming. We have been conditioned to write code to solve a problem; much like we are conditioned to "solve for X" in algebra by any means we see fit. Writing macros, however, puts the code into focus - we write code to reduce the number of steps required to reach a solution. These steps are expressions or Haxe source code, in simple terms.

Let's take for example your issue of outfitting an existing class with a variable of the map type. We know the data we want the map to store, and more importantly we know how to declare and initialize maps. Here is a simple map with Int keys mapped to String values:

var myMap : Map<Int, String> = [ 1 => "One", 2 => "Two", 3 => "Three" ];

We can see a pattern: key => value. And patterns are great for us programmers. We can exploit them to write less code, which is literally what happens when writing a macro.

Each one of those patterns is an expression, which means that the map is essentially an array of expressions. So here is what we need to do (in "pseudocode"):

myMap : Array<Expression> = [];

for (pair in dataset)
{
    myMap
.push(pair.key => pair.value);
}

Here is where it gets tricky because now we actually have to write a macro. For the sake of brevity I won't go into detail and let the actual asset loading macro do the talking.

package;

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

using haxe.macro.Tools;

class AssetServiceBuilder
{
    public static macro function build() : Array<Field>
    {
        // The build fields of the class being built
        var fields = Context.getBuildFields();
        // The asset data we want to store into a map
        var assetData : AssetData = haxe.Json.parse(sys.io.File.getContent("res/assets.json"));
        // The array of expressions representing the final map of assets
        var assets = [];
        
        // We use reflection to iterate through fields of the anonymous object that is assetData
        for (locale in Reflect.fields(assetData))
        {
            // This is akin to assetData[locale] or assetData.locale
            var l : AssetData.AssetLocaleData = Reflect.field(assetData, locale);
            // The array of expressions representing the internal map of assets per platform
            var localAssets = [];
            
            for (platform in Reflect.fields(l))
            {
                var p : AssetData.AssetPlatformData = Reflect.field(l, platform);
                // I'm only looking at the assets for a single platform to avoid duplicate keys
                if (platform == "ios")
                {
                    for (entry in p)
                    {
                        // Note the use of `macro` and expression reification (`$v{}`)
                        localAssets.push(macro $v{entry.name} => new Asset($v{locale}, $v{entry.name}, $v{entry.resource}, $v{entry.type}));
                    }
                }
            }
            // Note the use of `macro` and expression reification (`$a{}`)
            assets.push(macro $v{locale} => $a{localAssets});
        }
        
        // We "push" (add) a new field to the class
        fields.push({
            // Think of this as the line position that will be referenced on error
            pos: Context.currentPos(),
            // Field name
            name: "_assets",
            // We won't be adding any metadata
            meta: null,
            // The field will be a variable of the Map<String, Map<String, Asset>> type, and will contain the array of expressions making up a map
            kind: FieldType.FVar(macro : Map<String, Map<String, Asset>>, macro $a{assets}),
            // We won't be adding any documentation
            doc: null,
            // We'll give it private visibility
            access: [Access.APrivate]
        });
        
        // Once the build macro is done it needs to return the context's build fields
        return fields;
    }
}

Here is a comprehensive gist.

I would recommend building with -D dump=pretty when writing macros because the AST-like dump of the macro result is enlightening. In this case, it looks kind of like this:

class AssetService{
private _assets(var) : Map<String, Map<String, Asset>>

= {
var _g = new haxe.ds.StringMap();
{
var value;
{
var _g1 = new haxe.ds.StringMap();
{
var value1 = new Asset("en","textureA","some_url_to_a_texture_ios.png","texture");
__dollar__hset(_g1.h,"textureA".__s,value1,null);
};
{
var value1 = new Asset("en","meshA","some_url_to_a_texture_ios.fbx","mesh");
__dollar__hset(_g1.h,"meshA".__s,value1,null);
};
value = _g1;
};
__dollar__hset(_g.h,"en".__s,value,null);
};
{
var value;
{
var _g2 = new haxe.ds.StringMap();
{
var value1 = new Asset("fr","textureA","some_url_to_a_texture_ios.png","texture");
__dollar__hset(_g2.h,"textureA".__s,value1,null);
};
{
var value1 = new Asset("fr","meshA","some_url_to_a_texture_ios.fbx","mesh");
__dollar__hset(_g2.h,"meshA".__s,value1,null);
};
value = _g2;
};
__dollar__hset(_g.h,"fr".__s,value,null);
};
_g;
}

}

I hope this was helpful!

Domagoj

P.S.

Pozdrav iz Osijeka :) Ako želiš s nekim "lokalnim" razgovarati o Haxeu, slobodno mi se javi!

Dinko Pavicic

unread,
Mar 22, 2016, 6:57:06 PM3/22/16
to Haxe
Wow!

I don't know how to thank you for putting so much effort in this explanation! Many thanks, this really helps !!!

Nice to see that there's Haxers in Croatia.

Take care,
Dinko
Reply all
Reply to author
Forward
0 new messages