Skip to content

Commit

Permalink
Implemented XEmbed client support with GtkSharp usage example (#17446)
Browse files Browse the repository at this point in the history
  • Loading branch information
kekekeks authored Dec 16, 2024
1 parent eada7ba commit 0efe89e
Show file tree
Hide file tree
Showing 15 changed files with 722 additions and 47 deletions.
1 change: 1 addition & 0 deletions Avalonia.Desktop.slnf
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"samples\\Sandbox\\Sandbox.csproj",
"samples\\UnloadableAssemblyLoadContext\\UnloadableAssemblyLoadContextPlug\\UnloadableAssemblyLoadContextPlug.csproj",
"samples\\UnloadableAssemblyLoadContext\\UnloadableAssemblyLoadContext\\UnloadableAssemblyLoadContext.csproj",
"samples\\XEmbedSample\\XEmbedSample.csproj",
"src\\Avalonia.Base\\Avalonia.Base.csproj",
"src\\Avalonia.Build.Tasks\\Avalonia.Build.Tasks.csproj",
"src\\Avalonia.Controls.ColorPicker\\Avalonia.Controls.ColorPicker.csproj",
Expand Down
6 changes: 6 additions & 0 deletions Avalonia.sln
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,7 @@ EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.RenderTests.WpfCompare", "tests\Avalonia.RenderTests.WpfCompare\Avalonia.RenderTests.WpfCompare.csproj", "{9AE1B827-21AC-4063-AB22-C8804B7F931E}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Win32.Automation", "src\Windows\Avalonia.Win32.Automation\Avalonia.Win32.Automation.csproj", "{0097673D-DBCE-476E-82FE-E78A56E58AA2}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "XEmbedSample", "samples\XEmbedSample\XEmbedSample.csproj", "{255614F5-CB64-4ECA-A026-E0B1AF6A2EF4}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Expand Down Expand Up @@ -707,6 +708,10 @@ Global
{0097673D-DBCE-476E-82FE-E78A56E58AA2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0097673D-DBCE-476E-82FE-E78A56E58AA2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0097673D-DBCE-476E-82FE-E78A56E58AA2}.Release|Any CPU.Build.0 = Release|Any CPU
{255614F5-CB64-4ECA-A026-E0B1AF6A2EF4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{255614F5-CB64-4ECA-A026-E0B1AF6A2EF4}.Debug|Any CPU.Build.0 = Debug|Any CPU
{255614F5-CB64-4ECA-A026-E0B1AF6A2EF4}.Release|Any CPU.ActiveCfg = Release|Any CPU
{255614F5-CB64-4ECA-A026-E0B1AF6A2EF4}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -795,6 +800,7 @@ Global
{DA5F1FF9-4259-4C54-B443-85CFA226EE6A} = {9CCA131B-DE95-4D44-8788-C3CAE28574CD}
{9AE1B827-21AC-4063-AB22-C8804B7F931E} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
{0097673D-DBCE-476E-82FE-E78A56E58AA2} = {B39A8919-9F95-48FE-AD7B-76E08B509888}
{255614F5-CB64-4ECA-A026-E0B1AF6A2EF4} = {9B9E3891-2366-4253-A952-D08BCEB71098}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A}
Expand Down
66 changes: 66 additions & 0 deletions samples/XEmbedSample/HarfbuzzWorkaround.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
using System.Runtime.InteropServices;

namespace XEmbedSample;

/*
This is needed specifically for GtkSharp:
https://github.com/mono/SkiaSharp/issues/3038
https://github.com/GtkSharp/GtkSharp/issues/443
Instead of using plain DllImport they are manually calling dlopen with RTLD_GLOBAL and RTLD_LAZY flags:
https://github.com/GtkSharp/GtkSharp/blob/b7303616129ab5a0ca64def45649ab522d83fa4a/Source/Libs/Shared/FuncLoader.cs#L80-L92
Which causes libHarfBuzzSharp.so from HarfBuzzSharp to resolve some of the symbols from the system libharfbuzz.so.0
which is a _different_ harfbuzz version.
That results in a segfault.
Previously there was a workaround - https://github.com/mono/SkiaSharp/pull/2247 but it got
disabled for .NET Core / .NET 5+.
Why linux linker builds shared libraries in a way that makes it possible for them to resolve their own symbols from
elsewhere escapes me.
Here we are loading libHarfBuzzSharp.so from the .NET-resolved location, saving it, unloading the library
and then defining a custom resolver that would call dlopen with RTLD_NOW + RTLD_DEEPBIND
*/

public unsafe class HarfbuzzWorkaround
{
[DllImport("libc")]
static extern int dlinfo(IntPtr handle, int request, IntPtr info);

[DllImport("libc")]
static extern IntPtr dlopen(string filename, int flags);

private const int RTLD_DI_ORIGIN = 6;
private const int RTLD_NOW = 2;
private const int RTLD_DEEPBIND = 8;

public static void Apply()
{
if (RuntimeInformation.RuntimeIdentifier.Contains("musl"))
throw new PlatformNotSupportedException("musl doesn't support RTLD_DEEPBIND");

var libraryPathBytes = Marshal.AllocHGlobal(4096);
var handle = NativeLibrary.Load("libHarfBuzzSharp", typeof(HarfBuzzSharp.Blob).Assembly, null);
dlinfo(handle, RTLD_DI_ORIGIN, libraryPathBytes);
var libraryOrigin = Marshal.PtrToStringUTF8(libraryPathBytes);
Marshal.FreeHGlobal(libraryPathBytes);
var libraryPath = Path.Combine(libraryOrigin, "libHarfBuzzSharp.so");

NativeLibrary.Free(handle);
var forceLoadedHandle = dlopen(libraryPath, RTLD_NOW | RTLD_DEEPBIND);
if (forceLoadedHandle == IntPtr.Zero)
throw new DllNotFoundException($"Unable to load {libraryPath} via dlopen");

NativeLibrary.SetDllImportResolver(typeof(HarfBuzzSharp.Blob).Assembly, (name, assembly, searchPath) =>
{
if (name.Contains("HarfBuzzSharp"))
return dlopen(libraryPath, RTLD_NOW | RTLD_DEEPBIND);
return NativeLibrary.Load(name, assembly, searchPath);
});

}
}
63 changes: 63 additions & 0 deletions samples/XEmbedSample/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Media;
using ControlCatalog;
using ControlCatalog.Models;
using Gtk;

namespace XEmbedSample;

class Program
{
static void Main(string[] args)
{
HarfbuzzWorkaround.Apply();
AppBuilder.Configure<App>()
.UseSkia()
.With(new X11PlatformOptions()
{
UseGLibMainLoop = true,
ExterinalGLibMainLoopExceptionLogger = e => Console.WriteLine(e.ToString())
})
.UseX11()
.SetupWithoutStarting();
App.SetCatalogThemes(CatalogTheme.Fluent);
Gdk.Global.AllowedBackends = "x11";
Gtk.Application.Init("myapp", ref args);





var w = new Gtk.Window("XEmbed Test Window");
var socket = new AvaloniaXEmbedGtkSocket(w.StyleContext.GetBackgroundColor(StateFlags.Normal))
{
Content = new ScrollViewer()
{
Content = new ControlCatalog.Pages.TextBoxPage(),
HorizontalScrollBarVisibility = ScrollBarVisibility.Auto
}
};
var vbox = new Gtk.Box(Gtk.Orientation.Vertical, 5);
var label = new Gtk.Label("Those are GTK controls");
vbox.Add(label);
vbox.Add(new Gtk.Entry());
vbox.Add(new Gtk.Button(new Gtk.Label("Do nothing")));
vbox.PackEnd(socket, true, true, 0);
socket.HeightRequest = 400;
socket.WidthRequest = 400;
w.Add(vbox);
socket.Realize();


w.AddSignalHandler("destroy", new EventHandler((_, __) =>
{
Gtk.Application.Quit();
socket.Destroy();
}));
w.ShowAll();
Gtk.Application.Run();

}
}
64 changes: 64 additions & 0 deletions samples/XEmbedSample/SocketEx.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
using Avalonia;
using Avalonia.X11;
using Gdk;
using Color = Cairo.Color;

namespace XEmbedSample;

public class AvaloniaXEmbedGtkSocket : Gtk.Socket
{
private readonly RGBA _backgroundColor;
private XEmbedPlug? _avaloniaPlug;
public AvaloniaXEmbedGtkSocket(RGBA backgroundColor)
{
_backgroundColor = backgroundColor;
}

private object _content;
public object Content
{
get => _content;
set
{
_content = value;
if (_avaloniaPlug != null)
_avaloniaPlug.Content = _content;
}
}

protected override void OnRealized()
{
base.OnRealized();
_avaloniaPlug ??= XEmbedPlug.Create();
_avaloniaPlug.ScaleFactor = ScaleFactor;
_avaloniaPlug.BackgroundColor = Avalonia.Media.Color.FromRgb((byte)(_backgroundColor.Red * 255),
(byte)(_backgroundColor.Green * 255),
(byte)(_backgroundColor.Blue * 255)
);
_avaloniaPlug.Content = _content;
ApplyInteractiveResize();
AddId((ulong)_avaloniaPlug.Handle);
}

void ApplyInteractiveResize()
{
// This is _NOT_ a part of XEmbed, but allows us to have smooth resize
GetAllocatedSize(out var rect, out _);
var scale = ScaleFactor;
_avaloniaPlug?.ProcessInteractiveResize(new PixelSize(rect.Width * scale, rect.Height * scale));
}

protected override void OnSizeAllocated(Rectangle allocation)
{
base.OnSizeAllocated(allocation);
Display.Default.Sync();
ApplyInteractiveResize();
}

protected override void OnDestroyed()
{
_avaloniaPlug?.Dispose();
_avaloniaPlug = null;
base.OnDestroyed();
}
}
20 changes: 20 additions & 0 deletions samples/XEmbedSample/XEmbedSample.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="GtkSharp" Version="3.24.24.95" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\Avalonia.X11\Avalonia.X11.csproj" />
<ProjectReference Include="..\ControlCatalog\ControlCatalog.csproj" />
</ItemGroup>

</Project>
6 changes: 4 additions & 2 deletions src/Avalonia.X11/Dispatching/GLibDispatcherImpl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ namespace Avalonia.X11.Dispatching;

internal class GlibDispatcherImpl :
IDispatcherImplWithExplicitBackgroundProcessing,
IControlledDispatcherImpl
IControlledDispatcherImpl,
IX11PlatformDispatcher
{
/*
GLib priorities and Avalonia priorities are a bit different. Avalonia follows the WPF model when there
Expand Down Expand Up @@ -309,5 +310,6 @@ public void Dispose()
}
}
}


public X11EventDispatcher EventDispatcher => _x11Events;
}
8 changes: 8 additions & 0 deletions src/Avalonia.X11/Dispatching/IX11PlatformDispatcher.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using Avalonia.Threading;

namespace Avalonia.X11.Dispatching;

interface IX11PlatformDispatcher : IDispatcherImpl
{
X11EventDispatcher EventDispatcher { get; }
}
4 changes: 3 additions & 1 deletion src/Avalonia.X11/Dispatching/X11PlatformThreading.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@
using System.Threading;
using Avalonia.Platform;
using Avalonia.Threading;
using Avalonia.X11.Dispatching;
using static Avalonia.X11.XLib;

namespace Avalonia.X11
{
internal unsafe class X11PlatformThreading : IControlledDispatcherImpl
internal unsafe class X11PlatformThreading : IControlledDispatcherImpl, IX11PlatformDispatcher
{
private readonly AvaloniaX11Platform _platform;
private Thread _mainThread = Thread.CurrentThread;
Expand Down Expand Up @@ -200,5 +201,6 @@ public void UpdateTimer(long? dueTimeInMs)
public bool CanQueryPendingInput => true;

public bool HasPendingInput => _platform.EventGrouperDispatchQueue.HasJobs || _x11Events.IsPending;
public X11EventDispatcher EventDispatcher => _x11Events;
}
}
Loading

0 comments on commit 0efe89e

Please sign in to comment.