Skip to content

Commit

Permalink
Add GObject-2.0.Integration.csproj
Browse files Browse the repository at this point in the history
  • Loading branch information
badcel committed Feb 5, 2025
1 parent a58d183 commit c2ba31b
Show file tree
Hide file tree
Showing 33 changed files with 447 additions and 510 deletions.
21 changes: 7 additions & 14 deletions docs/docs/faq.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,26 +54,19 @@ Gtk.Button.LabelPropertyDefinition.Notify(
```

## 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:
Creating a subclass for a GObject based class requires creating a partial class with no parent class defined. An attribute is added which defines the parent class. Using the attribute results in a source generator generating the needed code to integrate a custom class with the GObject type system. The source generator is part of the [GObject-2.0.Integration nuget package](https://www.nuget.org/packages/GirCore.GObject-2.0.Integration/) which must be referenced from the project.

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.
Be aware that the GObject type system requires a class to always have a parameterless constructor. This is the reason why such a constructor is generated. Please ensure that your class uses sensible defaults or that there are ways which allow to initialize your instance after its creation. This gets relevant if instances of your classes get created by some GObject library as those would use the parameterless constructor.

```csharp
public class Data : GObject.Object, GTypeProvider, InstanceFactory
[Subclass<GObject.Object>]
public partial class Data
{
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 string? MyString { get; set; }


public Data() : base(ObjectHandle.For<Data>(true, []))
public Data(string myString) : this()
{
MyString = myString;
}

private Data(IntPtr ptr, bool ownsHandle) : base(new ObjectHandle(ptr, ownsHandle)) { }
}
```
4 changes: 2 additions & 2 deletions docs/docs/tutorial/gtk/src/HelloWorld/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@
{
// Create a new instance of the main application window.
var window = new HelloWorld.HelloWorld();

// Set the "Application" property of the window to the current application instance.
// This links the window to the application, allowing them to work together.
window.Application = (Gtk.Application) sender;

// Show the window on the screen.
// This makes the window visible to the user.
window.Show();
Expand Down
1 change: 0 additions & 1 deletion src/Extensions/Directory.Build.props
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
<Project>
<Import Project="../Properties/GirCore.Libraries.props" />
<Import Project="../Properties/GirCore.Publishing.props" />
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<PackageId>GirCore.GObject-2.0.Integration</PackageId>
<TargetFramework>net8.0</TargetFramework>
<RootNamespace>GObject.Integration</RootNamespace>
<Description>Source Generator to make it easy to integrate C# with the GObject type system.</Description>

<AnalysisMode>Recommended</AnalysisMode>
<Nullable>enable</Nullable>
<TreatWarningsErrors>true</TreatWarningsErrors>

<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<IncludeBuildOutput>false</IncludeBuildOutput>
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
</PropertyGroup>

<ItemGroup>
<!-- Reference 4.8 as it is the first version which supports net8.0 -->
<PackageReference Include="Microsoft.CodeAnalysis.Common" Version="4.8.0"/>
</ItemGroup>

<ItemGroup>
<None Include="$(OutputPath)\$(AssemblyName).dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"$schema": "http://json.schemastore.org/launchsettings.json",
"profiles": {
"GObject-2.0.Integration": {
"commandName": "DebugRoslynComponent",
"targetProject": "../../Samples/Gtk-4.0/GridView/GridView.csproj"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using Microsoft.CodeAnalysis;

namespace GObject.Integration.SourceGenerator;

[Generator]
public class Generator : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
context.EnableSubclassSupport();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using Microsoft.CodeAnalysis;

namespace GObject.Integration.SourceGenerator;

internal static class Subclass
{
public static void EnableSubclassSupport(this IncrementalGeneratorInitializationContext context)
{
context.RegisterImplementationSourceOutput(
source: context.GetSubclassValuesProvider(),
action: SubclassCode.Generate
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
using System.Globalization;
using System.Text;
using Microsoft.CodeAnalysis;

namespace GObject.Integration.SourceGenerator;

internal static class SubclassCode
{
public static void Generate(SourceProductionContext context, SubclassData subclassData)
{
context.AddSource(
hintName: $"{subclassData.FileName}.Subclass.g.cs",
source: ToCode(subclassData)
);
}
private static string ToCode(SubclassData subclassData)
{
return $$"""
namespace {{subclassData.Namespace}};
{{RenderClassHierarchy(subclassData)}}
""";
}

private static string RenderClassHierarchy(SubclassData subclassData)
{
var sb = new StringBuilder();
foreach (var typeData in subclassData.UpperNestedClasses)
sb.AppendLine(CultureInfo.InvariantCulture, $"{typeData.Accessibility} partial {typeData.Kind} {typeData.NameGenericArguments} {{");

sb.AppendLine(RenderClassContent(subclassData));

foreach (var _ in subclassData.UpperNestedClasses)
sb.AppendLine("}");

return sb.ToString();
}

private static string RenderClassContent(SubclassData subclassData)
{
return $$"""
{{subclassData.Accessibility}} partial class {{subclassData.NameGenericArguments}}({{subclassData.ParentHandle}} handle) : {{subclassData.Parent}}(handle), GObject.GTypeProvider, GObject.InstanceFactory
{
private static readonly GObject.Type GType = GObject.Internal.SubclassRegistrar.Register<{{subclassData.NameGenericArguments}}, {{subclassData.Parent}}>();
public static new GObject.Type GetGType() => GType;
static object GObject.InstanceFactory.Create(System.IntPtr handle, bool ownsHandle)
{
return new {{subclassData.NameGenericArguments}}(new {{subclassData.ParentHandle}}(handle, ownsHandle));
}
public {{subclassData.Name}}(params GObject.ConstructArgument[] constructArguments) : this({{subclassData.ParentHandle}}.For<{{subclassData.NameGenericArguments}}>(constructArguments)) { }
}
""";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using System.Collections.Generic;

namespace GObject.Integration.SourceGenerator;

internal sealed record SubclassData(
string Name,
string NameGenericArguments,
string Parent,
string ParentHandle,
string Namespace,
string Accessibility,
string FileName,
Stack<TypeData> UpperNestedClasses
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using Microsoft.CodeAnalysis;

namespace GObject.Integration.SourceGenerator;

internal static class SubclassValuesProvider
{
private const string SubclassAtributeName = "GObject.SubclassAttribute`1";

public static IncrementalValuesProvider<SubclassData> GetSubclassValuesProvider(this IncrementalGeneratorInitializationContext context)
{
return context.SyntaxProvider
.ForAttributeWithMetadataName(
fullyQualifiedMetadataName: SubclassAtributeName,
predicate: static (_, _) => true,
transform: GetSubclassData)
.Where(data => data is not null)!;
}

private static SubclassData? GetSubclassData(GeneratorAttributeSyntaxContext context, CancellationToken cancellationToken)
{
if (context.TargetSymbol is not INamedTypeSymbol subclass)
return null;

var subclassAttribute = context.Attributes.First().AttributeClass;
if (subclassAttribute is null)
return null;

var parentType = subclassAttribute.TypeArguments.First();

var parentHandle = GetParentHandle(parentType);
if (parentHandle is null)
return null;

var accessibility = GetAccessibility(context.TargetSymbol);
if (accessibility is null)
return null;

var upperNestedClasses = GetUpperNestedClasses(context.TargetSymbol);
if (upperNestedClasses is null)
return null;

return new SubclassData(
Name: subclass.Name,
NameGenericArguments: subclass.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat),
Parent: parentType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat),
ParentHandle: parentHandle,
Namespace: context.TargetSymbol.ContainingNamespace.ToDisplayString(),
Accessibility: accessibility,
FileName: GetFileName(subclass),
UpperNestedClasses: upperNestedClasses
);
}

private static Stack<TypeData>? GetUpperNestedClasses(ISymbol symbol)
{
var stack = new Stack<TypeData>();
var containingType = symbol.ContainingType;

while (containingType is not null)
{
var accessibility = GetAccessibility(containingType);
if (accessibility is null)
return null;

var kind = GetKind(containingType);
if (kind is null)
return null;

var typeData = new TypeData(
NameGenericArguments: containingType.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat),
Accessibility: accessibility,
Kind: kind
);
stack.Push(typeData);
containingType = containingType.ContainingType;
}

return stack;
}

private static string? GetKind(INamedTypeSymbol symbol)
{
return (symbol.TypeKind, symbol.IsRecord) switch
{
(TypeKind.Struct, _) => "struct",
(TypeKind.Class, true) => "record",
(TypeKind.Class, _) => "class",
_ => null
};
}

private static string GetFileName(INamedTypeSymbol typeSymbol)
{
var prefix = GetFileNamePrefix(typeSymbol);
var suffix = typeSymbol.Arity == 0 ? string.Empty : $"_{typeSymbol.Arity}";

return prefix + typeSymbol.Name + suffix;
}

private static string GetFileNamePrefix(INamedTypeSymbol typeSymbol)
{
var sb = new StringBuilder();

var containingType = typeSymbol.ContainingType;
while (containingType is not null)
{
sb.Insert(0, $"{containingType.Name}.");
containingType = containingType.ContainingType;
}

return sb.ToString();
}

private static string? GetAccessibility(ISymbol type)
{
var accessibility = type.DeclaredAccessibility switch
{
Accessibility.Public => "public",
Accessibility.Internal => "internal",
Accessibility.Private => "private",
Accessibility.NotApplicable => "internal",
_ => null
};
return accessibility;
}

private static string? GetParentHandle(ITypeSymbol type)
{
ITypeSymbol? currentType = type;
INamedTypeSymbol? parentHandleAttribute;
do
{
parentHandleAttribute = GetHandleAttribute(currentType);

if (parentHandleAttribute is null)
currentType = GetTypeInSubclassAttribute(currentType);

} while (parentHandleAttribute is null && currentType is not null);

return parentHandleAttribute?.TypeArguments.First().ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
}

private static INamedTypeSymbol? GetHandleAttribute(ITypeSymbol type)
{
var attributeData = type
.GetAttributes()
.FirstOrDefault(x => x.AttributeClass?.OriginalDefinition.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) == "global::GObject.HandleAttribute<T>");

return attributeData?.AttributeClass;
}

private static ITypeSymbol? GetTypeInSubclassAttribute(ITypeSymbol type)
{
var subclassAttribute = type
.GetAttributes()
.FirstOrDefault(x => x.AttributeClass?.ConstructedFrom.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) == "global::GObject.SubclassAttribute<T>");

if (subclassAttribute is null)
return null;

return subclassAttribute.AttributeClass?.TypeArguments.First();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace GObject.Integration.SourceGenerator;

internal sealed record TypeData(
string NameGenericArguments,
string? Accessibility,
string Kind
);
19 changes: 0 additions & 19 deletions src/Extensions/Integration/Extensions/AssemblyExtension.cs

This file was deleted.

Loading

0 comments on commit c2ba31b

Please sign in to comment.