Haxe Custom Metadata to Macro Call
Asked Answered
R

2

5

Let's say that I've created a build macro that can be used like so

@:build(macros.SampleMacro.build("arg"))
class Main {}

Is it possible to convert it into a custom, shorthand metadata?

@:samplemacro("arg")
class Main {}

Any documentation on this?

Repudiation answered 9/5, 2017 at 14:43 Comment(0)
R
5

After much stumbling, I've figured out that it is possible to do.

Part of the solution is to use

--macro addGlobalMetadata('$path', '@:build(Build.build())')

This lets you assign a @:build function to all of the classes in the $path. This can be used as either a compiler option or a haxeflag.

But that on its own is not enough to adopt metadata tags that take dynamic arguments. But because we now have a Build.build() that executes for all of our classes, that Build.build() function can both handle checking which classes have our custom metadata tag(s), as well as build anything for those classes we may need.

To check for our custom metadata tags, we set up our checking macro as follows:

class Build {

    static var META_STR:String = ":samplemacro";

    macro static public function build():Array<Field> {

        // We check only those Contexts for where a class type exists
        var localClass:Null<Ref<ClassType>> = Context.getLocalClass();
        if(localClass == null) return null; // no class type

        // We check if the metadata for the class contains our 
        // custom metadata string at all
        if(!localClass.get().meta.has(META_STR)) return null;

        // This class does have our custom metadata!
        // Because there may be more than one of the same type
        // of metadata, we extract a list of all the custom metadata tags

        var customTags = localClass.get().meta.extract(META_STR);

        // For each tag we can get at the arguments passed in 
        // by accessing the params field
        for(customTag in customTags){
            var params = customTag.params;

            // Here we can handle each of our @:samplemacro(params) tags,
            // save the params for use later, or 
            // pass the arguments over to another class to deal with
        }

        var fields = Context.getBuildFields();

        // Modify the class fields the way you want

        // Optionally destroy the metadata tags afterwards
        // with localClass.get().meta.remove(META_STR);

        return fields;
    }
}

More details on addGlobalMetadata can be found at: https://mcmap.net/q/2032936/-haxe-add-build-metadata-to-all-classes-in-project

Repudiation answered 9/5, 2017 at 20:19 Comment(4)
You might want to check how this affects compilation performance. Running a build macro on everything could be expensive in a large project (basically the reason I didn't suggest this approach).Yawn
I've yet to run into a performance issue, and moreover, I can't seem to find any other way around this. Luckily the test to check for the existence of a tag really only involves checking a map of metadata tags.Repudiation
there is a library that does the exact thing, with some additional features: github.com/haxetink/tink_syntaxhubCornerwise
I imagine I would swap to using the tink_syntaxhub once I had more than a few custom macros registered across my codebase.Repudiation
Y
3

I'm not sure that's possible, but you can make use of the fact that @:autoBuild() metadata works on interfaces. This is often used for "marker interfaces" like this:

class Main implements ISampleMacro {}

@:autoBuild(macros.SampleMacro.build("arg"))
interface ISampleMacro {}

However, presumably you want to have a different "arg" per usage, rather than hardcoding it. You can achieve this by using a @:const type parameter:

class Main implements ISampleMacro<"foo"> {}

@:autoBuild(macros.SampleMacro.build())
interface ISampleMacro<@:const T> {}

Extracting the type parameter's value in your build macro requires a bit more effort than simply passing a parameter to it:

switch (Context.getLocalClass().get().interfaces[0].params[0]) {
    case TInst(_.get() => t, params):
        switch (t.kind) {
            case KExpr({expr: EConst(CString(arg)), pos: _}):
                trace(arg); // "foo"
            case _:
        }
    case _:
}
Yawn answered 9/5, 2017 at 15:22 Comment(1)
Thank you for your suggestion - this is another way of doing things, but not what I was looking for.Repudiation

© 2022 - 2024 — McMap. All rights reserved.