Browse Source

Implemented GLib-based dispatcher (#17281)

* Implemented GLib-based dispatcher

This should allow running Avalonia on the same thread as another UI toolkit that supports running on top of GRunLoop (e. g. GTK)

* Force-drain the X11 event queue, since g_runloop_quit won't exit the loop otherwise
#Conflicts:
#	src/Avalonia.X11/X11Platform.cs
release/11.2.0-rc2
Nikita Tsukanov 1 year ago
committed by Max Katz
parent
commit
0dbf5c22ee
  1. 1
      samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj
  2. 2
      samples/ControlCatalog.NetCore/NativeControls/Gtk/GtkHelper.cs
  3. 313
      src/Avalonia.X11/Dispatching/GLibDispatcherImpl.cs
  4. 62
      src/Avalonia.X11/Dispatching/X11EventDispatcher.cs
  5. 57
      src/Avalonia.X11/Dispatching/X11PlatformThreading.cs
  6. 192
      src/Avalonia.X11/Interop/Glib.cs
  7. 2
      src/Avalonia.X11/Interop/GtkInteropHelper.cs
  8. 180
      src/Avalonia.X11/NativeDialogs/Gtk.cs
  9. 2
      src/Avalonia.X11/NativeDialogs/GtkNativeFileDialogs.cs
  10. 4
      src/Avalonia.X11/X11FocusProxy.cs
  11. 24
      src/Avalonia.X11/X11Platform.cs
  12. 2
      src/Avalonia.X11/XLib.cs

1
samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj

@ -9,6 +9,7 @@
<ItemGroup>
<Compile Include="..\..\src\Avalonia.X11\NativeDialogs\Gtk.cs" Link="NativeControls\Gtk\Gtk.cs" />
<Compile Include="..\..\src\Avalonia.X11\Interop\Glib.cs" Link="NativeControls\Gtk\Glib.cs" />
<Compile Include="..\..\src\Avalonia.Base\Platform\Interop\Utf8Buffer.cs" Link="NativeControls\Utf8Buffer.cs" />
</ItemGroup>

2
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;

313
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<ManagedLoopFrame> _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<ExceptionDispatchInfo> 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);
}
}
}
}

62
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<IntPtr, EventHandler> _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);
}

57
src/Avalonia.X11/X11PlatformThreading.cs → 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<IntPtr, EventHandler> _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;
}
}

192
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<bool>)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<bool> 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<int, GIOCondition, bool>)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<int, GIOCondition, bool> 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<T>(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<T> RunOnGlibThread<T>(Func<T> action)
{
var tcs = new TaskCompletionSource<T>(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;
}

2
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);
}
}

180
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<T>(IntPtr obj, string name, T handler)
{
var handle = GCHandle.Alloc(handler);
var ptr = Marshal.GetFunctionPointerForDelegate<T>(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<bool>)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<bool> callback)
{
var handle = GCHandle.Alloc(callback);
g_timeout_add_full(priority, interval, s_pinnedHandler, GCHandle.ToIntPtr(handle), IntPtr.Zero);
}
public static Task<T> RunOnGlibThread<T>(Func<T> action)
{
var tcs = new TaskCompletionSource<T>();
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<bool> 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<bool> 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<bool> StartGtkCore()
{
var tcs = new TaskCompletionSource<bool>();
new Thread(() => GtkThread(tcs)) {Name = "GTK3THREAD", IsBackground = true}.Start();
return tcs.Task;
if (AvaloniaLocator.Current.GetService<X11PlatformOptions>()?.UseGLibMainLoop == true)
{
return Task.FromResult(InitializeGtk());
}
else
{
var tcs = new TaskCompletionSource<bool>();
new Thread(() => GtkThread(tcs)) { Name = "GTK3THREAD", IsBackground = true }.Start();
return tcs.Task;
}
}
}
}

2
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

4
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;
/// <summary>
/// Initializes instance and creates the underlying X window.
@ -36,7 +36,7 @@ namespace Avalonia.X11
/// <param name="parent">The parent window to proxy the focus for.</param>
/// <param name="eventHandler">An event handler that will handle X events that come to the proxy.</param>
internal X11FocusProxy(AvaloniaX11Platform platform, IntPtr parent,
X11PlatformThreading.EventHandler eventHandler)
X11EventDispatcher.EventHandler eventHandler)
{
_handle = PrepareXWindow(platform.Info.Display, parent);
_platform = platform;

24
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> _keyboardDevice = new Lazy<KeyboardDevice>(() => new KeyboardDevice());
public KeyboardDevice KeyboardDevice => _keyboardDevice.Value;
public Dictionary<IntPtr, X11PlatformThreading.EventHandler> Windows =
new Dictionary<IntPtr, X11PlatformThreading.EventHandler>();
public Dictionary<IntPtr, X11EventDispatcher.EventHandler> Windows { get; } = new ();
public XI2Manager XI2;
public X11Info Info { get; private set; }
public X11Screens X11Screens { get; private set; }
@ -73,7 +73,9 @@ namespace Avalonia.X11
AvaloniaLocator.CurrentMutable.BindToSelf(this)
.Bind<IWindowingPlatform>().ToConstant(this)
.Bind<IDispatcherImpl>().ToConstant(new X11PlatformThreading(this))
.Bind<IDispatcherImpl>().ToConstant<IDispatcherImpl>(options.UseGLibMainLoop
? new GlibDispatcherImpl(this)
: new X11PlatformThreading(this))
.Bind<IRenderTimer>().ToConstant(timer)
.Bind<PlatformHotkeyConfiguration>().ToConstant(new PlatformHotkeyConfiguration(KeyModifiers.Control))
.Bind<KeyGestureFormatInfo>().ToConstant(new KeyGestureFormatInfo(new Dictionary<Key, string>() { }, meta: "Super"))
@ -370,6 +372,22 @@ namespace Avalonia
/// </summary>
public bool? UseRetainedFramebuffer { get; set; }
/// <summary>
/// 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
/// </summary>
public bool UseGLibMainLoop { get; set; }
/// <summary>
/// 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
/// </summary>
public Action<Exception>? ExterinalGLibMainLoopExceptionLogger { get; set; }
public X11PlatformOptions()
{
try

2
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);

Loading…
Cancel
Save