i want to report a bug (?) in Nim which occurs when using a widget of Owlkettle (mainly a wrapper for GTK, based on Nim, to build GUIs). It's best to read my 2 detailed Issues at their GitHub page : first i did https://github.com/can-lehmann/owlkettle/issues/142 and this continued at https://github.com/can-lehmann/owlkettle/issues/145
the contents of their Text View widget can not be changed by a thread process .. i managed to create a simple script to run a process by a thread, but this can't be used to change the text content property .. i got nice feedback, and my script seems OK, but at this moment the conclusion is : this is a bug in Nim, not Owlkettle ..
that's why i write this post .. to give more info about the concerning crash, i executed my binary (called "uci") using 2 debuggers, GDB and LLDB (i'm on Linux) .. their output seems almost same.
can anyone confirm this is a Nim bug ? can it be solved ?
~/Compiled/owlkettle-main-v2.2.0$ lldb-15 ./examples/widgets/uci
(lldb) target create "./examples/widgets/uci"
Current executable set to '/home/roelof/Compiled/owlkettle-main-v2.2.0/examples/widgets/uci' (x86_64).
(lldb) run
Process 506697 launched: '/home/roelof/Compiled/owlkettle-main-v2.2.0/examples/widgets/uci' (x86_64)
Process 506697 stopped
thread #16, name = 'uci', stop reason = signal SIGSEGV: invalid address (fault address: 0x8)
frame #0: 0x0000555555565530 uci`unregisterCycle__system_u2986 + 348
uci`unregisterCycle__system_u2986:
-> 0x555555565530 <+348>: movq %rdx, 0x8(%rax)
0x555555565534 <+352>: movq $0x95, -0x30(%rbp)
0x55555556553c <+360>: movq %fs:-0x2b08, %rax
0x555555565545 <+369>: movl $0x0, %edx
(lldb) q
Quitting LLDB will kill one or more processes. Do you really want to proceed: [Y/n] Y
~/Compiled/owlkettle-main-v2.2.0$ gdb ./examples/widgets/uci
GNU gdb (Ubuntu 12.1-0ubuntu1~22.04) 12.1
Copyright (C) 2022 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Type "show copying" and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<https://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from ./examples/widgets/uci...
(No debugging symbols found in ./examples/widgets/uci)
(gdb) run
Starting program: /home/roelof/Compiled/owlkettle-main-v2.2.0/examples/widgets/uci
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
[New Thread 0x7ffff5cfb640 (LWP 507600)]
[Thread 0x7ffff5cfb640 (LWP 507600) exited]
[New Thread 0x7ffff5cfb640 (LWP 507601)]
[New Thread 0x7ffff48c3640 (LWP 507602)]
[New Thread 0x7fffe5575640 (LWP 507603)]
[New Thread 0x7fffdffff640 (LWP 507604)]
[Thread 0x7fffdffff640 (LWP 507604) exited]
[Thread 0x7ffff48c3640 (LWP 507602) exited]
[Thread 0x7fffe5575640 (LWP 507603) exited]
[Thread 0x7ffff5cfb640 (LWP 507601) exited]
[New Thread 0x7ffff5cfb640 (LWP 507606)]
[New Thread 0x7fffdffff640 (LWP 507607)]
[New Thread 0x7fffe5575640 (LWP 507608)]
[New Thread 0x7ffff48c3640 (LWP 507609)]
[New Thread 0x7fffe4c33640 (LWP 507610)]
[New Thread 0x7fffdf7fe640 (LWP 507611)]
[New Thread 0x7fffdeffd640 (LWP 507612)]
[New Thread 0x7fffde7fc640 (LWP 507613)]
[New Thread 0x7fffddffb640 (LWP 507614)]
[New Thread 0x7fffdd7fa640 (LWP 507615)]
[New Thread 0x7fffdcff9640 (LWP 507616)]
[New Thread 0x7fffb7fff640 (LWP 507617)]
[New Thread 0x7fffb6d96640 (LWP 507618)]
[New Thread 0x7fffb6595640 (LWP 507619)]
[New Thread 0x7fffb5d94640 (LWP 507620)]
[New Thread 0x7fffb5593640 (LWP 507621)]
[New Thread 0x7fffb4d92640 (LWP 507622)]
[Thread 0x7fffb6595640 (LWP 507619) exited]
[Thread 0x7fffb5d94640 (LWP 507620) exited]
[Thread 0x7fffb5593640 (LWP 507621) exited]
[Thread 0x7fffb4d92640 (LWP 507622) exited]
[New Thread 0x7fffb4d92640 (LWP 507623)]
[New Thread 0x7fffb5593640 (LWP 507624)]
[Thread 0x7fffb4d92640 (LWP 507623) exited]
[Thread 0x7fffb5593640 (LWP 507624) exited]
[New Thread 0x7fffb5593640 (LWP 507625)]
[New Thread 0x7fffb4d92640 (LWP 507626)]
[Thread 0x7fffb5593640 (LWP 507625) exited]
[New Thread 0x7fffb5593640 (LWP 507627)]
[New Thread 0x7fffb5d94640 (LWP 507628)]
[Thread 0x7fffb5593640 (LWP 507627) exited]
[Thread 0x7fffb4d92640 (LWP 507626) exited]
[Thread 0x7fffb5d94640 (LWP 507628) exited]
[Thread 0x7fffddffb640 (LWP 507614) exited]
[Thread 0x7fffdd7fa640 (LWP 507615) exited]
[New Thread 0x7fffb6595640 (LWP 507644)]
Thread 30 "uci" received signal SIGSEGV, Segmentation fault.
[Switching to Thread 0x7fffb6595640 (LWP 507644)]
0x0000555555565530 in unregisterCycle__system_u2986 ()
(gdb) q
A debugging session is active.
Inferior 1 [process 507597] will be killed.
Quit anyway? (y or n) y
You absolutely cannot conclude that Nim is the cause.
Posting my recent Discord messages here:
yeah there are many clear issues in eg this unfortunately: https://github.com/can-lehmann/owlkettle/blob/main/examples/misc/threading.nim where is the type AppState? is it a ref object? if so, problem no locks around cross-thread state modification = behavior that cannot be reasoned about https://github.com/can-lehmann/owlkettle/blob/main/owlkettle.nim#L84 race condition drawing? no lock to ensure only one thread is drawing at a time what thread assumtions does the gtk4 api make? probably that all procs are only touched by the main / event loop thread if using threads does it require complied with --mm:orc/arc otherwise does not work i would suggest owlkettle actually be explicitly single threaded and have any other threads send messages to the owlkettle thread / main thread through its event loop, which would allow other threads to post messages having mutliple threads touching ui state is not a good idea
further additions and improvement of my script are working, but not always .. i'm running a chess engine binary as a thread process and i catch its output while it's evaluating a chess position .. but the process isn't "stable" : a severe error may happen anytime ? I tried different chess binaries, sleep(100) at some points, and more silly things to find and correct the error.
then i found https://github.com/ix/notewell/issues/3 : user "ix" had similar problems, and finally he seems to have solved the issue .. he writes :
EDIT I was using 1 as the priority. Should've done things properly and used PRIORITY_HIGH_IDLE, which waits on redraw & resize operations before executing the callback.
so, i found info about PRIORITY_HIGH_IDLE, eg. https://docs.gtk.org/glib/const.PRIORITY_HIGH_IDLE.html and i want to try this, because it looks like an obvious and simple cure ?
but how to set the GTK Constant PRIORITY_HIGH_IDLE by Nim ?
and here is the latest "unstable" version of the script :
import std/[streams, os, osproc, strutils, strformat]
import owlkettle
viewable App:
buffer: TextBuffer
monospace: bool = false
cursorVisible: bool = true
editable: bool = true
acceptsTab: bool = true
indent: int = 0
sensitive: bool = true
sizeRequest: tuple[x, y: int] = (-1, -1)
tooltip: string = ""
var MS = 1 # sleep [#] MilliSeconds
var DP = 18 # eval DePth
var TFecho = true
#var TFecho = false
when compileOption("threads"):
var thread: Thread[AppState]
proc chgBuffer(app: AppState, s: string) =
var abt = ""
var s2 = ""
if TFecho:
echo s
try:
abt = app.buffer.text
except Exception as e:
var error = getCurrentException()
echo "*** ERROR ABT 1"
try:
s2 = abt & s & "\n"
app.buffer.text = s2
#app.buffer.text = s & "\n"
#app.buffer.text = app.buffer.text & s & "\n"
except Exception as e:
var error = getCurrentException()
echo "*** ERROR ABT 2"
sleep(500)
try:
app.redrawFromThread()
except Exception as e:
var error = getCurrentException()
echo "*** ERROR ABT 3"
sleep(500)
#discard
proc threadProc(app: AppState) {.thread.} =
#let process = startProcess( "./examples/widgets/", args = [], options = { poInteractive, poUsePath } )
let process = startProcess( "./examples/widgets/Clubfoot_1.0.8e3b4da_compiled_HP", args = [], options = { poInteractive, poUsePath } )
#let process = startProcess( "./examples/widgets/amoeba_v3.4_compiled_fast", args = [], options = { poInteractive, poUsePath } )
#let process = startProcess( "./examples/widgets/camel_v1.2.0-PR90-compiled_HP", args = [], options = { poInteractive, poUsePath } ) # ???
let (fromP, toP) = ( process.outputStream , process.inputStream )
var s = ""
var TF = false
proc mySleep() =
sleep(MS)
#discard
toP.write("uci\n") # returns "uciok"
toP.flush
s = fromP.readLine # should return "uciok"
mySleep()
chgBuffer(app,s)
TF = true
while TF:
s = fromP.readLine
if not isNil(s) and len(s.strip()) > 0:
mySleep()
else:
echo "*** CONTINUE A"
continue
chgBuffer(app, "-=> " & s)
if s == "uciok":
toP.write("ucinewgame\n") # returns nothing
toP.flush
mySleep()
chgBuffer(app,"<=- ucinewgame")
toP.write("isready\n") # returns "readyok"
toP.flush
mySleep()
chgBuffer(app,"<=- isready")
s = fromP.readLine
if not isNil(s) and len(s.strip()) > 0:
mySleep()
else:
echo "*** CONTINUE B"
continue
# should be "readyok"
chgBuffer(app,"-=> " & s)
toP.write("position fen 3B4/1r2p3/r2p1p2/bkp1P1p1/1p1P1PPp/p1P1K2P/PPB5/8 w - - 1 1\n") # returns nothing
toP.flush
mySleep()
chgBuffer(app,"<=- position fen 3B4/1r2p3/r2p1p2/bkp1P1p1/1p1P1PPp/p1P1K2P/PPB5/8 w - - 1 1")
toP.write("isready\n") # returns "readyok"
toP.flush
mySleep()
chgBuffer(app,"<=- isready")
s = fromP.readLine
if not isNil(s) and len(s.strip()) > 0:
mySleep()
else:
echo "*** CONTINUE C"
continue
# should be "readyok"
chgBuffer(app,"-=> " & s)
# returns a lot of info lines, ending with "bestmove [...]"
toP.write(&"go depth {DP}\n") # NOTE: using fmt"..." does NOT work here !? : use the '&' syntax..
toP.flush
mySleep()
chgBuffer(app, fmt"<=- go depth {DP}")
elif s == "readyok":
try:
chgBuffer(app,"<=- readyok")
except Exception as e:
echo "FAILED READYOK ***********************"
elif s.startsWith("bestmove"):
TF = false # this will be the last echoed line
# this example script ends here,
# but we could input other UCI commands now (keeping TF true)
#sleep(1000)
toP.write("quit\n")
toP.flush
mySleep()
echo "<=- quit"
discard process.waitForExit
process.close
echo "MSG : process closed"
method view(app: AppState): Widget =
result = gui:
Window:
title = "*** TEST ***"
defaultSize = (1100, 600)
HeaderBar {.addTitlebar.}:
Button {.addLeft.}:
style = [ButtonFlat]
text = "GO"
proc clicked() =
when compileOption("threads"):
createThread(thread, threadProc, app)
ScrolledWindow:
TextView:
margin = 12
buffer = app.buffer
monospace = app.monospace
cursorVisible = app.cursorVisible
editable = app.editable
acceptsTab = app.acceptsTab
indent = app.indent
sensitive = app.sensitive
tooltip = app.tooltip
sizeRequest = app.sizeRequest
proc changed() = discard
#when compileOption("threads") and (defined(gcOrc) or defined(gcArc)):
#when compileOption("threads") and defined(mmArc):
when compileOption("threads"):
let buffer = newTextBuffer()
buffer.text = "first text line\n"
brew(gui(App(buffer = buffer)))
else:
#quit "Compile with --threads:on and --gc:orc to enable threads" # ORG
quit "Compile with --threads:on and --mm:arc to enable threads"
which, can output something like this (when all goes well) .. it takes about 1 minute before "bestmove" comes (and i then quit the script by Ctrl-C) :
~/Compiled/owlkettle-main-v2.2.0$ ./examples/widgets/uci_v3
-=> uciok
-=> id author Richard Delorme
-=> option name Ponder type check default false
-=> option name Hash type spin default 64 min 1 max 1048576
-=> option name Clear Hash type button
-=> option name Threads type spin default 1 min 1 max 256
-=> option name MultiPV type spin default 1 min 1 max 256
-=> option name UCI_AnalyseMode type check default false
-=> option name Affinity type string default 0:0
-=> uciok
<=- ucinewgame
<=- isready
-=> readyok
<=- position fen 3B4/1r2p3/r2p1p2/bkp1P1p1/1p1P1PPp/p1P1K2P/PPB5/8 w - - 1 1
<=- isready
-=> readyok
<=- go depth 18
-=> info depth 1 seldepth 20 score cp -434 time 7 nodes 13226 nps 1762926 pv c2d3 c5c4
-=> info depth 2 seldepth 21 score cp -257 time 13 nodes 21768 nps 1569385 pv c3c4 b5c4 c2d3 c4d5
-=> info depth 3 seldepth 5 score cp -257 time 14 nodes 22187 nps 1561420 pv c3c4 b5c4 c2d3 c4d5 d3a6
-=> info depth 4 seldepth 7 score cp -257 time 14 nodes 22386 nps 1556204 pv c3c4 b5c4 c2d3 c4d5 d3a6 c5d4 e3d3
-=> info depth 5 seldepth 17 score cp -236 time 15 nodes 24011 nps 1561599 pv c3c4 b5c4 c2d3 c4d5 d3a6 b7d7 d8a5
-=> info depth 6 seldepth 21 score cp -330 time 27 nodes 45847 nps 1683515 pv c3c4 b5c4 c2d3 c4d5 d3e4 d5e6 e4b7 c5d4 e3d4 f6e5 f4e5
-=> info depth 7 seldepth 16 score cp -329 time 32 nodes 54813 nps 1681669 pv c3c4 b5c4 c2d3 c4d5 d3e4 d5e6 e4b7 a3b2 b7c8 e6d5 c8b7 d5e6
-=> info depth 8 seldepth 21 score cp -409 time 138 nodes 266182 nps 1919154 pv c3c4 b5c4 c2d3 c4d5 d3e4 d5e6 e4b7 a3b2 b7c8 e6d5 c8b7 a6c6 b7c6 d5c6
-=> info depth 9 seldepth 25 score cp -415 time 200 nodes 384037 nps 1916277 pv c3c4 b5c4 c2d3 c4d5 d3e4 d5e6 e4b7 a3b2 b7a6 g5f4 e3f4 f6e5 d4e5 b2b1Q
-=> info depth 10 seldepth 25 score cp -403 time 347 nodes 633637 nps 1821606 pv c3c4 b5c4 c2d3 c4d5 e5f6 a5d8 d3a6 b7b8 b2a3 c5d4 e3d3 e7f6 a6c4 d5c5
-=> info depth 11 seldepth 22 score cp -409 time 530 nodes 972651 nps 1832222 pv c3c4 b5c4 c2d3 c4d5 e5f6 g5f4 e3f3 e7f6 d3a6 b7a7 d8f6 a7a6 b2a3 a5b6
-=> info depth 12 seldepth 24 score cp -414 time 646 nodes 1200283 nps 1855648 pv c3c4 b5c4 c2d3 c4d5 e5f6 g5f4 e3f3 e7f6 d3a6 b7a7 d8f6 a7a6 b2a3 a5b6 a3b4
-=> info depth 13 seldepth 31 score cp -422 time 1234 nodes 2380377 nps 1928188 hashfull 354 pv c3c4 b5c4 c2d3 c4d5 d3a6 b7a7 e5f6 a3b2 a6d3 g5f4 e3f2 e7f6 d8f6 c5c4 d3f5 a5b6 f2f3 b6d4 f5e4 d5c5 f6h4 d6d5
-=> info depth 14 seldepth 26 score cp -427 time 1633 nodes 3141554 nps 1922969 hashfull 454 pv c3c4 b5c4 c2d3 c4d5 d3a6 b7a7 e5f6 a3b2 a6d3 g5f4 e3f2 e7f6 d8f6 c5c4 d3f5 a5b6 f2f3
-=> info depth 15 seldepth 40 score cp -429 time 2148 nodes 4144414 nps 1928994 hashfull 565 pv c3c4 b5c4 c2d3 c4d5 d3a6 b7a7 e5f6 a3b2 a6d3 g5f4 e3f2 e7f6 d8f6 c5c4 d3f5 a5b6 f2f3 b6d4 f5e4 d5c5 f6h4 a7a2 f3f4
-=> info depth 16 seldepth 29 score cp -434 time 2924 nodes 5557593 nps 1900090 hashfull 703 pv c3c4 b5c4 c2d3 c4d5 d3a6 b7a7 e5f6 a3b2 a6d3 g5f4 e3f2 e7f6 d8f6 c5c4 d3f5 a5b6 f2f3 b6d4
-=> info depth 17 seldepth 30 score cp -441 time 4760 nodes 8692636 nps 1825900 hashfull 891 pv c3c4 b5c4 c2d3 c4d5 e5f6 g5f4 e3f3 e7f6 d3a6 b7a7 d8f6 a7a6 b2b3 c5d4 f3f4 a6c6 f6h4 c6c2 g4g5 c2a2 g5g6
-=> info depth 18 seldepth 29 score cp -451 time 8345 nodes 15057773 nps 1804390 hashfull 981 pv c3c4 b5c4 c2d3 c4d5 d3a6 b7a7 e5f6 e7f6 d8f6 a7a6 b2a3 c5d4 f6d4 g5f4 e3d3 b4a3 d4f6 a6c6 g4g5 d5e6 d3e4 c6c4 e4d3 c4c6 d3e4
-=> bestmove c3c4
<=- quit
MSG : process closed
^CTraceback (most recent call last)
/home/roelof/Compiled/owlkettle-main-v2.2.0/examples/widgets/uci_v3.nim(239) uci_v3
/home/roelof/Compiled/owlkettle-main-v2.2.0/owlkettle.nim(160) brew
/home/roelof/Compiled/owlkettle-main-v2.2.0/owlkettle/mainloop.nim(97) runMainloop
SIGINT: Interrupted by Ctrl-C.
however, it can end like this :
[...]
-=> info depth 9 seldepth 25 nodes 453775 time 4624 nps 98134 score cp -554 pv c3c4 b5c4 c2d3 c4d5 d3e4 d5e6 e4b7 a3b2 b7c8 e6f7 e5e6 f7e8 c8a6 b2b1q d8a5 b1a2
-=> info depth 9 seldepth 25 nodes 558907 time 5704 nps 97985 currmovenumber 5 currmove e5f6
-=> info depth 9 seldepth 27 nodes 645276 time 6704 nps 96252 currmovenumber 5 currmove e5f6
-=> info depth 9 seldepth 27 nodes 746912 time 7705 nps 96938 currmovenumber 23 currmove e3f3
-=> info depth 10 seldepth 27 nodes 844811 time 8706 nps 97037 currmovenumber 1 currmove c3c4
-=> info depth 10 seldepth 27 nodes 939526 time 9706 nps 96798 currmovenumber 1 currmove c3c4
-=> info depth 10 seldepth 27 nodes 1029896 time 10707 nps 96189 currmovenumber 1 currmove c3c4
-=> info depth 10 seldepth 27 nodes 1108090 time 11616 nps 95393 score cp -578 pv c3c4 b5c4 c2d3 c4d5 d3a6 b7a7 d8a5 a3b2 a6d3 c5d4 e3f3 a7a5 d3e4 d5c5
-=> info depth 10 seldepth 27 nodes 1202469 time 12708 nps 94622 currmovenumber 3 currmove c2d3
-=> info depth 10 seldepth 27 nodes 1289222 time 13709 nps 94042 currmovenumber 5 currmove e5f6
-=> info depth 10 seldepth 27 nodes 1372099 time 14710 nps 93276 currmovenumber 5 currmove e5f6
-=> info depth 10 seldepth 27 nodes 1458539 time 15710 nps 92841 currmovenumber 6 currmove f4g5
-=> info depth 10 seldepth 27 nodes 1546697 time 16711 nps 92555 currmovenumber 11 currmove c2g6
-=> info depth 10 seldepth 27 nodes 1639341 time 17712 nps 92555 currmovenumber 17 currmove c2d1
**
Gtk:ERROR:../../../gtk/gtktextview.c:4744:gtk_text_view_validate_onscreen: assertion failed: (priv->onscreen_validated)
Bail out! Gtk:ERROR:../../../gtk/gtktextview.c:4744:gtk_text_view_validate_onscreen: assertion failed: (priv->onscreen_validated)
SIGABRT: Abnormal termination.
Aborted (core dumped)
The problem is not within Owlkettle, but in the graphical stack. Almost every GUI library isn't thread-safe and can be updated only from its main thread. The only multithread libraries are Vulkan, DirectX 12 and Metal. Anything else will crash when you will try to update GUI from another thread.
The problem can be solved by adding a timer to a main thread and read the data shared between threads. That's why almost every GUI library has implemented timers. :)
@Araq : please don't be rude, i'm a beginner and i try to understand threads my way .. thus far, this project and Owlkettle example codes gives me new insight in many ways.
There is no point in debugging your program as it's wrong
it probably is, but how and why ? Some users gave me nice input to understand things and learn. Eg. i'm trying to understand how GTK is involved and how Owlkettle interacts with it, in a Nim way.
but you don't listen
well, let's say i don't yet grasp certain functions and constructions. therefor i like examples.
The AppState type gets generated by the viewable macro. It is a ref-type
Strictly speaking 2 ref-types, one representing the state of the GTKWidget (AppState) and one representing the "new" state within owlkettle with all the values it got from elsewhere (App).
For those curious, this is the type it generates from one of the examples:
type
App = ref object of Widget
hasBuffer*: bool
valBuffer*: TextBuffer
hasMonospace*: bool
valMonospace*: bool
hasCursorVisible*: bool
valCursorVisible*: bool
hasEditable*: bool
valEditable*: bool
hasAcceptsTab*: bool
valAcceptsTab*: bool
hasIndent*: bool
valIndent*: int
hasSensitive*: bool
valSensitive*: bool
hasSizeRequest*: bool
valSizeRequest*: tuple[x, y: int]
hasTooltip*: bool
valTooltip*: string
AppState = ref object of Viewable
buffer*: TextBuffer
monospace*: bool
cursorVisible*: bool
editable*: bool
acceptsTab*: bool
indent*: int
sensitive*: bool
sizeRequest*: tuple[x, y: int]
tooltip*: string
And afaik yes, threads:on is mostly looked at combined with arc/orc, I'm not aware of any looking too much on the "nim 1.6 with refc" case, though note that I don't speak all that much for the project, I just contribute.
As for the thread assumptions GTK makes (to keep the info level here the same as in discord):
GTK requires that all GTK API calls are made from the same thread in which the GtkApplication was created, or gtk_init() was called (the main thread).
A quote from a user from GTK's discourse forum:
Your second example is actually calling update_rows in another thread, but in GTK you should never try to do that. GTK is not thread safe, it is only safe to call GTK functions from the main thread.
What this mostly made me wonder is how a server-client style architecture with owlkettle could look like, with the backend being just a separately running process.
I did some research and playing around to figure out how a client-server architecture with owlkettle could look like and it works pretty well with using threads and channels (As Araq pretty much suggested if I understood him correctly).
Below the minimal example that I got to work.
Basically you have 3 threads:
And 2 channels:
import owlkettle, owlkettle/[widgetutils, adw]
import owlkettle/bindings/gtk
import std/[options, os]
var counter: int = 0
## Communication
type ChannelHub = ref object
serverChannel: Channel[string]
clientChannel: Channel[string]
proc sendToServer(hub: ChannelHub, msg: string): bool =
echo "send client => server: ", msg
hub.clientChannel.trySend(msg)
proc sendToClient(hub: ChannelHub, msg: string): bool =
echo "send client <= server: ", msg
hub.serverChannel.trySend(msg)
proc readClientMsg(hub: ChannelHub): Option[string] =
let response: tuple[dataAvailable: bool, msg: string] = hub.clientChannel.tryRecv()
result = if response.dataAvailable:
echo "read client => server: ", response.repr
some(response.msg)
else:
none(string)
proc readServerMsg(hub: ChannelHub): Option[string] =
let response: tuple[dataAvailable: bool, msg: string] = hub.serverChannel.tryRecv()
result = if response.dataAvailable:
echo "read client <= server: ", response.repr
some(response.msg)
else:
none(string)
proc hasServerMsg(hub: ChannelHub): bool =
hub.serverChannel.peek() > 0
## Server
proc setupServer(channels: ChannelHub): Thread[ChannelHub] =
proc serverLoop(hub: ChannelHub) =
while true:
let msg = hub.readClientMsg()
if msg.isSome():
discard hub.sendToClient("Received Message " & $counter)
counter.inc
sleep(0) # Reduces stress on CPU when idle, increase when higher latency is acceptable for even better idle efficiency
createThread(result, serverLoop, channels)
## Client
proc addServerListener(app: Viewable, hub: ChannelHub, priority: int = 200) # Forward declaration
viewable App:
hub: ChannelHub
backendMsg: string = ""
hooks:
afterBuild:
addServerListener(state, state.hub)
type ListenerData = object
hub: ChannelHub
app: Viewable
proc addServerListener(app: Viewable, hub: ChannelHub, priority: int = 200) =
proc listener(cell: pointer): cbool {.cdecl.} =
let data = cast[ptr ListenerData](cell)[]
if data.hub.hasServerMsg():
discard data.app.redraw()
sleep(0) # Reduces stress on CPU when idle, increase when higher latency is acceptable for even better idle efficiency
return true.cbool
let data = allocSharedCell(ListenerData(hub: hub, app: app))
discard g_idle_add_full(priority.cint, listener, data, nil)
method view(app: AppState): Widget =
let msg: Option[string] = app.hub.readServerMsg()
if msg.isSome():
app.backendMsg = msg.get()
result = gui:
Window:
defaultSize = (500, 150)
title = "Client Server Example"
Box:
orient = OrientY
margin = 12
spacing = 6
Button {.hAlign: AlignCenter, vAlign: AlignCenter.}:
Label(text = "Click me")
proc clicked() =
discard app.hub.sendToServer("Frontend message!")
Label(text = "Message sent by Backend: ")
Label(text = app.backendMsg)
proc setupClient(channels: ChannelHub): Thread[ChannelHub] =
proc startOwlkettle(hub: ChannelHub) =
adw.brew(gui(App(hub = hub)))
createThread(result, startOwlkettle, channels)
## Main
proc main() =
var serverToClientChannel: Channel[string]
var clientToServerChannel: Channel[string]
serverToClientChannel.open()
clientToServerChannel.open()
let hub = ChannelHub(serverChannel: serverToClientChannel, clientChannel: clientToServerChannel)
let client = setupClient(hub)
let server = setupServer(hub)
joinThreads(server, client)
main()
This likely still requires some clean-up, for example I think updating AppState could possibly be pushed into the listener proc. But as a first stab at it (And this being my first proper attempt at doing multi-threading myself in nim), that seems reasonable.
I am an absolute beginner and I hardly understand the problem that @Isofruit code has solved. Bui I believe to have a similar problem in the code that I post here below. My question is: how can I add to textArea the same text it is echoed on console in miohttpd on the line echo "responding on port 8080..."?
import std/asynchttpserver
import std/asyncdispatch
import nigui
var
thread1: Thread[void]
textarea:TextArea
app.init()
var window = newWindow("NiGui Example")
var container = newLayoutContainer(Layout_Vertical)
window.add(container)
var button = newButton("Button 1")
container.add(button)
textArea = newTextArea()
container.add(textArea)
button.onClick = proc(event: ClickEvent) =
textArea.addLine("Button 1 clicked...")
proc contenitore () = {.gcsafe.}:
proc miohttpd {.async} =
var server = newAsyncHttpServer()
proc cb(req: Request) {.async.} =
let headers = {"Content-type": "text/plain; charset=utf-8"}
await req.respond(Http200, "Hello World\n", headers.newHttpHeaders())
echo "responding on port 8080..."
server.listen(Port(8080))
echo "listening on port 8080..."
while true:
if server.shouldAcceptRequest():
await server.acceptRequest(cb)
else:
echo "await sleepAsync"
await sleepAsync(500)
waitFor miohttpd()
createThread(thread1, contenitore)
window.show()
app.run