Last active
May 26, 2017 19:39
-
-
Save RSDuck/e6969f0331d3bc9dd4fd6666e2bebad3 to your computer and use it in GitHub Desktop.
Nim event system
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import macros, tables, hashes, strutils, oids | |
type | |
VariantTypes* = enum | |
variInt, variFloat32, variString, variBool | |
Variant* = object | |
case variant: VariantTypes | |
of variInt: intVal: int | |
of variFloat32: float32Val: float32 | |
of variString: stringVal: string | |
of variBool: boolVal: bool | |
Listener = tuple | |
event: string | |
handler: proc(event: string, args: openArray[Variant]) | |
ListenerBranch = ref object | |
anyBranch: ListenerBranch | |
listeners: seq[Listener] | |
children: TableRef[Hash, ListenerBranch] | |
EventSystem* = ref object | |
listeners: TableRef[Oid, Listener] | |
listenerRoot: ListenerBranch | |
template variantConv(name: untyped, t: typed): untyped = | |
converter fromVariant*(self: Variant): t = | |
if likely(self.variant == `vari name`): | |
return self.`t Val` | |
raise ValueError.new | |
variantConv(Int, int) | |
variantConv(Float32, float32) | |
variantConv(String, string) | |
variantConv(Bool, bool) | |
template toVariantConv(name: untyped, t: typed): untyped = | |
converter toVariant*(orgin: t): Variant = | |
return Variant(variant: `vari name`, `t Val`: orgin) | |
toVariantConv(Int, int) | |
toVariantConv(Float32, float32) | |
toVariantConv(String, string) | |
toVariantConv(Bool, bool) | |
proc newEventSystem*(): EventSystem = | |
result = new(EventSystem) | |
result.listenerRoot = new(ListenerBranch) | |
#result.listeners = newTable[Oid, Listener]() | |
proc fire*(self: EventSystem, event: string, args: varargs[Variant, toVariant]) = | |
let | |
path = event.split('.') | |
proc work(branch: ListenerBranch, depth: int, args: varargs[Variant]) = | |
if depth == path.len: | |
for listener in branch.listeners: | |
listener.handler(event, args) | |
return | |
if branch.anyBranch != nil: | |
work(branch.anyBranch, depth + 1, args) | |
let child = branch.children.getOrDefault(hash(path[depth])) | |
if child != nil: | |
work(child, depth + 1, args) | |
work(self.listenerRoot, 0, args) | |
macro generateWrapperClosure(procedure: proc, selfParamType: typedesc): untyped = | |
let procSignature = procedure.getType() | |
var | |
params: seq[NimNode] = @[] | |
takesDirectArray = false | |
let | |
argSelfPos = (if selfParamType is bool: -1 else: 0) | |
argEventTypePos = argSelfPos + 1 | |
otherArgsBeginning = argEventTypePos + 1 | |
if argSelfPos != -1: | |
params.add(newIdentNode("handlerSelf")) | |
if procSignature[argEventTypePos + 2].kind == nnkSym and $procSignature[argEventTypePos + 2].symbol == "string": | |
params.add(newIdentNode("event")) | |
else: | |
error "The first parameter(after the optional self) has to be a string which will contain the fired event" | |
if procSignature[otherArgsBeginning + 2].kind == nnkBracketExpr and | |
$procSignature[otherArgsBeginning + 2][0].symbol == "openArray" and $procSignature[otherArgsBeginning + 2][1].symbol == "Variant": | |
if procSignature.len - 2 > 3 + argSelfPos: | |
error "Other arguments may not follow the Variant array" | |
params.add(newIdentNode("args")) | |
takesDirectArray = true | |
else: | |
for i in (otherArgsBeginning + 2)..(procSignature.len - 1): | |
params.add(newNimNode(nnkBracketExpr).add([newIdentNode("args"), newIntLitNode(i - (otherArgsBeginning + 2))])) | |
var body = newStmtList() | |
if not defined(release) and not takesDirectArray: | |
body.add(parseStmt("assert(args.len == " & $(procSignature.len - (4 + argSelfPos)) & ", \"The supplied amount of arguments aren't sufficient to call the procedure\")")) | |
body.add(newCall(procedure, params)) | |
result = newProc(params = [ | |
newEmptyNode(), | |
newIdentDefs(newIdentNode("event"), newIdentNode("string")), | |
newIdentDefs(newIdentNode("args"), newTree(nnkBracketExpr, newIdentNode("openArray"), newIdentNode("Variant"))) | |
], | |
body = body, | |
procType = nnkLambda | |
) | |
result[4] = newTree(nnkPragma, newIdentNode("closure")) | |
proc listenImpl(self: EventSystem, event: string, handler: proc, handlerSelf: ref or bool): Oid {.discardable.} = | |
var listener = self.listenerRoot | |
for path in event.split('.'): | |
if listener.children == nil: | |
listener.children = newTable[Hash, ListenerBranch]() | |
if path != "*": | |
let hashed = hash(path) | |
if not listener.children.hasKey(hashed): | |
listener.children[hashed] = new(ListenerBranch) | |
listener = listener.children[hashed] | |
else: | |
if listener.anyBranch == nil: | |
listener.anyBranch = new(ListenerBranch) | |
listener = listener.anyBranch | |
if listener.listeners == nil: | |
listener.listeners = @[] | |
listener.listeners.add((event, generateWrapperClosure(handler, type(handlerSelf)))) | |
proc listen*(self: EventSystem, event: string, handler: proc): Oid {.discardable.} = self.listenImpl(event, handler, false) | |
proc listen*(self: EventSystem, handlerSelf: ref, event: string, handler: proc): Oid {.discardable.} = self.listenImpl(event, handler, handlerSelf) | |
proc unlisten*(self: EventSystem) = | |
discard | |
when isMainModule: | |
from times import cpuTime | |
let | |
testSystem = newEventSystem() | |
var testEventTriggered = 0 | |
testSystem.listen("test") do (event: string, x: int): | |
echo "Test Event!, x: ", x | |
testEventTriggered = x | |
testSystem.listen(testSystem, "player.spawned") do (self: EventSystem, event: string, name: string): | |
echo name, " joined the game!" | |
self.fire("test", 3) | |
for c in "abcdefghijklmnop": | |
testSystem.listen("player.*.death" & c) do (event: string, args: openArray[Variant]): | |
echo "event" | |
discard | |
testSystem.listen("ui.*.click") do (event: string, args: openArray[Variant]): | |
echo "Someone clicked on something" | |
discard | |
if event == "ui.player.click": | |
discard | |
echo "To be precise, ", args[0].stringVal, " clicked on something" | |
testSystem.listen("ui.player.*") do (event: string, args: openArray[Variant]): | |
echo "Player UI event!" | |
discard | |
testSystem.listen("ui.*.doubleclick") do (event: string, args: openArray[Variant]): | |
echo "Double click!" | |
discard | |
proc dumpBranch(branch: ListenerBranch, depth: int) = | |
let ident = repeat('\t', depth) | |
echo ident, "Node:" | |
echo ident, "Listeners: " | |
if branch.listeners != nil: | |
for listener in branch.listeners: | |
echo ident, "\t* ", listener.event | |
else: echo ident, "\tNil" | |
echo ident, "Children: " | |
if branch.children != nil: | |
for child in branch.children.values: | |
dumpBranch(child, depth + 1) | |
else: echo ident, "\tNil" | |
#testSystem.listenerRoot.dumpBranch(0) | |
testSystem.fire("ui.player.click", "Mr. Fusio") | |
let startTime = cpuTime() | |
for i in 1..#[10_000 * (32 div 4)]#1: | |
testSystem.fire("player.spawned", "Mr. Fusio") | |
testSystem.fire("player.x.death", "Mr. Fusio") | |
testSystem.fire("ui.player.click", "Mr. Fusio") | |
testSystem.fire("ui.test.doubleclick", "Mr. Fusio") | |
let endTime = cpuTime() | |
echo "Duration: ", endTime - startTime | |
echo "Per function: ", ((endTime - startTime) / 320_000) * 1000, "ms" | |
#echo $$testSystem |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment