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 #1118

Merged
merged 1 commit into from
Jan 21, 2025
Merged
Show file tree
Hide file tree
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
3 changes: 1 addition & 2 deletions .github/actions/create/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,8 @@ runs:
uses: actions/setup-dotnet@v4
with:
dotnet-version: |
6.0.x
7.0.x
8.0.x
9.0.x
- name: Compile native library
run: dotnet fsi GenerateGirTestLib.fsx
Expand Down
25 changes: 25 additions & 0 deletions docs/docs/faq.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,4 +51,29 @@ Gtk.Button.LabelPropertyDefinition.Notify(
sender: myButton,
signalHandler: OnButtonLabelChanged
);
```

## How to create subclasses of a GObject based class?
Creating a subclass for a GObject based class is a bit more complex than for a regular C# class. The reason for this is that the GObject type system is working in parallel to the C# type system. For a subclass to be known to the GObject type system it must be registered first. To be able to create an instance of a GObject based class every type must not only be registered but also implement some interfaces to allow seamless integration into the GObject type system:

1. Implement GTypeProvider interface. This allows to get the GObject Type of the class during runtime.
2. Implement InstanceFactory interface. This allows to create an instance of the class during runtime for a given pointer.

```csharp
public class Data : GObject.Object, GTypeProvider, InstanceFactory
{
private static readonly Type GType = SubclassRegistrar.Register<Data, GObject.Object>();
public static new Type GetGType() => GType;
static object InstanceFactory.Create(IntPtr handle, bool ownsHandle)
{
return new Data(handle, ownsHandle);
}


public Data() : base(ObjectHandle.For<Data>(true, []))
{
}

private Data(IntPtr ptr, bool ownsHandle) : base(new ObjectHandle(ptr, ownsHandle)) { }
}
```
1 change: 1 addition & 0 deletions src/Generation/Generator/Classes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ public static void Generate(IEnumerable<GirModel.Class> classes, string path)
//Standard generators
new Generator.Internal.ClassMethods(publisher),
new Generator.Internal.ClassStruct(publisher),
new Generator.Internal.ClassHandle(publisher),
new Generator.Public.ClassConstructors(publisher),
new Generator.Public.ClassMethods(publisher),
new Generator.Public.ClassFunctions(publisher),
Expand Down
32 changes: 32 additions & 0 deletions src/Generation/Generator/Generator/Internal/ClassHandle.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
using Generator.Model;

namespace Generator.Generator.Internal;

internal class ClassHandle : Generator<GirModel.Class>
{
private readonly Publisher _publisher;

public ClassHandle(Publisher publisher)
{
_publisher = publisher;
}

public void Generate(GirModel.Class obj)
{
if (obj.Fundamental)
return;

if (obj.Parent is null)
return; //Do not generate a handle for GObject.Object itself

var source = Renderer.Internal.ClassHandle.Render(obj);
var codeUnit = new CodeUnit(
Project: Namespace.GetCanonicalName(obj.Namespace),
Name: Class.GetInternalHandleName(obj),
Source: source,
IsInternal: true
);

_publisher.Publish(codeUnit);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ public void Generate(GirModel.Class obj)
if (obj.Fundamental)
return;

if (obj.Parent is null)
return; //Do not generate Framework for GObject.Object itself

var source = Renderer.Public.ClassFramework.Render(obj);
var codeUnit = new CodeUnit(
Project: Namespace.GetCanonicalName(obj.Namespace),
Expand Down

This file was deleted.

1 change: 0 additions & 1 deletion src/Generation/Generator/Interfaces.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ public static void Generate(IEnumerable<GirModel.Interface> interfaces, string p
var generators = new List<Generator<GirModel.Interface>>()
{
new Generator.Internal.InterfaceMethods(publisher),
new Generator.Public.InterfaceFramework(publisher),
new Generator.Public.InterfaceMethods(publisher),
new Generator.Public.InterfaceProperties(publisher),

Expand Down
24 changes: 24 additions & 0 deletions src/Generation/Generator/Model/Class.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,18 @@ public static string GetFullyQualifiedInternalStructName(GirModel.Class @class)
public static string GetInternalStructName(GirModel.Class @class)
=> @class.Name + "Data";

public static string GetInternalHandleName(GirModel.Class @class)
=> @class.Name + "Handle";

public static string GetFullyQualifiedInternalHandleName(GirModel.Class @class)
=> Namespace.GetInternalName(@class.Namespace) + "." + GetInternalHandleName(@class);

public static string GetFullyQualifiedPublicName(GirModel.Class @class)
=> Namespace.GetPublicName(@class.Namespace) + "." + @class.Name;

public static string GetFullyQualifiedInternalName(GirModel.Class @class)
=> Namespace.GetInternalName(@class.Namespace) + "." + @class.Name;

public static bool HidesConstructor(GirModel.Class? cls, GirModel.Constructor constructor)
{
if (cls is null)
Expand Down Expand Up @@ -84,4 +96,16 @@ private static bool ParameterMatch(GirModel.Parameter[] p1, GirModel.Parameter[]

return true;
}

public static bool IsInitiallyUnowned(GirModel.Class cls) => IsNamedInitiallyUnowned(cls.Name) || InheritsInitiallyUnowned(cls);

private static bool InheritsInitiallyUnowned(GirModel.Class @class)
{
if (@class.Parent is null)
return false;

return IsNamedInitiallyUnowned(@class.Parent.Name) || InheritsInitiallyUnowned(@class.Parent);
}

private static bool IsNamedInitiallyUnowned(string name) => name == "InitiallyUnowned";
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ namespace {Namespace.GetInternalName(callback.Namespace)};
// AUTOGENERATED FILE - DO NOT MODIFY
// <summary>
/// <summary>
/// Call Handler for {callback.Name}. A call annotation indicates the closure should
/// be valid for the duration of the call. This handler does not implement any special
/// memory management.
Expand Down
101 changes: 101 additions & 0 deletions src/Generation/Generator/Renderer/Internal/Class/ClassHandle.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
using System;
using Generator.Model;

namespace Generator.Renderer.Internal;

internal static class ClassHandle
{
public static string Render(GirModel.Class cls)
{
return cls.Final
? RenderFinalClassHandle(cls)
: RenderStandardClassHandle(cls);
}

private static string RenderFinalClassHandle(GirModel.Class cls)
{
var handleName = Class.GetInternalHandleName(cls);

return $$"""
using System;
using System.Linq;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;

#nullable enable

namespace {{Namespace.GetInternalName(cls.Namespace)}};

public partial class {{handleName}} : {{RenderParent(cls)}}
{
public {{handleName}}(IntPtr handle, bool ownsHandle) : base(handle, ownsHandle){ }

public static {{handleName}} Create(bool owned, GObject.ConstructArgument[] constructArguments)
{
// 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: {{Class.GetFullyQualifiedInternalName(cls)}}.GetGType(),
nProperties: (uint) constructArguments.Length,
names: constructArguments.Select(x => x.Name).ToArray(),
values: GObject.Internal.ValueArray2OwnedHandle.Create(constructArguments.Select(x => x.Value).ToArray())
);

return new {{handleName}}(ptr, owned);
}
}
""";
}

private static string RenderStandardClassHandle(GirModel.Class cls)
{
var handleName = Class.GetInternalHandleName(cls);

return $$"""
using System;
using System.Linq;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;

#nullable enable

namespace {{Namespace.GetInternalName(cls.Namespace)}};

public partial class {{handleName}} : {{RenderParent(cls)}}
{
public {{handleName}}(IntPtr handle, bool ownsHandle) : base(handle, ownsHandle){ }

public static new {{handleName}} For<T>(bool owned, GObject.ConstructArgument[] constructArguments) where T : {{Class.GetFullyQualifiedPublicName(cls)}}, GObject.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: GObject.Internal.ValueArray2OwnedHandle.Create(constructArguments.Select(x => x.Value).ToArray())
);

return new {{handleName}}(ptr, owned);
}
}
""";
}

private static string RenderParent(GirModel.Class cls)
{
return cls.Parent is null
? throw new Exception("Class is missing parent")
: Class.GetFullyQualifiedInternalHandleName(cls.Parent);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ public static string Render(GirModel.Namespace ns)
using System.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices;
using GObject;
using GObject.Internal;

namespace {Namespace.GetInternalName(ns)};

Expand All @@ -29,6 +31,10 @@ internal static void RegisterTypes()
.Select(RenderRegistration)
.Join(Environment.NewLine)}

{ns.Interfaces
.Select(RenderInterfaceRegistration)
.Join(Environment.NewLine)}

{ns.Records
.Where(record => record.TypeFunction is not null)
.Select(RenderRegistration)
Expand All @@ -40,24 +46,29 @@ internal static void RegisterTypes()
.Join(Environment.NewLine)}
}}

private static void Register<T>(Func<nuint> getType, params OSPlatform[] supportedPlatforms) where T : class
private static void Register<T>(params OSPlatform[] supportedPlatforms) where T : InstanceFactory, GTypeProvider
{{
try
{{
try
{{
if(supportedPlatforms.Any(RuntimeInformation.IsOSPlatform))
GObject.Internal.TypeDictionary.Add(typeof(T), new GObject.Type(getType()));
}}
catch(System.Exception e)
{{
Debug.WriteLine($""Could not register type '{{nameof(T)}}': {{e.Message}}"");
}}
}}
GObject.Internal.DynamicInstanceFactory.Register(T.GetGType(), T.Create);
}}
catch(System.Exception e)
{{
Debug.WriteLine($""Could not register type: {{e.Message}}"");
}}
}}
}}";
}

private static string RenderRegistration(GirModel.ComplexType type)
{
return @$"Register<{ComplexType.GetFullyQualified(type)}>(Internal.{type.Name}.{Function.GetGType}{RenderPlatforms(type as GirModel.PlatformDependent)});";
return $"Register<{ComplexType.GetFullyQualified(type)}>({RenderPlatforms(type as GirModel.PlatformDependent)});";
}

private static string RenderInterfaceRegistration(GirModel.Interface type)
{
return $"Register<{Interface.GetFullyQualifiedImplementationName(type)}>({RenderPlatforms(type as GirModel.PlatformDependent)});";
}

private static string RenderPlatforms(GirModel.PlatformDependent? platformDependent)
Expand All @@ -76,6 +87,6 @@ private static string RenderPlatforms(GirModel.PlatformDependent? platformDepend
if (platformDependent.SupportsWindows)
statements.Add("OSPlatform.Windows");

return ", " + string.Join(", ", statements);
return string.Join(", ", statements);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,14 @@ private static void Default(ParameterToManagedData parameterData)
var parameterName = Model.Parameter.GetName(parameterData.Parameter);
var callName = Model.Parameter.GetConvertedName(parameterData.Parameter);

var type = Model.ComplexType.GetFullyQualified(cls);

var wrapHandle = parameterData.Parameter.Nullable
? "GObject.Internal.ObjectWrapper.WrapNullableHandle"
: "GObject.Internal.ObjectWrapper.WrapHandle";
? $"({type}?) GObject.Internal.InstanceWrapper.WrapNullableHandle"
: $"({type}) GObject.Internal.InstanceWrapper.WrapHandle";

parameterData.SetSignatureName(() => parameterName);
parameterData.SetExpression(() => $"var {callName} = {wrapHandle}<{Model.ComplexType.GetFullyQualified(cls)}>({parameterName}, {Model.Transfer.IsOwnedRef(parameterData.Parameter.Transfer).ToString().ToLower()});");
parameterData.SetExpression(() => $"var {callName} = {wrapHandle}<{type}>({parameterName}, {Model.Transfer.IsOwnedRef(parameterData.Parameter.Transfer).ToString().ToLower()});");
parameterData.SetCallName(() => callName);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public void Initialize(ParameterToManagedData parameterData, IEnumerable<Paramet
var callName = Model.Parameter.GetConvertedName(parameterData.Parameter);

parameterData.SetSignatureName(() => signatureName);
parameterData.SetExpression(() => $"var {callName} = GObject.Internal.ObjectWrapper.WrapInterfaceHandle<{Model.Interface.GetFullyQualifiedImplementationName(iface)}>({signatureName}, {Model.Transfer.IsOwnedRef(parameterData.Parameter.Transfer).ToString().ToLower()});");
parameterData.SetExpression(() => $"var {callName} = ({Model.Type.GetPublicNameFullyQuallified(iface)}) GObject.Internal.InstanceWrapper.WrapHandle<{Model.Interface.GetFullyQualifiedImplementationName(iface)}>({signatureName}, {Model.Transfer.IsOwnedRef(parameterData.Parameter.Transfer).ToString().ToLower()});");
parameterData.SetCallName(() => callName);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ public string GetString(GirModel.ReturnType returnType, string fromVariableName)
throw new NotImplementedException($"{returnType.AnyType}: class return type which is no pointer can not be converted to native");

return returnType.Nullable
? fromVariableName + "?.Handle ?? IntPtr.Zero"
: fromVariableName + ".Handle";
? fromVariableName + "?.Handle.DangerousGetHandle() ?? IntPtr.Zero"
: fromVariableName + ".Handle.DangerousGetHandle()";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ public bool Supports(GirModel.AnyType type)
public string GetString(GirModel.ReturnType returnType, string fromVariableName)
{
return returnType.Nullable
? fromVariableName + "?.Handle ?? IntPtr.Zero"
: fromVariableName + ".Handle";
? fromVariableName + "?.Handle.DangerousGetHandle() ?? IntPtr.Zero"
: fromVariableName + ".Handle.DangerousGetHandle()";
}
}
Loading
Loading