Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support class based safe handles #1116

Closed
wants to merge 12 commits into from
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions src/Libs/GLib-2.0/GObject/Public/Type.cs
Original file line number Diff line number Diff line change
@@ -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)
{
377 changes: 377 additions & 0 deletions src/Libs/GObject-2.0/Public/Object2.cs
Original file line number Diff line number Diff line change
@@ -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);
}
/// <summary>
/// Registers a custom subclass with the GObject type system.
/// </summary>
public static class SubclassRegistrar2
{
public static Type Register<TSubclass, TParent>()
where TSubclass : ClassFactory
where TParent : GTypeProvider
{
var newType = RegisterNewGType<TSubclass, TParent>();
InstanceFactory.Register(newType, TSubclass.Create);

return newType;
}

private static Type RegisterNewGType<TSubclass, TParent>()
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<Type, InstanceFactoryForType> 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<TypeInstanceData>((void*) handle).GClass;
var gtype = Unsafe.AsRef<TypeClassData>((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;
}
}

/// <summary>
/// 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.
/// </summary>
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<IntPtr, ToggleRef2> 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<T>(IntPtr handle, bool ownedRef) where T : Object2, InterfaceFactory
{
return handle == IntPtr.Zero
? null
: WrapHandle<T>(handle, ownedRef);
}

public static T WrapHandle<T>(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() { }
}
}



163 changes: 163 additions & 0 deletions src/Libs/GdkPixbuf-2.0/Public/Pixbuf2.cs
Original file line number Diff line number Diff line change
@@ -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<MyPixbuf, Pixbuf2>();
}

public MyPixbuf() : base(Pixbuf2Handle.For<MyPixbuf>(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<TestInterfaceHelper>(resultApplyEmbeddedOrientation, true);
return (Pixbuf2?) InstanceWrapper.WrapNullableHandle(resultApplyEmbeddedOrientation, true);
}
}
}

namespace GdkPixbuf.Internal
{
internal static class TypeRegistration2
{
public static void RegisterTypes()
{
Register<Pixbuf2>(OSPlatform.Linux, OSPlatform.OSX, OSPlatform.Windows);
}

private static void Register<T>(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<T>(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);
}
}
}



2 changes: 1 addition & 1 deletion src/Properties/GirCore.Libraries.props
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8" ?>
<Project>
<PropertyGroup>
<TargetFrameworks>net6.0;net7.0;net8.0</TargetFrameworks>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<TreatWarningsErrors>true</TreatWarningsErrors>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
10 changes: 10 additions & 0 deletions src/Samples/GdkPixbuf-2.0/TestMemoryLeaks/Program.cs
Original file line number Diff line number Diff line change
@@ -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<MyPixbuf>(bla.GetHandle(), false);

var cycles = 10000;
var fileName = "test.bmp";

Original file line number Diff line number Diff line change
@@ -2,7 +2,7 @@

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
</PropertyGroup>