Skip to content

Instantly share code, notes, and snippets.

@RSDuck
Last active May 26, 2017 19:39
Show Gist options
  • Save RSDuck/e6969f0331d3bc9dd4fd6666e2bebad3 to your computer and use it in GitHub Desktop.
Save RSDuck/e6969f0331d3bc9dd4fd6666e2bebad3 to your computer and use it in GitHub Desktop.
Nim event system
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