diff --git a/src/Libs/GLib-2.0/GObject/Public/Type.cs b/src/Libs/GLib-2.0/GObject/Public/Type.cs index 3ff15da8a..a86d04b1b 100644 --- a/src/Libs/GLib-2.0/GObject/Public/Type.cs +++ b/src/Libs/GLib-2.0/GObject/Public/Type.cs @@ -3,7 +3,7 @@ namespace GObject; [StructLayout(LayoutKind.Explicit)] -public struct Type +public record struct Type { //This is a manual implementation of GObject.Type inside GLib project inside the GObject namespace. @@ -42,7 +42,8 @@ public struct Type //Offsets see: https://gitlab.gnome.org/GNOME/glib/blob/master/gobject/gtype.h - [FieldOffset(0)] private readonly nuint _value; + [FieldOffset(0)] + private readonly nuint _value; public Type(nuint value) { diff --git a/src/Libs/GObject-2.0/Public/Object2.cs b/src/Libs/GObject-2.0/Public/Object2.cs new file mode 100644 index 000000000..80f9aa887 --- /dev/null +++ b/src/Libs/GObject-2.0/Public/Object2.cs @@ -0,0 +1,377 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using GLib; +using GObject.Internal; + +namespace GObject +{ + public class Object2 : IDisposable + { + private readonly Object2Handle _handle; + + public Object2(Object2Handle handle) + { + _handle = handle; + _handle.Cache(this); + _handle.AddMemoryPressure(); + } + + public IntPtr GetHandle() => _handle.DangerousGetHandle(); + + public void Dispose() + { + _handle.Dispose(); + } + } +} + + +namespace GObject.Internal +{ + public delegate Object2 InstanceFactoryForType(IntPtr handle, bool ownsHandle); + + public interface InterfaceFactory + { + static abstract Object2 Create(IntPtr handle, bool ownsHandle); + } + + public interface ClassFactory + { + static abstract Object2 Create(IntPtr handle, bool ownsHandle); + } +/// +/// Registers a custom subclass with the GObject type system. +/// +public static class SubclassRegistrar2 +{ + public static Type Register() + where TSubclass : ClassFactory + where TParent : GTypeProvider + { + var newType = RegisterNewGType(); + InstanceFactory.Register(newType, TSubclass.Create); + + return newType; + } + + private static Type RegisterNewGType() + where TSubclass : ClassFactory + where TParent : GTypeProvider + { + var parentType = TParent.GetGType(); + var parentTypeInfo = TypeQueryOwnedHandle.Create(); + Functions.TypeQuery(parentType, parentTypeInfo); + + if (parentTypeInfo.GetType() == 0) + throw new TypeRegistrationException("Could not query parent type"); + + Debug.WriteLine($"Registering new type {typeof(TSubclass).FullName} with parent {typeof(TParent).FullName}"); + + // Create TypeInfo + //TODO: Callbacks for "ClassInit" and "InstanceInit" are disabled because if multiple instances + //of the same type are created, the typeInfo object can get garbagec collected in the mean time + //and with it the instances of "DoClassInit" and "DoInstanceInit". If the callback occurs the + //runtime can't do the call anymore and crashes with: + //A callback was made on a garbage collected delegate of type 'GObject-2.0!GObject.Internal.InstanceInitFunc::Invoke' + //Fix this by caching the garbage collected instances somehow + var handle = TypeInfoOwnedHandle.Create(); + handle.SetClassSize((ushort) parentTypeInfo.GetClassSize()); + handle.SetInstanceSize((ushort) parentTypeInfo.GetInstanceSize()); + //handle.SetClassInit(); + //handle.SetInstanceInit(); + + var qualifiedName = QualifyName(typeof(TSubclass)); + var typeid = Functions.TypeRegisterStatic(parentType, GLib.Internal.NonNullableUtf8StringOwnedHandle.Create(qualifiedName), handle, 0); + + if (typeid == 0) + throw new TypeRegistrationException("Type Registration Failed!"); + + return new Type(typeid); + } + + private static string QualifyName(System.Type type) + => type.ToString() + .Replace(".", string.Empty) + .Replace("+", string.Empty) + .Replace("`", string.Empty) + .Replace("[", "_") + .Replace("]", string.Empty) + .Replace(" ", string.Empty) + .Replace(",", "_"); + + /* TODO: Enable if init functions are supported again + // Default Handler for class initialisation. + private static void DoClassInit(IntPtr gClass, IntPtr classData) + { + Console.WriteLine("Subclass type class initialised!"); + } + + // Default Handler for instance initialisation. + private static void DoInstanceInit(IntPtr gClass, IntPtr classData) + { + Console.WriteLine("Subclass instance initialised!"); + } + */ +} + +public static class InstanceFactory +{ + private static readonly Dictionary TypeFactories = new(); + + internal static Object2 Create(IntPtr handle, bool ownsHandle) + { + var type = GetType(handle); + var instanceFactory = GetInstanceFactory(type); + return instanceFactory(handle, ownsHandle); + } + + public static void Register(Type type, InstanceFactoryForType handleWrapper) + { + TypeFactories.Add(type, handleWrapper); + } + + private static InstanceFactoryForType GetInstanceFactory(Type gtype) + { + if (TypeFactories.TryGetValue(gtype, out InstanceFactoryForType? factory)) + return factory; + + // If gtype is not in the type dictionary, walk up the + // tree until we find a type that is. As all objects are + // descended from GObject, we will eventually find a parent + // type that is registered. + + while (!TypeFactories.TryGetValue(gtype, out factory)) + { + gtype = new Type(Functions.TypeParent(gtype.Value)); + if (gtype.Value == (nuint) BasicType.Invalid || + gtype.Value == (nuint) BasicType.None) + throw new Exception("Could not retrieve parent type - is the typeid valid?"); + } + + return factory; + } + + private static unsafe Type GetType(IntPtr handle) + { + var gclass = Unsafe.AsRef((void*) handle).GClass; + var gtype = Unsafe.AsRef((void*) gclass).GType; + + if (gtype == 0) + throw new Exception("Could not retrieve type from class struct - is the struct valid?"); + + return new Type(gtype); + } +} + + internal class ToggleRef2 : IDisposable + { + private readonly IntPtr _handle; + private readonly ToggleNotify _callback; + + private object _reference; + + public Object2? Object + { + get + { + if(_reference is WeakReference weakRef) + return (Object2?) weakRef.Target; + + return (Object2) _reference; + } + } + + /// + /// Initializes a toggle ref. The given object must be already owned by C# as the owned + /// reference is exchanged with a toggling reference meaning the toggle reference is taking control + /// over the reference. + /// This object saves a strong reference to the given object which prevents it from beeing garbage + /// collected. This strong reference is hold as long as there are other than our own toggling ref + /// on the given object. + /// If our toggeling ref is the last ref on the given object the strong reference is changed into a + /// weak reference. This allows the garbage collector to free the C# object which must result in the + /// call of the Dispose method of the ToggleRef. The Dispose method removes the added toggle reference + /// and thus frees the last reference to the C object. + /// + public ToggleRef2(Object2 obj) + { + _reference = obj; + _handle = obj.GetHandle(); + + _callback = ToggleReference; + + RegisterToggleRef(); + } + + private void RegisterToggleRef() + { + Internal.Object.AddToggleRef(_handle, _callback, IntPtr.Zero); + Internal.Object.Unref(_handle); + } + + private void ToggleReference(IntPtr data, IntPtr @object, bool isLastRef) + { + if (!isLastRef && _reference is WeakReference weakRef) + { + if (weakRef.Target is { } weakObj) + _reference = weakObj; + else + throw new Exception($"Handle {_handle}: Could not toggle reference to strong. It got garbage collected."); + } + else if (isLastRef && _reference is not WeakReference) + { + _reference = new WeakReference(_reference); + } + } + + public void Dispose() + { + var sourceFunc = new GLib.Internal.SourceFuncAsyncHandler(() => + { + Internal.Object.RemoveToggleRef(_handle, _callback, IntPtr.Zero); + return false; + }); + GLib.Internal.MainContext.Invoke(GLib.Internal.MainContextUnownedHandle.NullHandle, sourceFunc.NativeCallback, IntPtr.Zero); + } + } + + internal static class InstanceCache2 + { + private static readonly Dictionary Cache = new(); + + public static bool TryGetObject(IntPtr handle, [NotNullWhen(true)] out Object2? obj) + { + if (Cache.TryGetValue(handle, out ToggleRef2? toggleRef)) + { + if (toggleRef.Object is not null) + { + obj = toggleRef.Object; + return true; + } + } + + obj = null; + return false; + } + + public static void Add(IntPtr handle, Object2 obj) + { + lock (Cache) + { + Cache[handle] = new ToggleRef2(obj); + } + + Debug.WriteLine($"Handle {handle}: Added object of type '{obj.GetType()}' to {nameof(InstanceCache2)}"); + } + + public static void Remove(IntPtr handle) + { + lock (Cache) + { + if (Cache.Remove(handle, out var toggleRef)) + toggleRef.Dispose(); + } + + Debug.WriteLine($"Handle {handle}: Removed object from {nameof(InstanceCache2)}."); + } + } + + public static class InterfaceWrapper + { + public static T? WrapNullableHandle(IntPtr handle, bool ownedRef) where T : Object2, InterfaceFactory + { + return handle == IntPtr.Zero + ? null + : WrapHandle(handle, ownedRef); + } + + public static T WrapHandle(IntPtr handle, bool ownedRef) where T : Object2, InterfaceFactory + { + if (handle == IntPtr.Zero) + throw new NullReferenceException($"Failed to wrap handle as type <{typeof(T).FullName}>. Null handle passed to WrapHandle."); + + if (InstanceCache2.TryGetObject(handle, out var obj)) + return (T) obj; + + //In case of interfaces prefer the given type over the type reported by the gobject + //type system as the reported type is probably not part of the public API. Otherwise the + //class itself would be returned and not an interface. + + return (T) T.Create(handle, ownedRef); + } + } + + public static class InstanceWrapper + { + public static Object2? WrapNullableHandle(IntPtr handle, bool ownedRef) + { + return handle == IntPtr.Zero + ? null + : WrapHandle(handle, ownedRef); + } + + public static Object2 WrapHandle(IntPtr handle, bool ownedRef) + { + if (handle == IntPtr.Zero) + throw new NullReferenceException("Failed to wrap handle: Null handle passed to WrapHandle."); + + if (InstanceCache2.TryGetObject(handle, out var obj)) + return obj; + + return InstanceFactory.Create(handle, ownedRef); + } + } + + public class Object2Handle : SafeHandle + { + public override bool IsInvalid => handle == IntPtr.Zero; + + public Object2Handle(IntPtr handle, bool ownsHandle) : base(IntPtr.Zero, true) + { + SetHandle(handle); + OwnReference(ownsHandle); + } + + private void OwnReference(bool ownedRef) + { + if (!ownedRef) + { + // - Unowned GObjects need to be refed to bind them to this instance + // - Unowned InitiallyUnowned floating objects need to be ref_sinked + // - Unowned InitiallyUnowned non-floating objects need to be refed + // As ref_sink behaves like ref in case of non floating instances we use it for all 3 cases + Object.RefSink(handle); + } + else + { + //In case we own the ref because the ownership was fully transfered to us we + //do not need to ref the object at all. + + Debug.Assert(!Internal.Object.IsFloating(handle), $"Handle {handle}: Owned floating references are not possible."); + } + } + + internal void Cache(Object2 obj) + { + InstanceCache2.Add(handle, obj); + } + + protected override bool ReleaseHandle() + { + RemoveMemoryPressure(); + InstanceCache2.Remove(handle); + return true; + } + + protected internal virtual void AddMemoryPressure() { } + protected virtual void RemoveMemoryPressure() { } + } +} + + + diff --git a/src/Libs/GdkPixbuf-2.0/Public/Pixbuf2.cs b/src/Libs/GdkPixbuf-2.0/Public/Pixbuf2.cs new file mode 100644 index 000000000..77f2bc288 --- /dev/null +++ b/src/Libs/GdkPixbuf-2.0/Public/Pixbuf2.cs @@ -0,0 +1,163 @@ +using System; +using System.Diagnostics; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using GdkPixbuf.Internal; +using GObject; +using GObject.Internal; +using Type = GObject.Type; +using TypePlugin = GObject.TypePlugin; + +namespace GdkPixbuf +{ + public interface TestInterface + { + void Bla(); + } + + public class TestInterfaceHelper : Object2, TestInterface, InterfaceFactory, GTypeProvider + { + public TestInterfaceHelper(Object2Handle handle) : base(handle) + { + } + + public void Bla() + { + } + + static Object2 InterfaceFactory.Create(IntPtr handle, bool ownsHandle) + { + return new TestInterfaceHelper(new Object2Handle(handle, ownsHandle)); + } + + static Type GTypeProvider.GetGType() + { + throw new NotImplementedException(); + } + } + + public class MyPixbuf : Pixbuf2, ClassFactory, GTypeProvider + { + private static readonly Type GType; + static MyPixbuf() + { + GType = SubclassRegistrar2.Register(); + } + + public MyPixbuf() : base(Pixbuf2Handle.For(true, [])) + { + } + + private MyPixbuf(IntPtr handle, bool ownsHandle) : base(new Pixbuf2Handle(handle, ownsHandle)) { } + + static Type GTypeProvider.GetGType() => GType; + + static Object2 ClassFactory.Create(IntPtr handle, bool ownsHandle) + { + return new MyPixbuf(handle, ownsHandle); + } + } + public class Pixbuf2 : GObject.Object2, ClassFactory, GTypeProvider + { + protected internal Pixbuf2(Pixbuf2Handle handle) : base(handle) { } + + public static Pixbuf2 New(Colorspace colorspace, bool hasAlpha, int bitsPerSample, int width, int height) + { + var handle = Internal.Pixbuf.New(colorspace, hasAlpha, bitsPerSample, width, height); + return (Pixbuf2) Create(handle, true); + } + + private static Object2 Create(IntPtr handle, bool ownsHandle) + { + var safeHandle = new Pixbuf2Handle(handle, ownsHandle); + return new Pixbuf2(safeHandle); + } + + static Type GTypeProvider.GetGType() + { + var resultGetGType = GdkPixbuf.Internal.Pixbuf.GetGType(); + return resultGetGType; + } + + static Object2 ClassFactory.Create(IntPtr handle, bool ownsHandle) + { + return Create(handle, ownsHandle); + } + + + [Version("2.12")] + public Pixbuf2? ApplyEmbeddedOrientation() + { + var resultApplyEmbeddedOrientation = GdkPixbuf.Internal.Pixbuf.ApplyEmbeddedOrientation(GetHandle()); + //return InterfaceWrapper.WrapNullableHandle(resultApplyEmbeddedOrientation, true); + return (Pixbuf2?) InstanceWrapper.WrapNullableHandle(resultApplyEmbeddedOrientation, true); + } + } +} + +namespace GdkPixbuf.Internal +{ + internal static class TypeRegistration2 + { + public static void RegisterTypes() + { + Register(OSPlatform.Linux, OSPlatform.OSX, OSPlatform.Windows); + } + + private static void Register(params OSPlatform[] supportedPlatforms) where T : ClassFactory, GTypeProvider + { + try + { + if(supportedPlatforms.Any(RuntimeInformation.IsOSPlatform)) + GObject.Internal.InstanceFactory.Register(new Type(T.GetGType()), T.Create); + } + catch(System.Exception e) + { + Debug.WriteLine($"Could not register type: {e.Message}"); + } + } + } + + public class Pixbuf2Handle : GObject.Internal.Object2Handle + { + private long _size; + + public Pixbuf2Handle(IntPtr handle, bool ownsHandle) : base(handle, ownsHandle) + { + } + + public static Pixbuf2Handle For(bool owned, ConstructArgument[] constructArguments) where T : Pixbuf2, GTypeProvider + { + // We can't check if a reference is floating via "g_object_is_floating" here + // as the function could be "lying" depending on the intent of framework writers. + // E.g. A Gtk.Window created via "g_object_new_with_properties" returns an unowned + // reference which is not marked as floating as the gtk toolkit "owns" it. + // For this reason we just delegate the problem to the caller and require a + // definition whether the ownership of the new object will be transferred to us or not. + + var ptr = GObject.Internal.Object.NewWithProperties( + objectType: T.GetGType(), + nProperties: (uint) constructArguments.Length, + names: constructArguments.Select(x => x.Name).ToArray(), + values: ValueArray2OwnedHandle.Create(constructArguments.Select(x => x.Value).ToArray()) + ); + + return new Pixbuf2Handle(ptr, owned); + } + + protected override void AddMemoryPressure() + { + _size = (long) Internal.Pixbuf.GetByteLength(handle); + GC.AddMemoryPressure(_size); + } + + protected override void RemoveMemoryPressure() + { + GC.RemoveMemoryPressure(_size); + } + } +} + + + diff --git a/src/Properties/GirCore.Libraries.props b/src/Properties/GirCore.Libraries.props index 3daf1db32..c5c55490d 100644 --- a/src/Properties/GirCore.Libraries.props +++ b/src/Properties/GirCore.Libraries.props @@ -1,7 +1,7 @@ - net6.0;net7.0;net8.0 + net8.0 enable true true diff --git a/src/Samples/GdkPixbuf-2.0/TestMemoryLeaks/Program.cs b/src/Samples/GdkPixbuf-2.0/TestMemoryLeaks/Program.cs index 4343a3c5c..f4d33831e 100644 --- a/src/Samples/GdkPixbuf-2.0/TestMemoryLeaks/Program.cs +++ b/src/Samples/GdkPixbuf-2.0/TestMemoryLeaks/Program.cs @@ -2,6 +2,10 @@ using System.IO; using System.Threading.Tasks; using GdkPixbuf; +using GdkPixbuf.Internal; +using GObject.Internal; +using Pixbuf = GdkPixbuf.Pixbuf; +using PixbufLoader = GdkPixbuf.PixbufLoader; namespace TestMemoryLeaks; @@ -17,6 +21,12 @@ public static void Main(string[] args) { GdkPixbuf.Module.Initialize(); + //TypeRegistration2.RegisterTypes(); + var pb = Pixbuf2.New(Colorspace.Rgb, false, 8, 10, 10); + var bla = new MyPixbuf(); + + var ii = InstanceWrapper.WrapHandle(bla.GetHandle(), false); + var cycles = 10000; var fileName = "test.bmp"; diff --git a/src/Samples/GdkPixbuf-2.0/TestMemoryLeaks/TestMemoryLeaks.csproj b/src/Samples/GdkPixbuf-2.0/TestMemoryLeaks/TestMemoryLeaks.csproj index b6d5df728..4246e8406 100644 --- a/src/Samples/GdkPixbuf-2.0/TestMemoryLeaks/TestMemoryLeaks.csproj +++ b/src/Samples/GdkPixbuf-2.0/TestMemoryLeaks/TestMemoryLeaks.csproj @@ -2,7 +2,7 @@ Exe - net6.0 + net8.0 enable