Skip to content

Commit 5cc111c

Browse files
kekekeksmaxkatz6
authored andcommitted
Implemented XEmbed client support with GtkSharp usage example (#17446)
1 parent b977368 commit 5cc111c

15 files changed

+729
-47
lines changed

Avalonia.Desktop.slnf

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
"samples\\Sandbox\\Sandbox.csproj",
1616
"samples\\UnloadableAssemblyLoadContext\\UnloadableAssemblyLoadContextPlug\\UnloadableAssemblyLoadContextPlug.csproj",
1717
"samples\\UnloadableAssemblyLoadContext\\UnloadableAssemblyLoadContext\\UnloadableAssemblyLoadContext.csproj",
18+
"samples\\XEmbedSample\\XEmbedSample.csproj",
1819
"src\\Avalonia.Base\\Avalonia.Base.csproj",
1920
"src\\Avalonia.Build.Tasks\\Avalonia.Build.Tasks.csproj",
2021
"src\\Avalonia.Controls.ColorPicker\\Avalonia.Controls.ColorPicker.csproj",

Avalonia.sln

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Vulkan", "src\Aval
301301
EndProject
302302
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.RenderTests.WpfCompare", "tests\Avalonia.RenderTests.WpfCompare\Avalonia.RenderTests.WpfCompare.csproj", "{9AE1B827-21AC-4063-AB22-C8804B7F931E}"
303303
EndProject
304+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Win32.Automation", "src\Windows\Avalonia.Win32.Automation\Avalonia.Win32.Automation.csproj", "{0097673D-DBCE-476E-82FE-E78A56E58AA2}"
305+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "XEmbedSample", "samples\XEmbedSample\XEmbedSample.csproj", "{255614F5-CB64-4ECA-A026-E0B1AF6A2EF4}"
306+
EndProject
304307
Global
305308
GlobalSection(SolutionConfigurationPlatforms) = preSolution
306309
Debug|Any CPU = Debug|Any CPU
@@ -701,6 +704,14 @@ Global
701704
{9AE1B827-21AC-4063-AB22-C8804B7F931E}.Debug|Any CPU.Build.0 = Debug|Any CPU
702705
{9AE1B827-21AC-4063-AB22-C8804B7F931E}.Release|Any CPU.ActiveCfg = Release|Any CPU
703706
{9AE1B827-21AC-4063-AB22-C8804B7F931E}.Release|Any CPU.Build.0 = Release|Any CPU
707+
{0097673D-DBCE-476E-82FE-E78A56E58AA2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
708+
{0097673D-DBCE-476E-82FE-E78A56E58AA2}.Debug|Any CPU.Build.0 = Debug|Any CPU
709+
{0097673D-DBCE-476E-82FE-E78A56E58AA2}.Release|Any CPU.ActiveCfg = Release|Any CPU
710+
{0097673D-DBCE-476E-82FE-E78A56E58AA2}.Release|Any CPU.Build.0 = Release|Any CPU
711+
{255614F5-CB64-4ECA-A026-E0B1AF6A2EF4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
712+
{255614F5-CB64-4ECA-A026-E0B1AF6A2EF4}.Debug|Any CPU.Build.0 = Debug|Any CPU
713+
{255614F5-CB64-4ECA-A026-E0B1AF6A2EF4}.Release|Any CPU.ActiveCfg = Release|Any CPU
714+
{255614F5-CB64-4ECA-A026-E0B1AF6A2EF4}.Release|Any CPU.Build.0 = Release|Any CPU
704715
EndGlobalSection
705716
GlobalSection(SolutionProperties) = preSolution
706717
HideSolutionNode = FALSE
@@ -788,6 +799,8 @@ Global
788799
{D7FE3E0F-3FE0-4F87-A2F5-24F1454D84C0} = {9CCA131B-DE95-4D44-8788-C3CAE28574CD}
789800
{DA5F1FF9-4259-4C54-B443-85CFA226EE6A} = {9CCA131B-DE95-4D44-8788-C3CAE28574CD}
790801
{9AE1B827-21AC-4063-AB22-C8804B7F931E} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
802+
{0097673D-DBCE-476E-82FE-E78A56E58AA2} = {B39A8919-9F95-48FE-AD7B-76E08B509888}
803+
{255614F5-CB64-4ECA-A026-E0B1AF6A2EF4} = {9B9E3891-2366-4253-A952-D08BCEB71098}
791804
EndGlobalSection
792805
GlobalSection(ExtensibilityGlobals) = postSolution
793806
SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
using System.Runtime.InteropServices;
2+
3+
namespace XEmbedSample;
4+
5+
/*
6+
This is needed specifically for GtkSharp:
7+
https://github.com/mono/SkiaSharp/issues/3038
8+
https://github.com/GtkSharp/GtkSharp/issues/443
9+
10+
Instead of using plain DllImport they are manually calling dlopen with RTLD_GLOBAL and RTLD_LAZY flags:
11+
https://github.com/GtkSharp/GtkSharp/blob/b7303616129ab5a0ca64def45649ab522d83fa4a/Source/Libs/Shared/FuncLoader.cs#L80-L92
12+
13+
Which causes libHarfBuzzSharp.so from HarfBuzzSharp to resolve some of the symbols from the system libharfbuzz.so.0
14+
which is a _different_ harfbuzz version.
15+
16+
That results in a segfault.
17+
18+
Previously there was a workaround - https://github.com/mono/SkiaSharp/pull/2247 but it got
19+
disabled for .NET Core / .NET 5+.
20+
21+
Why linux linker builds shared libraries in a way that makes it possible for them to resolve their own symbols from
22+
elsewhere escapes me.
23+
24+
Here we are loading libHarfBuzzSharp.so from the .NET-resolved location, saving it, unloading the library
25+
and then defining a custom resolver that would call dlopen with RTLD_NOW + RTLD_DEEPBIND
26+
27+
*/
28+
29+
public unsafe class HarfbuzzWorkaround
30+
{
31+
[DllImport("libc")]
32+
static extern int dlinfo(IntPtr handle, int request, IntPtr info);
33+
34+
[DllImport("libc")]
35+
static extern IntPtr dlopen(string filename, int flags);
36+
37+
private const int RTLD_DI_ORIGIN = 6;
38+
private const int RTLD_NOW = 2;
39+
private const int RTLD_DEEPBIND = 8;
40+
41+
public static void Apply()
42+
{
43+
if (RuntimeInformation.RuntimeIdentifier.Contains("musl"))
44+
throw new PlatformNotSupportedException("musl doesn't support RTLD_DEEPBIND");
45+
46+
var libraryPathBytes = Marshal.AllocHGlobal(4096);
47+
var handle = NativeLibrary.Load("libHarfBuzzSharp", typeof(HarfBuzzSharp.Blob).Assembly, null);
48+
dlinfo(handle, RTLD_DI_ORIGIN, libraryPathBytes);
49+
var libraryOrigin = Marshal.PtrToStringUTF8(libraryPathBytes);
50+
Marshal.FreeHGlobal(libraryPathBytes);
51+
var libraryPath = Path.Combine(libraryOrigin, "libHarfBuzzSharp.so");
52+
53+
NativeLibrary.Free(handle);
54+
var forceLoadedHandle = dlopen(libraryPath, RTLD_NOW | RTLD_DEEPBIND);
55+
if (forceLoadedHandle == IntPtr.Zero)
56+
throw new DllNotFoundException($"Unable to load {libraryPath} via dlopen");
57+
58+
NativeLibrary.SetDllImportResolver(typeof(HarfBuzzSharp.Blob).Assembly, (name, assembly, searchPath) =>
59+
{
60+
if (name.Contains("HarfBuzzSharp"))
61+
return dlopen(libraryPath, RTLD_NOW | RTLD_DEEPBIND);
62+
return NativeLibrary.Load(name, assembly, searchPath);
63+
});
64+
65+
}
66+
}

samples/XEmbedSample/Program.cs

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
using Avalonia;
2+
using Avalonia.Controls;
3+
using Avalonia.Controls.Primitives;
4+
using Avalonia.Media;
5+
using ControlCatalog;
6+
using ControlCatalog.Models;
7+
using Gtk;
8+
9+
namespace XEmbedSample;
10+
11+
class Program
12+
{
13+
static void Main(string[] args)
14+
{
15+
HarfbuzzWorkaround.Apply();
16+
AppBuilder.Configure<App>()
17+
.UseSkia()
18+
.With(new X11PlatformOptions()
19+
{
20+
UseGLibMainLoop = true,
21+
ExterinalGLibMainLoopExceptionLogger = e => Console.WriteLine(e.ToString())
22+
})
23+
.UseX11()
24+
.SetupWithoutStarting();
25+
App.SetCatalogThemes(CatalogTheme.Fluent);
26+
Gdk.Global.AllowedBackends = "x11";
27+
Gtk.Application.Init("myapp", ref args);
28+
29+
30+
31+
32+
33+
var w = new Gtk.Window("XEmbed Test Window");
34+
var socket = new AvaloniaXEmbedGtkSocket(w.StyleContext.GetBackgroundColor(StateFlags.Normal))
35+
{
36+
Content = new ScrollViewer()
37+
{
38+
Content = new ControlCatalog.Pages.TextBoxPage(),
39+
HorizontalScrollBarVisibility = ScrollBarVisibility.Auto
40+
}
41+
};
42+
var vbox = new Gtk.Box(Gtk.Orientation.Vertical, 5);
43+
var label = new Gtk.Label("Those are GTK controls");
44+
vbox.Add(label);
45+
vbox.Add(new Gtk.Entry());
46+
vbox.Add(new Gtk.Button(new Gtk.Label("Do nothing")));
47+
vbox.PackEnd(socket, true, true, 0);
48+
socket.HeightRequest = 400;
49+
socket.WidthRequest = 400;
50+
w.Add(vbox);
51+
socket.Realize();
52+
53+
54+
w.AddSignalHandler("destroy", new EventHandler((_, __) =>
55+
{
56+
Gtk.Application.Quit();
57+
socket.Destroy();
58+
}));
59+
w.ShowAll();
60+
Gtk.Application.Run();
61+
62+
}
63+
}

samples/XEmbedSample/SocketEx.cs

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
using Avalonia;
2+
using Avalonia.X11;
3+
using Gdk;
4+
using Color = Cairo.Color;
5+
6+
namespace XEmbedSample;
7+
8+
public class AvaloniaXEmbedGtkSocket : Gtk.Socket
9+
{
10+
private readonly RGBA _backgroundColor;
11+
private XEmbedPlug? _avaloniaPlug;
12+
public AvaloniaXEmbedGtkSocket(RGBA backgroundColor)
13+
{
14+
_backgroundColor = backgroundColor;
15+
}
16+
17+
private object _content;
18+
public object Content
19+
{
20+
get => _content;
21+
set
22+
{
23+
_content = value;
24+
if (_avaloniaPlug != null)
25+
_avaloniaPlug.Content = _content;
26+
}
27+
}
28+
29+
protected override void OnRealized()
30+
{
31+
base.OnRealized();
32+
_avaloniaPlug ??= XEmbedPlug.Create();
33+
_avaloniaPlug.ScaleFactor = ScaleFactor;
34+
_avaloniaPlug.BackgroundColor = Avalonia.Media.Color.FromRgb((byte)(_backgroundColor.Red * 255),
35+
(byte)(_backgroundColor.Green * 255),
36+
(byte)(_backgroundColor.Blue * 255)
37+
);
38+
_avaloniaPlug.Content = _content;
39+
ApplyInteractiveResize();
40+
AddId((ulong)_avaloniaPlug.Handle);
41+
}
42+
43+
void ApplyInteractiveResize()
44+
{
45+
// This is _NOT_ a part of XEmbed, but allows us to have smooth resize
46+
GetAllocatedSize(out var rect, out _);
47+
var scale = ScaleFactor;
48+
_avaloniaPlug?.ProcessInteractiveResize(new PixelSize(rect.Width * scale, rect.Height * scale));
49+
}
50+
51+
protected override void OnSizeAllocated(Rectangle allocation)
52+
{
53+
base.OnSizeAllocated(allocation);
54+
Display.Default.Sync();
55+
ApplyInteractiveResize();
56+
}
57+
58+
protected override void OnDestroyed()
59+
{
60+
_avaloniaPlug?.Dispose();
61+
_avaloniaPlug = null;
62+
base.OnDestroyed();
63+
}
64+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<OutputType>Exe</OutputType>
5+
<TargetFramework>net6.0</TargetFramework>
6+
<ImplicitUsings>enable</ImplicitUsings>
7+
<Nullable>enable</Nullable>
8+
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
9+
</PropertyGroup>
10+
11+
<ItemGroup>
12+
<PackageReference Include="GtkSharp" Version="3.24.24.95" />
13+
</ItemGroup>
14+
15+
<ItemGroup>
16+
<ProjectReference Include="..\..\src\Avalonia.X11\Avalonia.X11.csproj" />
17+
<ProjectReference Include="..\ControlCatalog\ControlCatalog.csproj" />
18+
</ItemGroup>
19+
20+
</Project>

src/Avalonia.X11/Dispatching/GLibDispatcherImpl.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ namespace Avalonia.X11.Dispatching;
1212

1313
internal class GlibDispatcherImpl :
1414
IDispatcherImplWithExplicitBackgroundProcessing,
15-
IControlledDispatcherImpl
15+
IControlledDispatcherImpl,
16+
IX11PlatformDispatcher
1617
{
1718
/*
1819
GLib priorities and Avalonia priorities are a bit different. Avalonia follows the WPF model when there
@@ -309,5 +310,6 @@ public void Dispose()
309310
}
310311
}
311312
}
312-
313+
314+
public X11EventDispatcher EventDispatcher => _x11Events;
313315
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
using Avalonia.Threading;
2+
3+
namespace Avalonia.X11.Dispatching;
4+
5+
interface IX11PlatformDispatcher : IDispatcherImpl
6+
{
7+
X11EventDispatcher EventDispatcher { get; }
8+
}

src/Avalonia.X11/Dispatching/X11PlatformThreading.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,12 @@
55
using System.Threading;
66
using Avalonia.Platform;
77
using Avalonia.Threading;
8+
using Avalonia.X11.Dispatching;
89
using static Avalonia.X11.XLib;
910

1011
namespace Avalonia.X11
1112
{
12-
internal unsafe class X11PlatformThreading : IControlledDispatcherImpl
13+
internal unsafe class X11PlatformThreading : IControlledDispatcherImpl, IX11PlatformDispatcher
1314
{
1415
private readonly AvaloniaX11Platform _platform;
1516
private Thread _mainThread = Thread.CurrentThread;
@@ -200,5 +201,6 @@ public void UpdateTimer(long? dueTimeInMs)
200201
public bool CanQueryPendingInput => true;
201202

202203
public bool HasPendingInput => _platform.EventGrouperDispatchQueue.HasJobs || _x11Events.IsPending;
204+
public X11EventDispatcher EventDispatcher => _x11Events;
203205
}
204206
}

0 commit comments

Comments
 (0)