├── tests ├── tsan.ignore ├── config.nims ├── experEffects.md ├── tslotsFwdDecl.nim ├── tmultiexample.nim ├── tclosures.nim ├── experEffects.nim ├── torc_badness.nim ├── tslotsGeneric.nim ├── tslotsThreadQueued.nim ├── tbenchmarks2.nim ├── tbenchmarks.nim ├── tisolateutils.nim ├── tslotsThreadAsync.nim ├── tweakrefs.nim ├── tmultiThreads.nim ├── tslotsThreadSelectors.nim ├── tslots.nim └── treactiveSigil.nim ├── .gitignore ├── sigils.nimble ├── sigils.nim ├── sigils ├── threads.nim ├── isolateutils.nim ├── svariant.nim ├── core.nim ├── threadDefault.nim ├── weakrefs.nim ├── closures.nim ├── registry.nim ├── protocol.nim ├── signals.nim ├── threadAsyncs.nim ├── reactive.nim ├── threadSelectors.nim ├── slots.nim ├── threadBase.nim ├── agents.nim └── threadProxies.nim ├── CHANGES.md ├── config.nims ├── .github └── workflows │ ├── build-full.yml │ └── build-pr.yml ├── LICENSE ├── AGENTS.md ├── README.md └── docs └── threading.md /tests/tsan.ignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !*/ 3 | !*.* 4 | nim.cfg 5 | vendor/*/ 6 | nimcache 7 | *.out 8 | 9 | .tool-versions 10 | *.dSYM/ 11 | /*.png 12 | nimble.develop 13 | nimble.paths 14 | .nimcache 15 | deps/ 16 | .nimcache 17 | -------------------------------------------------------------------------------- /sigils.nimble: -------------------------------------------------------------------------------- 1 | version = "0.18.2" 2 | author = "Jaremy Creechley" 3 | description = "A slot and signals implementation for the Nim programming language" 4 | license = "MIT" 5 | srcDir = "." 6 | 7 | requires "nim >= 2.0.2" 8 | requires "variant >= 0.2.12" 9 | requires "threading >= 0.2.1" 10 | requires "stack_strings" 11 | 12 | feature "cbor": 13 | requires "cborious" 14 | -------------------------------------------------------------------------------- /sigils.nim: -------------------------------------------------------------------------------- 1 | import sigils/weakrefs 2 | import sigils/agents 3 | import sigils/signals 4 | import sigils/slots 5 | import sigils/core 6 | import sigils/threads 7 | 8 | export weakrefs, agents, signals, slots, threads, core 9 | 10 | when not defined(gcArc) and not defined(gcOrc) and not defined(gcAtomicArc) and not defined(nimdoc): 11 | {.error: "Sigils requires --gc:arc, --gc:orc, or --gc:atomicArc".} 12 | -------------------------------------------------------------------------------- /sigils/threads.nim: -------------------------------------------------------------------------------- 1 | import std/sets 2 | import std/isolation 3 | import std/options 4 | import std/locks 5 | import threading/smartptrs 6 | import threading/channels 7 | 8 | import isolateutils 9 | import agents 10 | import core 11 | import threadBase 12 | import threadDefault 13 | import threadProxies 14 | import threadAsyncs 15 | 16 | export isolateutils 17 | export threadBase 18 | export threadDefault 19 | export threadProxies 20 | # export threadAsyncs 21 | -------------------------------------------------------------------------------- /tests/config.nims: -------------------------------------------------------------------------------- 1 | --path: 2 | "../" 3 | 4 | --gc: 5 | arc 6 | --threads: 7 | on 8 | --d: 9 | useMalloc 10 | 11 | #--debuginfo: 12 | # on 13 | --debugger: 14 | native 15 | 16 | #--d: 17 | # sigilsDebug 18 | 19 | --passc: 20 | "-Wno-int-conversion" 21 | 22 | when defined(tsan): 23 | --debugger: 24 | native 25 | --passc: 26 | "-fsanitize=thread -fno-omit-frame-pointer -mno-omit-leaf-frame-pointer" 27 | --passl: 28 | "-fsanitize=thread -fno-omit-frame-pointer -mno-omit-leaf-frame-pointer" 29 | --passc: 30 | "-fsanitize-blacklist=tests/tsan.ignore" 31 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | - `v0.9.7` - add `disconnect` for slots (only for non-threaded agents currently) 2 | - `v0.10.0` - fix disconnect errors 3 | - `v0.10.1` - fix Agent destruction with circular subscriptions 4 | - `v0.11.0` - added `Sigil[T]` reactive data type built ontop signals 5 | - `v0.11.1` - change Sigil impl to use internalSigil when using `{}` 6 | - `v0.11.2` - change Sigil floats to use `near` 7 | - `v0.12.1` - refine forward-declared slot support; add test `tests/tslotsFwdDecl.nim`, format touched files, and bump version. 8 | - `v0.12.0` - support forward-declared slots: allow `{.slot.}` on body-less proc declarations (pre-declare slot types) by skipping wrapper generation for forward decls; adds `tests/tslotsFwdDecl.nim`. Also includes Nim 2.2 closure compatibility note. 9 | -------------------------------------------------------------------------------- /config.nims: -------------------------------------------------------------------------------- 1 | # NimScript configuration and tasks for this repo 2 | switch("nimcache", ".nimcache") 3 | 4 | import std/[os, strformat, strutils] 5 | 6 | const testDir = "tests" 7 | 8 | task test, "Compile and run all tests in tests/": 9 | withDir(testDir): 10 | for kind, path in walkDir("."): 11 | if kind == pcFile and path.endsWith(".nim") and not path.endsWith("config.nims"): 12 | let name = splitFile(path).name 13 | if not name.startsWith("t"): continue # run only t*.nim files 14 | echo fmt"[sigils] Running {path}" 15 | exec fmt"nim c -r {path}" 16 | 17 | task testTsan, "Compile and run all tests in tests/": 18 | putEnv("TSAN_OPTIONS", "suppressions=tests/tsan.ignore") 19 | exec fmt"nim c -r tests/tmultiThreads.nim" 20 | 21 | -------------------------------------------------------------------------------- /.github/workflows/build-full.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - "main" 5 | 6 | jobs: 7 | tests: 8 | runs-on: ${{ matrix.os }} 9 | strategy: 10 | matrix: 11 | nimversion: 12 | - '2.2.6' 13 | os: 14 | - ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v1 17 | 18 | - uses: iffy/install-nim@v4 19 | with: 20 | version: ${{ matrix.nimversion }} 21 | env: 22 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 23 | 24 | - name: Cache packages 25 | uses: actions/cache@v3 26 | with: 27 | path: ~/.nimble 28 | key: ${{ runner.os }}-${{ hashFiles('sigils.nimble') }} 29 | 30 | - name: Install Nimble 31 | run: | 32 | # nimble install nimble@\#master 33 | echo "Nim:: " 34 | nim -v 35 | echo "Nimble:: " 36 | nimble -v 37 | 38 | - name: Install Deps 39 | run: | 40 | # sync deps 41 | nimble install --useSystemNim -d --verbose 42 | 43 | - name: Build Tests 44 | run: | 45 | nimble --useSystemNim test 46 | 47 | -------------------------------------------------------------------------------- /.github/workflows/build-pr.yml: -------------------------------------------------------------------------------- 1 | on: 2 | pull_request: 3 | branches: 4 | - "*" 5 | 6 | jobs: 7 | tests: 8 | runs-on: ${{ matrix.os }} 9 | strategy: 10 | matrix: 11 | nimversion: 12 | - '2.2.6' 13 | os: 14 | - ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v1 17 | 18 | - uses: iffy/install-nim@v4 19 | with: 20 | version: ${{ matrix.nimversion }} 21 | env: 22 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 23 | 24 | - name: Cache packages 25 | uses: actions/cache@v3 26 | with: 27 | path: ~/.nimble 28 | key: ${{ runner.os }}-${{ hashFiles('sigils.nimble') }} 29 | 30 | - name: Install Nimble 31 | run: | 32 | # nimble install nimble@\#master 33 | echo "Nim:: " 34 | nim -v 35 | echo "Nimble:: " 36 | nimble -v 37 | 38 | - name: Install Deps 39 | run: | 40 | # new atlas workspace 41 | nimble install --useSystemNim -d --verbose 42 | 43 | - name: Build Tests 44 | run: | 45 | nimble --useSystemNim test 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Jaremy Creechley 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /tests/experEffects.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Proposal: Effect Handlers in Nim 4 | 5 | Algebraic effects have been used successfully in a few different languages. 6 | Notably OCaml's multi-core primitives are built on it. 7 | 8 | Functional languages tend to wrap things up in talk of `monads` and such. 9 | However, algebraic effects can also be conceptualized as resumable exceptions 10 | (see [MSFT-Leijen2017](https://www.microsoft.com/en-us/research/wp-content/uploads/2017/06/algeff-in-c-tr-v2.pdf)). 11 | It's in this sense that Nim could adopt algebraic effects into it's existing effects system. 12 | 13 | Ideally the effect handlers would be transformed by the compiler after macro and tempplate expansions 14 | and would be used for lower level core constructs like `async`, `exceptions`, and `allocations`. 15 | 16 | ## Description 17 | 18 | Algebraic effects are very simple at their core and reduce down to: 19 | 20 | image 21 | 22 | [XieLeijen2023](https://www.microsoft.com/en-us/research/uploads/prod/2021/03/multip-tr-v4.pdf) 23 | 24 | ## Use Cases 25 | 26 | Algebraic effects are useful in a few different ways, but they can be used at compile time for useful transforms. 27 | 28 | ```nim 29 | type AllocationEffect* = object 30 | 31 | proc main*() = 32 | 33 | 34 | ``` 35 | -------------------------------------------------------------------------------- /tests/tslotsFwdDecl.nim: -------------------------------------------------------------------------------- 1 | import sigils/signals 2 | import sigils/slots 3 | import sigils/core 4 | import std/unittest 5 | 6 | type 7 | FwdA* = ref object of Agent 8 | value: int 9 | 10 | proc valueChanged*(tp: FwdA, val: int) {.signal.} 11 | 12 | # Forward declare slot without a body 13 | proc onChange*(self: FwdA, val: int) {.slot.} 14 | 15 | suite "forward-declared slots": 16 | test "predeclared slot type compiles and connects": 17 | var a = FwdA() 18 | connect(a, valueChanged, a, onChange) 19 | 20 | # Implement the slot later 21 | proc onChange*(self: FwdA, val: int) {.slot.} = 22 | self.value = val 23 | 24 | suite "forward-declared slots": 25 | test "predeclared slot type compiles and connects": 26 | var a = FwdA() 27 | connect(a, valueChanged, a, onChange) 28 | check SignalTypes.onChange(FwdA) is (int, ) 29 | check a.value == 0 30 | emit a.valueChanged(7) 31 | check a.value == 7 32 | 33 | block testFwdDecl: 34 | type 35 | AppBase = ref object of Agent 36 | 37 | # application: 38 | App = ref object of AppBase 39 | foo: int 40 | 41 | proc appEvent(self: AppBase) {.signal.} 42 | 43 | proc handling2(self: App) {.slot.} 44 | 45 | template replace(self: App, event: typed, handler: typed) = 46 | disconnect(self, event, self) 47 | connect(self, event, self, handler) 48 | 49 | proc handling1(self: App) {.slot.} = 50 | self.foo = 1 51 | replace(self, appEvent, handling2) 52 | 53 | proc handling2(self: App) {.slot.} = 54 | self.foo = 2 55 | replace(self, appEvent, handling1) 56 | 57 | when isMainModule: 58 | 59 | let a = App() 60 | connect(a, appEvent, a, handling1) 61 | 62 | emit a.appEvent() 63 | echo "event1: ",a.foo 64 | emit a.appEvent() 65 | echo "event2: ",a.foo 66 | 67 | emit a.appEvent() 68 | echo "event3: ",a.foo 69 | -------------------------------------------------------------------------------- /tests/tmultiexample.nim: -------------------------------------------------------------------------------- 1 | import sigils, sigils/threads 2 | 3 | type 4 | Trigger = ref object of Agent 5 | 6 | Worker = ref object of Agent 7 | value: int 8 | 9 | Collector = ref object of Agent 10 | a: int 11 | b: int 12 | 13 | proc valueChanged(tp: Trigger, val: int) {.signal.} 14 | proc updated(tp: Worker, final: int) {.signal.} 15 | 16 | proc setValue(self: Worker, value: int) {.slot.} = 17 | self.value = value 18 | echo "worker:setValue: ", value, " (th: ", getThreadId(), ")" 19 | emit self.updated(self.value) 20 | 21 | proc gotA(self: Collector, final: int) {.slot.} = 22 | echo "collector: gotA: ", final, " (th: ", getThreadId(), ")" 23 | self.a = final 24 | 25 | proc gotB(self: Collector, final: int) {.slot.} = 26 | echo "collector: gotB: ", final, " (th: ", getThreadId(), ")" 27 | self.b = final 28 | 29 | let trigger = Trigger() 30 | let collector = Collector() 31 | 32 | let threadA = newSigilThread() 33 | let threadB = newSigilThread() 34 | threadA.start() 35 | threadB.start() 36 | startLocalThreadDefault() 37 | 38 | var wA = Worker() 39 | var wB = Worker() 40 | 41 | let workerA: AgentProxy[Worker] = wA.moveToThread(threadA) 42 | let workerB: AgentProxy[Worker] = wB.moveToThread(threadB) 43 | 44 | connectThreaded(trigger, valueChanged, workerA, setValue) 45 | connectThreaded(trigger, valueChanged, workerB, setValue) 46 | connectThreaded(workerA, updated, collector, Collector.gotA()) 47 | connectThreaded(workerB, updated, collector, Collector.gotB()) 48 | 49 | let ct = getCurrentSigilThread() 50 | 51 | for n in [42, 137]: 52 | echo "N: ", n 53 | emit trigger.valueChanged(n) 54 | discard ct.poll() # workerA result 55 | discard ct.poll() # workerB result 56 | doAssert collector.a == n 57 | doAssert collector.b == n 58 | 59 | setRunning(threadA, false) 60 | setRunning(threadB, false) 61 | threadA.join() 62 | threadB.join() 63 | -------------------------------------------------------------------------------- /tests/tclosures.nim: -------------------------------------------------------------------------------- 1 | import sigils/signals 2 | import sigils/slots 3 | import sigils/core 4 | import sigils/closures 5 | 6 | import std/sugar 7 | 8 | type 9 | Counter* = ref object of Agent 10 | value: int 11 | avg: int 12 | 13 | Originator* = ref object of Agent 14 | 15 | proc valueChanged*(tp: Counter, val: int) {.signal.} 16 | 17 | proc setValue*(self: Counter, value: int) {.slot.} = 18 | echo "setValue! ", value 19 | if self.value != value: 20 | self.value = value 21 | emit self.valueChanged(value) 22 | 23 | import unittest 24 | 25 | suite "agent closure slots": 26 | test "callback manual creation": 27 | type ClosureRunner[T] = ref object of Agent 28 | rawEnv: pointer 29 | rawProc: pointer 30 | 31 | proc callClosure[T](self: ClosureRunner[T], value: int) {.slot.} = 32 | echo "calling closure" 33 | if self.rawEnv.isNil(): 34 | let c2 = cast[T](self.rawProc) 35 | c2(value) 36 | else: 37 | let c3 = cast[proc(a: int, env: pointer) {.nimcall.}](self.rawProc) 38 | c3(value, self.rawEnv) 39 | 40 | var 41 | a {.used.} = Counter.new() 42 | base = 100 43 | 44 | let 45 | c1: proc(a: int) {.closure.} = proc(a: int) {.closure.} = 46 | base = a 47 | e = c1.rawEnv() 48 | p = c1.rawProc() 49 | cc = ClosureRunner[proc(a: int) {.nimcall.}](rawEnv: e, rawProc: p) 50 | connect(a, valueChanged, cc, ClosureRunner[proc(a: int) {.nimcall.}].callClosure) 51 | 52 | a.setValue(42) 53 | 54 | check a.value == 42 55 | check base == 42 56 | 57 | test "callback creation": 58 | var 59 | a = Counter() 60 | b = Counter(value: 100) 61 | 62 | let clsAgent = connectTo(a, valueChanged) do(val: int): 63 | b.value = val 64 | 65 | check not compiles( 66 | connectTo(a, valueChanged) do(val: float): 67 | b.value = val 68 | ) 69 | 70 | echo "cc3: Type: ", $typeof(clsAgent) 71 | emit a.valueChanged(42) 72 | check b.value == 42 73 | check clsAgent.typeof() is ClosureAgent[(int,)] 74 | -------------------------------------------------------------------------------- /sigils/isolateutils.nim: -------------------------------------------------------------------------------- 1 | import std/strformat 2 | import std/isolation 3 | import threading/smartptrs 4 | 5 | import weakrefs 6 | export isolation 7 | 8 | proc checkThreadSafety[T, V](field: T, parent: V) = 9 | when T is ref: 10 | if not checkThreadSafety(v, sig): 11 | {. 12 | error: 13 | "Signal type with ref's aren't thread safe! Signal type: " & $(typeof(sig)) & 14 | ". Use `Isolate[" & $(typeof(v)) & "]` to use it." 15 | .} 16 | elif T is tuple or T is object: 17 | {.hint: "checkThreadSafety: object: " & $(T).} 18 | for n, v in field.fieldPairs(): 19 | checkThreadSafety(v, parent) 20 | else: 21 | {.hint: "checkThreadSafety: skip: " & $(T).} 22 | 23 | template checkSignalThreadSafety*(sig: typed) = 24 | checkThreadSafety(sig, sig) 25 | 26 | type IsolationError* = object of CatchableError 27 | 28 | import std/macros 29 | 30 | import std/private/syslocks 31 | proc verifyUniqueSkip(tp: typedesc[SysLock]) = 32 | discard 33 | 34 | proc verifyUnique[T, V](field: T, parent: V) = 35 | when T is ref: 36 | if not field.isNil: 37 | if not field.isUniqueRef(): 38 | raise newException( 39 | IsolationError, 40 | &"reference not unique! Cannot safely isolate {$typeof(field)} parent: {$typeof(parent)} ", 41 | ) 42 | for v in field[].fields(): 43 | verifyUnique(v, parent) 44 | elif T is tuple or T is object: 45 | when compiles(verifyUniqueSkip(T)): 46 | discard 47 | else: 48 | for n, v in field.fieldPairs(): 49 | verifyUnique(v, parent) 50 | else: 51 | discard 52 | 53 | proc isolateRuntime*[T](item: sink T): Isolated[T] {.raises: [IsolationError].} = 54 | ## Isolates a ref type or type with ref's and ensure that 55 | ## each ref is unique. This allows safely isolating it. 56 | when compiles(isolate(item)): 57 | result = isolate(item) 58 | else: 59 | verifyUnique(item, item) 60 | result = unsafeIsolate(item) 61 | 62 | proc isolateRuntime*[T](item: SharedPtr[T]): Isolated[SharedPtr[T]] = 63 | var item = item 64 | unsafeIsolate(move item) 65 | -------------------------------------------------------------------------------- /sigils/svariant.nim: -------------------------------------------------------------------------------- 1 | 2 | import std/streams 3 | include variant 4 | 5 | type 6 | VBuffer* = object 7 | buff*: string 8 | WBuffer*[T] = VBuffer 9 | 10 | WVariant* = Variant 11 | 12 | proc asPtr*[T](wt: WBuffer[T]): ptr T = 13 | static: assert sizeof(T) > 0 14 | cast[ptr T](addr(wt.buff[0])) 15 | 16 | proc duplicate*(v: WVariant): WVariant = 17 | result = VariantConcrete[VBuffer]() 18 | result.typeId = v.typeId 19 | when defined(variantDebugTypes): 20 | result.mangledName = v.mangledName 21 | cast[VariantConcrete[VBuffer]](result).val.buff = 22 | cast[VariantConcrete[VBuffer]](v).val.buff 23 | 24 | proc initWrapper*[T](val: sink T): WBuffer[T] = 25 | let sz = sizeof(val) 26 | result.buff.setLen(sz) 27 | result.asPtr()[] = move val 28 | 29 | proc newWrapperVariant*[T](val: sink T): WVariant = 30 | newVariant(initWrapper(val)) 31 | 32 | proc getWrapped*(v: Variant, T: typedesc): T = 33 | cast[VariantConcrete[WBuffer[T]]](v).val.asPtr()[] 34 | 35 | proc resetTo*[T](v: WVariant, val: T) = 36 | let sz = sizeof(val) 37 | v.typeId = getTypeId(WBuffer[T]) 38 | when defined(variantDebugTypes): 39 | v.mangledName = getMangledName(T) 40 | 41 | if cast[VariantConcrete[VBuffer]](v).val.buff.len() < sz: 42 | cast[VariantConcrete[VBuffer]](v).val.buff.setLen(sz) 43 | cast[VariantConcrete[WBuffer[T]]](v).val.asPtr()[] = val 44 | 45 | when isMainModule: 46 | 47 | import std/unittest 48 | 49 | test "basic": 50 | var x: int16 = 7 51 | echo "x: ", x 52 | 53 | let vx = newVariant(initWrapper(x)) 54 | echo "=> vx: ", vx.getWrapped(int16) 55 | check x == vx.getWrapped(int16) 56 | 57 | var y: array[1024, int] 58 | y[0] = 0xAA 59 | y[^1] = 0xFF 60 | echo "y: ", y[0] 61 | 62 | vx.resetTo(y) 63 | echo "=> vy: ", vx.getWrapped(array[1024, int]) 64 | 65 | var z: int = 16 66 | echo "z: ", z 67 | 68 | vx.resetTo(z) 69 | echo "=> vz: ", vx.getWrapped(int) 70 | 71 | test "wrapper": 72 | var x: int16 = 7 73 | echo "x: ", x 74 | 75 | let vx = newWrapperVariant(x) 76 | echo "=> vx: ", vx.getWrapped(int16) 77 | check x == vx.getWrapped(int16) 78 | 79 | -------------------------------------------------------------------------------- /sigils/core.nim: -------------------------------------------------------------------------------- 1 | import signals 2 | import slots 3 | import agents 4 | 5 | when defined(sigilsDebug): 6 | from system/ansi_c import c_raise 7 | 8 | export signals, slots, agents 9 | 10 | method callMethod*( 11 | ctx: Agent, req: SigilRequest, slot: AgentProc 12 | ): SigilResponse {.base, gcsafe, effectsOf: slot.} = 13 | ## Route's an rpc request. 14 | debugPrint "callMethod: normal: ", 15 | $ctx.unsafeWeakRef().asAgent(), 16 | " slot: ", 17 | repr(slot) 18 | 19 | if slot.isNil: 20 | let msg = $req.procName & " is not a registered RPC method." 21 | let err = SigilError(code: METHOD_NOT_FOUND, msg: msg) 22 | result = wrapResponseError(req.origin, err) 23 | else: 24 | slot(ctx, req.params) 25 | let res = rpcPack(true) 26 | 27 | result = SigilResponse(kind: Response, id: req.origin.int, result: res) 28 | 29 | from system/ansi_c import c_raise 30 | 31 | type AgentSlotError* = object of CatchableError 32 | 33 | proc callSlots*(obj: Agent | WeakRef[Agent], req: SigilRequest) {.gcsafe.} = 34 | {.cast(gcsafe).}: 35 | for sub in obj.getSubscriptions(req.procName): 36 | when defined(sigilsDebug): 37 | if sub.tgt[].freedByThread != 0: 38 | debugPrint "exec:call:thread: ", $getThreadId() 39 | debugPrint "exec:call:sub.tgt[].freed:thread: ", $sub.tgt[].freedByThread 40 | debugPrint "exec:call:sub.tgt[]:id: ", $sub.tgt[].getSigilId() 41 | debugPrint "exec:call:sub.req: ", req.repr 42 | debugPrint "exec:call:obj:id: ", $obj.getSigilId() 43 | discard c_raise(11.cint) 44 | assert sub.tgt[].freedByThread == 0 45 | var res: SigilResponse = sub.tgt[].callMethod(req, sub.slot) 46 | 47 | when defined(nimscript) or defined(useJsonSerde): 48 | discard 49 | elif defined(sigilsCborSerde): 50 | discard 51 | else: 52 | discard 53 | variantMatch case res.result.buf as u 54 | of SigilError: 55 | raise newException(AgentSlotError, $u.code & " msg: " & u.msg) 56 | else: 57 | discard 58 | 59 | proc emit*(call: (Agent | WeakRef[Agent], SigilRequest)) = 60 | let (obj, req) = call 61 | callSlots(obj, req) 62 | -------------------------------------------------------------------------------- /tests/experEffects.nim: -------------------------------------------------------------------------------- 1 | type OpKinds* {.pure.} = enum 2 | opNull 3 | opNoResume #// never resumes 4 | opTail #// only uses `resume` in tail-call position 5 | opScoped #// only uses `resume` inside the handler 6 | opGeneral #// `resume` is a first-class value 7 | 8 | # const char* effect_exn[3] = {"exn","exn_raise",NULL}; 9 | # const optag optag_exn_raise = { effect_exn, 1 }; 10 | 11 | type 12 | Resume* = object 13 | OpTag* = object 14 | name: cstring 15 | op: cstring 16 | 17 | Continuation* = proc(r: ptr Resume, local, value: pointer): pointer 18 | Operation* = object 19 | kind: OpKinds 20 | tag: OpTag 21 | fn: Continuation 22 | 23 | FooObj* = object 24 | value*: int 25 | 26 | Foo* = ref FooObj 27 | 28 | HandlerDef = object 29 | 30 | Handler* = object 31 | entry: pointer #// used to jump back to a handler 32 | hdef: ptr HandlerDef #// operation definitions 33 | arg: pointer #// the operation argument is passed here 34 | arg_op: ptr Operation #// the yielded operation is passed here 35 | arg_resume: ptr Resume #// the resumption function 36 | stackbase: pointer #// stack frame address of the handler function 37 | 38 | proc opTag(name, op: static string): OpTag = 39 | OpTag(name: name, op: op) 40 | 41 | proc handle_exn_raise*(r: ptr Resume, local, arg: pointer): pointer = 42 | echo("exception raised: ", $cast[cstring](arg)) 43 | return nil 44 | 45 | const exn_ops = 46 | [Operation(kind: opNoResume, tag: opTag("exn", "raise"), fn: handle_exn_raise)] 47 | 48 | # const exn_def: handlerdef = { EFFECT(exn), NULL, NULL, NULL, _exn_ops }; 49 | # proc my_exn_handle(action: proc (arg: pointer): pointer, arg: pointer): pointer = 50 | # return handle(addr exn_def, nil, action, arg) 51 | type 52 | Cont* = object 53 | Allocation*[T] = object 54 | 55 | proc new*[T](obj: var T) {.tags: [Allocation[T]].} = 56 | discard 57 | 58 | proc newFoo*(value: int): Foo = 59 | result.new() 60 | result.value = value 61 | 62 | template withEffects(blk, handles: untyped) = 63 | block: 64 | blk 65 | 66 | proc main*() = 67 | # normal allocation 68 | let f = newFoo(23) 69 | echo "f: ", f.value, " at 0x", cast[pointer](f).repr 70 | 71 | withEffects: 72 | let f = newFoo(23) 73 | echo "f: ", f.value, " at 0x", cast[pointer](f).repr 74 | except Allocation[T] as (r: ptr Cont, local, arg: var T): 75 | echo "allocation: " 76 | 77 | main() 78 | -------------------------------------------------------------------------------- /AGENTS.md: -------------------------------------------------------------------------------- 1 | # Repository Guidelines 2 | 3 | ## Project Structure & Modules 4 | - `sigils/`: Core library modules (e.g., `agents.nim`, `signals.nim`, `threads.nim`). Public entry is `sigils.nim` which re-exports key modules. 5 | - `tests/`: Unit tests using Nim's `unittest` plus a `config.nims` that enables ARC/threads and debug flags. 6 | - Root files: `sigils.nimble` (package manifest), `README.md` (usage), `CHANGES.md` (history). 7 | 8 | ## Build, Test, and Development 9 | - Install deps (atlas workspace): `atlas install` (ensure `atlas` is installed and configured for your environment). 10 | - Run all tests: `nim test` (uses the `test` task in `config.nims` to compile and run every `tests/*.nim`). 11 | - Run a single test locally: 12 | - `nim c -r tests/treactiveSigil.nim` 13 | - Helpful flags: `-d:sigilsDebug` for verbose names; define `tsan` to enable ThreadSanitizer per `tests/config.nims`. 14 | 15 | ## Coding Style & Naming 16 | - Indentation: 2 spaces; no tabs. 17 | - Nim style: Types in `PascalCase`, procs/vars in `camelCase`, modules in `lowercase` or concise `lowerCamel` (e.g., `threadAsyncs.nim`). 18 | - Prefer explicit exports via `export` in `sigils.nim` or module-level as needed. 19 | - Formatting: run `nimpretty --backup:off sigils/*.nim` and format any touched test files. 20 | 21 | ## Testing Guidelines 22 | - Framework: `unittest` with descriptive `suite` and `test` names. 23 | - Location: add new tests under `tests/`, mirroring module names (e.g., `tslots.nim` for `slots.nim`). 24 | - Run all: `nim test`. Run one: `nim c -r tests/tslots.nim`. 25 | - Concurrency: tests run with `--threads:on` (see `tests/config.nims`). Use `when defined(sigilsDebug)` to gate extra diagnostics. 26 | 27 | ## Commit & Pull Requests 28 | - Commits: short, imperative mood (e.g., "add isRunning"), optionally reference PR/issue like `(#21)`. 29 | - PRs: include a clear description, linked issues, summary of changes, any threading or GC considerations, and test coverage notes. Attach logs or minimal repros if fixing concurrency. 30 | - Requirements: CI (`nimble test`) must pass; include tests for new behavior and update `README.md`/`CHANGES.md` as needed. 31 | 32 | ## Security & Configuration Tips 33 | - GC: library requires ARC/ORC (`--gc:arc` or `--gc:orc`); enforced in `sigils.nim`. 34 | - Threads: prefer `AgentProxy` and provided helpers for cross-thread signaling; avoid manual ref cycles. Consider `-d:tsan` locally when touching threading code. 35 | -------------------------------------------------------------------------------- /tests/torc_badness.nim: -------------------------------------------------------------------------------- 1 | ## torc_badness.nim 2 | import std/tables 3 | import std/unittest 4 | 5 | type WeakRef*[T] {.acyclic.} = object 6 | pt* {.cursor.}: T # cursor effectively is just a pointer, also happens w/ pointer type 7 | 8 | template `[]`*[T](r: WeakRef[T]): lent T = 9 | ## using this in the destructor is fine because it's lent 10 | cast[T](r.pt) 11 | 12 | proc toRef*[T: ref](obj: WeakRef[T]): T = 13 | ## using this in the destructor breaks ORC 14 | result = cast[T](obj) 15 | 16 | type 17 | AgentObj = object of RootObj 18 | subcriptionsTable*: Table[int, WeakRef[Agent]] ## agents listening to me 19 | freedByThread*: int 20 | 21 | Agent* = ref object of AgentObj 22 | # Agent* {.acyclic.} = ref object of AgentObj ## this also avoids the issue 23 | 24 | # proc `=wasMoved`(agent: var AgentObj) = 25 | # echo "agent was moved" 26 | # agent.moved = true 27 | 28 | proc `=destroy`*(agentObj: AgentObj) = 29 | let xid: WeakRef[Agent] = WeakRef[Agent](pt: cast[Agent](addr agentObj)) 30 | ##\ 31 | ## This is pretty hacky, but we need to get the address of the original 32 | ## Agent (ref object) since it's used to unsubscribe from other agents in the actual code, 33 | ## Luckily the agent address is the same as `addr agent` of the agent object here. 34 | echo "Destroying agent: ", 35 | " pt: ", 36 | cast[pointer](xid.pt).repr, 37 | " freed: ", 38 | agentObj.freedByThread, 39 | " lstCnt: ", 40 | xid[].subcriptionsTable.len() 41 | if agentObj.freedByThread != 0: 42 | raise newException(Defect, "already freed!") 43 | 44 | xid[].freedByThread = getThreadId() 45 | 46 | ## remove subcriptionsTable via their WeakRef's 47 | ## this is where we create a problem 48 | ## by using `toRef` which creates a *new* Agent reference 49 | ## which gets added to ORC as a potential cycle check (?) 50 | ## adding `{.acyclic.}` to 51 | when defined(breakOrc): 52 | if xid.toRef().subcriptionsTable.len() > 0: 53 | echo "has subcriptionsTable" 54 | else: 55 | if xid[].subcriptionsTable.len() > 0: 56 | echo "has subcriptionsTable" 57 | 58 | `=destroy`(xid[].subcriptionsTable) 59 | echo "finished destroy: agent: ", " pt: 0x", cast[pointer](xid.pt).repr 60 | 61 | type Counter* = ref object of Agent 62 | value: int 63 | 64 | suite "threaded agent slots": 65 | test "sigil object thread runner": 66 | block: 67 | var b = Counter.new() 68 | 69 | GC_fullCollect() 70 | -------------------------------------------------------------------------------- /tests/tslotsGeneric.nim: -------------------------------------------------------------------------------- 1 | import sigils 2 | 3 | type 4 | Counter*[T] = ref object of Agent 5 | value: T 6 | avg: int 7 | Other*[T] = ref object of Agent 8 | value: T 9 | 10 | proc valueChanged*[T](tp: Counter[T], val: T) {.signal.} 11 | 12 | proc someChange*[T](tp: Counter[T]) {.signal.} 13 | 14 | proc avgChanged*[T](tp: Counter[T], val: float) {.signal.} 15 | 16 | proc setValue*[T](self: Counter[T], value: T) {.slot.} = 17 | echo "setValue! ", value 18 | if self.value != value: 19 | self.value = value 20 | emit self.valueChanged(value) 21 | 22 | proc setValue*[T](self: Other[T], value: T) {.slot.} = 23 | discard 24 | 25 | proc value*(self: Counter): int = 26 | self.value 27 | 28 | when isMainModule: 29 | import unittest 30 | import typetraits 31 | 32 | suite "agent slots": 33 | setup: 34 | var 35 | a {.used.} = Counter[uint].new() 36 | b {.used.} = Counter[uint].new() 37 | c {.used.} = Counter[uint].new() 38 | d {.used.} = Counter[uint].new() 39 | 40 | test "signal / slot types": 41 | check SignalTypes.avgChanged(Counter[uint]) is (float,) 42 | check SignalTypes.valueChanged(Counter[uint]) is (uint,) 43 | check SignalTypes.setValue(Counter[uint]) is (uint,) 44 | 45 | test "signal connect": 46 | connect(a, valueChanged, b, Counter[uint].setValue()) 47 | connect(a, valueChanged, c, Counter[uint].setValue()) 48 | check b.value == 0 49 | check c.value == 0 50 | check d.value == 0 51 | emit a.valueChanged(137) 52 | 53 | check a.value == 0 54 | check b.value == 137 55 | check c.value == 137 56 | check d.value == 0 57 | 58 | emit a.someChange() 59 | 60 | test "signal connect in generic proc": 61 | proc setup[T]() = 62 | connect(a, valueChanged, b, setValue) 63 | setup[uint]() 64 | 65 | test "signal connect": 66 | # TODO: how to do this? 67 | connect(a, valueChanged, b, setValue) 68 | connect(a, valueChanged, c, Counter[uint].setValue) 69 | # connect(a, valueChanged, c, Counter[float].setValue()) 70 | 71 | check a.value == 0 72 | check b.value == 0 73 | check c.value == 0 74 | 75 | # trigger emit in setValue so we set a's value and then trigger the rest 76 | a.setValue(42) 77 | check a.value == 42 78 | 79 | check b.value == 42 80 | check c.value == 42 81 | 82 | test "connect type errors": 83 | check not compiles(connect(a, avgChanged, c, setValue)) 84 | 85 | # connect(a, avgChanged, 86 | # c, Counter[uint].setValue) 87 | -------------------------------------------------------------------------------- /tests/tslotsThreadQueued.nim: -------------------------------------------------------------------------------- 1 | import std/unittest 2 | import std/os 3 | import threading/atomics 4 | 5 | import sigils 6 | import sigils/threads 7 | import sigils/threadAsyncs 8 | 9 | type 10 | SomeAction* = ref object of Agent 11 | value: int 12 | 13 | Counter* = ref object of Agent 14 | value: int 15 | 16 | proc valueChanged*(tp: SomeAction, val: int) {.signal.} 17 | 18 | var globalCounter: seq[int] 19 | 20 | proc setValueGlobal*(self: Counter, value: int) {.slot.} = 21 | if self.value != value: 22 | self.value = value 23 | globalCounter.add(value) 24 | 25 | proc timerRun*(self: Counter) {.slot.} = 26 | self.value.inc() 27 | echo "timerRun: ", self.value 28 | 29 | suite "connectQueued to local thread": 30 | test "queued connects a->b on local thread": 31 | globalCounter = @[] 32 | startLocalThreadDefault() 33 | var a = SomeAction() 34 | var b = Counter() 35 | 36 | block: 37 | connectQueued(a, valueChanged, b, setValueGlobal) 38 | 39 | emit a.valueChanged(314) 40 | emit a.valueChanged(139) 41 | emit a.valueChanged(278) 42 | 43 | # Drain the local thread scheduler to deliver the queued Call 44 | let ct = getCurrentSigilThread() 45 | 46 | let polled = ct.pollAll() 47 | check polled == 3 48 | check globalCounter == @[314, 139, 278] 49 | 50 | test "queued connects a->b on local thread": 51 | globalCounter = @[] 52 | startLocalThreadDefault() 53 | var a = SomeAction() 54 | var b = Counter() 55 | 56 | block: 57 | connectQueued(a, valueChanged, b, Counter.setValueGlobal()) 58 | 59 | emit a.valueChanged(139) 60 | emit a.valueChanged(314) 61 | emit a.valueChanged(278) 62 | 63 | # Drain the local thread scheduler to deliver the queued Call 64 | let ct = getCurrentSigilThread() 65 | 66 | let polled = ct.pollAll() 67 | check polled == 3 68 | check globalCounter == @[139, 314, 278] 69 | 70 | test "timer callback": 71 | setLocalSigilThread(newSigilAsyncThread()) 72 | let ct = getCurrentSigilThread() 73 | check ct of AsyncSigilThreadPtr 74 | 75 | var timer = newSigilTimer(duration=initDuration(milliseconds=2)) 76 | var a = Counter() 77 | 78 | connect(timer, timeout, a, Counter.timerRun()) 79 | 80 | start(timer) 81 | 82 | ct.poll(NonBlocking) 83 | check a.value == 0 84 | 85 | for i in 1 .. 10: 86 | ct.poll() 87 | check a.value == 10 88 | 89 | cancel(timer) 90 | ct.poll() 91 | 92 | test "timer callback": 93 | let ct = getCurrentSigilThread() 94 | check ct of AsyncSigilThreadPtr 95 | 96 | var timer = newSigilTimer(duration=initDuration(milliseconds=10), count=2) 97 | var a = Counter() 98 | 99 | connect(timer, timeout, a, Counter.timerRun()) 100 | 101 | start(timer) 102 | 103 | ct.poll() 104 | check a.value == 1 105 | ct.poll() 106 | check a.value == 2 107 | 108 | ct.poll() 109 | check a.value == 2 110 | -------------------------------------------------------------------------------- /sigils/threadDefault.nim: -------------------------------------------------------------------------------- 1 | import std/locks 2 | import std/options 3 | import threading/smartptrs 4 | import threading/channels 5 | import threading/atomics 6 | 7 | import isolateutils 8 | import agents 9 | import core 10 | import threadBase 11 | 12 | export isolateutils 13 | export threadBase 14 | 15 | type 16 | SigilThreadDefault* = object of SigilThread 17 | inputs*: SigilChan 18 | thr*: Thread[ptr SigilThreadDefault] 19 | 20 | SigilThreadDefaultPtr* = ptr SigilThreadDefault 21 | 22 | method send*( 23 | thread: SigilThreadDefaultPtr, msg: sink ThreadSignal, blocking: BlockingKinds 24 | ) {.gcsafe.} = 25 | var msg = isolateRuntime(msg) 26 | case blocking 27 | of Blocking: 28 | thread.inputs.send(msg) 29 | of NonBlocking: 30 | let sent = thread.inputs.trySend(msg) 31 | if not sent: 32 | raise newException(MessageQueueFullError, "could not send!") 33 | 34 | method recv*( 35 | thread: SigilThreadDefaultPtr, msg: var ThreadSignal, blocking: BlockingKinds 36 | ): bool {.gcsafe.} = 37 | case blocking 38 | of Blocking: 39 | msg = thread.inputs.recv() 40 | return true 41 | of NonBlocking: 42 | result = thread.inputs.tryRecv(msg) 43 | 44 | method setTimer*( 45 | thread: SigilThreadDefaultPtr, timer: SigilTimer 46 | ) {.gcsafe.} = 47 | raise newException(AssertionDefect, "not implemented for this thread type!") 48 | 49 | proc newSigilThread*(): ptr SigilThreadDefault = 50 | result = cast[ptr SigilThreadDefault](allocShared0(sizeof(SigilThreadDefault))) 51 | result[] = SigilThreadDefault() # important! 52 | result[].agent = ThreadAgent() 53 | result[].inputs = newSigilChan() 54 | result[].signaledLock.initLock() 55 | result[].threadId.store(-1, Relaxed) 56 | result[].running.store(true, Relaxed) 57 | 58 | proc startLocalThreadDefault*() = 59 | if not hasLocalSigilThread(): 60 | var st = newSigilThread() 61 | st[].threadId.store(getThreadId(), Relaxed) 62 | setLocalSigilThread(st) 63 | 64 | if getStartSigilThreadProc().isNil: 65 | setStartSigilThreadProc(startLocalThreadDefault) 66 | 67 | method poll*( 68 | thread: SigilThreadDefaultPtr, blocking: BlockingKinds = Blocking 69 | ): bool {.gcsafe, discardable.} = 70 | var sig: ThreadSignal 71 | case blocking 72 | of Blocking: 73 | discard thread.recv(sig, Blocking) 74 | thread.exec(sig) 75 | result = true 76 | of NonBlocking: 77 | if thread.recv(sig, NonBlocking): 78 | thread.exec(sig) 79 | result = true 80 | 81 | proc runThread*(thread: SigilThreadDefaultPtr) {.thread.} = 82 | {.cast(gcsafe).}: 83 | when defined(sigilsDebugPrint): 84 | pcnt.inc 85 | pidx = pcnt 86 | doAssert not hasLocalSigilThread() 87 | setLocalSigilThread(thread) 88 | thread[].threadId.store(getThreadId(), Relaxed) 89 | debugPrint "Sigil worker thread waiting!" 90 | thread.runForever() 91 | 92 | proc start*(thread: SigilThreadDefaultPtr) = 93 | if thread[].exceptionHandler.isNil: 94 | thread[].exceptionHandler = defaultExceptionHandler 95 | createThread(thread[].thr, runThread, thread) 96 | 97 | proc join*(thread: SigilThreadDefaultPtr) = 98 | doAssert not thread.isNil() 99 | thread[].thr.joinThread() 100 | 101 | proc peek*(thread: SigilThreadDefaultPtr): int = 102 | result = thread[].inputs.peek() 103 | -------------------------------------------------------------------------------- /sigils/weakrefs.nim: -------------------------------------------------------------------------------- 1 | import std/[hashes, isolation] 2 | 3 | type DestructorUnsafe* = object ## input/output effect 4 | 5 | type WeakRef*[T] {.acyclic.} = object 6 | ## type alias descring a weak ref that *must* be cleaned up 7 | ## when it's actual object is set to be destroyed 8 | when defined(sigilsWeakRefPointer): 9 | pt*: pointer 10 | else: 11 | pt* {.cursor.}: T 12 | 13 | proc `=destroy`*[T](obj: WeakRef[T]) = 14 | discard 15 | 16 | proc `=copy`*[T](dst: var WeakRef[T], src: WeakRef[T]) = 17 | dst.pt = src.pt 18 | 19 | proc `==`*[T](x, y: WeakRef[T]): bool = 20 | x.pt == y.pt 21 | 22 | proc `[]`*[T](r: WeakRef[T]): lent T {.inline.} = 23 | when defined(sigilsWeakRefPointer): 24 | cast[T](r.pt) 25 | else: 26 | r.pt 27 | 28 | template isNil*[T](r: WeakRef[T]): bool = 29 | r.pt == nil 30 | 31 | proc unsafeWeakRef*[T: ref](obj: T): WeakRef[T] = 32 | when defined(sigilsWeakRefPointer): 33 | result = WeakRef[T](pt: cast[pointer](obj)) 34 | else: 35 | let pt: WeakRef[pointer] = WeakRef[pointer](pt: cast[pointer](obj)) 36 | result = cast[WeakRef[T]](pt) 37 | 38 | proc unsafeWeakRef*[T](obj: ptr T): WeakRef[T] = 39 | when defined(sigilsWeakRefPointer): 40 | result = WeakRef[T](pt: cast[pointer](obj)) 41 | else: 42 | let pt: WeakRef[pointer] = WeakRef[pointer](pt: cast[pointer](obj)) 43 | result = cast[WeakRef[T]](pt) 44 | 45 | proc unsafeWeakRef*[T](obj: WeakRef[T]): WeakRef[T] = 46 | result = obj 47 | 48 | proc verifyUniqueSkip*(tp: typedesc[WeakRef]) = 49 | discard 50 | 51 | proc toPtr*[T](obj: WeakRef[T]): pointer = 52 | result = cast[pointer](obj.pt) 53 | 54 | proc toKind*[T, U](obj: WeakRef[T], tp: typedesc[U]): WeakRef[U] = 55 | cast[WeakRef[U]](obj) 56 | 57 | proc hash*[T](obj: WeakRef[T]): Hash = 58 | result = hash cast[pointer](obj.pt) 59 | 60 | template withRef*[T: ref](obj: WeakRef[T], name, blk: untyped) = 61 | block: 62 | var `name` {.inject.} = obj[] 63 | `blk` 64 | 65 | template withRef*[T: ref](obj: T, name, blk: untyped) = 66 | block: 67 | var `name` {.inject.} = obj 68 | `blk` 69 | 70 | proc isolate*[T](obj: WeakRef[T]): Isolated[WeakRef[T]] = 71 | result = unsafeIsolate(obj) 72 | 73 | proc `$`*[T](obj: WeakRef[T]): string = 74 | result = "Weak[" & $(T) & "]" 75 | result &= "(0x" 76 | result &= obj.toPtr().repr 77 | result &= ")" 78 | 79 | when true: 80 | when defined(gcOrc): 81 | const 82 | rcMask = 0b1111 83 | rcShift = 4 # shift by rcShift to get the reference counter 84 | else: 85 | const 86 | rcMask = 0b111 87 | rcShift = 3 # shift by rcShift to get the reference counter 88 | 89 | type 90 | RefHeader = object 91 | rc: int 92 | when defined(gcOrc): 93 | rootIdx: int 94 | # thanks to this we can delete potential cycle roots 95 | # in O(1) without doubly linked lists 96 | 97 | Cell = ptr RefHeader 98 | 99 | template head[T](p: ref T): Cell = 100 | cast[Cell](cast[int](cast[pointer](p)) -% sizeof(RefHeader)) 101 | 102 | template count(x: Cell): int = 103 | (x.rc shr rcShift) 104 | 105 | proc unsafeGcCount*[T](x: ref T): int = 106 | ## get the current gc count for ARC or ORC 107 | ## unsafe! Only intended for testing purposes! 108 | ## use `isUniqueRef` if you want to check a ref is unique 109 | if x.isNil: 110 | 0 111 | else: 112 | x.head().count() + 1 # count of 0 means 1 ref, -1 is 0 113 | -------------------------------------------------------------------------------- /sigils/closures.nim: -------------------------------------------------------------------------------- 1 | import signals 2 | import slots 3 | import agents 4 | import std/macros 5 | 6 | export signals, slots, agents 7 | 8 | type ClosureAgent*[T] = ref object of Agent 9 | rawEnv: pointer 10 | rawProc: pointer 11 | 12 | macro closureTyp(blk: typed) = 13 | ## figure out the signal type from the lambda and the function sig 14 | var 15 | signalTyp = nnkTupleConstr.newTree() 16 | blk = blk.copyNimTree() 17 | params = blk.params 18 | for i in 1 ..< params.len: 19 | signalTyp.add params[i][1] 20 | let 21 | fnSig = ident("fnSig") 22 | fnInst = ident("fnInst") 23 | fnTyp = getTypeImpl(blk) 24 | 25 | result = quote: 26 | var `fnSig`: `signalTyp` 27 | var `fnInst`: `fnTyp` = `blk` 28 | 29 | macro closureSlotImpl(fnSig, fnInst: typed) = 30 | ## figure out the slot implementation for the lambda type 31 | var 32 | blk = fnInst.getTypeImpl().copyNimTree() 33 | params = blk.params 34 | let 35 | fnSlot = ident("fnSlot") 36 | paramsIdent = ident("args") 37 | c1 = ident"c1" 38 | c2 = ident"c2" 39 | env = ident"rawEnv" 40 | 41 | # setup call without env pointer 42 | var 43 | fnSigCall1 = quote: 44 | proc() {.nimcall.} 45 | fnCall1 = nnkCall.newTree(c1) 46 | for idx, param in params[1 ..^ 1]: 47 | fnSigCall1.params.add(newIdentDefs(ident("a" & $idx), param[1])) 48 | let i = newLit(idx) 49 | fnCall1.add quote do: 50 | `paramsIdent`[`i`] 51 | 52 | # setup call with env pointer 53 | var 54 | fnSigCall2 = fnSigCall1.copyNimTree() 55 | fnCall2 = fnCall1.copyNimTree() 56 | 57 | fnSigCall2.params.add(newIdentDefs(env, ident("pointer"))) 58 | fnCall2[0] = c2 59 | fnCall2.add(env) 60 | 61 | result = quote: 62 | let `fnSlot`: AgentProc = proc(context: Agent, params: SigilParams) {.nimcall.} = 63 | let self = ClosureAgent[`fnSig`](context) 64 | if self == nil: 65 | raise newException(ConversionError, "bad cast") 66 | if context == nil: 67 | raise newException(ValueError, "bad value") 68 | var `paramsIdent`: `fnSig` 69 | rpcUnpack(`paramsIdent`, params) 70 | let rawProc: pointer = self.rawProc 71 | let `env`: pointer = self.rawEnv 72 | if `env`.isNil(): 73 | let `c1` = cast[`fnSigCall1`](rawProc) 74 | `fnCall1` 75 | else: 76 | let `c2` = cast[`fnSigCall2`](rawProc) 77 | `fnCall2` 78 | 79 | template connectTo*(a: Agent, signal: typed, blk: typed): ClosureAgent = 80 | ## creates an anonymous agent and slot that calls the given closure 81 | ## when the `signal` event is emitted. 82 | 83 | closureTyp(blk) 84 | 85 | var signalType {.used, inject.}: typeof(SignalTypes.`signal`(typeof(a))) 86 | var slotType {.used, inject.}: typeof(fnSig) 87 | 88 | when compiles(signalType = slotType): 89 | discard # don't need compile check when compiles 90 | else: 91 | signalType = slotType # let the compiler show the type mismatches 92 | 93 | closureSlotImpl(typeof(fnSig), fnInst) 94 | 95 | let 96 | e = 97 | when compiles(rawEnv(fnInst)): 98 | fnInst.rawEnv() 99 | else: 100 | pointer(nil) 101 | p = 102 | when compiles(rawProc(fnInst)): 103 | fnInst.rawProc() 104 | else: 105 | cast[pointer](fnInst) 106 | let agent = ClosureAgent[typeof(signalType)](rawEnv: e, rawProc: p) 107 | a.addSubscription(signalName(signal), agent, fnSlot) 108 | 109 | agent 110 | -------------------------------------------------------------------------------- /tests/tbenchmarks2.nim: -------------------------------------------------------------------------------- 1 | import std/monotimes 2 | import std/strformat 3 | import std/math 4 | import unittest 5 | 6 | # Core modules under test 7 | import sigils/signals 8 | import sigils/slots 9 | import sigils/reactive 10 | 11 | #[ 12 | Original benchmarks results: 13 | [Suite] benchmarks 14 | [bench] emit->slot: n=20000, time=106.00 ms, rate=188679. ops/s, time=106989 us 15 | [OK] emit->slot throughput (tight loop) 16 | [bench] slot direct call: n=20000, time=218.00 ms, rate=91743119. ops/s, time=218 us 17 | [OK] slot direct call (tight loop) 18 | [bench] reactive (lazy): n=20000, time=218.00 ms, rate=91743. iters/s 19 | [OK] reactive computed (lazy) update+read 20 | [bench] reactive (eager): n=20000, time=181.00 ms, rate=110497. iters/s 21 | [OK] reactive computedNow eager updates 22 | 23 | ]# 24 | 25 | # Simple Agents for benchmarking 26 | type 27 | Emitter* = ref object of Agent 28 | Counter* = ref object of Agent 29 | value: int 30 | 31 | proc bump*(tp: Emitter, val: array[1024, int]) {.signal.} 32 | 33 | proc onBump*(self: Counter, val: array[1024, int]) {.slot.} = 34 | self.value += 1 35 | 36 | var durationMicrosEmitSlot: float 37 | 38 | const n = block: 39 | when defined(slowbench): 1_000_000 40 | else: 100_000 41 | 42 | suite "benchmarks": 43 | test "emit->slot throughput (tight loop)": 44 | 45 | var a = Emitter() 46 | var b = Counter() 47 | 48 | connect(a, bump, b, onBump) 49 | 50 | let t0 = getMonoTime() 51 | var x: array[1024, int] 52 | for i in 0 ..< n: 53 | x[0] = i 54 | emit a.bump(x) 55 | let dt = getMonoTime() - t0 56 | 57 | check b.value == n 58 | 59 | durationMicrosEmitSlot = dt.inMicroseconds.float 60 | let ms = dt.inMilliseconds.float 61 | let opsPerSec = (n.float * 1000.0) / max(1.0, ms) 62 | echo &"[bench] emit->slot: n={n}, time={ms:.2f} ms, rate={opsPerSec:.0f} ops/s, time={dt.inMicroseconds} us" 63 | 64 | test "slot direct call (tight loop)": 65 | var a = Emitter() 66 | var b = Counter() 67 | 68 | let t0 = getMonoTime() 69 | var x: array[1024, int] 70 | for i in 0 ..< n: 71 | x[0] = i 72 | b.onBump(x) 73 | let dt = getMonoTime() - t0 74 | 75 | check b.value == n 76 | 77 | let us = dt.inMicroseconds.float 78 | let opsPerSec = (n.float * 1_000_000.0) / max(1.0, us) 79 | echo &"[bench] slot direct call: n={n}, time={us:.2f} us, rate={opsPerSec:.0f} ops/s, ratio={durationMicrosEmitSlot / us:.2f}" 80 | 81 | when false: 82 | test "reactive computed (lazy) update+read": 83 | let x = newSigil(0) 84 | let y = computed[int](x{} * 2) 85 | 86 | let t0 = getMonoTime() 87 | for i in 0 ..< n: 88 | x <- i 89 | discard y{} # triggers compute on read when dirty 90 | let dt = getMonoTime() - t0 91 | 92 | check y{} == (n - 1) * 2 93 | 94 | let ms = dt.inMilliseconds.float 95 | let itersPerSec = (n.float * 1000.0) / max(1.0, ms) 96 | echo &"[bench] reactive (lazy): n={n}, time={ms:.2f} ms, rate={itersPerSec:.0f} iters/s" 97 | 98 | test "reactive computedNow eager updates": 99 | let x = newSigil(0) 100 | let y = computedNow[int](x{} * 2) 101 | 102 | let t0 = getMonoTime() 103 | for i in 0 ..< n: 104 | x <- i # compute happens on set 105 | let dt = getMonoTime() - t0 106 | 107 | check y{} == (n - 1) * 2 108 | 109 | let ms = dt.inMilliseconds.float 110 | let itersPerSec = (n.float * 1000.0) / max(1.0, ms) 111 | echo &"[bench] reactive (eager): n={n}, time={ms:.2f} ms, rate={itersPerSec:.0f} iters/s" 112 | 113 | -------------------------------------------------------------------------------- /tests/tbenchmarks.nim: -------------------------------------------------------------------------------- 1 | import std/monotimes 2 | import std/strformat 3 | import std/math 4 | import unittest 5 | 6 | # Core modules under test 7 | import sigils/signals 8 | import sigils/slots 9 | import sigils/core 10 | 11 | when not defined(sigilsCborSerde) and not defined(sigilsJsonSerde): 12 | import sigils/reactive 13 | 14 | #[ 15 | Original benchmarks results: 16 | [Suite] benchmarks 17 | [bench] emit->slot: n=20000, time=106.00 ms, rate=188679. ops/s, time=106989 us 18 | [OK] emit->slot throughput (tight loop) 19 | [bench] slot direct call: n=20000, time=218.00 ms, rate=91743119. ops/s, time=218 us 20 | [OK] slot direct call (tight loop) 21 | [bench] reactive (lazy): n=20000, time=218.00 ms, rate=91743. iters/s 22 | [OK] reactive computed (lazy) update+read 23 | [bench] reactive (eager): n=20000, time=181.00 ms, rate=110497. iters/s 24 | [OK] reactive computedNow eager updates 25 | 26 | ]# 27 | 28 | # Simple Agents for benchmarking 29 | type 30 | Emitter* = ref object of Agent 31 | Counter* = ref object of Agent 32 | value: int 33 | 34 | proc bump*(tp: Emitter, val: int) {.signal.} 35 | 36 | proc onBump*(self: Counter, val: int) {.slot.} = 37 | self.value += 1 38 | 39 | var durationMicrosEmitSlot: float 40 | 41 | const n = block: 42 | when defined(slowbench): 1_000_000 43 | else: 100_000 44 | 45 | suite "benchmarks": 46 | test "emit->slot throughput (tight loop)": 47 | 48 | var a = Emitter() 49 | var b = Counter() 50 | 51 | connect(a, bump, b, onBump) 52 | 53 | let t0 = getMonoTime() 54 | for i in 0 ..< n: 55 | emit a.bump(i) 56 | let dt = getMonoTime() - t0 57 | 58 | check b.value == n 59 | 60 | durationMicrosEmitSlot = dt.inMicroseconds.float 61 | let ms = dt.inMilliseconds.float 62 | let opsPerSec = (n.float * 1000.0) / max(1.0, ms) 63 | echo &"[bench] emit->slot: n={n}, time={ms:.2f} ms, rate={opsPerSec:.0f} ops/s, time={dt.inMicroseconds} us" 64 | 65 | test "slot direct call (tight loop)": 66 | var a = Emitter() 67 | var b = Counter() 68 | 69 | let t0 = getMonoTime() 70 | for i in 0 ..< n: 71 | b.onBump(i) 72 | let dt = getMonoTime() - t0 73 | 74 | check b.value == n 75 | 76 | let us = dt.inMicroseconds.float 77 | let opsPerSec = (n.float * 1_000_000.0) / max(1.0, us) 78 | echo &"[bench] slot direct call: n={n}, time={us:.2f} us, rate={opsPerSec:.0f} ops/s, ratio={durationMicrosEmitSlot / us:.2f}" 79 | 80 | when not defined(sigilsCborSerde) and not defined(sigilsJsonSerde): 81 | test "reactive computed (lazy) update+read": 82 | let x = newSigil(0) 83 | let y = computed[int](x{} * 2) 84 | 85 | let t0 = getMonoTime() 86 | for i in 0 ..< n: 87 | x <- i 88 | discard y{} # triggers compute on read when dirty 89 | let dt = getMonoTime() - t0 90 | 91 | check y{} == (n - 1) * 2 92 | 93 | let ms = dt.inMilliseconds.float 94 | let itersPerSec = (n.float * 1000.0) / max(1.0, ms) 95 | echo &"[bench] reactive (lazy): n={n}, time={ms:.2f} ms, rate={itersPerSec:.0f} iters/s" 96 | 97 | test "reactive computedNow eager updates": 98 | let x = newSigil(0) 99 | let y = computedNow[int](x{} * 2) 100 | 101 | let t0 = getMonoTime() 102 | for i in 0 ..< n: 103 | x <- i # compute happens on set 104 | let dt = getMonoTime() - t0 105 | 106 | check y{} == (n - 1) * 2 107 | 108 | let ms = dt.inMilliseconds.float 109 | let itersPerSec = (n.float * 1000.0) / max(1.0, ms) 110 | echo &"[bench] reactive (eager): n={n}, time={ms:.2f} ms, rate={itersPerSec:.0f} iters/s" 111 | 112 | -------------------------------------------------------------------------------- /sigils/registry.nim: -------------------------------------------------------------------------------- 1 | import std/locks 2 | import std/options 3 | import ./protocol 4 | import ./agents 5 | import ./threads 6 | import ./svariant 7 | 8 | type AgentLocation* = object 9 | thread*: SigilThreadPtr 10 | agent*: WeakRef[Agent] 11 | typeId*: TypeId 12 | 13 | var registry: Table[SigilName, AgentLocation] 14 | var regLock: Lock 15 | regLock.initLock() 16 | 17 | type ProxyCacheKey = tuple[thread: SigilThreadPtr, agent: WeakRef[Agent]] 18 | 19 | var proxyCache {.threadVar.}: Table[ProxyCacheKey, AgentProxyShared] 20 | 21 | type SetupProxyParams = object 22 | proxy*: WeakRef[AgentProxyShared] 23 | 24 | proc keepAlive(context: Agent, params: SigilParams) {.nimcall.} = 25 | raise newException(AssertionDefect, "this should never be called!") 26 | 27 | proc registerGlobalName*[T](name: SigilName, proxy: AgentProxy[T], 28 | override = false) = 29 | withLock regLock: 30 | if not override and name in registry: 31 | raise newException(ValueError, "Name already registered! Name: " & $name) 32 | registry[name] = AgentLocation( 33 | thread: proxy.remoteThread, 34 | agent: proxy.remote, 35 | typeId: getTypeId(T), 36 | ) 37 | #proxy.remoteThread.extReference(proxy.remote) 38 | #withLock proxy.lock: 39 | # if not proxy.proxyTwin.isNil: 40 | # withLock proxy.remoteThread[].signaledLock: 41 | # proxy.remoteThread[].signaled.incl(proxy.proxyTwin.toKind(AgentRemote)) 42 | proxy.remoteThread.send(ThreadSignal(kind: Trigger)) 43 | proxy.remoteThread.send(ThreadSignal(kind: AddSubscription, 44 | src: proxy.remote, 45 | name: sn"sigils:registryKeepAlive", 46 | subTgt: proxy.remote, 47 | subProc: keepAlive)) 48 | 49 | 50 | proc removeGlobalName*[T](name: SigilName, proxy: AgentProxy[T]): bool = 51 | withLock regLock: 52 | if name in registry: 53 | registry.del(name) 54 | return true 55 | return false 56 | 57 | proc lookupGlobalName*(name: SigilName): Option[AgentLocation] = 58 | withLock regLock: 59 | if name in registry: 60 | result = some registry[name] 61 | 62 | proc lookupAgentProxyImpl[T](location: AgentLocation, tp: typeof[T]): AgentProxy[T] = 63 | if getTypeId(T) != location.typeId: 64 | raise newException(ValueError, "can't create proxy of the correct type!") 65 | if location.thread.isNil or location.agent.isNil: 66 | return nil 67 | 68 | let key: ProxyCacheKey = (location.thread, location.agent) 69 | if key in proxyCache: 70 | let cached = proxyCache[key] 71 | if not cached.isNil: 72 | return cast[AgentProxy[T]](cached) 73 | 74 | let ct = getCurrentSigilThread() 75 | 76 | result = AgentProxy[T]( 77 | remote: location.agent, 78 | remoteThread: location.thread, 79 | inbox: newChan[ThreadSignal](1_000), 80 | ) 81 | 82 | var remoteProxy = AgentProxy[T]( 83 | remote: location.agent, 84 | remoteThread: ct, 85 | inbox: newChan[ThreadSignal](1_000), 86 | ) 87 | 88 | result.lock.initLock() 89 | remoteProxy.lock.initLock() 90 | 91 | result.proxyTwin = remoteProxy.unsafeWeakRef().toKind(AgentProxyShared) 92 | remoteProxy.proxyTwin = result.unsafeWeakRef().toKind(AgentProxyShared) 93 | 94 | when defined(sigilsDebug): 95 | let aid = $location.agent 96 | result.debugName = "localProxy::" & aid 97 | remoteProxy.debugName = "remoteProxy::" & aid 98 | 99 | # Ensure the remote proxy is kept alive until it is wired on the remote thread. 100 | remoteProxy.addSubscription(AnySigilName, result, localSlot) 101 | 102 | let remoteProxyRef = remoteProxy.unsafeWeakRef().toKind(AgentProxyShared) 103 | location.thread.send(ThreadSignal(kind: Move, item: move remoteProxy)) 104 | location.thread.send(ThreadSignal(kind: AddSubscription, 105 | src: location.agent, 106 | name: AnySigilName, 107 | subTgt: remoteProxyRef.toKind(Agent), 108 | subProc: remoteSlot)) 109 | 110 | proxyCache[key] = AgentProxyShared(result) 111 | 112 | proc lookupAgentProxy*[T](name: SigilName, tp: typeof[T]): AgentProxy[T] = 113 | withLock regLock: 114 | if name notin registry: 115 | return nil 116 | else: 117 | return lookupAgentProxyImpl(registry[name], tp) 118 | 119 | -------------------------------------------------------------------------------- /tests/tisolateutils.nim: -------------------------------------------------------------------------------- 1 | import std/isolation 2 | import std/unittest 3 | import std/os 4 | import std/sequtils 5 | 6 | import sigils 7 | import sigils/isolateutils 8 | import sigils/weakrefs 9 | 10 | import std/private/syslocks 11 | 12 | import threading/smartptrs 13 | import threading/channels 14 | 15 | type 16 | SomeAction* = ref object of Agent 17 | value: int 18 | 19 | Counter* = ref object of Agent 20 | value: int 21 | 22 | proc valueChanged*(tp: SomeAction, val: int) {.signal.} 23 | proc updated*(tp: Counter, final: int) {.signal.} 24 | 25 | proc setValue*(self: Counter, value: int) {.slot.} = 26 | echo "setValue! ", value, " id: ", self.getSigilId, " (th:", getThreadId(), ")" 27 | if self.value != value: 28 | self.value = value 29 | echo "setValue:listening: ", self.listening.toSeq.mapIt(it.getSigilId) 30 | emit self.updated(self.value) 31 | 32 | proc completed*(self: SomeAction, final: int) {.slot.} = 33 | echo "Action done! final: ", 34 | final, " id: ", self.getSigilId(), " (th:", getThreadId(), ")" 35 | self.value = final 36 | 37 | proc value*(self: Counter): int = 38 | self.value 39 | 40 | suite "isolate utils": 41 | teardown: 42 | GC_fullCollect() 43 | 44 | test "isolateRuntime": 45 | type 46 | TestObj = object 47 | TestRef = ref object 48 | TestInner = object 49 | value: TestRef 50 | 51 | var a = SomeAction(value: 10) 52 | 53 | echo "A: ", a.unsafeGcCount() 54 | var isoA = isolateRuntime(move a) 55 | check a.isNil 56 | check isoA.extract().value == 10 57 | 58 | var 59 | b = 33 60 | isoB = isolateRuntime(b) 61 | check isoB.extract() == b 62 | 63 | var 64 | c = TestObj() 65 | isoC = isolateRuntime(c) 66 | check isoC.extract() == c 67 | 68 | var 69 | d = "test" 70 | isoD = isolateRuntime(d) 71 | check isoD.extract() == d 72 | 73 | expect(IsolationError): 74 | echo "expect error..." 75 | var 76 | e = TestRef() 77 | e2 = e 78 | isoE = isolateRuntime(e) 79 | check isoE.extract() == e 80 | 81 | var f = TestInner() 82 | var isoF = isolateRuntime(f) 83 | check isoF.extract() == f 84 | 85 | type 86 | NonCopy = object 87 | 88 | Foo = object of RootObj 89 | id: int 90 | 91 | BarImpl = object of Foo 92 | value: int 93 | # thr: Thread[int] 94 | # ch: Chan[int] 95 | 96 | proc `=copy`*(a: var NonCopy, b: NonCopy) {.error.} 97 | 98 | method test*(obj: Foo) {.base.} = 99 | echo "foo: ", obj.repr 100 | 101 | method test*(obj: BarImpl) = 102 | echo "barImpl: ", obj.repr 103 | 104 | proc newBarImpl*(): SharedPtr[BarImpl] = 105 | var thr = BarImpl(id: 1234) 106 | result = newSharedPtr(thr) 107 | 108 | var localFoo {.threadVar.}: SharedPtr[Foo] 109 | 110 | proc toFoo*[R: Foo](t: SharedPtr[R]): SharedPtr[Foo] = 111 | cast[SharedPtr[Foo]](t) 112 | 113 | proc startLocalFoo*() = 114 | echo "startLocalFoo" 115 | if localFoo.isNil: 116 | var st = newBarImpl() 117 | localFoo = st.toFoo() 118 | echo "startLocalThread: ", localFoo.repr 119 | 120 | proc getCurrentFoo*(): SharedPtr[Foo] = 121 | echo "getCurrentFoo" 122 | startLocalFoo() 123 | assert not localFoo.isNil 124 | return localFoo 125 | 126 | suite "isolate utils": 127 | when false: 128 | test "test foos": 129 | var b = BarImpl(id: 34, value: 101) 130 | var a: Foo 131 | a = b 132 | b.test() 133 | a.test() 134 | echo "a: ", a.repr 135 | 136 | test "test lets": 137 | let b = BarImpl(id: 34, value: 101) 138 | let a: Foo = b 139 | let c: Foo = a 140 | b.test() 141 | a.test() 142 | c.test() 143 | echo "a: ", a.repr 144 | 145 | test "test ptr": 146 | # var b = BarImpl(id: 34, value: 101) 147 | var bp: ptr BarImpl = cast[ptr BarImpl](allocShared0(sizeof(BarImpl))) 148 | 149 | bp[] = BarImpl(id: 34, value: 101) 150 | var ap: ptr Foo = bp 151 | 152 | bp[].test() 153 | ap[].test() 154 | check bp[].id == 34 155 | check bp[].value == 101 156 | let d = Foo(id: 56) 157 | d.test() 158 | 159 | proc testValue(bar: var BarImpl): int = 160 | bar.value 161 | 162 | check bp[].testValue() == 101 163 | 164 | test "isolateRuntime sharedPointer": 165 | echo "test" 166 | 167 | # let test = getCurrentFoo() 168 | # echo "test: ", test 169 | # check not test.isNil 170 | # check test[].id == 1234 171 | -------------------------------------------------------------------------------- /tests/tslotsThreadAsync.nim: -------------------------------------------------------------------------------- 1 | import std/[unittest, asyncdispatch, times, strutils, os] 2 | import sigils 3 | import sigils/threadAsyncs 4 | 5 | type 6 | SomeAction* = ref object of Agent 7 | value: int 8 | 9 | Counter* = ref object of Agent 10 | value: int 11 | 12 | proc valueChanged*(tp: SomeAction, val: int) {.signal.} 13 | proc updated*(tp: Counter, final: int) {.signal.} 14 | proc updated*(tp: AgentProxy[Counter], final: int) {.signal.} 15 | 16 | ## -------------------------------------------------------- ## 17 | let start = epochTime() 18 | 19 | proc ticker(self: Counter) {.async.} = 20 | ## This simple procedure will echo out "tick" ten times with 100ms between 21 | ## each tick. We use it to visualise the time between other procedures. 22 | for i in 1 .. 3: 23 | await sleepAsync(100) 24 | echo "tick ", 25 | i * 100, "ms ", split($((epochTime() - start) * 1000), '.')[0], "ms (real)" 26 | 27 | emit self.updated(1337) 28 | 29 | proc setValue*(self: Counter, value: int) {.slot.} = 30 | echo "setValue! ", value, " (th: ", getThreadId(), ")" 31 | self.value = value 32 | asyncCheck ticker(self) 33 | 34 | proc setValueNonAsync*(self: Counter, value: int) {.slot.} = 35 | echo "setValueNonAsync! ", value, " (th: ", getThreadId(), ")" 36 | self.value = value 37 | emit self.updated(1337) 38 | 39 | proc completed*(self: SomeAction, final: int) {.slot.} = 40 | echo "Action done! final: ", final, " (th: ", getThreadId(), ")" 41 | self.value = final 42 | 43 | proc value*(self: Counter): int = 44 | self.value 45 | 46 | ## -------------------------------------------------------- ## 47 | proc sendBad*(tp: SomeAction, val: Counter) {.signal.} 48 | 49 | proc setValueBad*(self: SomeAction, val: Counter) {.slot.} = 50 | discard 51 | 52 | suite "threaded agent slots": 53 | teardown: 54 | GC_fullCollect() 55 | 56 | test "sigil object thread runner non-async task": 57 | var 58 | a = SomeAction() 59 | b = Counter() 60 | 61 | echo "thread runner!", " (th: ", getThreadId(), ")" 62 | echo "obj a: ", $a.unsafeWeakRef() 63 | 64 | let thread = newSigilAsyncThread() 65 | thread.start() 66 | startLocalThreadDefault() 67 | # os.sleep(100) 68 | 69 | let bp: AgentProxy[Counter] = b.moveToThread(thread) 70 | connectThreaded(a, valueChanged, bp, setValueNonAsync) 71 | connectThreaded(bp, updated, a, SomeAction.completed()) 72 | 73 | # echo "bp.outbound: ", bp.outbound[].AsyncSigilChan.repr 74 | emit a.valueChanged(314) 75 | check a.value == 0 76 | let ct = getCurrentSigilThread() 77 | ct.poll() 78 | check a.value == 1337 79 | 80 | thread.stop() 81 | thread.join() 82 | 83 | test "sigil object thread runner": 84 | var 85 | a = SomeAction() 86 | b = Counter() 87 | 88 | echo "thread runner!", " (th: ", getThreadId(), ")" 89 | echo "obj a: ", $a.unsafeWeakRef() 90 | 91 | let thread = newSigilAsyncThread() 92 | thread.start() 93 | startLocalThreadDefault() 94 | # os.sleep(100) 95 | 96 | let bp: AgentProxy[Counter] = b.moveToThread(thread) 97 | connectThreaded(a, valueChanged, bp, setValue) 98 | connectThreaded(bp, updated, a, SomeAction.completed()) 99 | 100 | # echo "bp.outbound: ", bp.outbound[].AsyncSigilChan.repr 101 | emit a.valueChanged(314) 102 | check a.value == 0 103 | let ct = getCurrentSigilThread() 104 | ct.poll() 105 | check a.value == 1337 106 | 107 | thread.stop() 108 | thread.join() 109 | 110 | test "sigil object thread bad": 111 | var 112 | a = SomeAction() 113 | b = SomeAction() 114 | 115 | echo "thread runner!", " (th: ", getThreadId(), ")" 116 | echo "obj a: ", a.unsafeWeakRef 117 | 118 | let thread = newSigilAsyncThread() 119 | thread.start() 120 | startLocalThreadDefault() 121 | 122 | let bp: AgentProxy[SomeAction] = b.moveToThread(thread) 123 | check not compiles(connect(a, sendBad, bp, setValueBad)) 124 | # connect(a, sendBad, bp, setValueBad) 125 | 126 | test "local async thread": 127 | setLocalSigilThread(newSigilAsyncThread()) 128 | let ct = getCurrentSigilThread() 129 | 130 | check ct of AsyncSigilThreadPtr 131 | 132 | ct.poll() 133 | ct.poll(NonBlocking) 134 | check ct.pollAll() == 0 135 | 136 | test "remote async thread trigger using local proxy": 137 | var a = SomeAction() 138 | var b = Counter() 139 | 140 | let thread = newSigilAsyncThread() 141 | thread.start() 142 | startLocalThreadDefault() 143 | 144 | let bp: AgentProxy[Counter] = b.moveToThread(thread) 145 | connectThreaded(bp, updated, bp, Counter.setValueNonAsync()) 146 | connectThreaded(bp, updated, a, SomeAction.completed()) 147 | 148 | emit bp.updated(1337) 149 | 150 | let ct = getCurrentSigilThread() 151 | ct.poll() 152 | check a.value == 1337 153 | 154 | thread.stop() 155 | thread.join() 156 | -------------------------------------------------------------------------------- /sigils/protocol.nim: -------------------------------------------------------------------------------- 1 | import std/[tables, strutils] 2 | import stack_strings 3 | 4 | export tables 5 | export stack_strings 6 | 7 | type FastErrorCodes* = enum 8 | # Error messages 9 | FAST_PARSE_ERROR = -27 10 | INVALID_REQUEST = -26 11 | METHOD_NOT_FOUND = -25 12 | INVALID_PARAMS = -24 13 | INTERNAL_ERROR = -23 14 | SERVER_ERROR = -22 15 | 16 | when defined(nimscript) or defined(useJsonSerde) or defined(sigilsJsonSerde): 17 | import std/json 18 | export json 19 | elif defined(sigilsCborSerde): 20 | import cborious 21 | export cborious 22 | else: 23 | import svariant 24 | export svariant 25 | 26 | 27 | type SigilParams* {.acyclic.} = object ## implementation specific -- handles data buffer 28 | when defined(nimscript) or defined(useJsonSerde) or defined(sigilsJsonSerde): 29 | buf*: JsonNode 30 | elif defined(sigilsCborSerde): 31 | buf*: CborStream 32 | else: 33 | buf*: WVariant 34 | 35 | type 36 | RequestType* {.size: sizeof(uint8).} = enum 37 | # Fast RPC Types 38 | Request = 5 39 | Response = 6 40 | Notify = 7 41 | Error = 8 42 | Subscribe = 9 43 | Publish = 10 44 | SubscribeStop = 11 45 | PublishDone = 12 46 | SystemRequest = 19 47 | Unsupported = 23 48 | # rtpMax = 23 # numbers less than this store in single mpack/cbor byte 49 | 50 | SigilId* = distinct int 51 | 52 | SigilName* = StackString[48] 53 | 54 | SigilRequest* = object 55 | kind*: RequestType 56 | origin*: SigilId 57 | procName*: SigilName 58 | params*: SigilParams # - we handle params below 59 | 60 | SigilRequestTy*[T] = SigilRequest 61 | 62 | SigilResponse* = object 63 | kind*: RequestType 64 | id*: int 65 | result*: SigilParams # - we handle params below 66 | 67 | SigilError* = ref object 68 | code*: FastErrorCodes 69 | msg*: string # trace*: seq[(string, string, int)] 70 | 71 | type 72 | ConversionError* = object of CatchableError 73 | 74 | SigilErrorStackTrace* = object 75 | code*: int 76 | msg*: string 77 | stacktrace*: seq[string] 78 | 79 | proc duplicate*(params: SigilParams): SigilParams = 80 | when defined(nimscript) or defined(useJsonSerde) or defined(sigilsJsonSerde): 81 | result.buf = params.buf 82 | elif defined(sigilsCborSerde): 83 | result.buf = params.buf 84 | else: 85 | result.buf = params.buf.duplicate() 86 | 87 | proc duplicate*(req: SigilRequest): SigilRequest = 88 | result = SigilRequest( 89 | kind: req.kind, 90 | origin: req.origin, 91 | procName: req.procName, 92 | params: req.params.duplicate(), 93 | ) 94 | 95 | proc `$`*(id: SigilId): string = 96 | "0x" & id.int.toHex(16) 97 | 98 | proc rpcPack*(res: SigilParams): SigilParams {.inline.} = 99 | result = res 100 | 101 | proc rpcPack*[T](res: sink T): SigilParams = 102 | when defined(nimscript) or defined(sigilsJsonSerde): 103 | let jn = toJson(res) 104 | result = SigilParams(buf: jn) 105 | elif defined(sigilsOrigSerde): 106 | result = SigilParams(buf: newVariant(ensureMove res)) 107 | elif defined(sigilsCborSerde): 108 | var buf {.global, threadvar.}: CborStream 109 | buf = CborStream.init() 110 | buf.setPosition(0) 111 | buf.pack(res) 112 | result = SigilParams(buf: buf) 113 | else: 114 | var requestCache {.global, threadvar.}: WVariant 115 | if requestCache.isNil: 116 | requestCache = newWrapperVariant(res) 117 | requestCache.resetTo(res) 118 | result = SigilParams(buf: requestCache) 119 | 120 | proc rpcUnpack*[T](obj: var T, ss: SigilParams) = 121 | when defined(nimscript) or defined(useJsonSerde): 122 | obj.fromJson(ss.buf) 123 | discard 124 | elif defined(sigilsOrigSerde): 125 | assert not ss.buf.isNil 126 | obj = ss.buf.get(T) 127 | elif defined(sigilsCborSerde): 128 | ss.buf.setPosition(0) 129 | obj = unpack(ss.buf, T) 130 | else: 131 | assert not ss.buf.isNil 132 | obj = ss.buf.getWrapped(T) 133 | 134 | proc wrapResponse*(id: SigilId, resp: SigilParams, kind = Response): SigilResponse = 135 | # echo "WRAP RESP: ", id, " kind: ", kind 136 | result.kind = kind 137 | result.id = id.int 138 | result.result = resp 139 | 140 | proc wrapResponseError*(id: SigilId, err: SigilError): SigilResponse = 141 | echo "WRAP ERROR: ", id, " err: ", err.repr 142 | result.kind = Error 143 | result.id = id.int 144 | result.result = rpcPack(err) 145 | 146 | proc initSigilRequest*[S, T]( 147 | procName: SigilName, 148 | args: sink T, 149 | origin: SigilId = SigilId(-1), 150 | reqKind: RequestType = Request, 151 | ): SigilRequestTy[S] = 152 | result = SigilRequestTy[S]( 153 | kind: reqKind, 154 | origin: origin, 155 | procName: procName, 156 | params: rpcPack(ensureMove args) 157 | ) 158 | 159 | const sigilsMaxSignalLength* {.intdefine.} = 48 160 | 161 | proc toSigilName*(name: IndexableChars): SigilName = 162 | return toStackString(name, sigilsMaxSignalLength) 163 | 164 | proc toSigilName*(name: static string): SigilName = 165 | return toStackString(name, sigilsMaxSignalLength) 166 | 167 | proc toSigilName*(name: string): SigilName = 168 | return toStackString(name, sigilsMaxSignalLength) 169 | 170 | template sigName*(name: static string): SigilName = 171 | ## Static Signal Name template 172 | toSigilName(name) 173 | 174 | template sn*(name: static string): SigilName = 175 | ## Static Signal Name template 176 | toSigilName(name) 177 | 178 | const AnySigilName* = toSigilName(":any:") 179 | -------------------------------------------------------------------------------- /sigils/signals.nim: -------------------------------------------------------------------------------- 1 | import std/[macros, options] 2 | import std/times 3 | 4 | import slots 5 | 6 | import agents 7 | 8 | export agents 9 | export times 10 | 11 | proc getSignalName*(signal: NimNode): NimNode = 12 | # echo "getSignalName: ", signal.treeRepr 13 | if signal.kind in [nnkClosedSymChoice, nnkOpenSymChoice]: 14 | result = newStrLitNode signal[0].strVal 15 | else: 16 | result = newStrLitNode signal.strVal 17 | # echo "getSignalName:result: ", result.treeRepr 18 | 19 | macro signalName*(signal: untyped): SigilName = 20 | let sig = getSignalName(`signal`) 21 | result = quote do: toSigilName(`sig`) 22 | 23 | proc splitNamesImpl(slot: NimNode): Option[(NimNode, NimNode)] = 24 | # echo "splitNamesImpl: ", slot.treeRepr 25 | if slot.kind == nnkCall and slot[0].kind == nnkDotExpr: 26 | return splitNamesImpl(slot[0]) 27 | elif slot.kind == nnkCall: 28 | result = some (slot[1].copyNimTree, slot[0].copyNimTree) 29 | elif slot.kind == nnkDotExpr: 30 | result = some (slot[0].copyNimTree, slot[1].copyNimTree) 31 | # echo "splitNamesImpl:res: ", result.repr 32 | 33 | macro signalType*(s: untyped): auto = 34 | ## gets the type of the signal without 35 | ## the Agent proc type 36 | ## 37 | let p = s.getTypeInst 38 | # echo "\nsignalType: ", p.treeRepr 39 | # echo "signalType: ", p.repr 40 | # echo "signalType:orig: ", s.treeRepr 41 | if p.kind == nnkNone: 42 | error("cannot determine type of: " & repr(p), p) 43 | if p.kind == nnkSym and p.repr == "none": 44 | error("cannot determine type of: " & repr(p), p) 45 | let obj = 46 | if p.kind == nnkProcTy: 47 | p[0] 48 | else: 49 | p[0] 50 | # echo "signalType:p0: ", obj.repr 51 | result = nnkTupleConstr.newNimNode() 52 | for arg in obj[2 ..^ 1]: 53 | result.add arg[1] 54 | 55 | proc getAgentProcTy*[T](tp: AgentProcTy[T]): T = 56 | discard 57 | 58 | template checkSignalTypes*[T]( 59 | a: Agent, 60 | signal: typed, 61 | b: Agent, 62 | slot: Signal[T], 63 | acceptVoidSlot: static bool = false, 64 | ): void = 65 | block: 66 | ## statically verify signal / slot types match 67 | # echo "TYP: ", repr typeof(SignalTypes.`signal`(typeof(a))) 68 | var signalType {.used, inject.}: typeof(SignalTypes.`signal`(typeof(a))) 69 | var slotType {.used, inject.}: typeof(getAgentProcTy(slot)) 70 | when acceptVoidSlot and slotType is tuple[]: 71 | discard 72 | elif compiles(signalType = slotType): 73 | discard # don't need compile check when compiles 74 | else: 75 | signalType = slotType # let the compiler show the type mismatches 76 | 77 | template connect*[T]( 78 | a: Agent, 79 | signal: typed, 80 | b: Agent, 81 | slot: Signal[T], 82 | acceptVoidSlot: static bool = false, 83 | ): void = 84 | ## sets up `b` to recieve events from `a`. Both `a` and `b` 85 | ## must subtype `Agent`. The `signal` must be a signal proc, 86 | ## while `slot` must be a slot proc. 87 | ## 88 | runnableExamples: 89 | type 90 | Updater* = ref object of Agent 91 | 92 | Counter* = ref object of Agent 93 | value: int 94 | 95 | proc valueChanged*(tp: Counter, val: int) {.signal.} 96 | 97 | proc setValue*(self: Counter, value: int) {.slot.} = 98 | echo "setValue! ", value 99 | if self.value != value: 100 | self.value = value 101 | 102 | var 103 | a = Updater.new() 104 | a = Counter.new() 105 | connect(a, valueChanged, b, setValue) 106 | emit a.valueChanged(137) #=> prints "setValue! 137" 107 | 108 | checkSignalTypes(a, signal, b, slot, acceptVoidSlot) 109 | a.addSubscription(signalName(signal), b, slot) 110 | 111 | template connect*( 112 | a: Agent, 113 | signal: typed, 114 | b: Agent, 115 | slot: untyped, 116 | acceptVoidSlot: static bool = false, 117 | ): void = 118 | let agentSlot = `slot`(typeof(b)) 119 | checkSignalTypes(a, signal, b, agentSlot, acceptVoidSlot) 120 | a.addSubscription(signalName(signal), b, agentSlot) 121 | 122 | template connected*( 123 | a: Agent, 124 | signal: typed, 125 | ): bool = 126 | if a.hasSubscription(signalName(signal)): 127 | echo "CONNECTED: " 128 | true 129 | else: 130 | false 131 | 132 | template connected*( 133 | a: Agent, 134 | signal: typed, 135 | b: Agent, 136 | ): bool = 137 | let sn = signalName(signal).toSigilName() 138 | if a.hasSubscription(sn, b): 139 | true 140 | else: 141 | false 142 | 143 | template connected*( 144 | a: Agent, 145 | signal: typed, 146 | b: Agent, 147 | slots: untyped, 148 | ): bool = 149 | let agentSlot = `slots`(typeof(b)) 150 | let sn = signalName(signal).toSigilName() 151 | if a.hasSubscription(sn, b, agentSlot): 152 | true 153 | else: 154 | false 155 | 156 | template disconnect*[T]( 157 | a: Agent, 158 | signal: typed, 159 | b: Agent, 160 | slot: Signal[T], 161 | acceptVoidSlot: static bool = false, 162 | ): void = 163 | ## disconnect 164 | a.delSubscription(signalName(signal), b, slot) 165 | 166 | template disconnect*( 167 | a: Agent, 168 | signal: typed, 169 | b: Agent, 170 | slot: untyped, 171 | ): void = 172 | ## disconnect 173 | let agentSlot = `slot`(typeof(b)) 174 | a.delSubscription(signalName(signal), b, agentSlot) 175 | 176 | template disconnect*( 177 | a: Agent, 178 | signal: typed, 179 | b: Agent, 180 | ): void = 181 | ## disconnect 182 | a.delSubscription(signalName(signal), b, nil) 183 | -------------------------------------------------------------------------------- /tests/tweakrefs.nim: -------------------------------------------------------------------------------- 1 | import std/[unittest, sequtils] 2 | 3 | import sigils/signals 4 | import sigils/slots 5 | import sigils/core 6 | import sigils/weakrefs 7 | 8 | type 9 | Counter* = ref object of Agent 10 | value: int 11 | avg: int 12 | 13 | Originator* = ref object of Agent 14 | 15 | proc change*(tp: Originator, val: int) {.signal.} 16 | 17 | proc valueChanged*(tp: Counter, val: int) {.signal.} 18 | 19 | proc someChange*(tp: Counter) {.signal.} 20 | 21 | proc avgChanged*(tp: Counter, val: float) {.signal.} 22 | 23 | proc setValue*(self: Counter, value: int) {.slot.} = 24 | echo "setValue! ", value 25 | if self.value != value: 26 | self.value = value 27 | emit self.valueChanged(value) 28 | 29 | type TestObj = object 30 | val: int 31 | 32 | var lastDestroyedTestObj = -1 33 | 34 | proc `=destroy`*(obj: TestObj) = 35 | echo "destroying test object: ", obj.val 36 | lastDestroyedTestObj = obj.val 37 | 38 | suite "agent weak refs": 39 | test "subcriptionsTable freed": 40 | var x = Counter.new() 41 | 42 | block: 43 | var obj {.used.} = TestObj(val: 100) 44 | var y = Counter.new() 45 | 46 | # echo "Counter.setValue: ", "x: ", x.debugId, " y: ", y.debugId 47 | connect(x, valueChanged, y, setValue) 48 | 49 | check y.value == 0 50 | emit x.valueChanged(137) 51 | 52 | echo "x:subcriptions: ", x.subcriptions 53 | # echo "x:subscribed: ", x.subscribed 54 | echo "y:subcriptions: ", y.subcriptions 55 | # echo "y:subscribed: ", y.subscribed 56 | 57 | check y.subcriptions.len() == 0 58 | check y.listening.len() == 1 59 | 60 | check x.getSubscriptions(sigName"valueChanged").toSeq().len() == 1 61 | check x.listening.len() == 0 62 | 63 | echo "block done" 64 | 65 | echo "finishing outer block " 66 | # check x.listening.len() == 0 67 | echo "x:subcriptions: ", x.subcriptions 68 | # echo "x:subscribed: ", x.subscribed 69 | # check x.subcriptionsTable["valueChanged"].len() == 0 70 | check x.subcriptions.len() == 0 71 | check x.listening.len() == 0 72 | 73 | # check a.value == 0 74 | # check b.value == 137 75 | echo "done outer block" 76 | 77 | test "subcriptionsTable freed": 78 | var y = Counter.new() 79 | 80 | block: 81 | var obj {.used.} = TestObj(val: 100) 82 | var x = Counter.new() 83 | 84 | # echo "Counter.setValue: ", "x: ", x.debugId, " y: ", y.debugId 85 | connect(x, valueChanged, y, setValue) 86 | 87 | check y.value == 0 88 | emit x.valueChanged(137) 89 | 90 | echo "x:subcriptions: ", x.subcriptions 91 | # echo "x:subscribed: ", x.subscribed 92 | echo "y:subcriptions: ", y.subcriptions 93 | # echo "y:subscribed: ", y.subscribed 94 | 95 | check y.subcriptions.len() == 0 96 | check y.listening.len() == 1 97 | 98 | check x.getSubscriptions(sigName"valueChanged").toSeq().len() == 1 99 | check x.listening.len() == 0 100 | 101 | echo "block done" 102 | 103 | echo "finishing outer block " 104 | # check x.listening.len() == 0 105 | echo "y:subcriptions: ", y.subcriptions 106 | # echo "y:subscribed: ", y.listening.mapIt(it) 107 | # check x.subcriptionsTable["valueChanged"].len() == 0 108 | check y.subcriptions.len() == 0 109 | check y.listening.len() == 0 110 | 111 | # check a.value == 0 112 | # check b.value == 137 113 | echo "done outer block" 114 | 115 | test "refcount": 116 | type TestRef = ref TestObj 117 | 118 | var x = TestRef(val: 33) 119 | echo "X::count: ", x.unsafeGcCount() 120 | check x.unsafeGcCount() == 1 121 | block: 122 | let y = x 123 | echo "X::count: ", x.unsafeGcCount() 124 | check x.unsafeGcCount() == 2 125 | check y.unsafeGcCount() == 2 126 | echo "X::count: ", x.unsafeGcCount() 127 | check x.unsafeGcCount() == 1 128 | var y = move x 129 | echo "y: ", repr y 130 | check lastDestroyedTestObj != 33 131 | check x.isNil 132 | check y.unsafeGcCount() == 1 133 | 134 | test "weak refs": 135 | var x = Counter.new() 136 | echo "X::count: ", x.unsafeGcCount() 137 | check x.unsafeGcCount() == 1 138 | block: 139 | var obj {.used.} = TestObj(val: 100) 140 | var y = Counter.new() 141 | echo "X::count: ", x.unsafeGcCount() 142 | check x.unsafeGcCount() == 1 143 | 144 | # echo "Counter.setValue: ", "x: ", x.debugId, " y: ", y.debugId 145 | connect(x, valueChanged, y, setValue) 146 | check x.unsafeGcCount() == 1 147 | 148 | check y.value == 0 149 | emit x.valueChanged(137) 150 | echo "X::count:end: ", x.unsafeGcCount() 151 | echo "Y::count:end: ", y.unsafeGcCount() 152 | check x.unsafeGcCount() == 2 153 | 154 | # var xx = x 155 | # check x.unsafeGcCount() == 2 156 | 157 | echo "done with y" 158 | echo "X::count: ", x.unsafeGcCount() 159 | check x.subcriptions.len() == 0 160 | check x.listening.len() == 0 161 | check x.unsafeGcCount() == 1 162 | 163 | type 164 | Foo = object of RootObj 165 | value: int 166 | 167 | FooBar = object of Foo 168 | 169 | method test(obj: Foo) {.base.} = 170 | echo "test foos! value: ", obj.value 171 | 172 | method test(obj: FooBar) = 173 | echo "test foobar! value: ", obj.value 174 | 175 | suite "check object methods": 176 | test "methods": 177 | let f = Foo(value: 1) 178 | let fb = FooBar(value: 2) 179 | f.test() 180 | fb.test() 181 | let f1 = fb 182 | f1.test() 183 | check f.value == 1 184 | -------------------------------------------------------------------------------- /sigils/threadAsyncs.nim: -------------------------------------------------------------------------------- 1 | import std/sets 2 | import std/isolation 3 | import std/locks 4 | import threading/smartptrs 5 | import threading/channels 6 | import threading/atomics 7 | 8 | import std/os 9 | import std/monotimes 10 | import std/options 11 | import std/isolation 12 | import std/uri 13 | import std/asyncdispatch 14 | 15 | import agents 16 | import threadBase 17 | import threadDefault 18 | import core 19 | 20 | export smartptrs, isolation 21 | export threadBase 22 | 23 | type 24 | AsyncSigilThread* = object of SigilThread 25 | inputs*: SigilChan 26 | event*: AsyncEvent 27 | drain*: Atomic[bool] 28 | isReady*: bool 29 | thr*: Thread[ptr AsyncSigilThread] 30 | 31 | AsyncSigilThreadPtr* = ptr AsyncSigilThread 32 | 33 | proc newSigilAsyncThread*(): ptr AsyncSigilThread = 34 | result = cast[ptr AsyncSigilThread](allocShared0(sizeof(AsyncSigilThread))) 35 | result[] = AsyncSigilThread() # important! 36 | result[].event = newAsyncEvent() 37 | result[].agent = ThreadAgent() 38 | result[].signaledLock.initLock() 39 | result[].inputs = newSigilChan() 40 | result[].running.store(true, Relaxed) 41 | result[].drain.store(true, Relaxed) 42 | 43 | method send*( 44 | thread: AsyncSigilThreadPtr, msg: sink ThreadSignal, blocking: BlockingKinds 45 | ) {.gcsafe.} = 46 | debugPrint "threadSend: ", thread.toSigilThread()[].getThreadId() 47 | var msg = isolateRuntime(msg) 48 | case blocking 49 | of Blocking: 50 | thread.inputs.send(msg) 51 | of NonBlocking: 52 | let sent = thread.inputs.trySend(msg) 53 | if not sent: 54 | raise newException(MessageQueueFullError, "could not send!") 55 | thread.event.trigger() 56 | 57 | method recv*( 58 | thread: AsyncSigilThreadPtr, msg: var ThreadSignal, blocking: BlockingKinds 59 | ): bool {.gcsafe.} = 60 | debugPrint "threadRecv: ", thread.toSigilThread()[].getThreadId(), " blocking: ", blocking 61 | case blocking 62 | of Blocking: 63 | msg = thread.inputs.recv() 64 | return true 65 | of NonBlocking: 66 | result = thread.inputs.tryRecv(msg) 67 | thread.event.trigger() 68 | 69 | method setTimer*( 70 | thread: AsyncSigilThreadPtr, timer: SigilTimer 71 | ) {.gcsafe.} = 72 | if timer.isRepeat(): 73 | proc cb(fd: AsyncFD): bool {.closure, gcsafe.} = 74 | if thread.hasCancelTimer(timer): 75 | thread.removeTimer(timer) 76 | return true # stop timer 77 | else: 78 | emit timer.timeout() 79 | return false 80 | asyncdispatch.addTimer(timer.duration.inMilliseconds(), oneshot=false, cb) 81 | else: 82 | proc cb(fd: AsyncFD): bool {.closure, gcsafe.} = 83 | if timer.count == 0 or thread.hasCancelTimer(timer): 84 | thread.removeTimer(timer) 85 | return true # stop timer 86 | else: 87 | emit timer.timeout() 88 | timer.count.dec() 89 | asyncdispatch.addTimer(timer.duration.inMilliseconds(), oneshot=true, cb) 90 | return false 91 | asyncdispatch.addTimer(timer.duration.inMilliseconds(), oneshot=true, cb) 92 | 93 | proc setupThread*(thread: ptr AsyncSigilThread) = 94 | if thread[].isReady: 95 | return 96 | thread[].isReady = true 97 | 98 | let cb = proc(fd: AsyncFD): bool {.closure, gcsafe.} = 99 | var sig: ThreadSignal 100 | while isRunning(thread) and thread.recv(sig, NonBlocking): 101 | try: 102 | thread.exec(sig) 103 | except CatchableError as e: 104 | if thread[].exceptionHandler.isNil: 105 | raise e 106 | else: 107 | thread[].exceptionHandler(e) 108 | except Exception as e: 109 | if thread[].exceptionHandler.isNil: 110 | raise e 111 | else: 112 | thread[].exceptionHandler(e) 113 | except Defect as e: 114 | if thread[].exceptionHandler.isNil: 115 | raise e 116 | else: 117 | thread[].exceptionHandler(e) 118 | thread[].event.addEvent(cb) 119 | 120 | method poll*(thread: AsyncSigilThreadPtr, blocking: BlockingKinds = Blocking): bool {.gcsafe, discardable.} = 121 | if not thread[].isReady: 122 | thread.setupThread() 123 | 124 | case blocking 125 | of Blocking: 126 | asyncdispatch.poll() 127 | result = true 128 | of NonBlocking: 129 | asyncdispatch.poll(1) 130 | result = false 131 | 132 | proc runAsyncThread*(targ: AsyncSigilThreadPtr) {.thread.} = 133 | var 134 | thread = targ 135 | 136 | doAssert not hasLocalSigilThread() 137 | setGlobalDispatcher(newDispatcher()) 138 | setLocalSigilThread(thread) 139 | 140 | thread.setupThread() 141 | while thread.isRunning(): 142 | asyncdispatch.drain() 143 | 144 | try: 145 | if thread.drain.load(Relaxed): 146 | asyncdispatch.drain() 147 | except ValueError: 148 | discard 149 | 150 | proc startLocalThreadDispatch*() = 151 | if not hasLocalSigilThread(): 152 | var st = newSigilAsyncThread() 153 | st[].threadId.store(getThreadId(), Relaxed) 154 | setLocalSigilThread(st) 155 | 156 | proc start*(thread: ptr AsyncSigilThread) = 157 | if thread[].exceptionHandler.isNil: 158 | thread[].exceptionHandler = defaultExceptionHandler 159 | createThread(thread[].thr, runAsyncThread, thread) 160 | 161 | proc stop*(thread: ptr AsyncSigilThread, immediate: bool = false, drain: bool = false) = 162 | thread[].running.store(false, Relaxed) 163 | thread[].drain.store(drain, Relaxed) 164 | if immediate: 165 | thread[].drain.store(true, Relaxed) 166 | else: 167 | thread[].event.trigger() 168 | 169 | proc join*(thread: ptr AsyncSigilThread) = 170 | thread[].thr.joinThread() 171 | 172 | proc peek*(thread: ptr AsyncSigilThread): int = 173 | result = thread[].inputs.peek() 174 | -------------------------------------------------------------------------------- /tests/tmultiThreads.nim: -------------------------------------------------------------------------------- 1 | import std/isolation 2 | import std/unittest 3 | import std/os 4 | import std/sequtils 5 | import threading/atomics 6 | 7 | import sigils 8 | import sigils/threads 9 | import sigils/registry 10 | 11 | import std/terminal 12 | import std/strutils 13 | 14 | 15 | type 16 | SomeTrigger* = ref object of Agent 17 | 18 | Counter* = ref object of Agent 19 | value: int 20 | 21 | SomeTarget* = ref object of Agent 22 | value: int 23 | 24 | proc valueChanged*(tp: SomeTrigger, val: int) {.signal.} 25 | proc updated*(tp: Counter, final: int) {.signal.} 26 | 27 | proc setValue*(self: Counter, value: int) {.slot.} = 28 | echo "set value: ", value 29 | if self.value != value: 30 | self.value = value 31 | if value == 756809: 32 | os.sleep(1) 33 | emit self.updated(self.value) 34 | 35 | proc completed*(self: SomeTarget, final: int) {.slot.} = 36 | echo "Action done! final: ", 37 | final, " id: ", $self.unsafeWeakRef(), " (th: ", getThreadId(), ")" 38 | self.value = final 39 | 40 | proc valuePrint*(tp: SomeTrigger, val: int) {.slot.} = 41 | echo "print tp: ", $tp.unsafeWeakRef(), " value: ", val, " (th: ", getThreadId(), ")" 42 | 43 | var threadA = newSigilThread() 44 | var threadB = newSigilThread() 45 | var threadC = newSigilThread() 46 | 47 | threadA.start() 48 | threadB.start() 49 | threadC.start() 50 | 51 | var threadBRemoteReady: Atomic[int] 52 | threadBRemoteReady.store 0 53 | 54 | var actionA = SomeTrigger.new() 55 | var actionCProx: AgentProxy[SomeTrigger] 56 | var cpRef: AgentProxy[Counter] 57 | 58 | suite "threaded agent slots": 59 | setup: 60 | printConnectionsSlotNames = { 61 | remoteSlot.pointer: "remoteSlot", 62 | localSlot.pointer: "localSlot", 63 | SomeTarget.completed().pointer: "completed", 64 | Counter.setValue().pointer: "setValue", 65 | }.toTable() 66 | 67 | test "create globalCounter and move to threadA": 68 | 69 | echo "sigil object thread connect change" 70 | var 71 | counter = Counter.new() 72 | target1 = SomeTarget.new() 73 | 74 | echo "counter global: ", counter.unsafeWeakRef() 75 | when defined(sigilsDebug): 76 | counter.debugName = "counter" 77 | target1.debugName = "target1" 78 | 79 | echo "thread runner!", " (th: ", getThreadId(), ")" 80 | echo "obj actionA: ", actionA.getSigilId 81 | echo "obj counter: ", counter.getSigilId 82 | echo "obj target1: ", target1.getSigilId 83 | startLocalThreadDefault() 84 | 85 | connect(actionA, valueChanged, counter, setValue) 86 | connect(counter, updated, target1, SomeTarget.completed()) 87 | 88 | let counterProxy: AgentProxy[Counter] = counter.moveToThread(threadA) 89 | #cpRef = counterProxy 90 | echo "obj bp: ", counterProxy.getSigilId() 91 | when defined(sigilsDebug): 92 | counterProxy.debugName = "counterProxyLocal" 93 | 94 | registerGlobalName(sn"globalCounter", counterProxy) 95 | 96 | let bid = cast[int](counterProxy.remote.pt) 97 | emit actionA.valueChanged(bid) 98 | 99 | # Poll and check action response 100 | let ct = getCurrentSigilThread() 101 | ct.poll() 102 | check target1.value == bid 103 | 104 | let res = lookupGlobalName(sn"globalCounter").get() 105 | check res.agent == counterProxy.remote 106 | check res.thread == counterProxy.remoteThread 107 | 108 | test "connect target2 on threadB to globalCounter": 109 | proc remoteTrigger(counter: AgentProxy[SomeTarget]) {.signal.} 110 | 111 | proc remoteCompleted(self: SomeTarget, final: int) {.slot.} = 112 | echo "Action done on remote! final: ", 113 | final, " id: ", $self.unsafeWeakRef(), " (th: ", getThreadId(), ")" 114 | self.value = final 115 | threadBRemoteReady.store 3 116 | 117 | proc remoteRun(cc2: SomeTarget) {.slot.} = 118 | os.sleep(10) 119 | echo "REMOTE RUN!" 120 | let localCounterProxy = lookupAgentProxy(sn"globalCounter", Counter) 121 | if localCounterProxy != nil: 122 | connectThreaded(localCounterProxy, updated, cc2, remoteCompleted(SomeTarget)) 123 | threadBRemoteReady.store 1 124 | else: 125 | threadBRemoteReady.store 2 126 | 127 | var c2 = SomeTarget.new() 128 | let c2p: AgentProxy[SomeTarget] = c2.moveToThread(threadB) 129 | echo "obj c2p: ", c2p.getSigilId() 130 | 131 | connectThreaded(c2p, remoteTrigger, c2p, remoteRun) 132 | 133 | emit c2p.remoteTrigger() 134 | 135 | for i in 1..100_000_000: 136 | if threadBRemoteReady.load() != 0: break 137 | doAssert i != 100_000_000 138 | 139 | check threadBRemoteReady.load() == 1 140 | threadBRemoteReady.store 0 141 | 142 | GC_fullCollect() 143 | 144 | test "connect actionB on threadC to globalCounter": 145 | proc remoteTrigger(counter: AgentProxy[SomeTrigger]) {.signal.} 146 | 147 | proc remoteSetup(self: SomeTrigger) {.slot.} = 148 | os.sleep(10) 149 | echo "REMOTE RUN!" 150 | let localCounterProxy = lookupAgentProxy(sn"globalCounter", Counter) 151 | if localCounterProxy != nil: 152 | echo "connecting: ", self.unsafeWeakRef(), " to: ", localCounterProxy.remote, " th: ", " (th: ", getThreadId(), ")" 153 | connectThreaded(self, valueChanged, localCounterProxy, setValue(Counter)) 154 | #connect(self, valueChanged, self, valuePrint(SomeTrigger)) 155 | threadBRemoteReady.store 1 156 | else: 157 | threadBRemoteReady.store 2 158 | 159 | var actionC = SomeTrigger.new() 160 | #connect(actionC, valueChanged, actionC, valuePrint(SomeTrigger)) 161 | actionCProx = actionC.moveToThread(threadC) 162 | echo "obj actionBProx: ", actionCProx.getSigilId() 163 | 164 | connectThreaded(actionCProx, remoteTrigger, actionCProx, remoteSetup) 165 | 166 | threadBRemoteReady.store 0 167 | emit actionCProx.remoteTrigger() 168 | 169 | for i in 1..100_000_000: 170 | if threadBRemoteReady.load() == 1: break 171 | 172 | check threadBRemoteReady.load() == 1 173 | 174 | GC_fullCollect() 175 | 176 | test "ensure globalCounter update updates target2": 177 | echo "main thread: ", " (th: ", getThreadId(), ")" 178 | proc valueChanged(st: AgentProxy[SomeTrigger], val: int) {.signal.} 179 | 180 | proc setValue(c2: SomeTrigger, val: int) {.slot.} = 181 | emit c2.valueChanged(val) 182 | 183 | threadBRemoteReady.store 0 184 | connect(actionCProx, valueChanged, actionCProx, setValue(SomeTrigger)) 185 | printConnections(actionCProx) 186 | emit actionCProx.valueChanged(1010) 187 | 188 | for i in 1..1_000: 189 | os.sleep(1) 190 | if threadBRemoteReady.load() == 3: break 191 | 192 | check threadBRemoteReady.load() == 3 193 | 194 | -------------------------------------------------------------------------------- /sigils/reactive.nim: -------------------------------------------------------------------------------- 1 | import sigils/signals 2 | import sigils/slots 3 | import sigils/core 4 | import std/[sets, hashes] 5 | 6 | export signals, slots, core 7 | 8 | type 9 | SigilAttributes* = enum 10 | Dirty 11 | Lazy 12 | Changed 13 | 14 | SigilBase* = ref object of Agent 15 | attrs: set[SigilAttributes] 16 | fn: proc (arg: SigilBase) {.closure.} 17 | 18 | SigilEffect* = ref object of SigilBase 19 | 20 | Sigil*[T] = ref object of SigilBase 21 | ## Core *reactive* data type for doing reactive style programming 22 | ## akin to RXJS, React useState, Svelte, etc. 23 | ## 24 | ## This builds on the core signals and slots but provides a 25 | ## higher level API for working with propagating values. 26 | val: T 27 | 28 | SigilEffectRegistry* = ref object of Agent 29 | effects: HashSet[SigilEffect] 30 | 31 | proc `$`*(s: SigilBase): string = 32 | result = "Sigil" 33 | result &= $s.attrs 34 | 35 | proc `$`*[T](s: Sigil[T]): string = 36 | result &= $(SigilBase(s)) 37 | result &= "[" 38 | result &= $(T) 39 | result &= "]" 40 | result &= "(" 41 | result &= $(s.val) 42 | result &= ")" 43 | 44 | proc isDirty*(s: SigilBase): bool = 45 | s.attrs.contains(Dirty) 46 | proc isLazy*(s: SigilBase): bool = 47 | s.attrs.contains(Lazy) 48 | 49 | proc change*(s: SigilBase, attrs: set[SigilAttributes]) {.signal.} 50 | ## core reactive signal type 51 | 52 | 53 | proc near*[T](a, b: T): bool = 54 | let diff = abs(a-b) 55 | when T is float or T is float32: 56 | let eps = 1.0e-5 57 | elif T is float64: 58 | let eps = 1.0e-10 59 | result = diff <= eps 60 | 61 | proc setValue*[T](s: Sigil[T], val: T) {.slot.} = 62 | ## slot to update sigil values, synonym of `<-` 63 | mixin near 64 | when T is SomeFloat: 65 | if not near(s.val, val): 66 | s.val = val 67 | s.attrs.excl(Dirty) 68 | emit s.change({Dirty}) 69 | else: 70 | if s.val != val: 71 | s.val = val 72 | s.attrs.excl(Dirty) 73 | emit s.change({Changed}) 74 | 75 | proc compute*(sigil: SigilBase) {.slot.} = 76 | if sigil.isLazy() and sigil.isDirty(): 77 | sigil.fn(sigil) 78 | sigil.attrs.excl(Dirty) 79 | 80 | proc recompute*(sigil: SigilBase, attrs: set[SigilAttributes]) {.slot.} = 81 | ## default slot for updating sigils 82 | ## when `change` is emitted 83 | assert sigil.fn != nil 84 | if Lazy in sigil.attrs: 85 | sigil.attrs.incl Dirty 86 | sigil.attrs.incl({Changed} * attrs) 87 | emit sigil.change({Dirty}) 88 | else: 89 | sigil.fn(sigil) 90 | 91 | proc `<-`*[T](s: Sigil[T], val: T) = 92 | ## update a static (non-computed) sigils value 93 | s.setValue(val) 94 | 95 | import macros 96 | 97 | var enableSigilBinding* {.compileTime.}: seq[bool] = @[false] 98 | 99 | template getInternalSigilIdent*(): untyped = 100 | ## overridable template to provide the ident 101 | ## that `{}` uses to look for the current 102 | ## scoped to operate on – if one exists in 103 | ## this scope 104 | ## 105 | ## for example `internalSigil` is used as the 106 | ## default identifier in `computed` block to 107 | ## connect dereferenced sigils to 108 | internalSigil 109 | 110 | template bindSigilEvents*(blk: untyped): auto = 111 | static: enableSigilBinding.add true 112 | `blk` 113 | static: discard enableSigilBinding.pop() 114 | 115 | template bindSigilEvents*(sigilIdent, blk: untyped): auto = 116 | template getInternalSigilIdent(): untyped = 117 | sigilIdent 118 | static: enableSigilBinding.add true 119 | `blk` 120 | static: discard enableSigilBinding.pop() 121 | 122 | template unBindSigilEvents*(blk: untyped): auto = 123 | static: enableSigilBinding.add false 124 | `blk` 125 | static: discard enableSigilBinding.pop() 126 | 127 | template `{}`*[T](sigil: Sigil[T]): auto {.inject.} = 128 | ## deferences a typed Sigil to get it's value 129 | ## either from static sigils or computed sigils 130 | mixin getInternalSigilIdent 131 | when enableSigilBinding[^1]: 132 | sigil.connect(change, getInternalSigilIdent(), recompute) 133 | if Dirty in sigil.attrs: 134 | sigil.fn(sigil) 135 | sigil.attrs.excl(Dirty) 136 | sigil.val 137 | 138 | proc newSigil*[T](value: T): Sigil[T] = 139 | ## create a new sigil 140 | result = Sigil[T](val: value) 141 | 142 | template computedImpl[T](lazy, blk: untyped): Sigil[T] = 143 | block: 144 | let res = Sigil[T]() 145 | res.fn = proc(arg: SigilBase) {.closure.} = 146 | bindSigilEvents: 147 | let internalSigil {.inject.} = Sigil[T](arg) 148 | let val = block: 149 | `blk` 150 | internalSigil.setValue(val) 151 | if lazy: res.attrs.incl Lazy 152 | res.recompute({}) 153 | res 154 | 155 | template computedNow*[T](blk: untyped): Sigil[T] = 156 | ## returns a `computed` sigil that is eagerly evaluated 157 | computedImpl[T](false, blk) 158 | 159 | template computed*[T](blk: untyped): Sigil[T] = 160 | ## returns a `computed` sigil that is lazily evaluated 161 | computedImpl[T](true, blk) 162 | 163 | template `<==`*[T](tp: typedesc[T], blk: untyped): Sigil[T] = 164 | ## TODO: keep something like this? 165 | computedImpl[T](true, blk) 166 | 167 | 168 | proc registerEffect*(agent: Agent, s: SigilEffect) {.signal.} 169 | ## core signal for registering new effects 170 | 171 | proc triggerEffects*(agent: Agent) {.signal.} 172 | ## core signal for trigger effects 173 | 174 | iterator registered*(r: SigilEffectRegistry): SigilBase = 175 | for eff in r.effects: 176 | yield eff 177 | 178 | iterator dirty*(r: SigilEffectRegistry): SigilBase = 179 | for eff in r.effects: 180 | if Dirty in eff.attrs: 181 | yield eff 182 | 183 | proc onRegister*(reg: SigilEffectRegistry, s: SigilEffect) {.slot.} = 184 | reg.effects.incl(s) 185 | 186 | proc onTriggerEffects*(reg: SigilEffectRegistry) {.slot.} = 187 | for eff in reg.dirty: 188 | eff.compute() 189 | 190 | proc initSigilEffectRegistry*(): SigilEffectRegistry = 191 | result = SigilEffectRegistry(effects: initHashSet[SigilEffect]()) 192 | connect(result, registerEffect, result, onRegister) 193 | connect(result, triggerEffects, result, onTriggerEffects) 194 | 195 | 196 | template getSigilEffectsRegistry*(): untyped = 197 | ## identifier that is messaged with a new effect 198 | ## when it's created 199 | internalSigilEffectRegistry 200 | 201 | proc computeDeps(sigil: SigilEffect) = 202 | ## compute any Sigils we're listening to 203 | ## in order to trigger any chagnes 204 | for listened in sigil.listening: 205 | if listened[] of SigilBase: 206 | withRef(listened, item): 207 | let sh = SigilBase(item) 208 | sh.compute() 209 | 210 | template effect*(blk: untyped) = 211 | ## Creates a new fSigilEfect that is lazily 212 | ## evaluated whenever `triggerEffects` is sent to 213 | ## the SigilEffectRegistry in scope. 214 | ## 215 | ## The SigilEffectRegistry is gotten by 216 | ## `getSigilEffectsRegistry()` and can be overriden 217 | ## to provide a custom registry. 218 | ## 219 | let res = SigilEffect() 220 | when defined(sigilsDebug): 221 | res.debugName = "EFF" 222 | res.fn = proc(arg: SigilBase) {.closure.} = 223 | let internalSigil {.inject.} = SigilEffect(arg) 224 | bindSigilEvents: 225 | internalSigil.computeDeps() 226 | if Changed in internalSigil.attrs: 227 | `blk` 228 | internalSigil.attrs.excl {Dirty, ChangeD} 229 | # internalSigil.vhash = internalSigil.computeHash() 230 | res.attrs.incl {Dirty, Lazy, Changed} 231 | res.compute() 232 | emit getSigilEffectsRegistry().registerEffect(res) 233 | -------------------------------------------------------------------------------- /sigils/threadSelectors.nim: -------------------------------------------------------------------------------- 1 | import std/sets 2 | import std/isolation 3 | import std/locks 4 | import threading/smartptrs 5 | import threading/channels 6 | import threading/atomics 7 | 8 | import std/os 9 | import std/options 10 | import std/isolation 11 | import std/selectors 12 | import std/times 13 | import std/tables 14 | import std/net 15 | 16 | import agents 17 | import threadBase 18 | import threadDefault 19 | import core 20 | 21 | export smartptrs, isolation 22 | export threadBase 23 | 24 | type 25 | SigilSelectorThread* = object of SigilThread 26 | inputs*: SigilChan 27 | sel*: Selector[SigilThreadEvent] 28 | drain*: Atomic[bool] 29 | isReady*: bool 30 | thr*: Thread[ptr SigilSelectorThread] 31 | timerLock*: Lock 32 | 33 | SigilSelectorThreadPtr* = ptr SigilSelectorThread 34 | 35 | type 36 | SigilSocketEvent* = ref object of SigilThreadEvent 37 | fd*: int 38 | 39 | SigilSelectEvent* = ref object of SigilThreadEvent 40 | ## Wrapper for std/selectors SelectEvent so it can 41 | ## participate in the Sigils signaling system. 42 | evt*: SelectEvent 43 | 44 | proc dataReady*(ev: SigilSocketEvent) {.signal.} 45 | proc selectReady*(ev: SigilSelectEvent) {.signal.} 46 | proc selectEvent*(ev: SigilSelectEvent) {.signal.} 47 | 48 | proc newSigilSocketEvent*( 49 | thread: SigilSelectorThreadPtr, fd: int | Socket 50 | ): SigilSocketEvent {.gcsafe.} = 51 | ## Register a file/socket descriptor with the selector so that when it 52 | ## becomes readable, a `dataReady` signal is emitted on `ev`. 53 | when fd is Socket: 54 | let fd = fd.getFd().int 55 | result.new() 56 | result.fd = fd 57 | registerHandle(thread.sel, fd, {Event.Read}, SigilThreadEvent(result)) 58 | 59 | proc newSigilSelectEvent*( 60 | thread: SigilSelectorThreadPtr, event = newSelectEvent() 61 | ): SigilSelectEvent {.gcsafe.} = 62 | ## Register a custom std/selectors SelectEvent with this selector 63 | ## thread and emit `selectEvent` (and `selectReady` for 64 | ## compatibility) when it is triggered. 65 | result.new() 66 | result.evt = event 67 | registerEvent(thread.sel, event, SigilThreadEvent(result)) 68 | 69 | proc newSigilSelectorThread*(): ptr SigilSelectorThread = 70 | result = cast[ptr SigilSelectorThread](allocShared0(sizeof( 71 | SigilSelectorThread))) 72 | result[] = SigilSelectorThread() # important! 73 | result[].sel = newSelector[SigilThreadEvent]() 74 | result[].agent = ThreadAgent() 75 | result[].signaledLock.initLock() 76 | result[].timerLock.initLock() 77 | result[].inputs = newSigilChan() 78 | result[].running.store(true, Relaxed) 79 | result[].drain.store(true, Relaxed) 80 | 81 | method send*( 82 | thread: SigilSelectorThreadPtr, msg: sink ThreadSignal, 83 | blocking: BlockingKinds 84 | ) {.gcsafe.} = 85 | var msg = isolateRuntime(msg) 86 | case blocking 87 | of Blocking: 88 | thread.inputs.send(msg) 89 | of NonBlocking: 90 | let sent = thread.inputs.trySend(msg) 91 | if not sent: 92 | raise newException(MessageQueueFullError, "could not send!") 93 | 94 | method recv*( 95 | thread: SigilSelectorThreadPtr, msg: var ThreadSignal, 96 | blocking: BlockingKinds 97 | ): bool {.gcsafe.} = 98 | case blocking 99 | of Blocking: 100 | msg = thread.inputs.recv() 101 | return true 102 | of NonBlocking: 103 | result = thread.inputs.tryRecv(msg) 104 | 105 | method setTimer*( 106 | thread: SigilSelectorThreadPtr, timer: SigilTimer 107 | ) {.gcsafe.} = 108 | ## Schedule a timer on this selector-backed thread using selector timers. 109 | let durMs = max(timer.duration.inMilliseconds(), 1) 110 | withLock thread.timerLock: 111 | discard thread.sel.registerTimer(durMs.int, true, timer) 112 | 113 | proc pumpTimers(thread: SigilSelectorThreadPtr, timeoutMs: int) {.gcsafe.} = 114 | ## Wait up to timeoutMs and deliver any due timers via selector events. 115 | var keys = newSeq[ReadyKey](32) 116 | let n = thread.sel.selectInto(timeoutMs, keys) 117 | for i in 0 ..< n: 118 | let k = keys[i] 119 | # Each key corresponds to a fired selector event with associated 120 | # application data stored as a SigilThreadEvent (either SigilTimer or 121 | # SigilSocketEvent). 122 | let ev = getData(thread.sel, k.fd) 123 | if ev.isNil: 124 | continue 125 | 126 | if ev of SigilTimer: 127 | let tt = SigilTimer(ev) 128 | if thread.hasCancelTimer(tt): 129 | thread.removeTimer(tt) 130 | continue 131 | emit tt.timeout() 132 | 133 | # Reschedule if needed 134 | let dur = max(tt.duration.inMilliseconds(), 1).int 135 | if tt.isRepeat(): 136 | discard thread.sel.registerTimer(dur, true, tt) 137 | else: 138 | if tt.count > 0: 139 | tt.count.dec() 140 | if tt.count != 0: # schedule again while count remains 141 | discard thread.sel.registerTimer(dur, true, tt) 142 | elif ev of SigilSocketEvent: 143 | let dr = SigilSocketEvent(ev) 144 | # Only emit when the descriptor is readable. 145 | if Event.Read in k.events: 146 | emit dr.dataReady() 147 | elif ev of SigilSelectEvent: 148 | let se = SigilSelectEvent(ev) 149 | # Forward selector events into the Sigils signal system. 150 | emit se.selectEvent() 151 | emit se.selectReady() 152 | 153 | method poll*( 154 | thread: SigilSelectorThreadPtr, blocking: BlockingKinds = Blocking 155 | ): bool {.gcsafe, discardable.} = 156 | ## Process at most one message. For Blocking, wait briefly using 157 | ## selectInto then try a non-blocking recv to avoid hanging when idle. 158 | var sig: ThreadSignal 159 | case blocking 160 | of Blocking: 161 | # Check timers immediately to avoid missing short intervals. 162 | thread.pumpTimers(0) 163 | thread.pumpTimers(2) # brief wait in milliseconds 164 | if thread.recv(sig, NonBlocking): 165 | thread.exec(sig) 166 | result = true 167 | of NonBlocking: 168 | thread.pumpTimers(0) 169 | if thread.recv(sig, NonBlocking): 170 | thread.exec(sig) 171 | result = true 172 | 173 | proc runSelectorThread*(targ: SigilSelectorThreadPtr) {.thread.} = 174 | {.cast(gcsafe).}: 175 | doAssert not hasLocalSigilThread() 176 | setLocalSigilThread(targ) 177 | targ[].threadId.store(getThreadId(), Relaxed) 178 | emit targ[].agent.started() 179 | # Run until stopped; use selector timers to provide a light sleep between polls. 180 | while targ.isRunning(): 181 | targ.pumpTimers(0) 182 | # drain any queued signals first 183 | while targ.poll(NonBlocking): 184 | discard 185 | # brief wait so we're not busy-spinning 186 | targ.pumpTimers(5) 187 | # final drain if requested (mirrors async variant's behavior) 188 | try: 189 | if targ.drain.load(Relaxed): 190 | while targ.poll(NonBlocking): 191 | discard 192 | except CatchableError: 193 | discard 194 | 195 | proc start*(thread: ptr SigilSelectorThread) = 196 | if thread[].exceptionHandler.isNil: 197 | thread[].exceptionHandler = defaultExceptionHandler 198 | createThread(thread[].thr, runSelectorThread, thread) 199 | 200 | proc stop*(thread: ptr SigilSelectorThread, immediate: bool = false, 201 | drain: bool = false) = 202 | thread[].running.store(false, Relaxed) 203 | thread[].drain.store(drain or immediate, Relaxed) 204 | 205 | proc join*(thread: ptr SigilSelectorThread) = 206 | doAssert not thread.isNil() 207 | thread[].thr.joinThread() 208 | 209 | proc peek*(thread: ptr SigilSelectorThread): int = 210 | result = thread[].inputs.peek() 211 | -------------------------------------------------------------------------------- /tests/tslotsThreadSelectors.nim: -------------------------------------------------------------------------------- 1 | import std/[unittest, times, net, strutils, os, posix, selectors] 2 | import sigils 3 | import sigils/threadSelectors 4 | 5 | type 6 | SomeAction* = ref object of Agent 7 | value: int 8 | 9 | Counter* = ref object of Agent 10 | value: int 11 | 12 | DataWatcher* = ref object of Agent 13 | hits: int 14 | 15 | proc valueChanged*(tp: SomeAction, val: int) {.signal.} 16 | proc updated*(tp: Counter, final: int) {.signal.} 17 | proc updates*(tp: AgentProxy[Counter], final: int) {.signal.} 18 | 19 | proc setValue*(self: Counter, value: int) {.slot.} = 20 | echo "setValue: ", value, " (" & $getThreadId() & ")" 21 | self.value = value 22 | emit self.updated(value) 23 | 24 | proc timerRun*(self: Counter) {.slot.} = 25 | self.value.inc() 26 | echo "timeout! value: ", self.value 27 | emit self.updated(self.value) 28 | 29 | proc completed*(self: SomeAction, final: int) {.slot.} = 30 | echo "completed: ", final, " (" & $getThreadId() & ")" 31 | self.value = final 32 | 33 | proc value*(self: Counter): int = 34 | self.value 35 | 36 | proc onReady*(self: DataWatcher) {.slot.} = 37 | self.hits.inc() 38 | 39 | suite "threaded agent slots (selectors)": 40 | teardown: 41 | GC_fullCollect() 42 | 43 | test "sigil object selectors thread runner": 44 | var 45 | a = SomeAction() 46 | b = Counter() 47 | 48 | let thread = newSigilSelectorThread() 49 | thread.start() 50 | startLocalThreadDefault() 51 | 52 | let bp: AgentProxy[Counter] = b.moveToThread(thread) 53 | connectThreaded(a, valueChanged, bp, setValue) 54 | connectThreaded(bp, updated, a, SomeAction.completed()) 55 | 56 | emit a.valueChanged(314) 57 | check a.value == 0 58 | let ct = getCurrentSigilThread() 59 | discard ct.poll() 60 | check a.value == 314 61 | 62 | thread.stop() 63 | thread.join() 64 | 65 | test "local selectors thread type": 66 | setLocalSigilThread(newSigilSelectorThread()) 67 | let ct = getCurrentSigilThread() 68 | check ct of SigilSelectorThreadPtr 69 | discard ct.poll() 70 | discard ct.poll(NonBlocking) 71 | check ct.pollAll() == 0 72 | 73 | test "remote selectors thread trigger using local proxy": 74 | var a = SomeAction() 75 | var b = Counter() 76 | 77 | let thread = newSigilSelectorThread() 78 | thread.start() 79 | startLocalThreadDefault() 80 | 81 | let bp: AgentProxy[Counter] = b.moveToThread(thread) 82 | connectThreaded(a, valueChanged, bp, Counter.setValue()) 83 | 84 | check Counter(bp.remote[]).value == 0 85 | emit a.valueChanged(1337) 86 | 87 | let ct = getCurrentSigilThread() 88 | discard ct.poll() 89 | for i in 1..10: 90 | os.sleep(100) 91 | echo "test... value: ", Counter(bp.remote[]).value 92 | if Counter(bp.remote[]).value != 0: 93 | break 94 | 95 | check Counter(bp.remote[]).value == 1337 96 | 97 | thread.stop() 98 | thread.join() 99 | 100 | test "local selectors thread timer": 101 | setLocalSigilThread(newSigilSelectorThread()) 102 | let ct = getCurrentSigilThread() 103 | check ct of SigilSelectorThreadPtr 104 | 105 | var timer = newSigilTimer(duration = initDuration(milliseconds = 2)) 106 | var a = Counter() 107 | connect(timer, timeout, a, Counter.timerRun()) 108 | 109 | start(timer) 110 | 111 | discard ct.poll(NonBlocking) 112 | check a.value == 0 113 | 114 | for i in 1 .. 100: 115 | discard ct.poll() 116 | os.sleep(2) 117 | if a.value >= 1: break 118 | check a.value >= 1 119 | 120 | cancel(timer) 121 | discard ct.poll() 122 | 123 | test "remote selectors thread timer": 124 | var b = Counter() 125 | 126 | let thread = newSigilSelectorThread() 127 | thread.start() 128 | startLocalThreadDefault() 129 | 130 | let bp: AgentProxy[Counter] = b.moveToThread(thread) 131 | 132 | var timer = newSigilTimer(duration = initDuration(milliseconds = 10), count = 2) 133 | connectThreaded(timer, timeout, bp, Counter.timerRun()) 134 | start(timer, thread) 135 | 136 | let ct = getCurrentSigilThread() 137 | # Drain local default thread to deliver remote->local proxy Trigger events 138 | for i in 0 .. 20: 139 | discard ct.poll() 140 | os.sleep(10) 141 | 142 | check Counter(bp.remote[]).value >= 2 143 | 144 | cancel(timer, thread) 145 | thread.stop() 146 | thread.join() 147 | 148 | test "selectors dataReady for socket handle": 149 | ## Verify that registering a SigilSocketEvent with the selector 150 | ## results in a dataReady signal when the underlying socket 151 | ## becomes readable. 152 | setLocalSigilThread(newSigilSelectorThread()) 153 | let ct = getCurrentSigilThread() 154 | check ct of SigilSelectorThreadPtr 155 | 156 | let st = SigilSelectorThreadPtr(ct) 157 | 158 | var fds: array[0..1, cint] 159 | let res = socketpair(AF_UNIX, SOCK_STREAM, 0.cint, fds) 160 | check res == 0 161 | 162 | var watcher = DataWatcher() 163 | var ready = newSigilSocketEvent(st, fds[0].int) 164 | 165 | connect(ready, dataReady, watcher, DataWatcher.onReady()) 166 | 167 | # No data written yet; polling should not trigger the watcher. 168 | discard ct.poll(NonBlocking) 169 | check watcher.hits == 0 170 | 171 | let msg = "hello" 172 | let written = write(fds[1], cast[pointer](msg.cstring), msg.len) 173 | check written == msg.len 174 | 175 | var attempts = 0 176 | while watcher.hits == 0 and attempts < 50: 177 | discard ct.poll() 178 | os.sleep(2) 179 | attempts.inc() 180 | 181 | check watcher.hits >= 1 182 | 183 | discard close(fds[0]) 184 | discard close(fds[1]) 185 | 186 | test "selectors dataReady for readable socket": 187 | ## Similar to the previous test, but verifies the overload that 188 | ## accepts a high-level Socket rather than a raw file descriptor. 189 | setLocalSigilThread(newSigilSelectorThread()) 190 | let ct = getCurrentSigilThread() 191 | check ct of SigilSelectorThreadPtr 192 | let st = SigilSelectorThreadPtr(ct) 193 | 194 | var fds: array[0..1, cint] 195 | let res = socketpair(AF_UNIX, SOCK_STREAM, 0.cint, fds) 196 | check res == 0 197 | 198 | # Wrap the read end of the socketpair in a Nim Socket. 199 | var sock = newSocket(SocketHandle(fds[0]), Domain.AF_UNIX, 200 | SockType.SOCK_STREAM, Protocol.IPPROTO_TCP, buffered = false) 201 | 202 | var watcher = DataWatcher() 203 | var ready = newSigilSocketEvent(st, sock) 204 | 205 | connect(ready, dataReady, watcher, DataWatcher.onReady()) 206 | 207 | # No data written yet; polling should not trigger the watcher. 208 | discard ct.poll(NonBlocking) 209 | check watcher.hits == 0 210 | 211 | let msg = "ping" 212 | let written = write(fds[1], cast[pointer](msg.cstring), msg.len) 213 | check written == msg.len 214 | 215 | var attempts = 0 216 | while watcher.hits == 0 and attempts < 50: 217 | discard ct.poll() 218 | os.sleep(2) 219 | attempts.inc() 220 | 221 | check watcher.hits >= 1 222 | 223 | discard close(fds[1]) 224 | 225 | test "selectors custom SelectEvent emits selectEvent": 226 | ## Verify that a custom std/selectors SelectEvent registered via 227 | ## newSigilSelectEvent results in the SigilSelectEvent's 228 | ## selectEvent signal being emitted when triggered. 229 | setLocalSigilThread(newSigilSelectorThread()) 230 | let ct = getCurrentSigilThread() 231 | check ct of SigilSelectorThreadPtr 232 | let st = SigilSelectorThreadPtr(ct) 233 | 234 | var watcher = DataWatcher() 235 | let sev = newSigilSelectEvent(st) 236 | 237 | connect(sev, selectEvent, watcher, DataWatcher.onReady()) 238 | 239 | # No trigger yet; polling should not increment hitCount. 240 | discard ct.poll(NonBlocking) 241 | check watcher.hits == 0 242 | 243 | # Trigger the underlying std/selectors SelectEvent. 244 | sev.evt.trigger() 245 | 246 | var attempts2 = 0 247 | while watcher.hits == 0 and attempts2 < 50: 248 | discard ct.poll() 249 | os.sleep(2) 250 | attempts2.inc() 251 | 252 | check watcher.hits >= 1 253 | -------------------------------------------------------------------------------- /tests/tslots.nim: -------------------------------------------------------------------------------- 1 | import sigils/signals 2 | import sigils/slots 3 | import sigils/core 4 | 5 | import std/monotimes 6 | 7 | type 8 | Counter* = ref object of Agent 9 | value: int 10 | avg: int 11 | 12 | Originator* = ref object of Agent 13 | 14 | CounterWithDestroy* = ref object of Agent 15 | value: int 16 | avg: int 17 | 18 | proc `=destroy`*(x: var typeof(CounterWithDestroy()[])) = 19 | when defined(sigilsDebug): 20 | echo "CounterWithDestroy:destroy: ", x.debugName 21 | destroyAgent(x) 22 | 23 | proc change*(tp: Originator, val: int) {.signal.} 24 | 25 | proc valueChanged*(tp: Counter, val: int) {.signal.} 26 | proc valueChanged*(tp: CounterWithDestroy, val: int) {.signal.} 27 | 28 | proc someChange*(tp: Counter) {.signal.} 29 | 30 | proc avgChanged*(tp: Counter, val: float) {.signal.} 31 | 32 | proc setValue*(self: Counter, value: int) {.slot.} = 33 | echo "setValue! ", value 34 | if self.value != value: 35 | self.value = value 36 | emit self.valueChanged(value) 37 | 38 | proc setValue*(self: CounterWithDestroy, value: int) {.slot.} = 39 | echo "setValue! ", value 40 | if self.value != value: 41 | self.value = value 42 | emit self.valueChanged(value) 43 | 44 | proc setSomeValue*(self: Counter, value: int) = 45 | echo "setValue! ", value 46 | if self.value != value: 47 | self.value = value 48 | emit self.valueChanged(value) 49 | 50 | proc someAction*(self: Counter) {.slot.} = 51 | echo "action" 52 | self.avg = -1 53 | 54 | proc someOtherAction*(self: Counter) {.slot.} = 55 | echo "action" 56 | self.avg = -1 57 | 58 | proc value*(self: Counter): int = 59 | self.value 60 | 61 | proc doTick*(fig: Counter, tickCount: int, now: MonoTime) {.signal.} 62 | 63 | proc someTick*(self: Counter, tick: int, now: MonoTime) {.slot.} = 64 | echo "tick: ", tick, " now: ", now 65 | self.avg = now.ticks 66 | proc someTickOther*(self: Counter, tick: int, now: MonoTime) {.slot.} = 67 | echo "tick: ", tick, " now: ", now 68 | 69 | when isMainModule: 70 | import unittest 71 | import std/sequtils 72 | 73 | suite "agent slots": 74 | setup: 75 | var 76 | a {.used.} = Counter() 77 | b {.used.} = Counter() 78 | c {.used.} = Counter() 79 | d {.used.} = Counter() 80 | o {.used.} = Originator() 81 | 82 | when defined(sigilsDebug): 83 | a.debugName = "A" 84 | b.debugName = "B" 85 | c.debugName = "C" 86 | d.debugName = "D" 87 | o.debugName = "O" 88 | 89 | teardown: 90 | GC_fullCollect() 91 | 92 | test "signal / slot types": 93 | check SignalTypes.avgChanged(Counter) is (float,) 94 | check SignalTypes.valueChanged(Counter) is (int,) 95 | echo "someChange: ", SignalTypes.someChange(Counter).typeof.repr 96 | check SignalTypes.someChange(Counter) is tuple[] 97 | check SignalTypes.setValue(Counter) is (int,) 98 | 99 | test "signal connect": 100 | echo "Counter.setValue: ", Counter.setValue().repr 101 | connect(a, valueChanged, b, setValue) 102 | connect(a, valueChanged, c, Counter.setValue) 103 | connect(a, valueChanged, c, setValue Counter) 104 | check not (compiles(connect(a, someAction, c, Counter.setValue))) 105 | 106 | check b.value == 0 107 | check c.value == 0 108 | check d.value == 0 109 | 110 | emit a.valueChanged(137) 111 | 112 | check a.value == 0 113 | check b.value == 137 114 | check c.value == 137 115 | check d.value == 0 116 | 117 | emit a.someChange() 118 | connect(a, someChange, c, Counter.someAction) 119 | 120 | test "basic signal connect": 121 | # TODO: how to do this? 122 | echo "done" 123 | connect(a, valueChanged, b, setValue) 124 | connect(a, valueChanged, c, Counter.setValue) 125 | 126 | check a.value == 0 127 | check b.value == 0 128 | check c.value == 0 129 | 130 | a.setValue(42) 131 | check a.value == 42 132 | check b.value == 42 133 | check c.value == 42 134 | echo "TEST REFS: ", 135 | " aref: ", 136 | cast[pointer](a).repr, 137 | " 0x", 138 | addr(a[]).pointer.repr, 139 | " agent: 0x", 140 | addr(Agent(a)).pointer.repr 141 | check a.unsafeWeakRef().toPtr == cast[pointer](a) 142 | check a.unsafeWeakRef().toPtr == addr(a[]).pointer 143 | 144 | test "differing agents, same sigs": 145 | # TODO: how to do this? 146 | echo "done" 147 | connect(o, change, b, setValue) 148 | 149 | check b.value == 0 150 | 151 | emit o.change(42) 152 | 153 | check b.value == 42 154 | 155 | test "connect type errors": 156 | check not compiles(connect(a, avgChanged, c, setValue)) 157 | 158 | test "signal connect reg proc": 159 | # TODO: how to do this? 160 | static: 161 | echo "\n\n\nREG PROC" 162 | # let sv: proc (self: Counter, value: int) = Counter.setValue 163 | check not compiles(connect(a, valueChanged, b, setSomeValue)) 164 | 165 | test "empty signal conversion": 166 | connect(a, valueChanged, c, someAction, acceptVoidSlot = true) 167 | 168 | check connected(a, valueChanged) 169 | check not connected(a, someChange) 170 | check not connected(a, valueChanged, b) 171 | check connected(a, valueChanged, c) 172 | check connected(a, valueChanged, c, someAction) 173 | check not connected(a, valueChanged, b, someAction) 174 | check not connected(a, valueChanged, c, someOtherAction) 175 | 176 | a.setValue(42) 177 | 178 | check a.value == 42 179 | check c.avg == -1 180 | 181 | test "test multiarg": 182 | connect(a, doTick, c, someTick) 183 | 184 | let ts = getMonoTime() 185 | emit a.doTick(123, ts) 186 | 187 | check c.avg == ts.ticks 188 | 189 | test "test disconnect": 190 | connect(a, doTick, c, someTick) 191 | connect(a, doTick, c, someTickOther) 192 | connect(a, valueChanged, b, setValue) 193 | 194 | check c.listening.len() == 1 195 | check a.subcriptions.len() == 3 196 | check a.getSubscriptions(sigName"doTick").toSeq().len() == 2 197 | 198 | printConnections(a) 199 | printConnections(c) 200 | disconnect(a, doTick, c, someTick) 201 | 202 | emit a.valueChanged(137) 203 | echo "afert disconnect" 204 | printConnections(a) 205 | printConnections(c) 206 | check a.value == 0 207 | check a.subcriptions.len() == 2 208 | check a.getSubscriptions(sigName"doTick").toSeq().len() == 1 209 | check b.value == 137 210 | check c.listening.len() == 1 211 | 212 | let ts = getMonoTime() 213 | emit a.doTick(123, ts) 214 | 215 | check c.avg == 0 216 | 217 | test "test disconnect all for sig": 218 | connect(a, doTick, c, someTick) 219 | connect(a, doTick, c, someTickOther) 220 | connect(a, valueChanged, b, setValue) 221 | 222 | disconnect(a, doTick, c) 223 | 224 | printConnections(a) 225 | printConnections(c) 226 | emit a.valueChanged(137) 227 | check a.value == 0 228 | check a.subcriptions.len() == 1 229 | check a.getSubscriptions(sigName"doTick").toSeq().len() == 0 # maybe? 230 | check b.value == 137 231 | check c.listening.len() == 0 232 | 233 | let ts = getMonoTime() 234 | emit a.doTick(123, ts) 235 | 236 | check c.avg == 0 237 | 238 | test "test multi connect destroyed": 239 | connect(a, doTick, c, someTick) 240 | connect(c, doTick, a, someTickOther) 241 | connect(a, doTick, c, someTickOther) 242 | connect(a, valueChanged, c, setValue) 243 | connect(c, valueChanged, a, setValue) 244 | 245 | # printConnections(a) 246 | # printConnections(c) 247 | 248 | test "test multi connect disconnect without connecting": 249 | disconnect(a, doTick, c, someTick) 250 | disconnect(a, doTick, d, someTick) 251 | 252 | # printConnections(a) 253 | # printConnections(c) 254 | 255 | suite "test destroys": 256 | test "test multi connect disconnect with destroyed": 257 | var 258 | b = Counter() 259 | 260 | block: 261 | var 262 | awd {.used.} = CounterWithDestroy() 263 | 264 | when defined(sigilsDebug): 265 | awd.debugName = "AWD" 266 | b.debugName = "B" 267 | connect(awd, valueChanged, b, setValue) 268 | 269 | check b.value == 0 270 | 271 | awd.setValue(42) 272 | check awd.value == 42 273 | check b.value == 42 274 | echo "TEST REFS: ", 275 | " aref: ", 276 | cast[pointer](awd).repr, 277 | " 0x", 278 | addr(awd[]).pointer.repr, 279 | " agent: 0x", 280 | addr(Agent(awd)).pointer.repr 281 | check awd.unsafeWeakRef().toPtr == cast[pointer](awd) 282 | check awd.unsafeWeakRef().toPtr == addr(awd[]).pointer 283 | 284 | check b.subcriptions.len() == 0 285 | check b.listening.len() == 0 286 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sigils 2 | 3 | A [signal and slots library](https://en.wikipedia.org/wiki/Signals_and_slots) implemented for the Nim programming language. The signals and slots are type checked and implemented purely in Nim. It can be used for event based programming both with GUIs or standalone. 4 | 5 | > Signals and slots is a language construct introduced in Qt for communication between objects which makes it easy to implement the observer pattern while avoiding boilerplate code. The concept is that GUI widgets, and other objects, can send signals containing event information which can be received by other objects using special member functions known as slots. This is similar to C/C++ function pointers, but the signal/slot system ensures the type-correctness of callback arguments. 6 | > - Wikipedia 7 | 8 | Note that this implementation shares many or most of the limitations you'd see in Qt's implementation. Sigils currently only has basic multi-threading, but I hope to expand support over time. 9 | 10 | ## Basics 11 | 12 | Only objects inheriting from `Agent` can recieve signals. Slots must take an `Agent` object as the first argument. The rest of the arguments must match that of the `signal` you wish to connect a slot to. 13 | 14 | You need to wrap procs with a `slot` to setup the proc to support recieving signals. The proc can still be used as a normal function though. Signals use the proc syntax but don't have a implementation. They just provide the type checking and naming for the signal. 15 | 16 | Connecting signals and slots is accomplished using `connect`. Note that `connect` is idempotent, meaning that you can call it on the same objects the multiple times without ill effect. 17 | 18 | ## Examples 19 | 20 | ```nim 21 | import sigils 22 | 23 | type 24 | Counter*[T] = ref object of Agent 25 | value: T 26 | 27 | proc valueChanged*[T](tp: Counter[T], val: T) {.signal.} 28 | 29 | proc setValue*[T](self: Counter[T], value: T) {.slot.} = 30 | echo "setValue! ", value 31 | if self.value != value: 32 | # we want to be careful not to set circular triggers 33 | self.value = value 34 | emit self.valueChanged(value) 35 | 36 | var 37 | a = Counter[uint]() 38 | b = Counter[uint]() 39 | c = Counter[uint]() 40 | 41 | connect(a, valueChanged, 42 | b, setValue) 43 | connect(a, valueChanged, 44 | c, setValue) 45 | 46 | doAssert b.value == 0 47 | doAssert c.value == 0 48 | emit a.valueChanged(137) 49 | 50 | doAssert a.value == 0 51 | doAssert b.value == 137 52 | doAssert c.value == 137 53 | ``` 54 | 55 | ## Alternative Connect for Slots 56 | 57 | Sometimes the Nim compiler can't determine the which slot you want to use just by the types passed into the `connect` template. Othertimes you may want to specify a parent type's slot. 58 | 59 | The `{.slot.}` pragma generates some helper procs for these scenarios to allow you to ensure the specific slot passed to `connect`. These helpers procs take the type of their agent (the target) as the first argument. It looks like this: 60 | 61 | ```nim 62 | let b = Counter[uint]() 63 | connect(a, valueChanged, 64 | b, Counter[uint].setValue) 65 | 66 | a.setValue(42) # we can directly call `setValue` which will then call emit 67 | 68 | doAssert a.value == 42 69 | doAssert b.value == 42 70 | ``` 71 | 72 | The `{.signal.}` pragma generates these provide several helper procs to make it easy to get the type of the signal argument. The `SignalTypes` types is used as the first argument to differentiate from normal invocation of signals. Here are some examples: 73 | 74 | ```nim 75 | test "signal / slot types": 76 | doAssert SignalTypes.avgChanged(Counter[uint]) is (float, ) 77 | doAssert SignalTypes.valueChanged(Counter[uint]) is (uint, ) 78 | doAssert SignalTypes.setValue(Counter[uint]) is (uint, ) 79 | ``` 80 | 81 | ## Threads 82 | 83 | Sigils 0.9+ can now do threaded signals! 84 | 85 | **Note**: v0.16.0 changed threads module's `connect` to `connectThreaded` to be more explicit. 86 | 87 | ```nim 88 | test "agent connect then moveToThread and run": 89 | var 90 | a = SomeAction.new() 91 | 92 | block: 93 | echo "sigil object thread connect change" 94 | var 95 | b = Counter.new() 96 | c = SomeAction.new() 97 | echo "thread runner!", " (th: ", getThreadId(), ")" 98 | let thread = newSigilThread() 99 | thread.start() 100 | startLocalThread() 101 | 102 | connect(a, valueChanged, b, setValue) 103 | connect(b, updated, c, SomeAction.completed()) 104 | 105 | let bp: AgentProxy[Counter] = b.moveToThread(thread) 106 | echo "obj bp: ", bp.getSigilId() 107 | 108 | # Note: `connectThreaded` can be used with proxies 109 | emit a.valueChanged(314) 110 | let ct = getCurrentSigilThread() 111 | ct[].poll() # we need to either `poll` or do `runForever` similar to async 112 | check c.value == 314 113 | ``` 114 | 115 | ## Closures 116 | 117 | ```nim 118 | type 119 | Counter* = ref object of Agent 120 | 121 | test "callback creation": 122 | var 123 | a = Counter() 124 | b = Counter(value: 100) 125 | 126 | let 127 | clsAgent = 128 | connectTo(a, valueChanged) do (val: int): 129 | b.value = val 130 | 131 | emit a.valueChanged(42) 132 | check b.value == 42 # callback modifies base 133 | # beware capturing values like this 134 | # it causes headaches, but can be handy 135 | check clsAgent.typeof() is ClosureAgent[(int,)] 136 | ``` 137 | 138 | 139 | ## Advanced 140 | 141 | Signal names aren't `string` types for performance considerations. Instead they're arrays with a maximum name size of 48 bytes currently. This can be changed if needed. 142 | 143 | ### Overriding Destructors 144 | 145 | Overriding the `=destroy` destructors will result in bad things if you don't properly call the Agent destructor. See the following code for how to do this. Note that calling `=destroy` directly with casts doesn't seem to work. 146 | 147 | ```nim 148 | type CounterWithDestroy* = ref object of Agent 149 | 150 | proc `=destroy`*(x: var typeof(CounterWithDestroy()[])) = 151 | echo "CounterWithDestroy:destroy: ", x.debugName 152 | destroyAgent(x) 153 | ``` 154 | 155 | ### Void Slots 156 | 157 | There's an exception to the type checking. It's common in UI programming to want to trigger a `slot` without caring about the actual values in the signal. To achieve this you can call `connect` like this: 158 | 159 | ```nim 160 | proc valueChanged*(tp: Counter, val: int) {.signal.} 161 | 162 | proc someAction*(self: Counter) {.slot.} = 163 | echo "action" 164 | 165 | connect(a, valueChanged, c, someAction, acceptVoidSlot = true) 166 | emit a.valueChange(42) 167 | ``` 168 | 169 | Now whenever `valueChanged` is emitted then `someAction` will be triggered. 170 | 171 | ### WeakRefs 172 | 173 | Calling `connect` _does not_ create a new reference of either the target or source agents. This is done primarily to prevent cycles from being created accidentally. This is necessary for easing UI development with _Sigil_. 174 | 175 | However, `Agent` objects are still memory safe to use. They have a destructor which removes an `Agent` from any of it's "listeners" connections to ensure freed agents aren't signaled after they're freed. Nifty! 176 | 177 | Note however, that means you need to ensure your `Agent`'s aren't destroyed before you're done with them. This applies to threaded signals using `AgentProxy[T]` as well. 178 | 179 | ### Serialization 180 | 181 | Internally `sigils` was based on an RPC system. There are many similarities when calling typed compiled functions in a generic fashion. 182 | 183 | To wit `sigils` supports multiple serialization methods. The default uses [variant](https://github.com/yglukhov/variant). In theory we could use the `any` type. Additionally JSON and CBOR methods are also supported by passing `-d:sigilsCborSerde` or `-d:sigilsJsonSerde`. These can be useful for backeneds such as NimScript and JavaScript. Using CBOR can be handy for networking which might be added in the future. 184 | 185 | 186 | ### Multiple Threads 187 | 188 | This example sends one signal to two different agents living on two different threads, then collects both results back on the main thread. 189 | 190 | ```nim 191 | import sigils, sigils/threads 192 | 193 | type 194 | Trigger = ref object of Agent 195 | Worker = ref object of Agent 196 | value: int 197 | Collector = ref object of Agent 198 | a: int 199 | b: int 200 | 201 | proc valueChanged(tp: Trigger, val: int) {.signal.} 202 | proc updated(tp: Worker, final: int) {.signal.} 203 | 204 | proc setValue(self: Worker, value: int) {.slot.} = 205 | self.value = value 206 | echo "worker:setValue: ", value, " (th: ", getThreadId(), ")" 207 | emit self.updated(self.value) 208 | 209 | proc gotA(self: Collector, final: int) {.slot.} = 210 | echo "collector: gotA: ", final, " (th: ", getThreadId(), ")" 211 | self.a = final 212 | 213 | proc gotB(self: Collector, final: int) {.slot.} = 214 | echo "collector: gotB: ", final, " (th: ", getThreadId(), ")" 215 | self.b = final 216 | 217 | let trigger = Trigger() 218 | let collector = Collector() 219 | 220 | let threadA = newSigilThread() 221 | let threadB = newSigilThread() 222 | threadA.start() 223 | threadB.start() 224 | startLocalThreadDefault() 225 | 226 | var wA = Worker() 227 | var wB = Worker() 228 | 229 | let workerA: AgentProxy[Worker] = wA.moveToThread(threadA) 230 | let workerB: AgentProxy[Worker] = wB.moveToThread(threadB) 231 | 232 | connectThreaded(trigger, valueChanged, workerA, setValue) 233 | connectThreaded(trigger, valueChanged, workerB, setValue) 234 | connectThreaded(workerA, updated, collector, Collector.gotA()) 235 | connectThreaded(workerB, updated, collector, Collector.gotB()) 236 | 237 | emit trigger.valueChanged(42) 238 | 239 | let ct = getCurrentSigilThread() 240 | discard ct.poll() # workerA result 241 | discard ct.poll() # workerB result 242 | doAssert collector.a == 42 243 | doAssert collector.b == 42 244 | 245 | setRunning(threadA, false) 246 | setRunning(threadB, false) 247 | threadA.join() 248 | threadB.join() 249 | ``` 250 | 251 | -------------------------------------------------------------------------------- /sigils/slots.nim: -------------------------------------------------------------------------------- 1 | import tables, strutils, typetraits, macros 2 | 3 | import agents 4 | 5 | export agents 6 | 7 | const SigilDebugSlots {.strdefine: "sigils.DebugSlots".}: string = "" 8 | 9 | proc firstArgument(params: NimNode): (NimNode, NimNode) = 10 | if params.len() == 1: 11 | error("Slots must take an Agent as the first argument.", params) 12 | if params != nil and params.len > 0 and params[1] != nil and 13 | params[1].kind == nnkIdentDefs: 14 | result = (ident params[1][0].strVal, params[1][1]) 15 | else: 16 | result = (ident "", newNimNode(nnkEmpty)) 17 | 18 | iterator paramsIter(params: NimNode): tuple[name, ntype: NimNode] = 19 | for i in 1 ..< params.len: 20 | let arg = params[i] 21 | let argType = arg[^2] 22 | for j in 0 ..< arg.len - 2: 23 | yield (arg[j], argType) 24 | 25 | proc mkParamsVars*(paramsIdent, paramsType, params: NimNode): NimNode = 26 | ## Create local variables for each parameter in the actual RPC call proc 27 | if params.isNil: 28 | return 29 | 30 | result = newStmtList() 31 | var varList = newSeq[NimNode]() 32 | var cnt = 0 33 | for paramid, paramType in paramsIter(params): 34 | let idx = newIntLitNode(cnt) 35 | let vars = quote: 36 | var `paramid`: `paramType` = `paramsIdent`[`idx`] 37 | varList.add vars 38 | cnt.inc() 39 | result.add varList 40 | # echo "paramsSetup return:\n", treeRepr result 41 | 42 | proc mkParamsType*(paramsIdent, paramsType, params, 43 | genericParams: NimNode): NimNode = 44 | ## Create a type that represents the arguments for this rpc call 45 | ## 46 | ## Example: 47 | ## 48 | ## proc multiplyrpc(a, b: int): int {.rpc.} = 49 | ## result = a * b 50 | ## 51 | ## Becomes: 52 | ## proc multiplyrpc(params: RpcType_multiplyrpc): int = 53 | ## var a = params.a 54 | ## var b = params.b 55 | ## 56 | ## proc multiplyrpc(params: RpcType_multiplyrpc): int = 57 | ## 58 | if params.isNil: 59 | return 60 | 61 | var tup = quote: 62 | type `paramsType` = tuple[] 63 | for paramIdent, paramType in paramsIter(params): 64 | # processing multiple variables of one type 65 | tup[0][2].add newIdentDefs(paramIdent, paramType) 66 | result = tup 67 | result[0][1] = genericParams.copyNimTree() 68 | # echo "mkParamsType: ", genericParams.treeRepr 69 | 70 | proc updateProcsSig( 71 | node: NimNode, isPublic: bool, gens: NimNode, procLineInfo: LineInfo 72 | ) = 73 | if node.kind in [nnkProcDef, nnkTemplateDef]: 74 | node[0].setLineInfo(procLineInfo) 75 | let name = node[0] 76 | if isPublic: 77 | node[0] = nnkPostfix.newTree(newIdentNode("*"), name) 78 | node[2] = gens.copyNimTree() 79 | node[^1].setLineInfo(procLineInfo) 80 | else: 81 | for ch in node: 82 | ch.updateProcsSig(isPublic, gens, procLineInfo) 83 | 84 | 85 | macro rpcImpl*(p: untyped, publish: untyped, qarg: untyped): untyped = 86 | ## Define a remote procedure call. 87 | ## Input and return parameters are defined using proc's with the `rpc` 88 | ## pragma. 89 | ## 90 | ## For example: 91 | ## .. code-block:: nim 92 | ## proc methodname(param1: int, param2: float): string {.rpc.} = 93 | ## result = $param1 & " " & $param2 94 | ## ``` 95 | ## 96 | ## Input parameters are automatically marshalled from fast rpc binary 97 | ## format (msgpack) and output parameters are automatically marshalled 98 | ## back to the fast rpc binary format (msgpack) for transport. 99 | 100 | let 101 | path = $p[0] 102 | procLineInfo = p.lineInfoObj 103 | genericParams = p[2] 104 | params = p[3] 105 | # pragmas = p[4] 106 | body = p[6] 107 | 108 | result = newStmtList() 109 | var 110 | (_, firstType) = params.firstArgument() 111 | parameters = params.copyNimTree() 112 | 113 | let 114 | # determine if this is a "signal" rpc method 115 | isSignal = publish.kind == nnkStrLit and publish.strVal == "signal" 116 | 117 | parameters.del(0, 1) 118 | # echo "parameters: ", parameters.treeRepr 119 | 120 | let 121 | # rpc method names 122 | pathStr = $path 123 | signalName = pathStr.strip(false, true, {'*'}) 124 | procNameStr = p.name().repr 125 | isPublic = pathStr.endsWith("*") 126 | isGeneric = genericParams.kind != nnkEmpty 127 | 128 | rpcMethodGen = genSym(nskProc, procNameStr) 129 | procName = ident(procNameStr) # ident("agentSlot_" & rpcMethodGen.repr) 130 | rpcMethod = ident(procNameStr) 131 | 132 | paramsIdent = ident("args") 133 | paramTypeName = ident("RpcType" & procNameStr) 134 | 135 | # echo "SLOTS:slot:NAME: ", p.name(), " => ", procNameStr, " genname: ", rpcMethodGen 136 | # echo "SLOTS:paramTypeName:NAME: ", paramTypeName 137 | # echo "SLOTS:generic: ", genericParams.treeRepr 138 | # echo "SLOTS: rpcMethodGen:hash: ", rpcMethodGen.symBodyHash() 139 | # echo "SLOTS: rpcMethodGen:signatureHash: ", rpcMethodGen.signatureHash() 140 | 141 | var 142 | # process the argument types 143 | paramSetups = mkParamsVars(paramsIdent, paramTypeName, parameters) 144 | paramTypes = mkParamsType(paramsIdent, paramTypeName, parameters, genericParams) 145 | 146 | procBody = 147 | if body.kind == nnkStmtList: 148 | body 149 | elif body.kind == nnkEmpty: 150 | body 151 | else: 152 | body.body 153 | 154 | let 155 | contextType = firstType 156 | kd = ident "kd" 157 | tp = ident "tp" 158 | 159 | var signalTyp = nnkTupleConstr.newTree() 160 | for i in 2 ..< params.len: 161 | signalTyp.add params[i][1] 162 | if params.len == 2: 163 | # signalTyp = bindSym"void" 164 | signalTyp = quote: 165 | tuple[] 166 | 167 | # Create the proc's that hold the users code 168 | if not isSignal: 169 | let rmCall = nnkCall.newTree(rpcMethodGen) 170 | for param in parameters: 171 | rmCall.add param[0] 172 | 173 | # If the original proc has a body, emit a full definition. 174 | # If it's a forward declaration (no body), also emit a matching 175 | # declaration so wrappers can reference it and later 176 | # implementations can define it. 177 | if body.kind == nnkStmtList: 178 | let rm = quote: 179 | proc `rpcMethod`() {.nimcall.} = 180 | `procBody` 181 | 182 | for param in parameters: 183 | rm[3].add param 184 | result.add rm 185 | else: 186 | # Forward declaration: emit a prototype so the symbol exists 187 | # and wrappers can call it before a later implementation. 188 | let fwd = quote: 189 | proc `rpcMethod`() {.nimcall.} 190 | 191 | for param in parameters: 192 | fwd[3].add param 193 | result.add fwd 194 | 195 | var rpcType = paramTypeName.copyNimTree() 196 | if isGeneric: 197 | rpcType = nnkBracketExpr.newTree(paramTypeName) 198 | for arg in genericParams: 199 | rpcType.add arg 200 | 201 | # Create the rpc wrapper procs 202 | let objId = ident "obj" 203 | var tupTyp = nnkTupleConstr.newTree() 204 | for pt in paramTypes[0][^1]: 205 | tupTyp.add pt[1] 206 | if tupTyp.len() == 0: 207 | tupTyp = nnkTupleTy.newTree() 208 | let mcall = nnkCall.newTree(rpcMethod) 209 | mcall.add(objId) 210 | for param in parameters[1 ..^ 1]: 211 | mcall.add param[0] 212 | 213 | let agentSlotImpl = quote: 214 | proc slot(context: Agent, params: SigilParams) {.nimcall.} = 215 | if context == nil: 216 | raise newException(ValueError, "bad value") 217 | let `objId` = `contextType`(context) 218 | if `objId` == nil: 219 | raise newException(ConversionError, "bad cast") 220 | when `tupTyp` isnot tuple[]: 221 | var `paramsIdent`: `tupTyp` 222 | rpcUnpack(`paramsIdent`, params) 223 | `paramSetups` 224 | `mcall` 225 | 226 | let procTyp = quote: 227 | proc() {.nimcall.} 228 | procTyp.params = params.copyNimTree() 229 | 230 | result.add quote do: 231 | when not compiles(`rpcMethod`(`contextType`)): 232 | proc `rpcMethod`( 233 | `kd`: typedesc[SignalTypes], `tp`: typedesc[`contextType`] 234 | ): `signalTyp` = 235 | discard 236 | 237 | proc `rpcMethod`(`tp`: typedesc[`contextType`]): AgentProcTy[`signalTyp`] = 238 | `agentSlotImpl` 239 | slot 240 | 241 | result.updateProcsSig(isPublic, genericParams, procLineInfo) 242 | elif isSignal: 243 | var construct = nnkTupleConstr.newTree() 244 | for param in parameters[1 ..^ 1]: 245 | construct.add param[0] 246 | let objId = ident"obj" 247 | 248 | result.add quote do: 249 | proc `rpcMethod`(`objId`: `firstType`): (Agent, SigilRequestTy[`firstType`]) = 250 | var args = `construct` 251 | let name: SigilName = toSigilName(`signalName`) 252 | let req = initSigilRequest[`firstType`, typeof(args)]( 253 | procName = name, args = ensureMove args, origin = `objId`.getSigilId() 254 | ) 255 | result = (`objId`, req) 256 | 257 | for param in parameters[1 ..^ 1]: 258 | result[^1][3].add param 259 | 260 | result.add quote do: 261 | proc `rpcMethod`( 262 | `objId`: WeakRef[`firstType`] 263 | ): (WeakRef[Agent], SigilRequestTy[`firstType`]) = 264 | var args = `construct` 265 | let name: SigilName = toSigilName(`signalName`) 266 | let req = initSigilRequest[`firstType`, typeof(args)]( 267 | procName = name, args = ensureMove args, origin = `objId`.getSigilId() 268 | ) 269 | result = (`objId`.asAgent(), req) 270 | 271 | for param in parameters[1 ..^ 1]: 272 | result[^1][3].add param 273 | 274 | result.add quote do: 275 | proc `rpcMethod`( 276 | `kd`: typedesc[SignalTypes], `tp`: typedesc[`contextType`] 277 | ): `signalTyp` = 278 | discard 279 | 280 | result.updateProcsSig(isPublic, genericParams, procLineInfo) 281 | 282 | var gens: seq[string] 283 | for gen in genericParams: 284 | gens.add gen[0].strVal 285 | 286 | # echo "slot: " 287 | # echo result.lispRepr 288 | 289 | when SigilDebugSlots != "": 290 | if procNameStr in SigilDebugSlots: 291 | echo "slot:repr: isSignal: ", isSignal 292 | echo result.repr 293 | 294 | template slot*(p: untyped): untyped = 295 | rpcImpl(p, nil, nil) 296 | 297 | template signal*(p: untyped): untyped = 298 | rpcImpl(p, "signal", nil) 299 | -------------------------------------------------------------------------------- /sigils/threadBase.nim: -------------------------------------------------------------------------------- 1 | import std/sets 2 | import std/isolation 3 | import std/options 4 | import std/locks 5 | import std/tables 6 | import threading/smartptrs 7 | import threading/channels 8 | import threading/atomics 9 | 10 | import isolateutils 11 | import agents 12 | import core 13 | 14 | from system/ansi_c import c_raise 15 | 16 | export smartptrs, isolation, channels 17 | export isolateutils 18 | 19 | const SigilTimerRepeat* = -1 20 | 21 | type 22 | SigilThreadEvent* = ref object of Agent 23 | 24 | SigilTimer* = ref object of SigilThreadEvent 25 | duration*: Duration 26 | count*: int = SigilTimerRepeat # -1 for repeat forever, N > 0 for N times 27 | 28 | MessageQueueFullError* = object of CatchableError 29 | UnableToSubscribe* = object of CatchableError 30 | 31 | BlockingKinds* {.pure.} = enum 32 | Blocking 33 | NonBlocking 34 | 35 | ThreadSignalKind* {.pure.} = enum 36 | Call 37 | Move 38 | AddSubscription 39 | Trigger 40 | Deref 41 | Exit 42 | 43 | ThreadSignal* = object 44 | case kind*: ThreadSignalKind 45 | of Call: 46 | slot*: AgentProc 47 | req*: SigilRequest 48 | tgt*: WeakRef[Agent] 49 | of Move: 50 | item*: Agent 51 | of AddSubscription: 52 | src*: WeakRef[Agent] 53 | name*: SigilName 54 | subTgt*: WeakRef[Agent] 55 | subProc*: AgentProc 56 | of Trigger: 57 | discard 58 | of Deref: 59 | deref*: WeakRef[Agent] 60 | of Exit: 61 | discard 62 | 63 | SigilChan* = Chan[ThreadSignal] 64 | 65 | AgentRemote* = ref object of Agent 66 | inbox*: Chan[ThreadSignal] 67 | 68 | ThreadAgent* = ref object of Agent 69 | 70 | SigilThread* = object of RootObj 71 | threadId*: Atomic[int] 72 | 73 | signaledLock*: Lock 74 | signaled*: HashSet[WeakRef[AgentRemote]] 75 | 76 | references*: Table[WeakRef[Agent], Agent] 77 | agent*: ThreadAgent 78 | exceptionHandler*: proc(e: ref Exception) {.gcsafe, nimcall.} 79 | when defined(sigilsDebug): 80 | debugName*: string 81 | running*: Atomic[bool] 82 | toCancel*: HashSet[SigilTimer] 83 | 84 | SigilThreadPtr* = ptr SigilThread 85 | 86 | proc timeout*(timer: SigilTimer) {.signal.} 87 | 88 | proc started*(tp: ThreadAgent) {.signal.} 89 | 90 | proc getThreadId*(thread: SigilThread): int = 91 | addr(thread.threadId)[].load(Relaxed) 92 | 93 | proc repr*(obj: SigilThread): string = 94 | when defined(sigilsDebug): 95 | let dname = "name: " & obj.debugName & ", " 96 | else: 97 | let dname = "" 98 | 99 | result = 100 | fmt"SigilThread(id: {$getThreadId(obj)}, {dname}signaled: {$obj.signaled.len} agent: {$obj.agent.unsafeWeakRef} )" 101 | 102 | 103 | proc `=destroy`*(thread: var SigilThread) = 104 | # SigilThread 105 | thread.running.store(false, Relaxed) 106 | 107 | proc newSigilChan*(): SigilChan = 108 | result = newChan[ThreadSignal](1_000) 109 | 110 | method send*( 111 | thread: SigilThreadPtr, msg: sink ThreadSignal, blocking: BlockingKinds = Blocking 112 | ) {.base, gcsafe.} = 113 | raise newException(AssertionDefect, "this should never be called!") 114 | 115 | method recv*( 116 | thread: SigilThreadPtr, msg: var ThreadSignal, blocking: BlockingKinds 117 | ): bool {.base, gcsafe.} = 118 | raise newException(AssertionDefect, "this should never be called!") 119 | 120 | method setTimer*( 121 | thread: SigilThreadPtr, timer: SigilTimer 122 | ) {.base, gcsafe.} = 123 | raise newException(AssertionDefect, "this should never be called!") 124 | 125 | method poll*( 126 | thread: SigilThreadPtr, blocking: BlockingKinds = Blocking 127 | ): bool {.base, gcsafe, discardable.} = 128 | raise newException(AssertionDefect, "this should never be called!") 129 | 130 | proc isRunning*(thread: SigilThreadPtr): bool = 131 | thread.running.load(Relaxed) 132 | 133 | proc setRunning*(thread: SigilThreadPtr, state: bool, immediate = false) = 134 | if immediate: 135 | thread[].running.store(state, Relaxed) 136 | else: 137 | thread.send(ThreadSignal(kind: Exit)) 138 | 139 | proc gcCollectReferences(thread: SigilThreadPtr) = 140 | var derefs: HashSet[WeakRef[Agent]] 141 | for agent in thread.references.keys(): 142 | if not agent[].hasConnections(): 143 | derefs.incl(agent) 144 | 145 | for agent in derefs: 146 | debugPrint "\tderef cleanup: ", agent.unsafeWeakRef() 147 | thread.references.del(agent) 148 | 149 | proc exec*(thread: SigilThreadPtr, sig: ThreadSignal) {.gcsafe.} = 150 | debugPrint "\nthread got request: ", $sig.kind 151 | case sig.kind 152 | of Exit: 153 | debugPrint "\t threadExec:exit: ", $getThreadId() 154 | thread.running.store(false, Relaxed) 155 | of Move: 156 | debugPrint "\t threadExec:move: ", 157 | $sig.item.unsafeWeakRef(), " refcount: ", $sig.item.unsafeGcCount() 158 | var item = sig.item 159 | thread.references[item.unsafeWeakRef()] = move item 160 | of AddSubscription: 161 | echo "\t threadExec:subscribe: ", 162 | " src: ", $sig.src, 163 | " tgt: ", $sig.subTgt, 164 | " srcExists: ", sig.src in thread.references 165 | if sig.src.isNil: 166 | raise newException(UnableToSubscribe, "unable to subscribe nil" & 167 | " src: " & $sig.src & 168 | " to " & $sig.subTgt) 169 | if sig.src in thread.references and sig.subTgt in thread.references: 170 | sig.src[].addSubscription(sig.name, sig.subTgt[], sig.subProc) 171 | else: 172 | raise newException(UnableToSubscribe, "unable to subscribe to missing" & 173 | " src: " & $sig.src & 174 | " to " & $sig.subTgt) 175 | thread.gcCollectReferences() 176 | of Deref: 177 | debugPrint "\t threadExec:deref: ", $sig.deref.unsafeWeakRef() 178 | if thread.references.contains(sig.deref): 179 | debugPrint "\t threadExec:run:deref: ", $sig.deref.unsafeWeakRef() 180 | thread.references.del(sig.deref) 181 | withLock thread.signaledLock: 182 | thread.signaled.excl(cast[WeakRef[AgentRemote]](sig.deref)) 183 | thread.gcCollectReferences() 184 | of Call: 185 | debugPrint "\t threadExec:call: ", $sig.tgt[].getSigilId() 186 | # for item in thread.references.items(): 187 | # debugPrint "\t threadExec:refcheck: ", $item.getSigilId(), " rc: ", $item.unsafeGcCount() 188 | when defined(sigilsDebug) or defined(debug): 189 | if sig.tgt[].freedByThread != 0: 190 | echo "exec:call:sig.tgt[].freedByThread:thread: ", $sig.tgt[].freedByThread 191 | echo "exec:call:sig.req: ", sig.req.repr 192 | echo "exec:call:thr: ", $getThreadId() 193 | echo "exec:call: ", $sig.tgt[].getSigilId() 194 | echo "exec:call:isUnique: ", sig.tgt[].isUniqueRef 195 | # echo "exec:call:has: ", sig.tgt[] in getCurrentSigilThread()[].references 196 | # discard c_raise(11.cint) 197 | assert sig.tgt[].freedByThread == 0 198 | {.cast(gcsafe).}: 199 | let res = sig.tgt[].callMethod(sig.req, sig.slot) 200 | debugPrint "\t threadExec:tgt: ", 201 | $sig.tgt[].getSigilId(), " rc: ", $sig.tgt[].unsafeGcCount() 202 | of Trigger: 203 | debugPrint "Triggering" 204 | var signaled: HashSet[WeakRef[AgentRemote]] 205 | withLock thread.signaledLock: 206 | signaled = move thread.signaled 207 | {.cast(gcsafe).}: 208 | for signaled in signaled: 209 | debugPrint "triggering: ", signaled 210 | var sig: ThreadSignal 211 | debugPrint "triggering:inbox: ", signaled[].inbox.repr 212 | while signaled[].inbox.tryRecv(sig): 213 | debugPrint "\t threadExec:tgt: ", $sig.tgt, " rc: ", $sig.tgt[].unsafeGcCount() 214 | let res = sig.tgt[].callMethod(sig.req, sig.slot) 215 | 216 | proc runForever*(thread: SigilThreadPtr) {.gcsafe.} = 217 | emit thread.agent.started() 218 | while thread.isRunning(): 219 | try: 220 | discard thread.poll() 221 | except CatchableError as e: 222 | if thread.exceptionHandler.isNil: 223 | raise e 224 | else: 225 | thread.exceptionHandler(e) 226 | except Defect as e: 227 | if thread.exceptionHandler.isNil: 228 | raise e 229 | else: 230 | thread.exceptionHandler(e) 231 | except Exception as e: 232 | if thread.exceptionHandler.isNil: 233 | raise e 234 | else: 235 | thread.exceptionHandler(e) 236 | 237 | proc pollAll*(thread: SigilThreadPtr, blocking: BlockingKinds = NonBlocking): int {.discardable.} = 238 | var sig: ThreadSignal 239 | result = 0 240 | while thread.poll(blocking): 241 | result.inc() 242 | 243 | proc defaultExceptionHandler*(e: ref Exception) = 244 | echo "Sigil thread unhandled exception: ", e.msg, " ", e.name 245 | echo "Sigil thread unhandled stack trace: ", e.getStackTrace() 246 | 247 | proc setExceptionHandler*( 248 | thread: var SigilThread, 249 | handler: proc(e: ref Exception) {.gcsafe, nimcall.} 250 | ) = 251 | thread.exceptionHandler = handler 252 | 253 | var startSigilThreadProc: proc() 254 | var localSigilThread {.threadVar.}: ptr SigilThread 255 | 256 | proc toSigilThread*[R: SigilThread](t: ptr R): ptr SigilThread = 257 | cast[ptr SigilThread](t) 258 | 259 | proc hasLocalSigilThread*(): bool = 260 | not localSigilThread.isNil 261 | 262 | proc setLocalSigilThread*[R: ptr SigilThread](thread: R) = 263 | localSigilThread = thread.toSigilThread() 264 | 265 | proc setStartSigilThreadProc*(cb: proc()) = 266 | startSigilThreadProc = cb 267 | 268 | proc getStartSigilThreadProc*(): proc() = 269 | startSigilThreadProc 270 | 271 | template getCurrentSigilThread*(): SigilThreadPtr = 272 | if not hasLocalSigilThread(): 273 | doAssert not startSigilThreadProc.isNil, "startSigilThreadProc is not set!" 274 | startSigilThreadProc() 275 | doAssert hasLocalSigilThread() 276 | localSigilThread 277 | 278 | ## Timer API 279 | proc hasCancelTimer*(thread: SigilThreadPtr, timer: SigilTimer): bool = 280 | timer in thread.toCancel 281 | 282 | proc cancelTimer*(thread: SigilThreadPtr, timer: SigilTimer) = 283 | thread.toCancel.incl(timer) 284 | 285 | proc removeTimer*(thread: SigilThreadPtr, timer: SigilTimer) = 286 | thread.toCancel.excl(timer) 287 | 288 | proc newSigilTimer*(duration: Duration, count: int = SigilTimerRepeat): SigilTimer = 289 | result = SigilTimer() 290 | result.duration = duration 291 | result.count = count 292 | 293 | proc isRepeat*(timer: SigilTimer): bool = 294 | timer.count == SigilTimerRepeat 295 | 296 | proc start*(timer: SigilTimer, ct: SigilThreadPtr = getCurrentSigilThread()) = 297 | ct.setTimer(timer) 298 | 299 | proc cancel*(timer: SigilTimer, ct: SigilThreadPtr = getCurrentSigilThread()) = 300 | ct.cancelTimer(timer) 301 | -------------------------------------------------------------------------------- /sigils/agents.nim: -------------------------------------------------------------------------------- 1 | import std/[options, tables, sequtils, sets, macros, hashes] 2 | import std/times 3 | import std/isolation 4 | import std/[locks, options] 5 | import stack_strings 6 | 7 | import threading/atomics 8 | 9 | import protocol 10 | import weakrefs 11 | 12 | when (NimMajor, NimMinor, NimPatch) < (2, 2, 0): 13 | {.passc:"-fpermissive".} 14 | {.passl:"-fpermissive".} 15 | 16 | 17 | when defined(nimscript): 18 | import std/json 19 | import ../runtime/jsonutils_lite 20 | export json 21 | elif defined(useJsonSerde): 22 | import std/json 23 | import std/jsonutils 24 | export json 25 | else: 26 | import svariant 27 | 28 | export sets 29 | export options 30 | export svariant 31 | 32 | export IndexableChars 33 | export weakrefs 34 | export protocol 35 | 36 | import std/[terminal, strutils, strformat, sequtils] 37 | export strformat 38 | 39 | var 40 | pcolors* = [fgRed, fgYellow, fgMagenta, fgCyan] 41 | pcnt*: int = 0 42 | pidx* {.threadVar.}: int 43 | plock: Lock 44 | debugPrintQuiet* = false 45 | 46 | plock.initLock() 47 | 48 | proc debugPrintImpl*(msgs: varargs[string, `$`]) {.raises: [].} = 49 | {.cast(gcsafe).}: 50 | try: 51 | # withLock plock: 52 | block: 53 | let 54 | tid = getThreadId() 55 | color = 56 | if pidx == 0: 57 | fgBlue 58 | else: 59 | pcolors[pidx mod pcolors.len()] 60 | var msg = "" 61 | for m in msgs: 62 | msg &= m 63 | stdout.styledWriteLine color, msg, {styleBright}, &" [th: {$tid}]" 64 | stdout.flushFile() 65 | except IOError: 66 | discard 67 | 68 | template debugPrint*(msgs: varargs[untyped]) = 69 | when defined(sigilsDebugPrint): 70 | if not debugPrintQuiet: 71 | debugPrintImpl(msgs) 72 | 73 | proc brightPrint*(color: ForegroundColor, msg, value: string, msg2 = "", value2 = "") = 74 | if not debugPrintQuiet: 75 | stdout.styledWriteLine color, 76 | msg, 77 | {styleBright, styleItalic}, 78 | value, 79 | resetStyle, 80 | color, 81 | msg2, 82 | {styleBright, styleItalic}, 83 | value2 84 | 85 | proc brightPrint*(msg, value: string, msg2 = "", value2 = "") = 86 | brightPrint(fgGreen, msg, value, msg2, value2) 87 | 88 | type 89 | AgentObj = object of RootObj 90 | subcriptions*: seq[tuple[signal: SigilName, subscription: Subscription]] 91 | ## agents listening to me 92 | listening*: HashSet[WeakRef[Agent]] ## agents I'm listening to 93 | when defined(sigilsDebug) or defined(debug) or defined(sigilsDebugPrint): 94 | freedByThread*: int 95 | when defined(sigilsDebug): 96 | debugName*: string 97 | 98 | Agent* = ref object of AgentObj 99 | 100 | Subscription* = object 101 | tgt*: WeakRef[Agent] 102 | slot*: AgentProc 103 | 104 | # Procedure signature accepted as an RPC call by server 105 | AgentProc* = proc(context: Agent, params: SigilParams) {.nimcall.} 106 | 107 | AgentProcTy*[S] = AgentProc 108 | 109 | Signal*[S] = AgentProcTy[S] 110 | SignalTypes* = distinct object 111 | 112 | when defined(nimscript): 113 | proc getSigilId*(a: Agent): SigilId = 114 | a.debugId 115 | 116 | var lastUId {.compileTime.}: int = 1 117 | else: 118 | proc getSigilId*[T: Agent](a: WeakRef[T]): SigilId = 119 | cast[SigilId](a.toPtr()) 120 | 121 | proc getSigilId*(a: Agent): SigilId = 122 | cast[SigilId](cast[pointer](a)) 123 | 124 | proc `$`*[T: Agent](obj: WeakRef[T]): string = 125 | result = "Weak[" 126 | when defined(sigilsDebug): 127 | if obj.isNil: 128 | result &= "nil" 129 | else: 130 | result &= obj[].debugName 131 | result &= "; " 132 | result &= $(T) 133 | result &= "]" 134 | result &= "(0x" 135 | if obj.isNil: 136 | result &= "nil" 137 | else: 138 | result &= obj.toPtr().repr 139 | result &= ")" 140 | 141 | template removeSubscriptionsForImpl*(self: Agent, subscriber: WeakRef[Agent]) = 142 | ## Route's an rpc request. 143 | var toDel: seq[int] = newSeq[int](self.subcriptions.len()) 144 | for idx in countdown(self.subcriptions.len() - 1, 0): 145 | debugPrint " removeSubscriptionsFor subs sig: ", $self.subcriptions[idx].signal 146 | if self.subcriptions[idx].subscription.tgt == subscriber: 147 | self.subcriptions.delete(idx..idx) 148 | 149 | method removeSubscriptionsFor*( 150 | self: Agent, subscriber: WeakRef[Agent], slot: AgentProc 151 | ) {.base, gcsafe, raises: [].} = 152 | debugPrint " removeSubscriptionsFor:agent: ", " self:id: ", $self.unsafeWeakRef() 153 | removeSubscriptionsForImpl(self, subscriber) 154 | 155 | template unregisterSubscriberImpl*(self: Agent, listener: WeakRef[Agent]) = 156 | debugPrint "\tunregisterSubscriber: ", $listener, " from self: ", self.unsafeWeakRef() 157 | # debugPrint "\tlisterners:subscribed ", subscriber.tgt[].subscribed 158 | assert listener in self.listening 159 | self.listening.excl(listener) 160 | 161 | method unregisterSubscriber*( 162 | self: Agent, listener: WeakRef[Agent] 163 | ) {.base, gcsafe, raises: [].} = 164 | debugPrint &" unregisterSubscriber:agent: self: {$self.unsafeWeakRef()}" 165 | unregisterSubscriberImpl(self, listener) 166 | 167 | template unsubscribeFrom*(self: WeakRef[Agent], listening: HashSet[WeakRef[Agent]]) = 168 | ## unsubscribe myself from agents I'm subscribed (listening) to 169 | debugPrint " unsubscribeFrom:cnt: ", $listening.len(), " self: {$self}" 170 | for agent in listening: 171 | agent[].removeSubscriptionsFor(self, nil) 172 | 173 | template removeSubscriptions*( 174 | agent: WeakRef[Agent], subcriptions: seq[tuple[signal: SigilName, subscription: Subscription]] 175 | ) = 176 | ## remove myself from agents listening to me 177 | var tgts: HashSet[WeakRef[Agent]] = initHashSet[WeakRef[Agent]](subcriptions.len()) 178 | for idx in 0 ..< subcriptions.len(): 179 | tgts.incl(subcriptions[idx].subscription.tgt) 180 | 181 | for tgt in tgts: 182 | tgt[].unregisterSubscriber(agent) 183 | 184 | proc destroyAgent*(agentObj: AgentObj) {.forbids: [DestructorUnsafe].} = 185 | let agent: WeakRef[Agent] = unsafeWeakRef(cast[Agent](addr(agentObj))) 186 | 187 | debugPrint &"destroy: agent: ", 188 | &" pt: {$agent}", 189 | &" freedByThread: {agentObj.freedByThread}", 190 | &" subs: {agent[].subcriptions.len()}", 191 | &" subTo: {agent[].listening.len()}" 192 | # debugPrint "destroy agent: ", getStackTrace().replace("\n", "\n\t") 193 | when defined(debug) or defined(sigilsDebug): 194 | assert agentObj.freedByThread == 0 195 | agent[].freedByThread = getThreadId() 196 | 197 | agent.removeSubscriptions(agentObj.subcriptions) 198 | agent.unsubscribeFrom(agentObj.listening) 199 | 200 | `=destroy`(agent[].subcriptions) 201 | `=destroy`(agent[].listening) 202 | debugPrint "\tfinished destroy: agent: ", " pt: ", $agent 203 | when defined(sigilsDebug): 204 | `=destroy`(agent[].debugName) 205 | 206 | proc `=destroy`*(agentObj: AgentObj) {.forbids: [DestructorUnsafe].} = 207 | destroyAgent(agentObj) 208 | 209 | template toAgentObj*[T: Agent](agent: T): AgentObj = 210 | Agent(agent)[] 211 | 212 | proc hash*(a: Agent): Hash = 213 | hash(a.getSigilId()) 214 | 215 | method hasConnections*(self: Agent): bool {.base, gcsafe, raises: [].} = 216 | self.subcriptions.len() != 0 or self.listening.len() != 0 217 | 218 | iterator getSubscriptions*(obj: Agent, sig: SigilName): var Subscription = 219 | for item in obj.subcriptions.mitems(): 220 | if item.signal == sig or item.signal == AnySigilName: 221 | yield item.subscription 222 | 223 | iterator getSubscriptions*(obj: WeakRef[Agent], sig: SigilName): var Subscription = 224 | for item in obj[].subcriptions.mitems(): 225 | if item.signal == sig or item.signal == AnySigilName: 226 | yield item.subscription 227 | 228 | proc asAgent*[T: Agent](obj: WeakRef[T]): WeakRef[Agent] = 229 | result = WeakRef[Agent](pt: obj.pt) 230 | 231 | proc asAgent*[T: Agent](obj: T): Agent = 232 | result = obj 233 | 234 | proc hasSubscription*(obj: Agent, sig: SigilName): bool = 235 | for idx in 0 ..< obj.subcriptions.len(): 236 | if obj.subcriptions[idx].signal == sig: 237 | return true 238 | 239 | proc hasSubscription*(obj: Agent, sig: SigilName, tgt: Agent | WeakRef[Agent]): bool = 240 | let tgt = tgt.unsafeWeakRef().toKind(Agent) 241 | for idx in 0 ..< obj.subcriptions.len(): 242 | if obj.subcriptions[idx].signal == sig and 243 | obj.subcriptions[idx].subscription.tgt == tgt: 244 | return true 245 | 246 | proc hasSubscription*(obj: Agent, sig: SigilName, tgt: Agent | WeakRef[Agent], slot: AgentProc): bool = 247 | let tgt = tgt.unsafeWeakRef().toKind(Agent) 248 | for idx in 0 ..< obj.subcriptions.len(): 249 | if obj.subcriptions[idx].signal == sig and 250 | obj.subcriptions[idx].subscription.tgt == tgt and 251 | obj.subcriptions[idx].subscription.slot == slot: 252 | return true 253 | 254 | proc addSubscription*( 255 | obj: Agent, sig: SigilName, tgt: Agent | WeakRef[Agent], slot: AgentProc 256 | ): void = 257 | doAssert not obj.isNil(), "agent is nil!" 258 | assert slot != nil 259 | 260 | if not obj.hasSubscription(sig, tgt, slot): 261 | obj.subcriptions.add((sig, Subscription(tgt: tgt.unsafeWeakRef().asAgent(), slot: slot))) 262 | tgt.listening.incl(obj.unsafeWeakRef().asAgent()) 263 | 264 | template addSubscription*( 265 | obj: Agent, sig: IndexableChars, tgt: Agent | WeakRef[Agent], slot: AgentProc 266 | ): void = 267 | addSubscription(obj, sig.toSigilName(), tgt, slot) 268 | 269 | var printConnectionsSlotNames* = initTable[pointer, string]() 270 | 271 | proc delSubscription*( 272 | self: Agent, sig: SigilName, tgt: Agent | WeakRef[Agent], slot: AgentProc 273 | ): void = 274 | 275 | let tgt = tgt.unsafeWeakRef().toKind(Agent) 276 | 277 | var 278 | subsFound: int 279 | subsDeleted: int 280 | 281 | for idx in countdown(self.subcriptions.len() - 1, 0): 282 | if self.subcriptions[idx].signal == sig and 283 | self.subcriptions[idx].subscription.tgt == tgt: 284 | subsFound.inc() 285 | if slot == nil or self.subcriptions[idx].subscription.slot == slot: 286 | subsDeleted.inc() 287 | self.subcriptions.delete(idx..idx) 288 | 289 | if subsFound == subsDeleted: 290 | tgt[].listening.excl(self.unsafeWeakRef()) 291 | 292 | template delSubscription*( 293 | obj: Agent, sig: IndexableChars, tgt: Agent | WeakRef[Agent], slot: AgentProc 294 | ): void = 295 | delSubscription(obj, sig.toSigilName(), tgt, slot) 296 | 297 | proc printConnections*(agent: Agent) = 298 | withLock plock: 299 | if agent.isNil: 300 | brightPrint fgBlue, "connections for Agent: ", "nil" 301 | return 302 | when defined(sigilsDebug): 303 | if agent[].freedByThread != 0: 304 | brightPrint fgBlue, 305 | "connections for Agent: ", 306 | $agent.unsafeWeakRef(), 307 | " freedByThread: ", 308 | $agent[].freedByThread 309 | return 310 | brightPrint fgBlue, "connections for Agent: ", $agent.unsafeWeakRef() 311 | brightPrint fgMagenta, "\t subscribers:", "" 312 | for item in agent.subcriptions: 313 | let sname = printConnectionsSlotNames.getOrDefault(item.subscription.slot, item.subscription.slot.repr) 314 | brightPrint fgGreen, "\t\t:", $item.signal, ": => ", $item.subscription.tgt & " slot: " & $sname 315 | brightPrint fgMagenta, "\t listening:", "" 316 | for listening in agent.listening: 317 | brightPrint fgRed, "\t\t listen: ", $listening 318 | -------------------------------------------------------------------------------- /sigils/threadProxies.nim: -------------------------------------------------------------------------------- 1 | import std/sets 2 | import std/isolation 3 | import std/options 4 | import std/locks 5 | import threading/smartptrs 6 | import threading/channels 7 | 8 | import isolateutils 9 | import agents 10 | import core 11 | import threadBase 12 | 13 | from system/ansi_c import c_raise 14 | 15 | type 16 | AgentProxyShared* = ref object of AgentRemote 17 | remote*: WeakRef[Agent] 18 | proxyTwin*: WeakRef[AgentProxyShared] 19 | lock*: Lock 20 | remoteThread*: SigilThreadPtr 21 | 22 | AgentProxy*[T] = ref object of AgentProxyShared 23 | 24 | proc `=destroy`*(obj: var typeof(AgentProxyShared()[])) = 25 | when defined(sigilsWeakRefPointer): 26 | let agent = WeakRef[AgentRemote](pt: cast[pointer](addr obj)) 27 | else: 28 | let pt: WeakRef[pointer] = WeakRef[pointer](pt: cast[pointer](addr obj)) 29 | let agent = cast[WeakRef[AgentRemote]](pt) 30 | debugPrint "PROXY Destroy: ", cast[AgentProxyShared](addr(obj)).unsafeWeakRef() 31 | `=destroy`(toAgentObj(cast[AgentProxyShared](addr obj))) 32 | 33 | debugPrint "PROXY Destroy: proxyTwin: ", obj.proxyTwin 34 | # need to break to proxyTwin cycle on dirst destroy 35 | # TODO: seems like there's race condtions here as we could destroy 36 | # both remote and local proxies at the same time 37 | if not obj.proxyTwin.isNil: 38 | withLock obj.proxyTwin[].lock: 39 | obj.proxyTwin[].proxyTwin.pt = nil 40 | withLock obj.proxyTwin[].remoteThread[].signaledLock: 41 | obj.proxyTwin[].remoteThread[].signaled.excl(agent) 42 | try: 43 | let 44 | thr = obj.remoteThread 45 | proxyTwin = obj.proxyTwin.toKind(Agent) 46 | if not proxyTwin.isNil: 47 | debugPrint "send deref: ", $proxyTwin, " thr: ", getThreadId() 48 | thr.send(ThreadSignal(kind: Deref, deref: proxyTwin)) 49 | except Exception: 50 | echo "error sending deref message for ", $obj.proxyTwin 51 | 52 | `=destroy`(obj.remoteThread) 53 | `=destroy`(obj.lock) # careful on this one -- should probably figure out a test 54 | 55 | proc getRemote*[T](proxy: AgentProxy[T]): WeakRef[T] = 56 | proxy.remote.toKind(T) 57 | 58 | proc remoteSlot*(context: Agent, params: SigilParams) {.nimcall.} = 59 | raise newException(AssertionDefect, "this should never be called!") 60 | 61 | proc localSlot*(context: Agent, params: SigilParams) {.nimcall.} = 62 | raise newException(AssertionDefect, "this should never be called!") 63 | 64 | method hasConnections*(proxy: AgentProxyShared): bool {.gcsafe, raises: [].} = 65 | withLock proxy.lock: 66 | result = proxy.subcriptions.len() != 0 or proxy.listening.len() != 0 67 | 68 | method callMethod*( 69 | proxy: AgentProxyShared, req: SigilRequest, slot: AgentProc 70 | ): SigilResponse {.gcsafe, effectsOf: slot.} = 71 | ## Route's an rpc request. 72 | debugPrint "callMethod: proxy: ", 73 | $proxy.unsafeWeakRef().asAgent(), 74 | " refcount: ", 75 | proxy.unsafeGcCount(), 76 | " slot: ", 77 | repr(slot) 78 | if slot == remoteSlot: 79 | var req = req.duplicate() 80 | #debugPrint "\t proxy:callMethod:remoteSlot: ", "req: ", $req 81 | debugPrint "\t proxy:callMethod:remoteSlot: ", "proxy.remote: ", $proxy.remote 82 | var pt: WeakRef[AgentProxyShared] 83 | withLock proxy.lock: 84 | pt = proxy.proxyTwin 85 | 86 | var msg = isolateRuntime ThreadSignal( 87 | kind: Call, slot: localSlot, req: move req, tgt: pt.toKind(Agent) 88 | ) 89 | debugPrint "\t proxy:callMethod:remoteSlot: ", 90 | "msg: ", $msg, " proxyTwin: ", $proxy.proxyTwin 91 | when defined(sigilsDebug) or defined(debug): 92 | assert proxy.freedByThread == 0 93 | when defined(sigilNonBlockingThreads): 94 | discard 95 | else: 96 | withLock proxy.lock: 97 | if not proxy.proxyTwin.isNil: 98 | withLock proxy.remoteThread[].signaledLock: 99 | proxy.remoteThread[].signaled.incl(proxy.proxyTwin.toKind(AgentRemote)) 100 | proxy.proxyTwin[].inbox.send(msg) 101 | proxy.remoteThread.send(ThreadSignal(kind: Trigger)) 102 | elif slot == localSlot: 103 | debugPrint "\t proxy:callMethod:localSlot: " 104 | callSlots(proxy, req) 105 | else: 106 | var req = req.duplicate() 107 | debugPrint "\t callMethod:agentProxy:InitCall:Outbound: ", 108 | req.procName, " proxy:remote:obj: ", proxy.remote.getSigilId() 109 | var msg = isolateRuntime ThreadSignal( 110 | kind: Call, slot: slot, req: move req, tgt: proxy.remote 111 | ) 112 | when defined(sigilNonBlockingThreads): 113 | discard 114 | else: 115 | debugPrint "\t callMethod:agentProxy:proxyTwin: ", proxy.proxyTwin 116 | withLock proxy.lock: 117 | proxy.proxyTwin[].inbox.send(msg) 118 | withLock proxy.remoteThread[].signaledLock: 119 | proxy.remoteThread[].signaled.incl(proxy.proxyTwin.toKind(AgentRemote)) 120 | proxy.remoteThread.send(ThreadSignal(kind: Trigger)) 121 | 122 | method removeSubscriptionsFor*( 123 | self: AgentProxyShared, subscriber: WeakRef[Agent] 124 | ) {.gcsafe, raises: [].} = 125 | debugPrint " removeSubscriptionsFor:proxy: self:id: ", $self.unsafeWeakRef() 126 | withLock self.lock: 127 | # block: 128 | debugPrint " removeSubscriptionsFor:proxy:ready: self:id: ", $self.unsafeWeakRef() 129 | removeSubscriptionsForImpl(self, subscriber) 130 | 131 | method unregisterSubscriber*( 132 | self: AgentProxyShared, listener: WeakRef[Agent] 133 | ) {.gcsafe, raises: [].} = 134 | debugPrint " unregisterSubscriber:proxy: self:id: ", $self.unsafeWeakRef() 135 | withLock self.lock: 136 | # block: 137 | debugPrint " unregisterSubscriber:proxy:ready: self:id: ", $self.unsafeWeakRef() 138 | unregisterSubscriberImpl(self, listener) 139 | 140 | iterator findSubscribedTo( 141 | other: WeakRef[Agent], agent: WeakRef[Agent] 142 | ): tuple[signal: SigilName, subscription: Subscription] = 143 | for item in other[].subcriptions.mitems(): 144 | if item.subscription.tgt == agent: 145 | yield (item.signal, Subscription(tgt: other, slot: item.subscription.slot)) 146 | 147 | 148 | proc moveToThread*[T: Agent, R: SigilThread]( 149 | agentTy: var T, thread: ptr R 150 | ): AgentProxy[T] = 151 | ## move agent to another thread 152 | debugPrint "moveToThread: ", $agentTy.unsafeWeakRef() 153 | if not isUniqueRef(agentTy): 154 | raise newException( 155 | AccessViolationDefect, 156 | "agent must be unique and not shared to be passed to another thread! " & 157 | "GC ref is: " & $agentTy.unsafeGcCount(), 158 | ) 159 | var 160 | ct = getCurrentSigilThread() 161 | agent = agentTy.unsafeWeakRef.asAgent() 162 | 163 | var 164 | localProxy = AgentProxy[T]( 165 | remote: agent, 166 | remoteThread: thread.toSigilThread(), 167 | inbox: newChan[ThreadSignal](1_000), 168 | ) 169 | 170 | var 171 | remoteProxy = AgentProxy[T]( 172 | remote: agent, remoteThread: ct, inbox: newChan[ThreadSignal](1_000) 173 | ) 174 | 175 | 176 | 177 | localProxy.lock.initLock() 178 | remoteProxy.lock.initLock() 179 | localProxy.proxyTwin = remoteProxy.unsafeWeakRef().toKind(AgentProxyShared) 180 | remoteProxy.proxyTwin = localProxy.unsafeWeakRef().toKind(AgentProxyShared) 181 | when defined(sigilsDebug): 182 | localProxy.debugName = "localProxy::" & agentTy.debugName 183 | remoteProxy.debugName = "remoteProxy::" & agentTy.debugName 184 | 185 | # handle things subscribed to `agent`, ie the inverse 186 | var 187 | oldSubscribers = agent[].subcriptions 188 | oldListeningSubs: seq[tuple[signal: SigilName, subscription: Subscription]] 189 | 190 | for listener in agent[].listening: 191 | for item in listener.findSubscribedTo(agent[].unsafeWeakRef()): 192 | oldListeningSubs.add(item) 193 | 194 | agent.unsubscribeFrom(agent[].listening) 195 | agent.removeSubscriptions(agent[].subcriptions) 196 | agent[].listening.clear() 197 | agent[].subcriptions.setLen(0) 198 | 199 | # update subscriptions agent is listening to use the local proxy to send events 200 | var listenSubs = false 201 | for item in oldListeningSubs: 202 | item.subscription.tgt[].addSubscription(item.signal, localProxy, item.subscription.slot) 203 | listenSubs = true 204 | remoteProxy.addSubscription(AnySigilName, agentTy, localSlot) 205 | 206 | # update my subcriptionsTable so agent uses the remote proxy to send events back 207 | var hasSubs = false 208 | for item in oldSubscribers: 209 | localProxy.addSubscription(item.signal, item.subscription.tgt[], item.subscription.slot) 210 | hasSubs = true 211 | agent[].addSubscription(AnySigilName, remoteProxy, remoteSlot) 212 | 213 | thread.send(ThreadSignal(kind: Move, item: move agentTy)) 214 | thread.send(ThreadSignal(kind: Move, item: move remoteProxy)) 215 | 216 | return localProxy 217 | 218 | template connectThreaded*[T, U, S]( 219 | proxyTy: AgentProxy[T], 220 | signal: typed, 221 | b: AgentProxy[U], 222 | slot: Signal[S], 223 | acceptVoidSlot: static bool = false, 224 | ): void = 225 | ## connects `AgentProxy[T]` to remote signals 226 | ## 227 | checkSignalTypes(T(), signal, U(), slot, acceptVoidSlot) 228 | let localProxy = Agent(proxyTy) 229 | localProxy.addSubscription(signalName(signal), b, slot) 230 | 231 | template connectThreaded*[T, S]( 232 | a: Agent, 233 | signal: typed, 234 | localProxy: AgentProxy[T], 235 | slot: Signal[S], 236 | acceptVoidSlot: static bool = false, 237 | ): void = 238 | ## connects `AgentProxy[T]` to remote signals 239 | ## 240 | checkSignalTypes(a, signal, T(), slot, acceptVoidSlot) 241 | a.addSubscription(signalName(signal), localProxy, slot) 242 | assert not localProxy.proxyTwin.isNil 243 | assert not localProxy.remote.isNil 244 | withLock localProxy.proxyTwin[].lock: 245 | localProxy.proxyTwin[].addSubscription(AnySigilName, localProxy.remote[], localSlot) 246 | 247 | template connectThreaded*[T]( 248 | a: Agent, 249 | signal: typed, 250 | localProxy: AgentProxy[T], 251 | slot: typed, 252 | acceptVoidSlot: static bool = false, 253 | ): void = 254 | ## connects `AgentProxy[T]` to remote signals 255 | ## 256 | checkSignalThreadSafety(SignalTypes.`signal`(typeof(a))) 257 | let agentSlot = `slot`(T) 258 | checkSignalTypes(a, signal, T(), agentSlot, acceptVoidSlot) 259 | a.addSubscription(signalName(signal), localProxy, agentSlot) 260 | assert not localProxy.proxyTwin.isNil 261 | assert not localProxy.remote.isNil 262 | withLock localProxy.proxyTwin[].lock: 263 | localProxy.proxyTwin[].addSubscription(AnySigilName, localProxy.remote[], localSlot) 264 | 265 | template connectThreaded*[T, S]( 266 | proxyTy: AgentProxy[T], 267 | signal: typed, 268 | b: Agent, 269 | slot: Signal[S], 270 | acceptVoidSlot: static bool = false, 271 | ): void = 272 | ## connects `AgentProxy[T]` to remote signals 273 | ## 274 | checkSignalTypes(T(), signal, b, slot, acceptVoidSlot) 275 | let localProxy = Agent(proxyTy) 276 | localProxy.addSubscription(signalName(signal), b, slot) 277 | 278 | template connectThreaded*[T]( 279 | thr: SigilThreadPtr, 280 | signal: typed, 281 | localProxy: AgentProxy[T], 282 | slot: typed, 283 | acceptVoidSlot: static bool = false, 284 | ): void = 285 | ## connects `AgentProxy[T]` to remote signals 286 | ## 287 | checkSignalThreadSafety(SignalTypes.`signal`(typeof(thr.agent))) 288 | let agentSlot = `slot`(T) 289 | checkSignalTypes(thr.agent, signal, T(), agentSlot, acceptVoidSlot) 290 | assert not localProxy.proxyTwin.isNil 291 | assert not localProxy.remote.isNil 292 | thr.agent.addSubscription(signalName(signal), localProxy.getRemote()[], agentSlot) 293 | 294 | import macros 295 | 296 | macro callCode(s: static string): untyped = 297 | ## calls a code to get the signal type using a static string 298 | result = parseStmt(s) 299 | 300 | proc fwdSlotTy[A: Agent; B: Agent; S: static string](self: Agent, params: SigilParams) {.nimcall.} = 301 | let agentSlot = callCode(S) 302 | let req = SigilRequest( 303 | kind: Request, origin: SigilId(-1), procName: signalName(signal), params: params.duplicate() 304 | ) 305 | var msg = ThreadSignal(kind: Call) 306 | msg.slot = agentSlot 307 | msg.req = req 308 | msg.tgt = self.unsafeWeakRef().asAgent() 309 | let ct = getCurrentSigilThread() 310 | ct.send(msg) 311 | 312 | template connectQueued*[T]( 313 | a: Agent, 314 | signal: typed, 315 | b: Agent, 316 | slot: Signal[T], 317 | acceptVoidSlot: static bool = false, 318 | ): void = 319 | ## Queued connection helper: route a signal to a target slot by 320 | ## enqueueing a `Call` on a specific `SigilThread`'s inputs channel. 321 | checkSignalTypes(a, signal, b, slot, acceptVoidSlot) 322 | let ct = getCurrentSigilThread() 323 | let fs: AgentProc = fwdSlotTy[a, b, astToStr(slot)] 324 | a.addSubscription(signalName(signal), b, fs) 325 | 326 | macro callSlot(s: static string, a: typed): untyped = 327 | ## calls a slot to get the signal type using a static string 328 | let id = ident(s) 329 | result = quote do: 330 | `id`(`a`) 331 | echo "callSlot:result: ", result.repr 332 | 333 | proc fwdSlot[A: Agent; B: Agent; S: static string](self: Agent, params: SigilParams) {.nimcall.} = 334 | let agentSlot = callSlot(S, typeof(B)) 335 | let req = SigilRequest( 336 | kind: Request, origin: SigilId(-1), procName: signalName(signal), params: params.duplicate() 337 | ) 338 | var msg = ThreadSignal(kind: Call) 339 | msg.slot = agentSlot 340 | msg.req = req 341 | msg.tgt = self.unsafeWeakRef().asAgent() 342 | let ct = getCurrentSigilThread() 343 | ct.send(msg) 344 | 345 | template connectQueued*( 346 | a: Agent, 347 | signal: typed, 348 | b: Agent, 349 | slot: untyped, 350 | acceptVoidSlot: static bool = false, 351 | ): void = 352 | ## Queued connection helper: route a signal to a target slot by 353 | ## enqueueing a `Call` on a specific `SigilThread`'s inputs channel. 354 | let agentSlot = `slot`(typeof(b)) 355 | checkSignalTypes(a, signal, b, agentSlot, acceptVoidSlot) 356 | let ct = getCurrentSigilThread() 357 | let fs: AgentProc = fwdSlot[a, b, astToStr(slot)] 358 | a.addSubscription(signalName(signal), b, fs) 359 | 360 | -------------------------------------------------------------------------------- /docs/threading.md: -------------------------------------------------------------------------------- 1 | # Sigils Threading Architecture 2 | 3 | This document explains the threading architecture implemented in `sigils/threadBase.nim`, `sigils/threadDefault.nim`, and `sigils/threadProxies.nim` (re-exported via `sigils/threads.nim`), with examples from `tests/tslotsThread.nim`. It focuses on how agents are moved across threads, how calls and signals travel safely between threads, and the key safety guarantees and caveats. 4 | 5 | At a high level, Sigils follows a "don't share, communicate" approach to concurrency. Each thread is treated as the exclusive owner of the agents it runs: an agent's internal state is meant to be read and written by only one thread. Instead of letting multiple threads call into the same object directly (which would require locks around every access), Sigils keeps agent code single-threaded and uses message passing to request work from other threads. 6 | 7 | The key operation is a move. Moving an agent to another thread is an ownership transfer: after the move, the destination thread is the only place where the real agent lives and executes. The source thread should behave as if it no longer has the agent at all. What it keeps is a small local stand-in whose job is to forward method calls and signals across threads. This is similar in spirit to moving an object to another process and then talking to it through a handle. 8 | 9 | Communication is built around inboxes (message queues). When code on one thread wants something to happen on another thread, it packages the request as a message and puts it into the destination thread's inbox. The destination thread runs a simple loop: it repeatedly takes messages out of its inbox and executes them one at a time. Because all requests are handled serially on the owning thread, the agent doesn't need internal locking to stay consistent. 10 | 11 | Signals travel back the same way. When an agent running on a worker thread emits a signal that has listeners on a different thread, that signal becomes a message placed into the other thread's inbox. Importantly, the receiving thread must periodically drain its own inbox to deliver those callbacks. If you forget to pump the inbox on the receiving side, cross-thread signals won't be observed even though they were queued. 12 | 13 | ## Overview 14 | 15 | - Sigils uses a message-passing model to keep agent logic thread-confined while enabling cross-thread signaling. 16 | - Agents can be **moved** to a worker thread; callers interact with them through a local proxy that forwards requests. 17 | - Cross-thread work is executed by a per-thread scheduler that processes messages serially. 18 | - Worker threads run the scheduler loop automatically; threads without a built-in loop must periodically poll to process forwarded events. 19 | - Cleanup is synchronized: moved agents are owned by the destination thread, and proxies send dereference messages so the destination thread can release remote state. 20 | 21 | ## Mental Model 22 | 23 | - Every participating OS thread has a thread-local `SigilThread` (`getCurrentSigilThread()` in `sigils/threadBase.nim`). 24 | - A `SigilThread` executes messages serially; there is no parallel execution within a single `SigilThread`. 25 | - `SigilThreadDefault.start()` spawns a worker that calls `runForever()` and blocks in `poll()`. 26 | - If your current thread needs to receive events from other threads, you must pump its scheduler by calling `poll()`/`pollAll()` periodically. 27 | 28 | ## Core Types 29 | 30 | - `SigilThread` (threadBase): Common scheduler/state shared by all thread implementations. Holds: 31 | - `signaled: HashSet[WeakRef[AgentRemote]]` set of per-proxy mailboxes to drain. 32 | - `signaledLock: Lock` protects `signaled`. 33 | - `references: Table[WeakRef[Agent], Agent]` ownership table of agents moved onto this thread. 34 | - `agent: ThreadAgent` a local agent for thread lifecycle signals (e.g., `started`). 35 | - `exceptionHandler` invoked by `runForever()` if a slot raises. 36 | - `running: Atomic[bool]`, `threadId: Atomic[int]`. 37 | 38 | - `SigilThreadDefault` (threadDefault): Default blocking worker thread implementation. Adds: 39 | - `inputs: Chan[ThreadSignal]` main control channel for `Move`/`Deref`/`Trigger`/`Call`/`Exit`. 40 | - `thr: Thread[ptr SigilThreadDefault]` OS thread handle. 41 | 42 | - Thread-local access (threadBase): 43 | - `localSigilThread {.threadVar.}` stores the current scheduler pointer. 44 | - `getCurrentSigilThread()` lazily initializes it by calling `startSigilThreadProc` (defaults to `startLocalThreadDefault()` from `sigils/threadDefault.nim`). 45 | 46 | - `ThreadSignal` (threadBase): Control and invocation messages routed to threads. 47 | - `Call(slot, req, tgt)` — invoke `slot` on `tgt` with `req`. 48 | - `Move(item)` — move an `Agent` (or proxy) to the thread; stored in `references`. 49 | - `Trigger` — instructs the scheduler to drain signaled proxy inboxes. 50 | - `Deref(deref)` — drop a reference previously `Move`-ed to this thread and prune dead `references`. 51 | - `Exit` — stop the scheduler loop. 52 | 53 | - `AgentRemote` (threadBase): A mailbox-capable agent base with `inbox: Chan[ThreadSignal]`. Proxies extend this to implement per-proxy mailboxes. 54 | 55 | - `AgentProxy[T]`/`AgentProxyShared` (threadProxies): Twin proxies mediating cross-thread communication. 56 | - `remote: WeakRef[Agent]` — the real agent (owned by the destination thread after a move). 57 | - `proxyTwin: WeakRef[AgentProxyShared]` — the opposite-side proxy. 58 | - `remoteThread: SigilThreadPtr` — scheduler for the thread that owns `proxyTwin`. 59 | - `lock: Lock` — protects proxy twin access and local proxy state. 60 | 61 | ## Moving Agents Across Threads 62 | 63 | `moveToThread(agent, thread)` (threadProxies): 64 | 65 | - Preconditions: `agent` must be unique (`isUniqueRef`) to prevent sharing GC refs across threads. 66 | - Creates two proxies: 67 | - `localProxy` — lives on the current thread; used by local code to talk to the remote agent. 68 | - `remoteProxy` — lives on the destination thread; used to forward events back to the local side. 69 | - Rewrites subscriptions: 70 | - Outbound (agent listening to others): rebinds listeners so the local proxy receives those signals and forwards across threads. 71 | - Inbound (others listening to agent): rebinds so callers attach to `localProxy`; the remote agent publishes to `remoteProxy`, which forwards back. 72 | - Ownership transfer: 73 | - Sends `Move(agent)` and `Move(remoteProxy)` to the destination thread so they are owned (and managed) by that thread's `references` table. 74 | - Returns `localProxy` to the caller; the original `agent` variable is moved (becomes `nil`). 75 | 76 | The tests demonstrate this flow, e.g.: 77 | 78 | - `let bp: AgentProxy[Counter] = b.moveToThread(thread)` 79 | - Subsequent `connect(...)` calls attach to `bp` (local proxy) while execution runs on `thread`. 80 | 81 | ## Message Flow and Scheduling 82 | 83 | Two paths deliver work on the destination thread: 84 | 85 | 1) Per-proxy mailbox path (dominant path) 86 | - The sender enqueues a `Call` into `proxyTwin.inbox` (the twin on the destination thread). 87 | - The sender marks the twin as signaled in `remoteThread.signaled` under `signaledLock`. 88 | - The sender posts `Trigger` to `remoteThread` (via `remoteThread.send(ThreadSignal(kind: Trigger))`). 89 | - The scheduler handles `Trigger` by moving `signaled` into a local set, then for each signaled proxy drains its `inbox` with `tryRecv` and invokes `tgt.callMethod(req, slot)`. 90 | 91 | 2) Direct control path 92 | - `Move`, `Deref`, and `Exit` are sent to the destination thread via `thread.send(...)` and handled immediately. 93 | - `Call` is also supported as a direct `ThreadSignal(Call)` on a thread's input channel (used by `connectQueued`), though proxy code primarily uses per-proxy inbox + `Trigger`. 94 | 95 | All cross-thread messages are isolated (`isolateRuntime`) before enqueueing to ensure thread-safe transfer of data. 96 | 97 | ## Proxy Call Semantics 98 | 99 | `AgentProxyShared.callMethod(req, slot)` routes calls based on `slot`: 100 | 101 | - `slot == remoteSlot` — event forwarding from the remote agent back to the local side. 102 | - Wraps as `Call(localSlot, req, tgt = proxyTwin)` and enqueues into `proxyTwin.inbox` on the other thread with `Trigger`. 103 | 104 | - `slot == localSlot` — local delivery on the receiving side. 105 | - Executes `callSlots(self, req)` to fan out to local subscribers. 106 | 107 | - Otherwise — regular slot call bound for the remote agent. 108 | - Wraps as `Call(slot, req, tgt = proxy.remote)`, enqueues into `proxyTwin.inbox` (on remote), and `Trigger`s the remote thread. 109 | 110 | Locks are used to safely access `proxyTwin` and to coordinate with the destination thread's `signaled` set. 111 | 112 | ## Thread Loop and Lifecycle 113 | 114 | - `runForever(thread)` loops while `running` is true, calling `poll()` to receive and execute one `ThreadSignal` at a time (catching exceptions via `exceptionHandler` if set). 115 | - `Trigger` drains signaled proxy inboxes. 116 | - `Move` takes ownership of agents/proxies (store in `references`). 117 | - `Deref` removes owned references and clears any `signaled` entries for that proxy. 118 | - `Exit` sets `running = false` to end the loop. 119 | 120 | Helpers: 121 | - `newSigilThread()` allocates/initializes a `SigilThreadDefault`. 122 | - `start()/join()/peek()` manage the worker OS thread. 123 | - `setRunning(false)` (or sending `Exit`) stops `runForever()` for a thread. 124 | - `startLocalThreadDefault()` and `getCurrentSigilThread()` manage a thread-local `SigilThread` for the current (often main) thread; `getCurrentSigilThread()` is lazy and will call `startSigilThreadProc` if needed. 125 | 126 | ## Queued Calls (Same Thread) 127 | 128 | `connectQueued(...)` routes a signal to a slot by enqueueing a `ThreadSignal(Call)` onto the current thread's `SigilThread` instead of calling inline. The slot runs the next time you `poll()`/`pollAll()` that thread. 129 | 130 | Because it uses `SigilThread.send(...)` under the hood, the queued `ThreadSignal` is still passed through `isolateRuntime(...)` before it is enqueued. 131 | 132 | ## Destruction and Cleanup 133 | 134 | - Agents have a destructor (`destroyAgent`) that removes them from all subscriptions and listeners. Debug builds assert destruction happens on the owning thread (`freedByThread` checks). 135 | - Proxies break cycles on destruction: 136 | - `AgentProxyShared.=destroy` clears the opposite twin's link under `lock`, removes it from the remote thread's `signaled`, and posts a `Deref` to drop the remote reference. 137 | - Destroys local `lock` and `remoteThread` refs last. 138 | - The destination thread periodically cleans `references` via `gcCollectReferences()` (after `Deref`), removing entries for agents that have no connections. 139 | 140 | ## Using From Tests (tslotsThread.nim) 141 | 142 | Patterns illustrated by the tests: 143 | 144 | - Direct cross-thread emit via a `WeakRef`: build a request in a background thread and deliver it back to main via a channel, then `emit resp` on the main thread. 145 | - Moving and connecting: 146 | - Move `Counter` to a worker thread, hold `AgentProxy[Counter]` locally. 147 | - `connectThreaded(a, valueChanged, bp, setValue)` wires signal `a.valueChanged` to the remote `bp.setValue()`; the handler runs on the worker thread. 148 | - `connectThreaded(bp, updated, a, SomeAction.completed())` wires the remote signal back to a local slot (and demonstrates that the local thread must be polled). 149 | - Use `getCurrentSigilThread().poll()` or `pollAll()` on the local thread to process inbound forwarded events. 150 | - Thread lifecycle signals: 151 | - `connectThreaded(thread, started, bp, ticker)` to run a remote slot when the worker thread starts (emitted by `thread[].agent.started()` in `runForever()`). 152 | - Assertions about subscription topology are used to verify that proxies have the expected inbound/outbound connections after `moveToThread`. 153 | 154 | ## Thread Safety Notes 155 | 156 | - Ownership: After `moveToThread`, the destination thread exclusively owns the moved agent (and its `remoteProxy`) via `references`. Do not retain or use the original agent ref on the source thread. 157 | - Isolation: Cross-thread `ThreadSignal`s are passed through `isolateRuntime(...)` before enqueueing. If the payload contains non-unique `ref`s, `isolateRuntime` raises `IsolationError` to prevent unsafe sharing. 158 | - Signal params: Cross-thread signal parameter types must be thread-safe (no `ref` fields). The `connectThreaded` helpers use `checkSignalThreadSafety` for common patterns; use `Isolate[T]` for heap payloads you need to transfer. 159 | - No shared GC refs: The code defends against sharing by requiring unique refs before move and by using `WeakRef` identifiers for cross-thread targeting. 160 | - Synchronization: 161 | - `signaled` guarded by `signaledLock`. 162 | - Proxy internals guarded by `lock` when accessing `proxyTwin` and scheduling signals. 163 | - Atomic fields (`running`, `threadId`) avoid data races. 164 | - Backpressure: `newChan[ThreadSignal](1_000)` for per-proxy inbox and thread inputs. Non-blocking send (`trySend`) raises `MessageQueueFullError` when full. 165 | 166 | ## Async Safety Notes 167 | 168 | - The base API documented here is thread-centric. For integration with `asyncdispatch`, use the async variant in `sigils/threadAsyncs.nim` (`AsyncSigilThread`) (not currently re-exported by `sigils/threads.nim`, so import it directly). It: 169 | - Uses an `AsyncEvent` callback to drain signals during the event loop. 170 | - Triggers the event on every send/recv to schedule work. 171 | - Implements `setTimer` via `asyncdispatch.addTimer` (timers are not implemented by `SigilThreadDefault`). 172 | - To make `getCurrentSigilThread()` create an async scheduler for the current thread, call `setStartSigilThreadProc(startLocalThreadDispatch)` (or call `startLocalThreadDispatch()` manually). 173 | 174 | ## Gotchas and Best Practices 175 | 176 | - Always check uniqueness before moving (`moveToThread` enforces this and raises on violation). 177 | - After moving, update all connections through the returned `AgentProxy[T]`; direct references to the old agent are invalid on the source thread. 178 | - Use `connectThreaded(...)` when wiring signals across threads; it validates thread-safety and routes through the proxy correctly. 179 | - Poll the local thread (`poll`/`pollAll`) when expecting inbound events forwarded from a worker thread. 180 | - Consider setting a custom `exceptionHandler` on threads used in tests or long-running services to surface handler exceptions clearly. 181 | - In debug builds, heed `freedByThread` assertions; they catch cross-thread destruction misuse. 182 | 183 | ## Minimal Example 184 | 185 | ```nim 186 | import sigils 187 | import sigils/threads 188 | 189 | let t = newSigilThread() 190 | t.start() 191 | 192 | let ct = getCurrentSigilThread() # ensure local scheduler exists 193 | 194 | var src = SomeAction.new() 195 | var dst = Counter.new() 196 | let p: AgentProxy[Counter] = dst.moveToThread(t) 197 | 198 | connectThreaded(src, valueChanged, p, setValue) 199 | connectThreaded(p, updated, src, SomeAction.completed()) # optional: remote -> local flow 200 | 201 | emit src.valueChanged(42) 202 | discard ct.pollAll() # drain local forwarded events 203 | ``` 204 | 205 | This schedules `Counter.setValue` on `t` and keeps all cross-thread traffic safe through the proxy and thread scheduler. 206 | 207 | ## Diagrams 208 | 209 | The following Mermaid flowcharts illustrate the key event flows. 210 | 211 | ### Call: local to remote via AgentProxy 212 | 213 | ```mermaid 214 | flowchart LR 215 | subgraph ST[Source Thread] 216 | direction TB 217 | Caller[User emits signal on Remote Proxy]; 218 | 219 | Caller --emit --> LP; 220 | 221 | subgraph LP[Local AgentProxy] 222 | direction TB 223 | FWD[Local Proxy Forwards Signal]; 224 | Enqueue[Enqueue Call into Twin's Inbox]; 225 | Mark[Mark Twin as signaled under lock]; 226 | Trigger[Send Trigger Msg to RT's Inputs Channel]; 227 | 228 | FWD --> Enqueue --> Mark --> Trigger; 229 | end 230 | end 231 | 232 | subgraph RT[Remote Thread] 233 | RX[Polling Inputs Channel fa:fa-spinner]; 234 | Triggered[Move Messages to Remote Proxy]; 235 | Twin[Remote Proxy Handles Call]; 236 | Deliver[Call Method on Agent]; 237 | RX --> Triggered --> Twin --> Deliver; 238 | subgraph RS[Remote Signal] 239 | direction TB; 240 | Back[Agent emits return signal?]; 241 | Back -- Yes --> WrapBack[Wrap via remoteSlot to localSlot for other side]; 242 | WrapBack --> EnqueueBack[Enqueue to other side inbox and Trigger]; 243 | Back -- No --> Done[Done]; 244 | end 245 | Deliver --> Back; 246 | end 247 | 248 | ST e0@==>|Queue Message 249 | Remote Proxy| RT; 250 | ST e1@==>|Trigger Message| RT; 251 | e1@{ animate: true } 252 | 253 | ``` 254 | 255 | ### Deref: proxy and agent teardown 256 | 257 | ```mermaid 258 | flowchart TD 259 | subgraph ST[Source Thread] 260 | Dtor[Local proxy destructor]; 261 | TwinLock[Lock and clear proxyTwin link]; 262 | Unsig[Remove proxyTwin from remote.signaled under lock]; 263 | SendDeref[Send Deref to remote inputs]; 264 | Dtor --> TwinLock; 265 | TwinLock --> Unsig; 266 | Unsig --> SendDeref; 267 | end 268 | 269 | subgraph DT[Destination Thread] 270 | RT2[Remote SigilThread]; 271 | ExecDeref[On Deref: remove from references and signaled]; 272 | GC[gcCollectReferences prune unconnected entries]; 273 | end 274 | 275 | SendDeref --> RT2; 276 | RT2 --> ExecDeref; 277 | ExecDeref --> GC; 278 | ``` 279 | -------------------------------------------------------------------------------- /tests/treactiveSigil.nim: -------------------------------------------------------------------------------- 1 | import sigils/reactive 2 | import std/math 3 | 4 | import unittest 5 | import std/sequtils 6 | 7 | template isNear*[T](a, b: T, eps = 1.0e-5): bool = 8 | let same = near(a, b) 9 | if not same: 10 | checkpoint("a and b not almost equal: a: " & $a & " b: " & $b & " delta: " & $(a-b)) 11 | same 12 | 13 | 14 | suite "#sigil": 15 | test """ 16 | Given a sigil of value 5 17 | When the sigil is invoked 18 | Then it should return the value 5 19 | """: 20 | let x = newSigil(5) 21 | check x{} == 5 22 | 23 | test """ 24 | Given a sigil 25 | When an attempt is made to assign to the sigil's value directly 26 | Then it should not compile 27 | """: 28 | let x = newSigil(5) 29 | 30 | check not compiles(x{} = 4) 31 | check not compiles(x{} <- 4) 32 | 33 | test """ 34 | Given a sigil of value 5 35 | When an attempt is made to assign to the sigil using arrow syntax to 4 36 | Then it should change the value to 4 37 | """: 38 | let x = newSigil(5) 39 | 40 | x <- 4 41 | 42 | check x{} == 4 43 | 44 | suite "#computed sigil": 45 | test """ 46 | Given a computed sigil 47 | When an attempt is made to assign to the sigil 48 | Then it should not compile 49 | """: 50 | let 51 | x = newSigil(5) 52 | y = computed[int](x{} * 2) 53 | 54 | check not compiles(y{} = 4) 55 | check not compiles(y{} <- 4) 56 | 57 | test """ 58 | Given a sigil of value 5 and a computed that is double the sigil 59 | When the computed sigil is invoked 60 | Then it should return the value 10 61 | """: 62 | let 63 | x = newSigil(5) 64 | y = computed[int](2*x{}) 65 | 66 | check y{} == 10 67 | 68 | test """ 69 | """: 70 | let 71 | x = newSigil(5) 72 | y = int <== 2*x{} 73 | 74 | check y{} == 10 75 | 76 | test """ 77 | Given a sigil of value 5 and a computed that is double the sigil 78 | When the sigils value is changed to 2 and the computed sigil is invoked 79 | Then it should return the value 4 80 | """: 81 | # Given 82 | let 83 | x = newSigil(5) 84 | y = computed[int](2 * x{}) 85 | 86 | when defined(sigilsDebug): 87 | x.debugName = "X" 88 | y.debugName = "Y" 89 | 90 | check x{} == 5 91 | check y{} == 10 92 | 93 | x <- 2 94 | 95 | check x{} == 2 96 | check y{} == 4 97 | 98 | test """ 99 | Given a sigil of and a computedNow that is double the sigil 100 | When the sigils value is changed 101 | Then it should do an additional compute 102 | """: 103 | # Given 104 | let 105 | count = new(int) 106 | x = newSigil(5) 107 | y = computedNow[int]: 108 | count[] += 1 109 | 2 * x{} 110 | 111 | when defined(sigilsDebug): 112 | x.debugName = "X" 113 | y.debugName = "Y" 114 | 115 | check count[] == 1 116 | x <- 2 117 | check count[] == 2 118 | 119 | test """ 120 | Given a sigil of value 5 121 | When there is a computedNow sigil that is not being invoked 122 | Then it should still perform the computation immediately 123 | """: 124 | let 125 | count: ref int = new(int) 126 | x = newSigil(5) 127 | y = computedNow[int]: 128 | count[] += 1 129 | 2 * x{} 130 | 131 | when defined(sigilsDebug): 132 | x.debugName = "X" 133 | y.debugName = "Y" 134 | 135 | check count[] == 1 136 | 137 | test """ 138 | Given a sigil of value 5 and a computed that is double the sigil 139 | When the computed sigil is invoked multiple times 140 | Then it should perform the compute only once 141 | """: 142 | let 143 | count: ref int = new(int) 144 | x = newSigil(5) 145 | y = computed[int]: 146 | count[] += 1 147 | 2 * x{} 148 | 149 | when defined(sigilsDebug): 150 | x.debugName = "X" 151 | y.debugName = "Y" 152 | 153 | check count[] == 0 154 | discard y{} 155 | discard y{} 156 | check count[] == 1 157 | 158 | test """ 159 | Given a computed sigil that is the sum of 2 sigils 160 | When either sigil is changed 161 | Then the computed sigil should be recomputed when the 162 | sigil is read 163 | """: 164 | let 165 | count = new(int) 166 | x = newSigil(1) 167 | y = newSigil(2) 168 | z = computed[int](): 169 | count[] += 1 170 | x{} + y{} 171 | 172 | check count[] == 0 # hasn't been read yet 173 | check z{} == 3 174 | check count[] == 1 # hasn't been read yet 175 | 176 | x <- 2 177 | check count[] == 1 178 | check z{} == 4 179 | 180 | y <- 3 181 | x <- 3 182 | check z{} == 6 183 | check count[] == 3 184 | 185 | test """ 186 | Given a computedNow sigil that is the sum of 2 sigils 187 | When either sigil is changed 188 | Then the computed sigil should be recomputed for every change 189 | """: 190 | let 191 | count = new(int) 192 | x = newSigil(1) 193 | y = newSigil(2) 194 | z = computedNow[int](): 195 | count[] += 1 196 | x{} + y{} 197 | 198 | check count[] == 1 199 | check z{} == 3 200 | 201 | x <- 2 202 | check count[] == 2 203 | check z{} == 4 204 | 205 | y <- 3 206 | x <- 3 207 | check count[] == 4 208 | check z{} == 6 209 | 210 | test """ 211 | Given a computed sigil that is double a computed sigil that is double a sigil 212 | When the sigil value changes to 4 213 | Then the computed sigil should be recomputed once to 16 214 | """: 215 | let 216 | countB = new(int) 217 | countC = new(int) 218 | a = newSigil(1) 219 | b = computed[int]: 220 | countB[] += 1 221 | 2 * a{} 222 | c = computed[int]: 223 | countC[] += 1 224 | 2 * b{} 225 | 226 | check countB[] == 0 227 | check countC[] == 0 228 | 229 | check c{} == 4 230 | check countB[] == 1 231 | check countC[] == 1 232 | 233 | echo "A: ", a 234 | echo "B: ", b 235 | echo "C: ", c 236 | 237 | a <- 4 238 | 239 | echo "A': ", a 240 | echo "B': ", b 241 | echo "C': ", c 242 | 243 | check c{} == 16 244 | check countC[] == 2 245 | 246 | test """ 247 | Given a computedNow sigil that is double a computed sigil that is double a sigil 248 | When the sigil value changes to 4 249 | Then the computed sigil should be recomputed once to 16 250 | """: 251 | let 252 | count = new(int) 253 | a = newSigil(1) 254 | b = computedNow[int](2 * a{}) 255 | c = computedNow[int]: 256 | count[] += 1 257 | 2 * b{} 258 | 259 | check count[] == 1 260 | check c{} == 4 261 | 262 | a <- 4 263 | 264 | check count[] == 2 265 | check c{} == 16 266 | 267 | test """ 268 | Given a computed sigil A that depends on a computed sigil B and both of them depend directly on the same sigil C 269 | When the sigil value of C changes 270 | Then the computed sigil A should be recomputed twice, once from the change of sigil C, once from the change of the computed sigil B . 271 | """: 272 | let 273 | count = new(int) 274 | a = newSigil(1) 275 | b = computed[int](2 * a{}) 276 | c = computed[int]: 277 | count[] += 1 278 | a{} + b{} 279 | 280 | check count[] == 0 281 | check c{} == 3 282 | check count[] == 1 283 | 284 | a <- 4 285 | 286 | check c{} == 12 287 | check count[] == 2 288 | 289 | test """ 290 | Given a computed sigil that is double an int-sigil but is always 0 if a boolean sigil is false 291 | When the sigils update 292 | Then the computed sigil should recompute accordingly 293 | """: 294 | let x = newSigil(5) 295 | let y = newSigil(false) 296 | 297 | when defined(sigilsDebug): 298 | x.debugName = "X" 299 | y.debugName = "Y" 300 | 301 | let z = computed[int](): 302 | if y{}: 303 | x{} * 2 304 | else: 305 | 0 306 | 307 | when defined(sigilsDebug): 308 | z.debugName = "Z" 309 | 310 | check x{} == 5 311 | check y{} == false 312 | check z{} == 0 313 | 314 | y <- true 315 | check y{} == true 316 | check z{} == 10 317 | 318 | x <- 2 319 | check x{} == 2 320 | check z{} == 4 321 | 322 | y <- false 323 | check y{} == false 324 | check z{} == 0 325 | 326 | test """ 327 | Given a computed sigil of type float32 multiplying 2 float32 sigils 328 | When the float sigils are changed 329 | Then the computed sigil should update 330 | """: 331 | let x = newSigil(3.14'f32) 332 | let y = newSigil(2.718'f32) 333 | 334 | let z = computed[float32](): 335 | x{} * y{} 336 | 337 | check isNear(x{}, 3.14) 338 | check isNear(y{}, 2.718) 339 | check isNear(z{}, 8.53452, 3) 340 | 341 | x <- 1.0 342 | check isNear(x{}, 1.0) 343 | check isNear(y{}, 2.718) 344 | check isNear(z{}, 2.718, 3) 345 | 346 | test """ 347 | Given a computed sigil of type float multiplying a float32 and a float64 sigil 348 | When the float sigils are changed 349 | Then the computed sigil should update 350 | """: 351 | let x = newSigil(3.14'f64) 352 | let y = newSigil(2.718'f32) 353 | 354 | let z = computed[float](): 355 | x{} * y{} 356 | 357 | echo "X: ", x{}, " Z: ", z{} 358 | check isNear(x{}, 3.14) 359 | check isNear(y{}, 2.718) 360 | check isNear(z{}, 8.53451979637.float, 4) 361 | 362 | x <- 1.0 363 | check isNear(x{}, 1.0) 364 | check isNear(y{}, 2.718) 365 | check isNear(z{}, 2.718) 366 | 367 | suite "#computed sigil": 368 | test """ 369 | Given a computed sigil 370 | When an attempt is made to assign to the sigil 371 | Then it should not compile 372 | """: 373 | let 374 | x = newSigil(5) 375 | y = computed[int](x{} * 2) 376 | 377 | check not compiles(y{} = 4) 378 | check not compiles(y{} <- 4) 379 | 380 | test """ 381 | Given a sigil of value 5 and a computed that is double the sigil 382 | When the computed sigil is invoked 383 | Then it should return the value 10 384 | """: 385 | let 386 | x = newSigil(5) 387 | y = computed[int](2*x{}) 388 | 389 | check y{} == 10 390 | 391 | test """ 392 | Given a sigil of value 5 and a computed that is double the sigil 393 | When the sigils value is changed to 2 and the computed sigil is invoked 394 | Then it should return the value 4 395 | """: 396 | # Given 397 | let 398 | x = newSigil(5) 399 | y = computed[int](2 * x{}) 400 | 401 | when defined(sigilsDebug): 402 | x.debugName = "X" 403 | y.debugName = "Y" 404 | 405 | check x{} == 5 406 | check y{} == 10 407 | 408 | x <- 2 409 | 410 | check x{} == 2 411 | check y{} == 4 412 | 413 | test """ 414 | Given a sigil and a computed that is double the sigil 415 | When the sigils value is changed 416 | Then it should only do a compute after the computed sigil was invoked 417 | """: 418 | # Given 419 | let 420 | count = new(int) 421 | x = newSigil(5) 422 | y = computed[int]: 423 | count[] += 1 424 | 2 * x{} 425 | 426 | when defined(sigilsDebug): 427 | x.debugName = "X" 428 | y.debugName = "Y" 429 | 430 | check count[] == 0 431 | x <- 2 432 | check count[] == 0 433 | check y{} == 4 434 | check count[] == 1 435 | 436 | test """ 437 | Given a sigil of value 5 438 | When there is a computed sigil that is not being invoked 439 | Then it should not perform the computation 440 | """: 441 | let 442 | count: ref int = new(int) 443 | x = newSigil(5) 444 | y = computed[int]: 445 | count[] += 1 446 | 2 * x{} 447 | 448 | when defined(sigilsDebug): 449 | x.debugName = "X" 450 | y.debugName = "Y" 451 | 452 | check count[] == 0 453 | 454 | test """ 455 | Given a sigil of value 5 and a computed that is double the sigil 456 | When the computed sigil is invoked multiple times 457 | Then it should perform the compute only once after the first invocation 458 | """: 459 | let 460 | count: ref int = new(int) 461 | x = newSigil(5) 462 | y = computed[int]: 463 | count[] += 1 464 | 2 * x{} 465 | 466 | when defined(sigilsDebug): 467 | x.debugName = "X" 468 | y.debugName = "Y" 469 | 470 | check count[] == 0 471 | discard y{} 472 | check count[] == 1 473 | discard y{} 474 | check count[] == 1 475 | 476 | test """ 477 | Given a computed sigil that is the sum of 2 sigils 478 | When either sigil is changed 479 | Then the computed sigil only be recomputed each time after an invocation, not after a sigil change 480 | """: 481 | let 482 | count = new(int) 483 | x = newSigil(1) 484 | y = newSigil(2) 485 | z = computed[int](): 486 | count[] += 1 487 | x{} + y{} 488 | 489 | check count[] == 0 490 | check z{} == 3 491 | check count[] == 1 492 | 493 | x <- 2 494 | check count[] == 1 495 | check z{} == 4 496 | check count[] == 2 497 | 498 | y <- 3 499 | x <- 3 500 | check count[] == 2 501 | check z{} == 6 502 | check count[] == 3 503 | 504 | test """ 505 | Given a computed sigil A that is double a computed sigil B that is double a sigil C of value 1 506 | When sigil A is invoked 507 | Then it should return 4 and also compute computed sigil B, but only once 508 | """: 509 | let 510 | countA = new(int) 511 | countB = new(int) 512 | c = newSigil(1) 513 | b = computed[int]: 514 | countB[] += 1 515 | 2 * c{} 516 | a = computed[int]: 517 | countA[] += 1 518 | 2 * b{} 519 | 520 | check countA[] == 0 521 | check countB[] == 0 522 | 523 | check a{} == 4 524 | 525 | check countA[] == 1 526 | check countB[] == 1 527 | 528 | check b{} == 2 529 | check countB[] == 1 530 | 531 | suite "#bridge sigils and agents": 532 | test """ 533 | Test bridging Sigils to regular Sigil Agents. e.g. for comptability 534 | with Figuro where we wanna override hook in with {} when we 535 | use Sigils 536 | """: 537 | type SomeAgent = ref object of Agent 538 | value: int 539 | 540 | let 541 | a = newSigil(2) 542 | b = computed[int]: 2 * a{} 543 | foo = SomeAgent() 544 | 545 | check a{} == 2 546 | check b{} == 4 547 | 548 | ## Bit annoying, to have to use a regular proc 549 | ## since the slot pragma and forward proc decl's 550 | ## don't seem to mix 551 | ## In Figuro `recompute` would just call `refresh` 552 | proc doDraw(obj: SomeAgent) 553 | proc recompute(obj: SomeAgent, attrs: set[SigilAttributes]) {.slot.} = 554 | obj.doDraw() 555 | 556 | proc draw(agent: SomeAgent) {.slot.} = 557 | bindSigilEvents(agent): 558 | let value = b{} 559 | agent.value = value 560 | 561 | proc doDraw(obj: SomeAgent) = 562 | obj.draw() 563 | 564 | foo.draw() 565 | 566 | check b{} == 4 567 | check foo.value == 4 568 | b <- 5 569 | check b{} == 5 570 | check foo.value == 5 571 | 572 | suite "#effects": 573 | setup: 574 | var internalSigilEffectRegistry = initSigilEffectRegistry() 575 | let reg = internalSigilEffectRegistry 576 | 577 | test "basic a sigil effect works": 578 | let 579 | count = new(int) 580 | x = newSigil(5) 581 | 582 | when defined(sigilsDebug): 583 | x.debugName = "X" 584 | 585 | effect: 586 | count[].inc() 587 | echo "X is now: ", x{} 588 | 589 | check count[] == 1 590 | let effs = reg.registered().toSeq() 591 | check effs.len() == 1 592 | 593 | emit reg.triggerEffects() 594 | check reg.registered().toSeq().len() == 1 595 | check reg.dirty().toSeq().len() == 0 596 | check count[] == 1 597 | # echo "x: ", x 598 | # echo "eff: ", reg.registered().toSeq() 599 | 600 | # echo "setting x <- 3" 601 | x <- 3 602 | # echo "x: ", x 603 | # echo "eff: ", reg.registered().toSeq() 604 | check count[] == 1 605 | check reg.dirty().toSeq().len() == 1 606 | 607 | emit reg.triggerEffects() 608 | 609 | check reg.dirty().toSeq().len() == 0 610 | check count[] == 2 611 | 612 | test "test a chained sigil effect": 613 | let 614 | count = new(int) 615 | x = newSigil(2) 616 | isEven = computed[bool]: 617 | x{} mod 2 == 0 618 | 619 | check count[] == 0 620 | check reg.registered().toSeq().len() == 0 621 | when defined(sigilsDebug): 622 | x.debugName = "X" 623 | isEven.debugName = "isEven" 624 | 625 | effect: 626 | # echo "\tEFF running: " 627 | count[].inc() 628 | if isEven{}: 629 | echo "\tX is even!" 630 | 631 | check reg.registered().toSeq().len() == 1 632 | check count[] == 1 633 | when defined(sigilsDebug): 634 | reg.registered().toSeq()[0].debugName = "EFF" 635 | printConnections(reg.registered().toSeq()[0]) 636 | 637 | # echo "eff: ", reg.registered().toSeq()[0] 638 | # echo "setting x <- 4" 639 | x <- 4 640 | # echo "X: ", x 641 | # echo "eff: ", reg.registered().toSeq()[0] 642 | check count[] == 1 643 | emit reg.triggerEffects() 644 | 645 | check reg.dirty().toSeq().len() == 0 646 | check count[] == 1 647 | 648 | # echo "setting x <- 3" 649 | x <- 3 650 | check reg.dirty().toSeq().len() == 1 651 | check count[] == 1 652 | 653 | emit reg.triggerEffects() 654 | check reg.dirty().toSeq().len() == 0 655 | check count[] == 2 656 | --------------------------------------------------------------------------------