-
-
Save nadako/b086569b9fffb759a1b5 to your computer and use it in GitHub Desktop.
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 |
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.
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.
Howdy, I just tried this out in a project and it works great! But I've run into an issue where the second time I compile my project with the language server, I get the error "Type name util.Signal1 is redefined from module util.Signal1".
Looks like this issue is currently affecting other folks (HaxeFoundation/haxe#8368), but I was wondering if you've seen the issue when using this Signal implementation and had any tips?
Edit: Looks like this only occurs when I have the file open (in VS Code) that actually defines the Signal. For example, in my file
Components.hx
, I define a variable with the type ofSignal<Component>
. When I compile with that file open, it errors. When I close that file, it builds just fine. Weird!