diff --git a/.gitignore b/.gitignore index e7646ac8..90fb81a7 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/README.md b/README.md index b6bead00..fdcb23a4 100644 --- a/README.md +++ b/README.md @@ -94,6 +94,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? diff --git a/examples/goroutine6/goroutine6.png b/examples/goroutine6/goroutine6.png new file mode 100644 index 00000000..32fa57ec Binary files /dev/null and b/examples/goroutine6/goroutine6.png differ diff --git a/examples/goroutine6/main.go b/examples/goroutine6/main.go new file mode 100644 index 00000000..69802223 --- /dev/null +++ b/examples/goroutine6/main.go @@ -0,0 +1,57 @@ +package main + +import ( + "fmt" + "os" + "runtime" + "time" + + qt "github.com/mappu/miqt/qt6" + "github.com/mappu/miqt/qt6/mainthread" +) + +func main() { + threadcount := runtime.GOMAXPROCS(0) + + qt.NewQApplication(os.Args) + + window := qt.NewQMainWindow2() + window.QWidget.SetFixedSize2(250, 50*(threadcount+1)) + window.QWidget.SetWindowTitle("goroutine Example") + + widget := qt.NewQWidget(window.QWidget) + var layout = qt.NewQVBoxLayout2() + widget.SetLayout(layout.QLayout) + window.SetCentralWidget(widget) + + labels := make([]*qt.QLabel, threadcount) + 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) + + mainthread.Wait(func() { + qlabel.SetText(fmt.Sprintf("%v %v", tick, time.Now().UTC().Format("15:04:05.0000"))) + }) + } + }(i, label) + } + }) + widget.Layout().AddWidget(button.QWidget) + + window.Show() + + qt.QApplication_Exec() +} 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