Making cool stuff with Haxe macros

There is a rule in computer science: Whenever a developer learns a new feature, it will be used extensively in his next project.

Introduction

During the past half year, I worked on many games using HaxeFlixel, for example the platformer Meteor Dave or the action RPG and Ludum Dare project Raiders of the Ancient Technology.

In the process of creating those games, I gained some knowledge of lower level parts of HaxeFlixel and Haxe itself. Just some clarification: Haxe is the programming language, HaxeFlixel is the game framework. In this post, I want to share one of the best features of Haxe with you: macros.

History of macros

If you know C or C++, you probably may have stumbled upon macros yourself. In this family of languages, they are preprocessor directives. It is a simple replace mechanism, that runs before the actual compilation of the code.

Some C++ code might look like this:

#define PI 3.141592
#define MIN(X,Y) ((X) < (Y) ? : (X) : (Y))

Often times, macros are also used for compiler flags or for the famous include guards (prior to pragma once). If you look at the code it looks perfectly fine. Everywhere you write PI in your code, the preprocessor will insert the numeric value. The use of the MIN macro also works (mostly) as intended.

...at least until you don't rely on type safety for the Pi example. The second one is even worse, because consider something like

int c = MIN(a,--b); 

What will be the value of c? Remember, the code will just get substituted. Even worse, depending on the value of a and b, b might even be decremented twice. For a = 3 and b = 4, c will have a value of 2, which is just wrong. On a side note: the modern way of doing this in C++ would be to use constexpr and the template function std::min.

Macros in C++ are a powerful tool, but cumbersome and dangerous. The Haxe language also offers macros, but with improved features like type safety, syntax checks and another connotation that is very powerful.

Macros in Haxe

Haxe macros do not run before the build process, but as a part of it. Thus all Haxe macros are completely type safe, offer syntax validation and meaningful error messages, in contrast to C++ templates. But the best part about macros is, that they allow you to execute and modify code at compile time. Lets check out some examples!

Examples

As stated with the rule at the beginning of this post, I wanted to use these macros in some of my projects. Most of the examples are based on the Haxe Code Cookbook.

Showing compile time

When working on some larger projects, it would be nice to have the latest commit message and compile date displayed somewhere in the main menu. This makes it easy to identify which version is currently being tested. You could manually add a version string in a global variable, but that's not what you want, because the string would have to be changed every time you create a build.

This macro can do just that:

// macro function for getting the build date
public static macro function getBuildDate() : Expr
{
    var d : Date = Date.now();
    var date : String = d.toString();
    return macro $v { date };
}

This is a static function that has the keyword macro. Despite some peculiar syntax at the end, this looks very familiar. The return value Expr is a macro type and can be understood as a Dynamic at compile time.

The function itself is not very complicated. It gets the current date and converts it to a string. The return statement is somewhat special: since it is a macro function, returning a macro Expr, it needs to create such a type. $v {} is an abbreviation for creating an Expr value object.

And that's it. Congratulations on your first Haxe macro.

Getting the commit hash

The same can be done with the git commit hash or commit message:

var process = new sys.io.Process('git', ['rev-parse', 'HEAD']);
if (process.exitCode() != 0) 
{
    var message = process.stderr.readAll().toString();
    var pos = haxe.macro.Context.currentPos();
    Context.error("Cannot execute `git rev-parse HEAD`. " + message, pos);
}
// read the output of the process and create the string expression
var commitHash:String = process.stdout.readLine();
return macro $v{commitHash};

The code block uses sys.io.Process to call git rev-parse HEAD, which retrieves the commit hash. Since this might fail for reasons (e.g. no git repo initialized, git executable not found in path, no commit performed), it will print an error if something went wrong. A nice bonus is, that the error gets a line number, so the compilation error message will show you exactly where you have to look. The return statement works just as in the date example.

To get the last commit message, exchange the process line with

var process = new sys.io.Process('git', ['log', '-1', '--pretty=%B']);

If you are on a Windows machine, make sure the git executable can be found in the path.

A remark about compiler flags

Haxe offers some compiler flags to check whether it is in normal compilation or code completion mode. Because Haxe also evaluates macros during code completion it is advisable to exclude the commit hash. This can be done using a #if display ... #end block to check if you are in code completion mode. I did not add this to the code block due to readability.

Checking JSON during compilation

For games, JSON files are a handy way for configuration and Haxe offers an easy way to parse them. Unfortunately it will throw an exeption and thus crash if the JSON file's syntax is wrong. This is something that could (and should) be checked at compile time.

This is a very handy feature. You can check out the code for this in the Haxe Code Cookbook.

Creating a class from a CSV file

The inspiration for this came from grapefrukt. In most of my projects there is a class called GameProperties which has only static members. It includes variables like Gravity, WorldSizeInTiles, TileSizeInPixel, PlayerMovementSpeed and so on.

It would be super nice to have a CSV File instead that contains all those values. But parsing this at runtime (an push the values to a dictionary) will not allow any code completion when writing code. Again, this can be solved with Haxe macros:

public static macro function addFunctions(file : String) : Array<Field>
{
  // get the currently available fields
  var fields = Context.getBuildFields();

  // parse the file and split it in separate lines
  var f : String = File.getContent(file);
  var lines : Array<String> = f.split("\n");

  for (l in lines)
  {
    // get the info in the line
    var thisline : Array<String> = l.split(",");
    var myName : String = thisline[0];
    var value : Float = Std.parseFloat(thisline[1]);
    var pos = Context.currentPos();

    // create a new field
    var propertyField : Field = 
    {
      name : myName,
      access : [Access.APublic, Access.AStatic],
      kind: FieldType.FProp("default", "null", macro:Float,macro $v{value}), 
      pos: pos,
    };

    // append the field
    fields.push(propertyField);
  }
  return fields;
}

The largest part of this code block is just parsing a CSV file and extracting the values from it. One of the more interesting lines is Context.getBuildFields();, which gets the fields of the class where this function is called from. Later, our custom fields will be added to this. The var propertyField is the field that is parsed from the CSV file. We set some access identifiers (fields should be public and static) and tell Haxe this is a property that just returns that parsed value.

If you want to use this, just create an empty class

@:build(BuildExtender.addFunctions("assets/data/test.csv"))
class MyClass
{
}

If the file assets/data/test.csv looks like test, 10 gravity, -9.81

MyClass will now have two properties, called test and gravity, both with the respective values. This is done at compile time, so no performance loss for loading and parsing the file at run time.

The best part is, that this can also be combined with static members in the class. If your IDE supports code completion (like FlashDevelop) all the properties from the class and all that have been parsed from the CSV will show up in code completion.

Summary

I hope I could raise your interest for Haxe macros. Of course there are a lot of other applications not mentioned here and a lot of extensions for the examples I provided (like parsing a JSON file instead of a CSV and creating not only floats, but ints, strings or whatever you need). There are also great macro libraries on GitHub that can teach you a lot more about how to use macros.