Open dmknght opened 4 years ago
Does gintro 0.7.8 released yesterday work fine for you? Well I got no new issues yet, so maybe there are not too obvious issues?
Multi threaded GTK is difficult indeed. Multi threaded Nim too. I am interested myself in this topic, for my chess game I would like to have a non blocking GUI. And for cairo drawing multiple threads would be nice too. But unfortunately cairo is from design single threaded.
I have not really investigated this topic myself. Well I did some google search of course, there are simple solutions like the gtk idle function. The Nim cairo_anim is a very basic non blocking solution. Investigating this topic would be fun, but my time is too restricted for the next years, the GTK4 and the Nim book have priority, and there are many other tasks for me...
I would propose that you ask on the Nim forum. Or maybe you ask on the GTK forum for C solutions, we should be able to convert it to Nim then. If you ask on Nim forum, you may ask for the other Nim GUI toolkits for threading as well, maybe some of the other GUIs has a good solution already. I think Nim has more than a dozen GUI toolkits now, I have listed some of them in the GTK4 book already.
Does gintro 0.7.8 released yesterday work fine for you? Well I got no new issues yet, so maybe there are not too obvious issues?
I used gintro 0.7.7. I've upgraded to 0.7.8 and everything is good. It is just this threading issue and it is not very nice.
Multi threaded GTK is difficult indeed. Multi threaded Nim too Super agree with you. Well my plan is making an application firewall on Linux like opensnitch so a gui is needed and as you see, non-blocking GUI is something I have to think about (for further development). For now the hang is acceptable but i feel it is not good for user's experience.
there are simple solutions like the gtk idle function So i guess gintro is not supported this by now?
there are simple solutions like the gtk idle function I asked on telegram channel and there is a solution that i use threading, and timeout_add to check if is running. I think it is acceptable but for more complex software can it be good? If it can, we can write prebuilt function for gintro?
https://developer.gnome.org/gtkmm-tutorial/unstable/sec-multithread-example.html.en Other example of multi threaded gtk app. Hopefully we can do it with nim
So there is 1 way to do threading but for calling system command only: https://developer.gnome.org/glib/2.64/glib-Spawning-Processes.html#g-spawn-command-line-async StackOverflow answer: https://stackoverflow.com/a/7048806 I think it is a good way to start. I'm searching more about this topic.
My test code. It worked perfectly:
import gintro / [gtk, gobject, glib]
import osproc
let command = "ping 8.8.8.8 -c 6"
proc onClickIsRun(b: Button) =
echo "Clicked on test"
proc onClickPingThread(b: Button) =
if spawnCommandLineAsync(command):
echo "Done"
# Must use ChildWatch to get output
else:
echo "Failed"
proc onClickPing(b: Button) =
echo execCmd(command)
proc createArea(boxMainWindow: Box) =
let
btnPing = newButton("Ping")
btnPingThread = newButton("Ping Threads")
btnIsRunning = newButton("Click to test it is running")
boxTest = newBox(Orientation.horizontal, 3)
btnPing.connect("clicked", onClickPing)
btnPingThread.connect("clicked", onClickPingthread)
btnIsRunning.connect("clicked", onClickIsRun)
boxTest.add(btnPing)
boxTest.add(btnPingThread)
boxTest.add(btnIsRunning)
boxMainWindow.add(boxTest)
proc onClickStop(w: Window) =
#[
Close program by click on title bar
]#
mainQuit()
proc main =
#[
Create new window
http://blog.borovsak.si/2009/06/multi-threaded-gtk-applications.html
]#
gtk.init()
let
mainBoard = newWindow()
boxMainWindow = newBox(Orientation.vertical, 3)
mainBoard.title = "Test Threading"
createArea(boxMainWindow)
mainBoard.add(boxMainWindow)
mainBoard.setBorderWidth(3)
mainBoard.showAll()
mainBoard.connect("destroy", onClickStop)
gtk.main()
main()
Screenshot
spawnCommandLineAsync
. This button should run process in background without hanging whole GUIhere is an example to use multi threads in gtk with glib.g_idle_add. I hope you can help me try it on gintro @StefanSalewski
https://stackoverflow.com/a/37009232
binding source of Nim proc idleAdd(priority: int; function: SourceFunc; data: pointer; notify: DestroyNotify): int
here is an example to use multi threads in gtk with glib.g_idle_add. I hope you can help me try it on gintro @StefanSalewski https://stackoverflow.com/a/37009232 binding source of Nim
proc idleAdd(priority: int; function: SourceFunc; data: pointer; notify: DestroyNotify): int
It ran after i added doCheckIP(userData: pointer): gboolean {.cdecl.}
to call function. The code is still single thread. The code:
proc doCheckIP(userData: pointer): gboolean {.cdecl.} =
let ipInfo = checkIPwTorServer()
var iconName: string
# If program has error while getting IP address
if ipInfo[0].startsWith("Error"):
iconName = "error"
# If program runs but user didn't connect to tor
elif ipInfo[0].startsWith("Sorry"):
iconName = "security-medium"
# Connected to tor
else:
iconName = "security-high"
sendNotify(ipInfo[0], ipInfo[1], iconName)
proc onClickCheckIP*(b: Button) =
#[
Display IP when user click on CheckIP button
Show the information in system's notification
]#
# doCheckIP()
# idleAdd ( priority: 0 = default, function?, pointer, notify: DestroyNotify)
discard glib.idleAdd(0, doCheckIP, nil, nil)
Great that you continued your investigations. I did a short google search myself about that topic, but was not too satisfied with the results yet. I consider the use of the GTK idle functions a bit ugly, it seems to be some sort of polling for results, but maybe that is the only solution that works with GTK. Have still to consider the message passing between thread in Nim, using Channels? I heard ARC would support better ways for thread communication, but I have still to investigate that. But maybe it is enough when we get an ugly but working solution for the start, we can improve it later.
I consider the use of the GTK idle functions a bit ugly
Yes i agree and it didn't work for me.
Have still to consider the message passing between thread in Nim, using Channels?
I tried it before with help from members in Telegram channel. It works. Well the problem is multiple channel and multiple threads. I tried that for creating GUI for ClamAV. The requirement was more complex than the basic syntax of GTK can do.
So my idea is, if we can, we create a pre-built nim lang modules for threading in gtk with some basic check. But idk if it is good and further problems we can have.
Have not yet read all your post carefully sorry, here is a fast hack using GTK3 timeoutAdd() and Nim channels. Nim thread does a countdown, we display that number as label on a button, we can click the button to invert the numbers. Well maybe not really nice, but it is a start, and was really easy. Works with arc, have not jet tested with GTK4.
# nim c --threads:on --gc:arc -r t.nim
# https://nim-lang.org/docs/channels.html
import gintro/[gtk, glib, gobject, gio]
from os import sleep
var chan: Channel[int]
var worker: system.Thread[void]
proc work() =
var countdown {.global.} = 25
while countdown > 0:
sleep(1000)
dec(countdown)
chan.send(countdown)
proc invalidateCb(b: Button): bool =
let tried = chan.tryRecv()
if tried.dataAvailable:
#echo tried.msg
b.label = $tried.msg
if tried.msg == 0:
worker.joinThread
chan.close
return SOURCE_REMOVE
return SOURCE_CONTINUE
proc buttonClicked (button: Button) =
button.label = utf8Strreverse(button.label, -1)
proc appActivate (app: Application) =
let window = newApplicationWindow(app)
window.title = "Countdown"
window.defaultSize = (250, 50)
let button = newButton("Click Me")
window.add(button)
button.connect("clicked", buttonClicked)
window.showAll
chan.open
createThread(worker, work)
discard timeoutAdd(1000 div 60, invalidateCb, button)
proc main =
let app = newApplication("org.gtk.example")
connect(app, "activate", appActivate)
discard app.run
main()
The gtkmm example from
https://developer.gnome.org/gtkmm-tutorial/unstable/sec-multithread-example.html.en
is indeed interesting, it seems to not need something like timeout_add().
But I think you can do all what you need following my example. The same principle should work with multiple threads and multiple channels. The idea is to pass data between treads over Nim channels and to use the iddleAdd() function to receive the data and to display them. You should be able to add buttons for starting or terminating threads. Maybe the most difficult part is to terminate a background process before it is finished, i.e. to terminate a chess engine when time limit is reached. We may need something like semaphores and global variables for that, you may ask in IRC or Nim forum.
And here is a solution with g_idle_add():
# nim c --threads:on --gc:arc -r t.nim
import gintro/[gtk, glib, gobject, gio]
from os import sleep
var worker: system.Thread[void]
var button: Button
proc idleFunc(i: int): bool =
button.label = $i
return SOURCE_REMOVE
proc work() =
var countdown {.global.} = 25
while countdown > 0:
sleep(1000)
dec(countdown)
idleAdd(idleFunc, countdown)
proc buttonClicked (button: Button) =
button.label = utf8Strreverse(button.label, -1)
proc appActivate (app: Application) =
let window = newApplicationWindow(app)
window.title = "Countdown"
window.defaultSize = (250, 50)
button = newButton("Click Me")
window.add(button)
button.connect("clicked", buttonClicked)
window.showAll
createThread(worker, work)
proc main =
let app = newApplication("org.gtk.example")
connect(app, "activate", appActivate)
discard app.run
main()
E. Bassi recommends use of GTask but we have to investigate how it can be used with Nim, see
And here is a solution with g_idle_add():
# nim c --threads:on --gc:arc -r t.nim import gintro/[gtk, glib, gobject, gio] from os import sleep var worker: system.Thread[void] var button: Button proc idleFunc(i: int): bool = button.label = $i return SOURCE_REMOVE proc work() = var countdown {.global.} = 25 while countdown > 0: sleep(1000) dec(countdown) idleAdd(idleFunc, countdown) proc buttonClicked (button: Button) = button.label = utf8Strreverse(button.label, -1) proc appActivate (app: Application) = let window = newApplicationWindow(app) window.title = "Countdown" window.defaultSize = (250, 50) button = newButton("Click Me") window.add(button) button.connect("clicked", buttonClicked) window.showAll createThread(worker, work) proc main = let app = newApplication("org.gtk.example") connect(app, "activate", appActivate) discard app.run main()
E. Bassi recommends use of GTask but we have to investigate how it can be used with Nim, see
Thank you. I'm trying to understand it then trigger the worker (or idleAdd) after button is clicked.
Well i still don't know how to use it on my code right now but as far as i'm understand:
createThread
make a background threadidleAdd
is a method to "communicate" between the worker thread and main thread.
I have some idea but i have to test it.Yes, threading is never easy. I have still no idea how to do it for my chess game, I would use some bidirectional message passing and a way to stop the engine when it is deep in the tree of possible moves. I may try it in winter.
The key concept of idleAdd() or timeoutAdd() is that the GTK GUI is not directly updated from the second thread, but by a function that is executed in the main thread. You can investigate the Nim Channel module for threading and message passing. I have also added a section to the README:
@StefanSalewski i'm having this error (my code, your code works fine)
/home/dmknght/ParrotProjects/anonsurf/src/gui/actions/toolbar.nim(32, 10) template/generic instantiation of `idleAdd` from here
/usr/lib/nim/core/macros.nim(579, 69) Error: expression cannot be cast to pointer
Is your idleAdd from glib or other module? i'm using gintro#head
Code:
proc doCheckIP(ipInfo: array[2, string]): bool =
# let ipInfo = checkIPwTorServer()
var iconName: string
# If program has error while getting IP address
if ipInfo[0].startsWith("Error"):
iconName = "error"
# If program runs but user didn't connect to tor
elif ipInfo[0].startsWith("Sorry"):
iconName = "security-medium"
# Connected to tor
else:
iconName = "security-high"
sendNotify(ipInfo[0], ipInfo[1], iconName)
return SOURCE_CONTINUE
proc work() =
let ipAddr = checkIPwTorServer()
idleAdd(doCheckIP, ipAddr)
proc onClickCheckIP*(b: Button) =
#[
Display IP when user click on CheckIP button
Show the information in system's notification
]#
createThread(worker, work)
Nevermind. I completed the code without using idleAdd
. The code works fine.
idleAdd() and timoutAdd() are macros in gintro, the macro then calls the glib g_idle_add(). Macro is needed because of the different calling conventions, g_idle_add() wants to call a function with cdecl calling convention, but our Nim functions generally use nimcall, and we do not want to force the user to mark his user function with cdecl pragma. So we use some sort of trampoline function, that trampoline function is generated by the macro.
grep -A40 idleAdd ~/.nimble/pkgs/gintro-#head/gintro/gimpl.nim
macro idleAdd*(p: untyped; arg: typed): untyped =
var IdleID {.compileTime, global.}: int
inc(IdleID)
let ats = $getTypeInst(arg).toStrLit
let procName = "idlefunc_" & $IdleID
let procNameCdecl = "idlefunc_cdecl_" & $IdleID
var r1s = """
proc $1(p: pointer): gboolean {.cdecl.} =
let a = cast[$3](p)
result = $2(a).gboolean
#when (a is ref object) or (a is seq):
#GC_unref(a)
""" % [$procNameCdecl, $p, ats]
let r2s ="""
proc $1(a: $2): culong {.discardable.} =
when (a is ref object) or (a is seq):
GC_ref(a)
return g_idle_add_full(PRIORITY_DEFAULT_IDLE, $3, cast[pointer](a), nil)
else:
var ar: ref $2
new(ar)
#deepCopy(ar[], a)
ar[] = a
GC_ref(ar)
return g_idle_add_full(PRIORITY_DEFAULT_IDLE, $3, cast[pointer](ar[]), nil)
$1($4)
""" % [$procName, ats, $procNameCdecl, $arg]
result = parseStmt(r1s & r2s)
I use the same syntax for idleAdd
from your code and it give me error so i give up and use threading without idleAdd
. Can example of mine using spawnCommandLineAsync
be added in gintro examples? I think it is useful for everybody want to check syntax.
proc doCheckIP(ipInfo: array[2, string]): bool =
Maybe your problem with idleAdd() is that your doCheckIP() function has an array as parameter. I think I never tested with arrays. Maybe it would work when you use an object with two string fields?
Can example of mine using spawnCommandLineAsync be added in gintro examples?
This one?
https://github.com/StefanSalewski/gintro/issues/81#issuecomment-658307309
Yes I can add it to the example directory, but it is not very interesting as you do no communication with the background thread, you only call blocking execCmd() and non blocking spawnCommandLineAsync(). Unfortunately your onClickIsRun() proc is a plain echo, it does not really test if the background process is running.
But well maybe for a plain command launcher tool your example is useful, so I will add it to examples. But I think I will not add it to the README, as it is not too interesting.
Indeed I wonder if it is a good idea to use spawnCommandLineAsync() from glib at all. Does Nim not provide similar functions? Like startProcess() from osproc module? Well I never have used that one, but generally we use what is provided by Nim std lib and only fall back to external libs when necessary. (Similar for string processing, we generally use Nim strutils and not what is provided by glib.)
Maybe you can check osproc.startprocess() and tell us why it is inferior to glib.spawnCommandLineAsync.
Maybe your problem with idleAdd() is that your doCheckIP() function has an array as parameter. I think I never tested with arrays. Maybe it would work when you use an object with two string fields?
Well i think that is problem of extracting arguments maybe. It is like the connect button (i think i opened an issue about it and the solution is use an object). But i decided using Thread without idleAdd()
. The code structure is similar and it works fine.
it does not really test if the background process is running. Well it only shows output in terminal. Maybe i can do more complex example but idk if it worthy
Does Nim not provide similar functions? Nim has functions but the
spawnCommandLineAsync()
spawns a background process without hanging the GUI just like threading and from the result you don't have to care about creating thread object managing it and join it. You also don't have to provide--threads
flag when you compile it. It is anative gtk thing
as i call.
My new problem: Turned out the worker.join is not a good idea because it crashes application after function works.
I changed array[2, string] to an object and i still have template/generic instantiation of
idleAddfrom here
error
More information: The location from gobject that output error pointed
proc toggleNotify*(data: pointer; obj: ptr Object00; isLastRef: gboolean) {.cdecl.} =
if isLastRef.int == 0:
GC_ref(cast[RootRef](data)) # --> This line caused crash. L123
else:
GC_unref(cast[RootRef](data))
Tracing back, it goes to gtk.nim
#[Line 35]# proc finalizeGObject*[T](o: ref T) =
if not o.ignoreFinalizer:
gobject.g_object_remove_toggle_ref(o.impl, gobject.toggleNotify, addr(o[]))
My new problem: Turned out the worker.join is not a good idea because it crashes application after function works.
I think the reason is i call gtk notification inside threading. Well, the logic of how it work is good but do the actual code with threading is crazy
You told us that spawnCommandLineAsync() was working fine for you, so I have not further investigated idleAdd() macro. That macro is simple, so expanding it for other data types should be not that hard.
Have just tested with an ref object, that compiles, but seems to work only with ARC without crash. I am not really surprised, the default GC has problems with data shared between threads.
# nim c --threads:on --gc:arc -r thread2.nim
import gintro/[gtk, glib, gobject, gio]
from os import sleep
type
TwoStr = ref object
s1, s2: string
var workThread: system.Thread[void]
var button: Button
proc idleFunc(x: TwoStr): bool =
button.label = x.s1 & x.s2
return SOURCE_REMOVE
proc workProc =
var countdown {.global.} = 25
var x {.threadvar.}: TwoStr
if x == nil:
x = TwoStr(s1: "Nim", s2: "Rust")
while countdown > 0:
sleep(1000)
dec(countdown)
idleAdd(idleFunc, x)
proc buttonClicked(button: Button) =
button.label = utf8Strreverse(button.label, -1)
proc activate(app: Application) =
let window = newApplicationWindow(app)
window.title = "Countdown"
window.defaultSize = (250, 50)
button = newButton("Click Me")
window.add(button)
button.connect("clicked", buttonClicked)
window.showAll
createThread(workThread, workProc)
proc main =
let app = newApplication("org.gtk.example")
connect(app, "activate", activate)
discard app.run
main()
Compile with
nim c --threads:on --gc:arc thread2.nim
I am not sure if passing strings between threads works at all with the default GC.
Unfortunately I do not know much about threading in Nim. And I have still to investigate the GTask suggestion of Mr Bassi, I am not sure if GTask will work at all for Nim code that contains GC instructions.
Generally you should try to always compile with gc:arc as that is deterministic, you get a crash earlier if something is wrong. The default GC may take long to crash, which is bad for testing.
About all your other problems, it is hard to guess without having the full code.
Maybe you should ask on the forum, there are some people with some GTK and good threading knowledge.
the default GC has problems with data shared between threads.
I solved this by using Channel. The idea is using gtk notification
to pop information up so it has to be run after thread is completed. So i have to show notification in timeoutAdd()
My code
var
worker*: system.Thread[void]
channel*: Channel[MyIP]
channel.open()
proc doCheckIP() =
var finalResult: MyIP
let ipInfo = checkIPFromTorServer()
# If program has error while getting IP address
if ipInfo[0].startsWith("Error"):
finalResult.iconName = "error"
# If program runs but user didn't connect to tor
elif ipInfo[0].startsWith("Sorry"):
finalResult.iconName = "security-medium"
# Connected to tor
else:
finalResult.iconName = "security-high"
finalResult.isUnderTor = ipInfo[0]
finalResult.thisAddr = ipInfo[1]
channel.send(finalResult) # Crash second time
proc onClickCheckIP*(b: Button) =
#[
Display IP when user click on CheckIP button
Show the information in system's notification
]#
sendNotify("My IP", "Getting data from server", "dialog-information")
createThread(worker, doCheckIP)
And then in the function that is called by timeoutAdd()
i do something like this:
If channel.dataAvailable`:
showPopup()
The key here is not closing the channel so program won't crash if user click on button 2nd time.
I am having an application that have different buttons, each button does 1 job. So with normal way of coding, my code hangs until the callback completes running. I checked on google and i saw something about multi threaded gtk app:
So by creating this issue i want to ask does gintro support multi threaded by GTK and is there any example because i can't find any IP by grepping proc's names