Skip to content

Instantly share code, notes, and snippets.

@nadako
Last active November 27, 2020 16:08
Show Gist options
  • Save nadako/b086569b9fffb759a1b5 to your computer and use it in GitHub Desktop.
Save nadako/b086569b9fffb759a1b5 to your computer and use it in GitHub Desktop.
Signal builder using new Rest type parameter in Haxe
class Main {
static function main() {
var signal = new Signal<Int,String>();
var conn = signal.connect(function(a, b) {
trace('Well done $a $b');
});
signal.dispatch(10, "lol");
}
}
import haxe.Constraints.Function;
@:genericBuild(SignalMacro.build())
class Signal<Rest> {}
class SignalBase<T:Function> {
var head:SignalConnection<T>;
var tail:SignalConnection<T>;
var toAddHead:SignalConnection<T>;
var toAddTail:SignalConnection<T>;
var dispatching:Bool;
public function new() {
dispatching = false;
}
public function connect(listener:T, once = false):SignalConnection<T> {
var conn = new SignalConnection(this, listener, once);
if (dispatching) {
if (toAddHead == null) {
toAddHead = toAddTail = conn;
} else {
toAddTail.next = conn;
conn.previous = toAddTail;
toAddTail = conn;
}
} else {
if (head == null) {
head = tail = conn;
} else {
tail.next = conn;
conn.previous = tail;
tail = conn;
}
}
return conn;
}
function disconnect(conn:SignalConnection<T>):Void {
if (head == conn)
head = head.next;
if (tail == conn)
tail = tail.previous;
if (toAddHead == conn)
toAddHead = toAddHead.next;
if (toAddTail == conn)
toAddTail = toAddTail.previous;
if (conn.previous != null)
conn.previous.next = conn.next;
if (conn.next != null)
conn.next.previous = conn.previous;
}
inline function startDispatch():Void {
dispatching = true;
}
function endDispatch():Void {
dispatching = false;
if (toAddHead != null) {
if (head == null) {
head = toAddHead;
tail = toAddTail;
} else {
tail.next = toAddHead;
toAddHead.previous = tail;
tail = toAddTail;
}
toAddHead = toAddTail = null;
}
}
}
@:allow(SignalBase)
@:access(SignalBase)
class SignalConnection<T:Function> {
var signal:SignalBase<T>;
var listener:T;
var once:Bool;
var previous:SignalConnection<T>;
var next:SignalConnection<T>;
function new(signal:SignalBase<T>, listener:T, once:Bool) {
this.signal = signal;
this.listener = listener;
this.once = once;
}
public function dispose():Void {
if (signal != null) {
signal.disconnect(this);
signal = null;
}
}
}
#if macro
import haxe.macro.Context;
import haxe.macro.Expr;
import haxe.macro.Type;
using haxe.macro.Tools;
class SignalMacro {
static function build():ComplexType {
return switch (Context.getLocalType()) {
case TInst(_.get() => {name: "Signal"}, params):
buildSignalClass(params);
default:
throw "assert";
}
}
static function buildSignalClass(params:Array<Type>):ComplexType {
var numParams = params.length;
var name = 'Signal$numParams';
var typeExists = try { Context.getType(name); true; } catch (_:Any) false;
if (!typeExists) {
var typeParams:Array<TypeParamDecl> = [];
var superClassFunctionArgs:Array<ComplexType> = [];
var dispatchArgs:Array<FunctionArg> = [];
var listenerCallParams:Array<Expr> = [];
for (i in 0...numParams) {
typeParams.push({name: 'T$i'});
superClassFunctionArgs.push(TPath({name: 'T$i', pack: []}));
dispatchArgs.push({name: 'arg$i', type: TPath({name: 'T$i', pack: []})});
listenerCallParams.push(macro $i{'arg$i'});
}
var pos = Context.currentPos();
Context.defineType({
pack: [],
name: name,
pos: pos,
params: typeParams,
kind: TDClass({
pack: [],
name: "Signal",
sub: "SignalBase",
params: [TPType(TFunction(superClassFunctionArgs, macro : Void))]
}),
fields: [
{
name: "dispatch",
access: [APublic],
pos: pos,
kind: FFun({
args: dispatchArgs,
ret: macro : Void,
expr: macro {
startDispatch();
var conn = head;
while (conn != null) {
conn.listener($a{listenerCallParams});
if (conn.once)
conn.dispose();
conn = conn.next;
}
endDispatch();
}
})
}
]
});
}
return TPath({pack: [], name: name, params: [for (t in params) TPType(t.toComplexType())]});
}
}
#end
@nadako
Copy link
Author

nadako commented Feb 3, 2020

Hmm, yeah this macro is not cache-friendly. I think the signalTypes map is reset on every compilation, so it tries to define the type again. What we should do here instead is try Context.getType to see if the type is already there. I'll change the snipper when I get some time.

@AustinEast
Copy link

AustinEast commented Feb 3, 2020

Awesome! I was actually messing with Context.getType to fix it myself, so i’ll update if i get something working before you’re able to.

Edit: I added in this method to SignalMarco.hx:

static function typeExists(typeName:String):Bool {
    try {
      if (Context.getType(typeName) != null) return true;
    } catch (error:String) {}

    return false;
  }

and replaced line 23 with:

if (!typeExists('$name')) {

This seems to work! I ran into an issue with Hashlink (every other build it would error with JIT ERROR 0 (jit.c line 3527)), but I think that's due to something going on with Hashlink, not specific to this macro.

On another note, have you thought about submitting this to haxelib? this is the best signal implementation i’ve found for haxe, but i was only able to find it because someone (Gama11 😄) linked it to me.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment