Hope you’re having a great day. Today, we’re getting into the nitty-gritty of Haxe black magic, aka Macros. But, let’s describe what macros allow you to do in Haxe.

Power Of Macros

Macros allow you to write code that generates code. That’s essentially what macros allow you to do in Haxe. Now, at the first, you may think that’s not necessary. But, there are great uses for this that can save you a lot of time. You can parse files and embed them into your code, create specific classes automatically, or add fields to your pre-existing classes. All this stuff, you may have had to do by hand, all handled by a single function in your application.

Cool right? The best part, all of this happens before your program is compiled to its final output. With that said, I’m sure you’re wondering how you could write your own. While macros are not easy and are black magic to some degree. They can help you better understand the programming language and make your life easier. So, how do you write your own macro?

Writing Your Own Build Macro

To write your own macro, first, you’ll have to make sure that your macro is executed in the macro context. In Haxe, these functions are written with the macro keyword. This function is tapped into the Haxe AST (Abstract Syntax Tree) and allows you to create expressions; expressions are just Haxe code. Here is an example from a project of mine of what a function signature for a macro would look like.

As you can see all we’ve done is add the macro function signature and what the macro should return. In this case, we are returning an array of Field type; fields are properties that exist on a class. Now, this is just the first step, here’s an example of a macro that creates a function on a command class using Aseprite’s gui.xml. See the below example.

Here’s the XML. Now, let’s get into the actual code below.

package buildMacros;
import Structs.ZoomT;
import sys.FileSystem;
#if macro
import Structs.ChangeColorT;
import Structs.ChangeBrushT;
import haxe.macro.ExprTools;
import haxe.macro.Expr.Field;
import haxe.macro.Context;
import haxe.macro.Expr;
import haxe.macro.Type;
using haxe.macro.Tools;
using Lambda;
using StringTools;
import sys.Http;
import sys.io.File;
#end
/**
* Loads the Aseprite raw xml and parses it for
* creating commands for the command class.
* @return Array<Field>
*/
inline var FILE_PATH = 'aseprite-gui-link.txt';
//https://raw.githubusercontent.com/aseprite/aseprite/main/data/gui.xml <– aseprite-gui-link.txt content
//Establish macro context with #if
#if macro
macro function buildAppComands():Array<Field> {
var buildFields = Context.getBuildFields(); //Gets the fields on the class we apply the build macro to.
// Load Aseprite XML Data
var fileContents = File.getContent(FILE_PATH);
var result = Http.requestUrl(fileContents); //HTTP request to get XML data for parsing
var xmlContent = Xml.parse(result);
//Going to the command elements in the gui.xml
var keyboardCommandsNode = xmlContent.firstElement()
.firstElement()
.firstElement();
//Process each command in the XML element list
for (command in keyboardCommandsNode.elements()) {
//Filter out duplicate fields
if (command.exists('command') && !buildFields.exists((field) -> {
return field.name == command.get('command');
})) {
// Switch Case to determine struct to use as args
var args = [];
var documentation = null;
//Switch case dedicated to creating function arguments for the created function fields
documentation = FileSystem.exists('res/${command.get('command')}.hx') ? File.getContent('res/${command.get('command')}.hx') : File.getContent('res/Blank.hx');
var arg:FunctionArg = switch (command.get('command')) {
//Creates the type definition for the aseprite ChangeBrush command function and the name of the parameter.
case "ChangeBrush":
{
name: "ChangeBrush",
type: (macro:{
?change:String,
?slot:Int
})
}
case "ChangeColor":
{
name: "ChangeColor",
type: (macro:{
target:String,
change:String
})
}
case "Scroll":
{
name: "Scroll",
type: (macro:{
direction:String,
units:String,
quantity:Int,
})
}
case "MoveMask":
{
name: "MoveMask",
type: (macro:{
target:String,
direction:String,
units:String,
quantity:Int
})
}
case "SymmetryMode":
{
name: "SymmetryMode",
type: (macro:{
orientation:String
})
}
case "AutocropSprite":
{
name: "AutocropSprite",
type: (macro:{
byGrid:Bool
})
}
case "Screenshot":
{
name: "Screenshot",
type: (macro:{
srgb:Bool,
?save:Bool
})
}
case "LayerOpacity":
{
name: "LayerOpacity",
type: macro:{
opacity:Int
}
}
case "Zoom":
{
name: "Zoom",
type: (macro:{
?action:String,
?percentage:Int
})
}
case "SetInkType":
{
name: "SetInkType",
type: (macro:{
type:String
})
}
case "SetColorSelector":
{
name: "SetColorSelector",
type: (macro:{
type:String
})
}
case "AddColor":
{
name: "AddColor",
type: (macro:{
source:String
})
}
case "SelectTile":
{
name: "SelectTile",
type: (macro:{
mode:String
})
}
case _:
null;
}
if (arg != null) {
args.push(arg);
}
// Create Build Field
var buildField:Field = {
name: command.get('command'), //Name of the field
doc: documentation, //Haxe Doc comment that appears next to the command
access: [APublic], //Access to the field; in this case it's public
kind: FFun({ //Type of the field; this is a function field, which means it takes arguments and a return type
args: args,
ret: (macro:Void)
}),
pos: Context.currentPos(),
//Metatags that we use in Haxe to make code changes in the output
meta: [
{
name: ":luaDotMethod",
pos: Context.currentPos()
}
]
}
//Add the build field to the list of class fields
buildFields.push(buildField);
}
}
//Returns all of the fields associated with the class
return buildFields;
}
#end

Now, this might seem like a lot of code, but every step is commented to give you an idea of what each section is doing. The most important parts are the buildFields. Build fields are what is attached to the class once you return from this function. They build the fields onto the class. The switch case is used to create different types for our build field at the end, allowing us to make the gif you saw at the top of this post. Now, this is a function, but the function must be applied to something; in Haxe, we use build macros like so:

Before you even compile your code, that build macro will be applied, allowing you to use those commands in your code in a type-safe manner and saving you time, because all of the code in this case will be up to date with the Aseprite spec. If you’d like to see the code this project is used in you can find it here.

Conclusion

I hope this serves as a soft introduction to haxe macros and there is definitely more to come. Stay tuned!! Next time, you’ll get an introduction into how to write macros that add other field types using expression reification.

For more information or any comments, leave them below and I will reply with any information that I have.

For additional information you can find it below:

Haxe Macros Basics

https://code.haxe.org/category/macros/

%d bloggers like this: