StefanSalewski / gintro

High level GObject-Introspection based GTK3/GTK4 bindings for Nim language
MIT License
295 stars 20 forks source link

Using multi threaded GTK app with Nim and Gintro #81

Open dmknght opened 4 years ago

dmknght commented 4 years ago

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

StefanSalewski commented 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.

dmknght commented 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?

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?

dmknght commented 4 years ago

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

dmknght commented 4 years ago

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.

dmknght commented 4 years ago

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 image

  1. First button is ping command and it uses normal way to call a process which hangs the main gui until child process ends.
  2. Second button is test code using spawnCommandLineAsync. This button should run process in background without hanging whole GUI
  3. A simple button to make sure everything is working when we click button 1 or button 2
dmknght commented 4 years ago

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

dmknght commented 4 years ago

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)
StefanSalewski commented 4 years ago

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.

dmknght commented 4 years ago

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.

StefanSalewski commented 4 years ago

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()
StefanSalewski commented 4 years ago

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.

StefanSalewski commented 4 years ago

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

https://discourse.gnome.org/t/gtk-background-threads/3817/6

dmknght commented 4 years ago

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

https://discourse.gnome.org/t/gtk-background-threads/3817/6

Thank you. I'm trying to understand it then trigger the worker (or idleAdd) after button is clicked.

dmknght commented 4 years ago

Well i still don't know how to use it on my code right now but as far as i'm understand:

StefanSalewski commented 4 years ago

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:

http://ssalewski.de/gintroreadme.html#_threading_examples

dmknght commented 4 years ago

@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)
dmknght commented 4 years ago

Nevermind. I completed the code without using idleAdd. The code works fine.

StefanSalewski commented 4 years ago

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)
dmknght commented 4 years ago

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.

StefanSalewski commented 4 years ago

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.

StefanSalewski commented 4 years ago

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.

dmknght commented 4 years ago

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 a native gtk thing as i call.

dmknght commented 4 years ago

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 ofidleAddfrom here error

dmknght commented 4 years ago

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[]))
dmknght commented 4 years ago

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

StefanSalewski commented 4 years ago

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.

dmknght commented 4 years ago

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.