diff --git a/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj b/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj index c87d9fdead..9398987837 100644 --- a/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj +++ b/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj @@ -9,6 +9,7 @@ + diff --git a/samples/ControlCatalog.NetCore/NativeControls/Gtk/GtkHelper.cs b/samples/ControlCatalog.NetCore/NativeControls/Gtk/GtkHelper.cs index b1fef7c013..9d698716ee 100644 --- a/samples/ControlCatalog.NetCore/NativeControls/Gtk/GtkHelper.cs +++ b/samples/ControlCatalog.NetCore/NativeControls/Gtk/GtkHelper.cs @@ -5,7 +5,7 @@ using Avalonia.Platform.Interop; using Avalonia.X11.Interop; using Avalonia.X11.NativeDialogs; using static Avalonia.X11.NativeDialogs.Gtk; -using static Avalonia.X11.NativeDialogs.Glib; +using static Avalonia.X11.Interop.Glib; namespace ControlCatalog.NetCore; diff --git a/src/Avalonia.X11/Dispatching/GLibDispatcherImpl.cs b/src/Avalonia.X11/Dispatching/GLibDispatcherImpl.cs new file mode 100644 index 0000000000..29f2836594 --- /dev/null +++ b/src/Avalonia.X11/Dispatching/GLibDispatcherImpl.cs @@ -0,0 +1,313 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Runtime.ExceptionServices; +using System.Threading; +using Avalonia.Logging; +using Avalonia.Threading; +using static Avalonia.X11.Interop.Glib; +namespace Avalonia.X11.Dispatching; + +internal class GlibDispatcherImpl : + IDispatcherImplWithExplicitBackgroundProcessing, + IControlledDispatcherImpl +{ + /* + GLib priorities and Avalonia priorities are a bit different. Avalonia follows the WPF model when there + are "background" and "foreground" priority groups. Foreground jobs are executed before any user input processing, + background jobs are executed strictly after user input processing. + + GLib has numeric priorities that are used in the following way: + -100 G_PRIORITY_HIGH - "high" priority sources, not really used by GLib/GTK + 0 G_PRIORITY_DEFAULT - polling X11 events (GTK) and default value for g_timeout_add + 100 G_PRIORITY_HIGH_IDLE without a clear definition, used as an anchor value of sorts + 110 Resize/layout operations (GTK) + 120 Render operations (GTK) + 200 G_PRIORITY_DEFAULT_IDLE - "idle" priority sources + + So, unlike Avalonia, GTK puts way higher priority on input processing, then does resize/layout/render + + So, to map our model to GLib we do the following: + - foreground jobs (including grouped user events) are executed with (-1) priority (_before_ any normal GLib jobs) + - X11 socket is polled with G_PRIORITY_DEFAULT, all X11 events are read until socket is empty, + we also group input events at that stage (this matches our epoll-based dispatcher) + - background jobs are executed with G_PRIORITY_DEFAULT_IDLE, so they would have lower priority than GTK + foreground jobs + + Unfortunately we can't detect if there are pending _non-idle_ GLib jobs using g_main_context_pending, since + - g_main_context_pending doesn't accept max_priority argument + - even if it did, that would still involve a syscall to the kernel to poll for fds anyway + + So we just report that we don't support pending input query and let the dispatcher to + call RequestBackgroundProcessing every time, which results in g_idle_add call for every background job. + Background jobs are expected to be relatively expensive to execute since on Windows + MsgWaitForMultipleObjectsEx results isn't really free too. + + For signaling (aka waking up dispatcher for processing _high_ priority jobs we are using + g_idle_add_full with (-1) priority. While the naming suggests that it would enqueue an idle job, + it actually adds an always-triggered source that would be called before other sources with lower priority. + + For timers we are using a simple g_timeout_add_full and discard the previous one when dispatcher requests + an update + + Since GLib dispatches event sources in batches, we force-check for "signaled" flag to run high-prio jobs + whenever we get control back from GLib. We can still occasionally get GTK code to run before high-prio + Avalonia-jobs, but that should be fine since the point is to keep Avalonia-based jobs ordered properly + and to not have our low-priority jobs to prevent GLib-based code from running its own "foreground" jobs + + Another implementation note here is that GLib (just as any other C library) is NOT aware of C# exceptions, + so we are NOT allowed to have exceptions to escape native->managed call boundary. So we have exception handlers + that try to propagate those to the nearest run loop frame that was initiated by Avalonia. + + If there is no such frame, we have no choice but to log/swallow those + */ + + private readonly AvaloniaX11Platform _platform; + + // Note that we can't use g_main_context_is_owner outside a run loop, since context doesn't really have an + // inherent owner when run loop is not running and the context isn't explicitly "locked", so we just assume that + // the app author is initializing Avalonia on the intended UI thread and won't migrate the default run loop + // to a different thread + private readonly Thread _mainThread = Thread.CurrentThread; + + private readonly X11EventDispatcher _x11Events; + private bool _signaled; + private bool _signaledSourceAdded; + private readonly object _signalLock = new(); + private readonly Stack _runLoopStack = new(); + + private readonly Stopwatch _stopwatch = Stopwatch.StartNew(); + private uint? _glibTimerSourceTag; + + public GlibDispatcherImpl(AvaloniaX11Platform platform) + { + _platform = platform; + _x11Events = new X11EventDispatcher(platform); + var unixFdId = g_unix_fd_add_full(G_PRIORITY_DEFAULT, _x11Events.Fd, GIOCondition.G_IO_IN, + X11SourceCallback); + // We can trigger a nested event loop when handling X11 events, so we need to mark the source as recursive + var unixFdSource = g_main_context_find_source_by_id(IntPtr.Zero, unixFdId); + g_source_set_can_recurse(unixFdSource, 1); + } + + public bool CurrentThreadIsLoopThread => _mainThread == Thread.CurrentThread; + + public event Action? Signaled; + public void Signal() + { + lock (_signalLock) + { + if(_signaled) + return; + _signaled = true; + if(_signaledSourceAdded) + return; + _signaledSourceAdded = true; + } + g_idle_add_full(G_PRIORITY_DEFAULT - 1, SignalSourceCallback); + } + + private void CheckSignaled() + { + lock (_signalLock) + { + if (!_signaled) + return; + _signaled = false; + } + + try + { + Signaled?.Invoke(); + } + catch (Exception e) + { + HandleException(e); + } + _x11Events.Flush(); + } + + private bool SignalSourceCallback() + { + lock (_signalLock) + { + _signaledSourceAdded = false; + } + CheckSignaled(); + return false; + } + + public event Action? Timer; + public long Now => _stopwatch.ElapsedMilliseconds; + + public void UpdateTimer(long? dueTimeInMs) + { + if (_glibTimerSourceTag.HasValue) + { + g_source_remove(_glibTimerSourceTag.Value); + _glibTimerSourceTag = null; + } + + if (dueTimeInMs == null) + return; + + var interval = (uint)Math.Max(0, (int)Math.Min(int.MaxValue, dueTimeInMs.Value - Now)); + _glibTimerSourceTag = g_timeout_add_once(interval, TimerCallback); + } + + private void TimerCallback() + { + try + { + Timer?.Invoke(); + } + catch (Exception e) + { + HandleException(e); + } + _x11Events.Flush(); + } + + public event Action? ReadyForBackgroundProcessing; + + public void RequestBackgroundProcessing() => + g_idle_add_once(() => ReadyForBackgroundProcessing?.Invoke()); + + public bool CanQueryPendingInput => false; + public bool HasPendingInput => _platform.EventGrouperDispatchQueue.HasJobs || _x11Events.IsPending; + + private bool X11SourceCallback(int i, GIOCondition gioCondition) + { + CheckSignaled(); + var token = _runLoopStack.Count > 0 ? _runLoopStack.Peek().Cancelled : CancellationToken.None; + try + { + // Completely drain X11 socket while we are at it + while (_x11Events.IsPending) + { + // If we don't actually drain our X11 socket, GLib _will_ call us again even if + // we request the run loop to quit + _x11Events.DispatchX11Events(CancellationToken.None); + if (!token.IsCancellationRequested) + { + while (_platform.EventGrouperDispatchQueue.HasJobs) + { + CheckSignaled(); + _platform.EventGrouperDispatchQueue.DispatchNext(); + } + + _x11Events.Flush(); + } + } + } + catch (Exception e) + { + HandleException(e); + } + + return true; + } + + public void RunLoop(CancellationToken token) + { + if(token.IsCancellationRequested) + return; + + using var loop = new ManagedLoopFrame(token); + _runLoopStack.Push(loop); + loop.Run(); + _runLoopStack.Pop(); + + // Propagate any managed exceptions that we've captured from this frame + if(loop.Exceptions.Count == 1) + loop.Exceptions[0].Throw(); + else if (loop.Exceptions.Count > 1) + throw new AggregateException(loop.Exceptions.Select(x => x.SourceException)); + } + + void HandleException(Exception e) + { + if (_runLoopStack.Count > 0) + { + var frame = _runLoopStack.Peek(); + frame.Exceptions.Add(ExceptionDispatchInfo.Capture(e)); + frame.Stop(); + } + else + { + var externalLogger = _platform.Options.ExterinalGLibMainLoopExceptionLogger; + if (externalLogger != null) + externalLogger.Invoke(e); + else + Logger.TryGet(LogEventLevel.Error, LogArea.Control) + ?.Log("Dispatcher", "Unhandled exception: {exception}", e); + } + } + + private class ManagedLoopFrame : IDisposable + { + private readonly CancellationToken _externalToken; + private CancellationTokenSource? _internalTokenSource; + public CancellationToken Cancelled { get; private set; } + + private readonly IntPtr _loop = g_main_loop_new(IntPtr.Zero, 1); + public List Exceptions { get; } = new(); + private readonly object _destroyLock = new(); + private bool _disposed; + + public ManagedLoopFrame(CancellationToken token) + { + _externalToken = token; + } + + public void Stop() + { + try + { + _internalTokenSource?.Cancel(); + } + catch + { + // Ignore + } + } + + public void Run() + { + if (_externalToken.IsCancellationRequested) + return; + using (_internalTokenSource = new()) + using (var composite = + CancellationTokenSource.CreateLinkedTokenSource(_externalToken, _internalTokenSource.Token)) + { + Cancelled = composite.Token; + using (Cancelled.Register(() => + { + lock (_destroyLock) + { + if (_disposed) + return; + g_main_loop_quit(_loop); + } + })) + { + g_main_loop_run(_loop); + } + } + } + + public void Dispose() + { + lock (_destroyLock) + { + if(_disposed) + return; + _disposed = true; + g_main_loop_unref(_loop); + } + } + } + +} \ No newline at end of file diff --git a/src/Avalonia.X11/Dispatching/X11EventDispatcher.cs b/src/Avalonia.X11/Dispatching/X11EventDispatcher.cs new file mode 100644 index 0000000000..36625a61cd --- /dev/null +++ b/src/Avalonia.X11/Dispatching/X11EventDispatcher.cs @@ -0,0 +1,62 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using static Avalonia.X11.XLib; +namespace Avalonia.X11; + +internal class X11EventDispatcher +{ + private readonly AvaloniaX11Platform _platform; + private readonly IntPtr _display; + + public delegate void EventHandler(ref XEvent xev); + public int Fd { get; } + private readonly Dictionary _eventHandlers; + + public X11EventDispatcher(AvaloniaX11Platform platform) + { + _platform = platform; + _display = platform.Display; + _eventHandlers = platform.Windows; + Fd = XLib.XConnectionNumber(_display); + } + + public bool IsPending => XPending(_display) != 0; + + public unsafe void DispatchX11Events(CancellationToken cancellationToken) + { + while (IsPending) + { + if (cancellationToken.IsCancellationRequested) + return; + + XNextEvent(_display, out var xev); + if(XFilterEvent(ref xev, IntPtr.Zero)) + continue; + + if (xev.type == XEventName.GenericEvent) + XGetEventData(_display, &xev.GenericEventCookie); + try + { + if (xev.type == XEventName.GenericEvent) + { + if (_platform.XI2 != null && _platform.Info.XInputOpcode == + xev.GenericEventCookie.extension) + { + _platform.XI2.OnEvent((XIEvent*)xev.GenericEventCookie.data); + } + } + else if (_eventHandlers.TryGetValue(xev.AnyEvent.window, out var handler)) + handler(ref xev); + } + finally + { + if (xev.type == XEventName.GenericEvent && xev.GenericEventCookie.data != null) + XFreeEventData(_display, &xev.GenericEventCookie); + } + } + Flush(); + } + + public void Flush() => XFlush(_display); +} \ No newline at end of file diff --git a/src/Avalonia.X11/X11PlatformThreading.cs b/src/Avalonia.X11/Dispatching/X11PlatformThreading.cs similarity index 74% rename from src/Avalonia.X11/X11PlatformThreading.cs rename to src/Avalonia.X11/Dispatching/X11PlatformThreading.cs index de494eb059..9cbfbe8a87 100644 --- a/src/Avalonia.X11/X11PlatformThreading.cs +++ b/src/Avalonia.X11/Dispatching/X11PlatformThreading.cs @@ -12,11 +12,7 @@ namespace Avalonia.X11 internal unsafe class X11PlatformThreading : IControlledDispatcherImpl { private readonly AvaloniaX11Platform _platform; - private readonly IntPtr _display; - - public delegate void EventHandler(ref XEvent xev); - private readonly Dictionary _eventHandlers; - private Thread _mainThread; + private Thread _mainThread = Thread.CurrentThread; [StructLayout(LayoutKind.Explicit)] private struct epoll_data @@ -72,14 +68,12 @@ namespace Avalonia.X11 private long? _nextTimer; private int _epoll; private Stopwatch _clock = Stopwatch.StartNew(); + private readonly X11EventDispatcher _x11Events; public X11PlatformThreading(AvaloniaX11Platform platform) { _platform = platform; - _display = platform.Display; - _eventHandlers = platform.Windows; - _mainThread = Thread.CurrentThread; - var fd = XLib.XConnectionNumber(_display); + _x11Events = new X11EventDispatcher(platform); var ev = new epoll_event() { events = EPOLLIN, @@ -89,7 +83,7 @@ namespace Avalonia.X11 if (_epoll == -1) throw new X11Exception("epoll_create1 failed"); - if (epoll_ctl(_epoll, EPOLL_CTL_ADD, fd, ref ev) == -1) + if (epoll_ctl(_epoll, EPOLL_CTL_ADD, _x11Events.Fd, ref ev) == -1) throw new X11Exception("Unable to attach X11 connection handle to epoll"); var fds = stackalloc int[2]; @@ -117,40 +111,7 @@ namespace Avalonia.X11 Signaled?.Invoke(); } - - private unsafe void HandleX11(CancellationToken cancellationToken) - { - while (XPending(_display) != 0) - { - if (cancellationToken.IsCancellationRequested) - return; - - XNextEvent(_display, out var xev); - if(XFilterEvent(ref xev, IntPtr.Zero)) - continue; - - if (xev.type == XEventName.GenericEvent) - XGetEventData(_display, &xev.GenericEventCookie); - try - { - if (xev.type == XEventName.GenericEvent) - { - if (_platform.XI2 != null && _platform.Info.XInputOpcode == - xev.GenericEventCookie.extension) - { - _platform.XI2.OnEvent((XIEvent*)xev.GenericEventCookie.data); - } - } - else if (_eventHandlers.TryGetValue(xev.AnyEvent.window, out var handler)) - handler(ref xev); - } - finally - { - if (xev.type == XEventName.GenericEvent && xev.GenericEventCookie.data != null) - XFreeEventData(_display, &xev.GenericEventCookie); - } - } - } + public void RunLoop(CancellationToken cancellationToken) { @@ -166,9 +127,9 @@ namespace Avalonia.X11 return; //Flush whatever requests were made to XServer - XFlush(_display); + _x11Events.Flush(); epoll_event ev; - if (XPending(_display) == 0) + if (!_x11Events.IsPending) { now = _clock.ElapsedMilliseconds; if (_nextTimer < now) @@ -190,7 +151,7 @@ namespace Avalonia.X11 if (cancellationToken.IsCancellationRequested) return; CheckSignaled(); - HandleX11(cancellationToken); + _x11Events.DispatchX11Events(cancellationToken); while (_platform.EventGrouperDispatchQueue.HasJobs) { CheckSignaled(); @@ -238,6 +199,6 @@ namespace Avalonia.X11 public long Now => _clock.ElapsedMilliseconds; public bool CanQueryPendingInput => true; - public bool HasPendingInput => _platform.EventGrouperDispatchQueue.HasJobs || XPending(_display) != 0; + public bool HasPendingInput => _platform.EventGrouperDispatchQueue.HasJobs || _x11Events.IsPending; } } diff --git a/src/Avalonia.X11/Interop/Glib.cs b/src/Avalonia.X11/Interop/Glib.cs new file mode 100644 index 0000000000..fbc92960cd --- /dev/null +++ b/src/Avalonia.X11/Interop/Glib.cs @@ -0,0 +1,192 @@ +#nullable enable +using System; +using System.Runtime.InteropServices; +using System.Threading.Tasks; +using Avalonia.Platform.Interop; +using static Avalonia.X11.Interop.Glib; +namespace Avalonia.X11.Interop; + +internal static unsafe class Glib +{ + private const string GlibName = "libglib-2.0.so.0"; + private const string GObjectName = "libgobject-2.0.so.0"; + + [DllImport(GlibName)] + public static extern void g_slist_free(GSList* data); + + [DllImport(GObjectName)] + private static extern void g_object_ref(IntPtr instance); + + [DllImport(GObjectName)] + private static extern ulong g_signal_connect_object(IntPtr instance, Utf8Buffer signal, + IntPtr handler, IntPtr userData, int flags); + + [DllImport(GObjectName)] + private static extern void g_object_unref(IntPtr instance); + + [DllImport(GObjectName)] + private static extern ulong g_signal_handler_disconnect(IntPtr instance, ulong connectionId); + + public const int G_PRIORITY_HIGH = -100; + public const int G_PRIORITY_DEFAULT = 0; + public const int G_PRIORITY_HIGH_IDLE = 100; + public const int G_PRIORITY_DEFAULT_IDLE = 200; + + [DllImport(GlibName)] + public static extern IntPtr g_main_loop_new(IntPtr context, int is_running); + + [DllImport(GlibName)] + public static extern void g_main_loop_quit(IntPtr loop); + + [DllImport(GlibName)] + public static extern void g_main_loop_run(IntPtr loop); + + [DllImport(GlibName)] + public static extern void g_main_loop_unref(IntPtr loop); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate void GOnceSourceFunc(IntPtr userData); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate int GSourceFunc(IntPtr userData); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate void GDestroyNotify(IntPtr userData); + + [DllImport(GlibName)] + private static extern int g_idle_add_once(GOnceSourceFunc cb, IntPtr userData); + + private static readonly GOnceSourceFunc s_onceSourceCb = (userData) => + { + var h = GCHandle.FromIntPtr(userData); + var cb = (Action)h.Target!; + + h.Free(); + cb(); + }; + + public static void g_idle_add_once(Action cb) => + g_idle_add_once(s_onceSourceCb, GCHandle.ToIntPtr(GCHandle.Alloc(cb))); + + [DllImport(GlibName)] + private static extern uint g_timeout_add_once(uint interval, GOnceSourceFunc cb, IntPtr userData); + + public static uint g_timeout_add_once(uint interval, Action cb) => + g_timeout_add_once(interval, s_onceSourceCb, GCHandle.ToIntPtr(GCHandle.Alloc(cb))); + + private static readonly GDestroyNotify s_gcHandleDestroyNotify = handle => GCHandle.FromIntPtr(handle).Free(); + + private static readonly GSourceFunc s_sourceFuncDispatchCallback = + handle => ((Func)GCHandle.FromIntPtr(handle).Target)() ? 1 : 0; + + [DllImport(GlibName)] + private static extern uint g_idle_add_full (int priority, GSourceFunc function, IntPtr data, GDestroyNotify notify); + + public static uint g_idle_add_full(int priority, Func callback) + => g_idle_add_full(priority, s_sourceFuncDispatchCallback, GCHandle.ToIntPtr(GCHandle.Alloc(callback)), + s_gcHandleDestroyNotify); + + [DllImport(GlibName)] + public static extern int g_source_get_can_recurse (IntPtr source); + + [DllImport(GlibName)] + public static extern void g_source_set_can_recurse (IntPtr source, int can_recurse); + + [DllImport(GlibName)] + public static extern IntPtr g_main_context_find_source_by_id (IntPtr context, uint source_id); + + [Flags] + public enum GIOCondition + { + G_IO_IN = 1, + G_IO_OUT = 4, + G_IO_PRI = 2, + G_IO_ERR = 8, + G_IO_HUP = 16, + G_IO_NVAL = 32 + } + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate int GUnixFDSourceFunc(int fd, GIOCondition condition, IntPtr user_data); + + private static readonly GUnixFDSourceFunc s_unixFdSourceCallback = (fd, cond, handle) => + ((Func)GCHandle.FromIntPtr(handle).Target)(fd, cond) ? 1 : 0; + + [DllImport(GlibName)] + public static extern uint g_unix_fd_add_full (int priority, + int fd, + GIOCondition condition, + GUnixFDSourceFunc function, + IntPtr user_data, + GDestroyNotify notify); + + public static uint g_unix_fd_add_full(int priority, int fd, GIOCondition condition, + Func cb) => + g_unix_fd_add_full(priority, fd, condition, s_unixFdSourceCallback, GCHandle.ToIntPtr(GCHandle.Alloc(cb)), + s_gcHandleDestroyNotify); + + [DllImport(GlibName)] + public static extern int g_source_remove (uint tag); + + private class ConnectedSignal : IDisposable + { + private readonly IntPtr _instance; + private GCHandle _handle; + private readonly ulong _id; + + public ConnectedSignal(IntPtr instance, GCHandle handle, ulong id) + { + _instance = instance; + g_object_ref(instance); + _handle = handle; + _id = id; + } + + public void Dispose() + { + if (_handle.IsAllocated) + { + g_signal_handler_disconnect(_instance, _id); + g_object_unref(_instance); + _handle.Free(); + } + } + } + + public static IDisposable ConnectSignal(IntPtr obj, string name, T handler) + { + var handle = GCHandle.Alloc(handler); + var ptr = Marshal.GetFunctionPointerForDelegate(handler); + using (var utf = new Utf8Buffer(name)) + { + var id = g_signal_connect_object(obj, utf, ptr, IntPtr.Zero, 0); + if (id == 0) + throw new ArgumentException("Unable to connect to signal " + name); + return new ConnectedSignal(obj, handle, id); + } + } + + public static Task RunOnGlibThread(Func action) + { + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + g_timeout_add_once(0, () => + { + try + { + tcs.SetResult(action()); + } + catch (Exception e) + { + tcs.TrySetException(e); + } + }); + return tcs.Task; + } +} + +[StructLayout(LayoutKind.Sequential)] +internal unsafe struct GSList +{ + public readonly IntPtr Data; + public readonly GSList* Next; +} \ No newline at end of file diff --git a/src/Avalonia.X11/Interop/GtkInteropHelper.cs b/src/Avalonia.X11/Interop/GtkInteropHelper.cs index de0b755832..3560b20b7e 100644 --- a/src/Avalonia.X11/Interop/GtkInteropHelper.cs +++ b/src/Avalonia.X11/Interop/GtkInteropHelper.cs @@ -10,6 +10,6 @@ public class GtkInteropHelper { if (!await NativeDialogs.Gtk.StartGtk().ConfigureAwait(false)) throw new Win32Exception("Unable to initialize GTK"); - return await NativeDialogs.Glib.RunOnGlibThread(cb).ConfigureAwait(false); + return await Glib.RunOnGlibThread(cb).ConfigureAwait(false); } } \ No newline at end of file diff --git a/src/Avalonia.X11/NativeDialogs/Gtk.cs b/src/Avalonia.X11/NativeDialogs/Gtk.cs index 14bd1142dd..97874d71f0 100644 --- a/src/Avalonia.X11/NativeDialogs/Gtk.cs +++ b/src/Avalonia.X11/NativeDialogs/Gtk.cs @@ -3,131 +3,13 @@ using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; using Avalonia.Platform.Interop; +using Avalonia.Threading; +using Avalonia.X11.Dispatching; +using Avalonia.X11.Interop; // ReSharper disable IdentifierTypo namespace Avalonia.X11.NativeDialogs { - internal static unsafe class Glib - { - private const string GlibName = "libglib-2.0.so.0"; - private const string GObjectName = "libgobject-2.0.so.0"; - - [DllImport(GlibName)] - public static extern void g_slist_free(GSList* data); - - [DllImport(GObjectName)] - private static extern void g_object_ref(IntPtr instance); - - [DllImport(GObjectName)] - private static extern ulong g_signal_connect_object(IntPtr instance, Utf8Buffer signal, - IntPtr handler, IntPtr userData, int flags); - - [DllImport(GObjectName)] - private static extern void g_object_unref(IntPtr instance); - - [DllImport(GObjectName)] - private static extern ulong g_signal_handler_disconnect(IntPtr instance, ulong connectionId); - - private delegate bool timeout_callback(IntPtr data); - - [DllImport(GlibName)] - private static extern ulong g_timeout_add_full(int prio, uint interval, timeout_callback callback, IntPtr data, - IntPtr destroy); - - - private class ConnectedSignal : IDisposable - { - private readonly IntPtr _instance; - private GCHandle _handle; - private readonly ulong _id; - - public ConnectedSignal(IntPtr instance, GCHandle handle, ulong id) - { - _instance = instance; - g_object_ref(instance); - _handle = handle; - _id = id; - } - - public void Dispose() - { - if (_handle.IsAllocated) - { - g_signal_handler_disconnect(_instance, _id); - g_object_unref(_instance); - _handle.Free(); - } - } - } - - public static IDisposable ConnectSignal(IntPtr obj, string name, T handler) - { - var handle = GCHandle.Alloc(handler); - var ptr = Marshal.GetFunctionPointerForDelegate(handler); - using (var utf = new Utf8Buffer(name)) - { - var id = g_signal_connect_object(obj, utf, ptr, IntPtr.Zero, 0); - if (id == 0) - throw new ArgumentException("Unable to connect to signal " + name); - return new ConnectedSignal(obj, handle, id); - } - } - - - private static bool TimeoutHandler(IntPtr data) - { - var handle = GCHandle.FromIntPtr(data); - var cb = (Func)handle.Target; - if (!cb()) - { - handle.Free(); - return false; - } - - return true; - } - - private static readonly timeout_callback s_pinnedHandler; - - static Glib() - { - s_pinnedHandler = TimeoutHandler; - } - - private static void AddTimeout(int priority, uint interval, Func callback) - { - var handle = GCHandle.Alloc(callback); - g_timeout_add_full(priority, interval, s_pinnedHandler, GCHandle.ToIntPtr(handle), IntPtr.Zero); - } - - public static Task RunOnGlibThread(Func action) - { - var tcs = new TaskCompletionSource(); - AddTimeout(0, 0, () => - { - - try - { - tcs.SetResult(action()); - } - catch (Exception e) - { - tcs.TrySetException(e); - } - - return false; - }); - return tcs.Task; - } - } - - [StructLayout(LayoutKind.Sequential)] - internal unsafe struct GSList - { - public readonly IntPtr Data; - public readonly GSList* Next; - } - internal enum GtkFileChooserAction { Open, @@ -247,6 +129,9 @@ namespace Avalonia.X11.NativeDialogs [DllImport(GdkName)] private static extern IntPtr gdk_display_get_default(); + + [DllImport(GdkName)] + private static extern IntPtr gdk_x11_display_get_xdisplay(IntPtr display); [DllImport(GtkName)] private static extern IntPtr gtk_application_new(Utf8Buffer appId, int flags); @@ -265,10 +150,21 @@ namespace Avalonia.X11.NativeDialogs return s_startGtkTask ??= StartGtkCore(); } - private static void GtkThread(TaskCompletionSource tcs) + static bool InitializeGtk() { try { + // Check if GTK was already initialized + var existingDisplay = gdk_display_get_default(); + if (existingDisplay != IntPtr.Zero) + { + if (gdk_x11_display_get_xdisplay(existingDisplay) == IntPtr.Zero) + return false; + s_display = existingDisplay; + return true; + + } + try { using (var backends = new Utf8Buffer("x11")) @@ -284,8 +180,7 @@ namespace Avalonia.X11.NativeDialogs if (!gtk_init_check(0, IntPtr.Zero)) { - tcs.SetResult(false); - return; + return false; } IntPtr app; @@ -293,26 +188,51 @@ namespace Avalonia.X11.NativeDialogs app = gtk_application_new(utf, 0); if (app == IntPtr.Zero) { - tcs.SetResult(false); - return; + return false; } s_display = gdk_display_get_default(); + } + catch + { + return false; + } + + return true; + } + + private static void GtkThread(TaskCompletionSource tcs) + { + try + { + if (!InitializeGtk()) + { + tcs.SetResult(false); + return; + } + tcs.SetResult(true); while (true) gtk_main_iteration(); } catch { - tcs.SetResult(false); + tcs.TrySetResult(false); } } private static Task StartGtkCore() { - var tcs = new TaskCompletionSource(); - new Thread(() => GtkThread(tcs)) {Name = "GTK3THREAD", IsBackground = true}.Start(); - return tcs.Task; + if (AvaloniaLocator.Current.GetService()?.UseGLibMainLoop == true) + { + return Task.FromResult(InitializeGtk()); + } + else + { + var tcs = new TaskCompletionSource(); + new Thread(() => GtkThread(tcs)) { Name = "GTK3THREAD", IsBackground = true }.Start(); + return tcs.Task; + } } } } diff --git a/src/Avalonia.X11/NativeDialogs/GtkNativeFileDialogs.cs b/src/Avalonia.X11/NativeDialogs/GtkNativeFileDialogs.cs index e7ff04a2dd..568875c507 100644 --- a/src/Avalonia.X11/NativeDialogs/GtkNativeFileDialogs.cs +++ b/src/Avalonia.X11/NativeDialogs/GtkNativeFileDialogs.cs @@ -10,7 +10,7 @@ using Avalonia.Platform; using Avalonia.Platform.Interop; using Avalonia.Platform.Storage; using Avalonia.Platform.Storage.FileIO; -using static Avalonia.X11.NativeDialogs.Glib; +using static Avalonia.X11.Interop.Glib; using static Avalonia.X11.NativeDialogs.Gtk; namespace Avalonia.X11.NativeDialogs diff --git a/src/Avalonia.X11/X11FocusProxy.cs b/src/Avalonia.X11/X11FocusProxy.cs index 4fdaf643ab..4856dbef65 100644 --- a/src/Avalonia.X11/X11FocusProxy.cs +++ b/src/Avalonia.X11/X11FocusProxy.cs @@ -26,7 +26,7 @@ namespace Avalonia.X11 internal IntPtr _handle; private readonly AvaloniaX11Platform _platform; - private readonly X11PlatformThreading.EventHandler _ownerEventHandler; + private readonly X11EventDispatcher.EventHandler _ownerEventHandler; /// /// Initializes instance and creates the underlying X window. @@ -36,7 +36,7 @@ namespace Avalonia.X11 /// The parent window to proxy the focus for. /// An event handler that will handle X events that come to the proxy. internal X11FocusProxy(AvaloniaX11Platform platform, IntPtr parent, - X11PlatformThreading.EventHandler eventHandler) + X11EventDispatcher.EventHandler eventHandler) { _handle = PrepareXWindow(platform.Info.Display, parent); _platform = platform; diff --git a/src/Avalonia.X11/X11Platform.cs b/src/Avalonia.X11/X11Platform.cs index 64ff98e7c7..4b1b3731b7 100644 --- a/src/Avalonia.X11/X11Platform.cs +++ b/src/Avalonia.X11/X11Platform.cs @@ -15,6 +15,7 @@ using Avalonia.Rendering.Composition; using Avalonia.Threading; using Avalonia.Vulkan; using Avalonia.X11; +using Avalonia.X11.Dispatching; using Avalonia.X11.Glx; using Avalonia.X11.Vulkan; using Avalonia.X11.Screens; @@ -26,8 +27,7 @@ namespace Avalonia.X11 { private Lazy _keyboardDevice = new Lazy(() => new KeyboardDevice()); public KeyboardDevice KeyboardDevice => _keyboardDevice.Value; - public Dictionary Windows { get; } = - new Dictionary(); + public Dictionary Windows { get; } = new (); public XI2Manager XI2 { get; private set; } public X11Info Info { get; private set; } public X11Screens X11Screens { get; private set; } @@ -73,7 +73,9 @@ namespace Avalonia.X11 AvaloniaLocator.CurrentMutable.BindToSelf(this) .Bind().ToConstant(this) - .Bind().ToConstant(new X11PlatformThreading(this)) + .Bind().ToConstant(options.UseGLibMainLoop + ? new GlibDispatcherImpl(this) + : new X11PlatformThreading(this)) .Bind().ToConstant(timer) .Bind().ToConstant(new PlatformHotkeyConfiguration(KeyModifiers.Control)) .Bind().ToConstant(new KeyGestureFormatInfo(new Dictionary() { }, meta: "Super")) @@ -370,6 +372,22 @@ namespace Avalonia /// public bool? UseRetainedFramebuffer { get; set; } + /// + /// If this option is set to true, GMainLoop and GSource based dispatcher implementation will be used instead + /// of epoll-based one. + /// Use this if you need to use GLib-based libraries on the main thread + /// + public bool UseGLibMainLoop { get; set; } + + /// + /// If Avalonia is in control of a run loop, we propagate exceptions by stopping the run loop frame + /// and rethrowing an exception. However, if there is no Avalonia-controlled run loop frame, + /// there is no way to report such exceptions, since allowing those to escape native->managed call boundary + /// will likely brick GLib machinery since it's not aware of managed Exceptions + /// This property allows to inspect such exceptions before they will be ignored + /// + public Action? ExterinalGLibMainLoopExceptionLogger { get; set; } + public X11PlatformOptions() { try diff --git a/src/Avalonia.X11/XLib.cs b/src/Avalonia.X11/XLib.cs index 03dad8e0e2..33daacf936 100644 --- a/src/Avalonia.X11/XLib.cs +++ b/src/Avalonia.X11/XLib.cs @@ -733,7 +733,7 @@ namespace Avalonia.X11 } } - public static IntPtr CreateEventWindow(AvaloniaX11Platform plat, X11PlatformThreading.EventHandler handler) + public static IntPtr CreateEventWindow(AvaloniaX11Platform plat, X11EventDispatcher.EventHandler handler) { var win = XCreateSimpleWindow(plat.Display, plat.Info.DefaultRootWindow, 0, 0, 1, 1, 0, IntPtr.Zero, IntPtr.Zero);