From 60022e37e761c40cfba92e7f792878316c5e865a Mon Sep 17 00:00:00 2001 From: Rick Calixte <10281587+rcalixte@users.noreply.github.com> Date: Sun, 10 Nov 2024 16:05:35 -0500 Subject: [PATCH 1/7] Add goroutine6 Ported from: https://github.com/therecipe/examples/blob/master/advanced/widgets/goroutine/main.go --- .gitignore | 1 + examples/goroutine6/main.go | 50 +++++++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+) create mode 100644 examples/goroutine6/main.go diff --git a/.gitignore b/.gitignore index 5576b734..4845016f 100644 --- a/.gitignore +++ b/.gitignore @@ -20,6 +20,7 @@ cmd/genbindings/genbindings cmd/miqt-uic/miqt-uic cmd/miqt-rcc/miqt-rcc +examples/goroutine6/goroutine6 examples/helloworld/helloworld examples/helloworld6/helloworld6 examples/mdoutliner/mdoutliner diff --git a/examples/goroutine6/main.go b/examples/goroutine6/main.go new file mode 100644 index 00000000..0d8b06e4 --- /dev/null +++ b/examples/goroutine6/main.go @@ -0,0 +1,50 @@ +package main + +import ( + "fmt" + "os" + "time" + + qt "github.com/mappu/miqt/qt6" +) + +func main() { + qt.NewQApplication(os.Args) + + window := qt.NewQMainWindow2() + window.QWidget.SetFixedSize2(250, 200) + window.QWidget.SetWindowTitle("goroutine Example") + + widget := qt.NewQWidget(window.QWidget) + var layout = qt.NewQVBoxLayout2() + widget.SetLayout(layout.QBoxLayout.QLayout) + window.SetCentralWidget(widget) + + labels := make([]*qt.QLabel, 3) + for i := range labels { + label := qt.NewQLabel(window.QWidget) + label.SetAlignment(qt.AlignCenter) + widget.Layout().AddWidget(label.QWidget) + labels[i] = label + } + + button := qt.NewQPushButton5("start!", window.QWidget) + button.OnClicked1(func(bool) { + button.SetDisabled(true) + for i, label := range labels { + go func(index int, qlabel *qt.QLabel) { + var tick int + for range time.NewTicker(time.Duration((index+1)*25) * time.Millisecond).C { + tick++ + time.Sleep(50 * time.Millisecond) + qlabel.SetText(fmt.Sprintf("%v %v", tick, time.Now().UTC().Format("15:04:05.0000"))) + } + }(i, label) + } + }) + widget.Layout().AddWidget(button.QAbstractButton.QWidget) + + window.Show() + + qt.QApplication_Exec() +} From 38a75178ca3dd95b3c0f2e80635a0d4f5c03191b Mon Sep 17 00:00:00 2001 From: mappu Date: Sun, 29 Dec 2024 18:05:13 +1300 Subject: [PATCH 2/7] mainthread: add helper function to run code in main Qt thread --- qt6/mainthread/mainthread.cpp | 13 +++++++ qt6/mainthread/mainthread.go | 69 +++++++++++++++++++++++++++++++++++ qt6/mainthread/mainthread.h | 18 +++++++++ 3 files changed, 100 insertions(+) create mode 100644 qt6/mainthread/mainthread.cpp create mode 100644 qt6/mainthread/mainthread.go create mode 100644 qt6/mainthread/mainthread.h diff --git a/qt6/mainthread/mainthread.cpp b/qt6/mainthread/mainthread.cpp new file mode 100644 index 00000000..a87d2b6b --- /dev/null +++ b/qt6/mainthread/mainthread.cpp @@ -0,0 +1,13 @@ +#include +#include + +#ifndef _Bool +#define _Bool bool +#endif +#include "_cgo_export.h" + +void mainthread_exec(intptr_t cb) { + QMetaObject::invokeMethod(qApp, [=]{ + mainthread_exec_handle(cb); + }, Qt::QueuedConnection); +} diff --git a/qt6/mainthread/mainthread.go b/qt6/mainthread/mainthread.go new file mode 100644 index 00000000..b37921d1 --- /dev/null +++ b/qt6/mainthread/mainthread.go @@ -0,0 +1,69 @@ +package mainthread + +import ( + "sync" + "runtime/cgo" +) + +/* +#cgo pkg-config: Qt6Core + +#include "mainthread.h" +*/ +import "C" + +// Start runs the callback in the main Qt thread. You should use this whenever +// accessing the main Qt GUI from inside a goroutine. +// This function is non-blocking. +func Start(gofunc func()) { + h := cgo.NewHandle(gofunc) + + C.mainthread_exec(C.intptr_t(h)) +} + +// Wait runs the callback in the main Qt thread. You should use this whenever +// accessing the main Qt GUI from inside a goroutine. +// The call blocks until the callback is executed in the main thread's eventloop. +func Wait(gofunc func()) { + // It's possible to use Qt::BlockingQueuedConnection to implement the + // blocking, but it has a deadlock risk + var wg sync.WaitGroup + wg.Add(1) + outerfunc := func() { + gofunc() + wg.Done() + } + Start(outerfunc) + wg.Wait() +} + +func Wait2[T any](gofunc func() T) (ret T) { + outerfunc := func() { + ret = gofunc() + } + Wait(outerfunc) + return ret +} + +func Wait3[T any](gofunc func() (T, error)) (ret T, err error) { + outerfunc := func() { + ret, err = gofunc() + } + Wait(outerfunc) + return ret, err +} + +//export mainthread_exec_handle +func mainthread_exec_handle(u uintptr) { + h := cgo.Handle(u) + + gofunc, ok := h.Value().(func()) + if !ok { + panic("miqt: callback of non-callback type (heap corruption?)") + } + + gofunc() + + // Free handle after use + h.Delete() +} diff --git a/qt6/mainthread/mainthread.h b/qt6/mainthread/mainthread.h new file mode 100644 index 00000000..f3124d07 --- /dev/null +++ b/qt6/mainthread/mainthread.h @@ -0,0 +1,18 @@ +#pragma once + +#ifndef QT_MAINTHREAD_H +#define QT_MAINTHREAD_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +void mainthread_exec(intptr_t cb); + +#ifdef __cplusplus +} +#endif + +#endif From 8777a730813307cba9a8534bf239fb62ab423b49 Mon Sep 17 00:00:00 2001 From: mappu Date: Sun, 29 Dec 2024 18:24:37 +1300 Subject: [PATCH 3/7] goroutine6: run as many workers as GOMAXPROCS --- examples/goroutine6/main.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/examples/goroutine6/main.go b/examples/goroutine6/main.go index 0d8b06e4..3f3ecf0b 100644 --- a/examples/goroutine6/main.go +++ b/examples/goroutine6/main.go @@ -3,16 +3,19 @@ package main import ( "fmt" "os" + "runtime" "time" qt "github.com/mappu/miqt/qt6" ) func main() { + threadcount := runtime.GOMAXPROCS(0) + qt.NewQApplication(os.Args) window := qt.NewQMainWindow2() - window.QWidget.SetFixedSize2(250, 200) + window.QWidget.SetFixedSize2(250, 50*(threadcount+1)) window.QWidget.SetWindowTitle("goroutine Example") widget := qt.NewQWidget(window.QWidget) @@ -20,7 +23,7 @@ func main() { widget.SetLayout(layout.QBoxLayout.QLayout) window.SetCentralWidget(widget) - labels := make([]*qt.QLabel, 3) + labels := make([]*qt.QLabel, threadcount) for i := range labels { label := qt.NewQLabel(window.QWidget) label.SetAlignment(qt.AlignCenter) From f71dc1c30320d793f3508ffdc347b42649d93e2e Mon Sep 17 00:00:00 2001 From: mappu Date: Sun, 29 Dec 2024 18:25:07 +1300 Subject: [PATCH 4/7] goroutine6: use simpler downcasts --- examples/goroutine6/main.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/goroutine6/main.go b/examples/goroutine6/main.go index 3f3ecf0b..f5e79b13 100644 --- a/examples/goroutine6/main.go +++ b/examples/goroutine6/main.go @@ -20,7 +20,7 @@ func main() { widget := qt.NewQWidget(window.QWidget) var layout = qt.NewQVBoxLayout2() - widget.SetLayout(layout.QBoxLayout.QLayout) + widget.SetLayout(layout.QLayout) window.SetCentralWidget(widget) labels := make([]*qt.QLabel, threadcount) @@ -45,7 +45,7 @@ func main() { }(i, label) } }) - widget.Layout().AddWidget(button.QAbstractButton.QWidget) + widget.Layout().AddWidget(button.QWidget) window.Show() From e347084d948e41ab8e5a979c5ea238577471e5a6 Mon Sep 17 00:00:00 2001 From: mappu Date: Sun, 29 Dec 2024 18:25:21 +1300 Subject: [PATCH 5/7] goroutine6: use mainthread.Wait() helper function --- examples/goroutine6/main.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/examples/goroutine6/main.go b/examples/goroutine6/main.go index f5e79b13..69802223 100644 --- a/examples/goroutine6/main.go +++ b/examples/goroutine6/main.go @@ -7,6 +7,7 @@ import ( "time" qt "github.com/mappu/miqt/qt6" + "github.com/mappu/miqt/qt6/mainthread" ) func main() { @@ -39,8 +40,11 @@ func main() { var tick int for range time.NewTicker(time.Duration((index+1)*25) * time.Millisecond).C { tick++ - time.Sleep(50 * time.Millisecond) - qlabel.SetText(fmt.Sprintf("%v %v", tick, time.Now().UTC().Format("15:04:05.0000"))) + // time.Sleep(50 * time.Millisecond) + + mainthread.Wait(func() { + qlabel.SetText(fmt.Sprintf("%v %v", tick, time.Now().UTC().Format("15:04:05.0000"))) + }) } }(i, label) } From 6768a3909085ae5446fdc138881fbf42dd6e7378 Mon Sep 17 00:00:00 2001 From: mappu Date: Sun, 29 Dec 2024 18:25:52 +1300 Subject: [PATCH 6/7] doc/README: add guidelines about mainthread --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 8fef0644..7bf44f9c 100644 --- a/README.md +++ b/README.md @@ -93,6 +93,8 @@ Qt class inherited types are projected as a Go embedded struct. For example, to The Go runtime migrates goroutines between OS threads, but Qt expects fixed OS threads to be used for each QObject. When you first call `qt.NewQApplication` in MIQT, that will be considered the [Qt main thread](https://doc.qt.io/qt-6/thread-basics.html#gui-thread-and-worker-thread) and will automatically signal the Go runtime to bind to a fixed OS thread using `runtime.LockOSThread()`. +- When accessing Qt objects from inside another goroutine, it's safest to use `(qt6/mainthread).Wait()` to access the Qt objects from Qt's main thread. + Some C++ idioms that were difficult to project were omitted from the binding. But, this can be improved in the future. ### Q6. Can I use Qt Designer and the Qt Resource system? From d72bc4c5689b7e5889368bd81f0ef54bcb03119c Mon Sep 17 00:00:00 2001 From: mappu Date: Sun, 29 Dec 2024 18:27:19 +1300 Subject: [PATCH 7/7] doc/goroutine6: add example screenshot --- examples/goroutine6/goroutine6.png | Bin 0 -> 24692 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 examples/goroutine6/goroutine6.png diff --git a/examples/goroutine6/goroutine6.png b/examples/goroutine6/goroutine6.png new file mode 100644 index 0000000000000000000000000000000000000000..32fa57ec5c92223ad579e7455b8aa3be99fc112b GIT binary patch literal 24692 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