Monitor read and write accesses in general

Hi there Haxe folks!

I would like to know if it is possible to monitor any read and write accesses that are made on a variable public/private var (so a predeclared class field, without even a get/set function). By that I mean to track whenever the field was read or written to:

class Main {
    static function main() {
        var o : Observed = new Observed();

        // each of the following should trace the altered `var` field:
        // trace('$var has been changed to $newValue');
        Observed.hello = 81;
        o.i     = 1038;
        o.i    += 1038;                   // would also trace that o.i has been read
        o.i     = 1038 + Observed.hello;  // would also trace that Observed.hello has been read
        o.obj   = new Object();           // would trace info about the new object
        trace( o.i ); // would call for instance: trace('Observed.i has been read (inside Main.main)");
    }
}

class Observed {
    public static var hello : Int = 0;
    public var i : Int = 0;
    public var obj : Object;
    public function new() {}
}

class Object {
    public function new() {}
}

(Of course it’s not only about trace, but also calling other functions right after the read/write action.)

I’m curious if these state mutations are accessible to the programmer. I was wondering because they for sure must exist on assembler level. Alternatively of course this kind of monitoring would be possible by only giving access through get / set methods. But this would mean to give up on common programming style / whole parts of the language syntax…

Have a great Sunday! :+1:

Macros are your friend for this solution.

If you are looking for specific functions, you can use haxe macro context. E.g.:

package; #if macro

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

class Macro {

    public static macro function build():Array<Field>
    {
        var fields = Context.getBuildFields();
        for (f in fields)
        {
            switch (f.kind):
            {
		        case FFun(func):
                {
                   // do something with function declaration here,
                   // such as iterating the statements of
                   // func.expr
                }
                default:
            }
        }
        return fields;
    }

}

And then call @:build(Observer.build()) against your class.

You could go one step further and make a custom tag, marking only the functions you want the Observer class to observe and even a custom tag against the class which you detect at compile time to generate Observer instances.

However, once you start working with the individual expressions in macro context, dealing with them can get incredibly complicated. Best thing to do would be to check each ExprDef in a trace and compile, finding out which type is which and how to handle them.

Also, take a look at the API: haxe.macro.ExprDef - Haxe 4.2.1 API This will show you what ExprDef are what in your switch statements.

1 Like

Haxe just transpiles to other languages, if you know other language with described functionality, make it as target language. Best option you have right now is to use onAfterTyping, onGenerate, onAfterGenerate. Check getAllClasses by Jason O’Neil, he made Simple Haxe Macro Helpers that let you do or get things at compile-time.

1 Like

Before diving into macros, I’d say have a look at abstracts. Using operator overloading (see fieldRead and fieldWrite in the example) you can pick up any get/set combination on an instance. Picking up the static (hello) read/write won’t be possible though, that will eventually require some macro work.

2 Likes

If you’re looking for implementing Observables interface then there is already well maintained solution out there: GitHub - haxetink/tink_state: Handle those pesky states.

1 Like