Skip to content

Commit 8eacde5

Browse files
committed
Add support for IFlecsStruct auto-reflection via source generator
Similar to C++ alternative here SanderMertens/flecs#2063 Signed-off-by: Tomas Slusny <slusnucky@gmail.com>
1 parent 9e9c05b commit 8eacde5

12 files changed

Lines changed: 501 additions & 0 deletions

File tree

Flecs.NET.sln

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Flecs.NET.Benchmarks", "src
1616
EndProject
1717
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Flecs.NET.Codegen", "src\Flecs.NET.Codegen\Flecs.NET.Codegen.csproj", "{A76AE670-D7DD-47DE-8F5C-DDF7B31C030A}"
1818
EndProject
19+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Flecs.NET.Sourcegen", "src\Flecs.NET.Sourcegen\Flecs.NET.Sourcegen.csproj", "{8704FC32-087C-4C65-A650-71CF333CA3A3}"
20+
EndProject
1921
Global
2022
GlobalSection(SolutionConfigurationPlatforms) = preSolution
2123
Debug|Any CPU = Debug|Any CPU
@@ -54,5 +56,9 @@ Global
5456
{A76AE670-D7DD-47DE-8F5C-DDF7B31C030A}.Debug|Any CPU.Build.0 = Debug|Any CPU
5557
{A76AE670-D7DD-47DE-8F5C-DDF7B31C030A}.Release|Any CPU.ActiveCfg = Release|Any CPU
5658
{A76AE670-D7DD-47DE-8F5C-DDF7B31C030A}.Release|Any CPU.Build.0 = Release|Any CPU
59+
{8704FC32-087C-4C65-A650-71CF333CA3A3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
60+
{8704FC32-087C-4C65-A650-71CF333CA3A3}.Debug|Any CPU.Build.0 = Debug|Any CPU
61+
{8704FC32-087C-4C65-A650-71CF333CA3A3}.Release|Any CPU.ActiveCfg = Release|Any CPU
62+
{8704FC32-087C-4C65-A650-71CF333CA3A3}.Release|Any CPU.Build.0 = Release|Any CPU
5763
EndGlobalSection
5864
EndGlobal

src/Flecs.NET.Examples/Flecs.NET.Examples.csproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@
1616

1717
<ItemGroup>
1818
<ProjectReference Include="../Flecs.NET/Flecs.NET.csproj"/>
19+
<!-- Source generator that emits compile-time struct reflection registrars for IFlecsStruct types. -->
20+
<ProjectReference Include="../Flecs.NET.Sourcegen/Flecs.NET.Sourcegen.csproj"
21+
OutputItemType="Analyzer"
22+
ReferenceOutputAssembly="false"/>
1923
</ItemGroup>
2024

2125
</Project>
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
using Flecs.NET.Core;
2+
using Flecs.NET.Examples.Reflection.AutoDefineNestedStruct;
3+
4+
// Components
5+
namespace Flecs.NET.Examples.Reflection.AutoDefineNestedStruct
6+
{
7+
internal record struct Point(int X, int Y) : IFlecsStruct;
8+
9+
internal record struct Line(Point Start, Point Stop) : IFlecsStruct;
10+
}
11+
12+
public static class Reflection_AutoDefineNestedStruct
13+
{
14+
public static void Main()
15+
{
16+
using World world = World.Create();
17+
18+
// Register component - Point is reflected transitively as a member type.
19+
world.Component<Line>();
20+
21+
// Create entity with Line component as usual.
22+
Entity e = world.Entity()
23+
.Set(new Line(new Point(1, 2), new Point(3, 4)));
24+
25+
// Convert Line component to flecs expression string.
26+
ref Line reference = ref e.Ensure<Line>();
27+
Console.WriteLine(world.ToExpr(ref reference));
28+
// {Start: {X: 1, Y: 2}, Stop: {X: 3, Y: 4}}
29+
}
30+
}
31+
32+
// Output:
33+
// {Start: {X: 1, Y: 2}, Stop: {X: 3, Y: 4}}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
using Flecs.NET.Core;
2+
using Flecs.NET.Examples.Reflection.AutoDefineStruct;
3+
4+
// Components
5+
namespace Flecs.NET.Examples.Reflection.AutoDefineStruct
6+
{
7+
internal record struct Position(float X, float Y) : IFlecsStruct;
8+
}
9+
10+
public static class Reflection_AutoDefineStruct
11+
{
12+
public static void Main()
13+
{
14+
using World world = World.Create();
15+
16+
// Register component - members are reflected automatically via IFlecsStruct.
17+
world.Component<Position>();
18+
19+
// Create entity with Position as usual.
20+
Entity e = world.Entity()
21+
.Set(new Position(10, 20));
22+
23+
// Convert position component to flecs expression string.
24+
ref Position reference = ref e.Ensure<Position>();
25+
Console.WriteLine(world.ToExpr(ref reference)); // {X: 10, Y: 20}
26+
}
27+
}
28+
29+
// Output:
30+
// {X: 10, Y: 20}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>netstandard2.0</TargetFramework>
5+
<LangVersion>latest</LangVersion>
6+
<Nullable>enable</Nullable>
7+
<RootNamespace>Flecs.NET.Sourcegen</RootNamespace>
8+
<AssemblyName>Flecs.NET.Sourcegen</AssemblyName>
9+
<PackageId>Flecs.NET.Sourcegen</PackageId>
10+
<IsRoslynComponent>true</IsRoslynComponent>
11+
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
12+
<GenerateDocumentationFile>false</GenerateDocumentationFile>
13+
<Description>IFlecsStruct auto-reflection source generator for Flecs.NET.</Description>
14+
15+
<IsPackable>true</IsPackable>
16+
<BuildOutputTargetFolder>analyzers/dotnet/cs</BuildOutputTargetFolder>
17+
<NoPackageAnalysis>true</NoPackageAnalysis>
18+
<DevelopmentDependency>true</DevelopmentDependency>
19+
20+
<NoWarn>$(NoWarn);RS1036;RS2008</NoWarn>
21+
</PropertyGroup>
22+
23+
<ItemGroup>
24+
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.8.0" PrivateAssets="all"/>
25+
</ItemGroup>
26+
27+
</Project>
Lines changed: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,257 @@
1+
using System.Collections.Generic;
2+
using System.Linq;
3+
using System.Text;
4+
using Microsoft.CodeAnalysis;
5+
using Microsoft.CodeAnalysis.CSharp;
6+
using Microsoft.CodeAnalysis.CSharp.Syntax;
7+
8+
namespace Flecs.NET.Sourcegen;
9+
10+
/// <summary>
11+
/// Emits a <c>[ModuleInitializer]</c> registrar per <c>IFlecsStruct</c> struct.
12+
/// </summary>
13+
[Generator]
14+
public sealed class StructReflectionGenerator : IIncrementalGenerator
15+
{
16+
private const string InterfaceFullyQualifiedName = "Flecs.NET.Core.IFlecsStruct";
17+
18+
private static readonly DiagnosticDescriptor OpenGenericTypeRule = new(
19+
id: "FLECSREFL001",
20+
title: "IFlecsStruct on open generic type",
21+
messageFormat: "IFlecsStruct is not supported on open generic type '{0}'",
22+
category: "Flecs.NET.Reflection",
23+
defaultSeverity: DiagnosticSeverity.Warning,
24+
isEnabledByDefault: true);
25+
26+
private static readonly DiagnosticDescriptor FileScopedTypeRule = new(
27+
id: "FLECSREFL002",
28+
title: "IFlecsStruct on file-scoped type",
29+
messageFormat: "IFlecsStruct is not supported on file-scoped type '{0}'",
30+
category: "Flecs.NET.Reflection",
31+
defaultSeverity: DiagnosticSeverity.Warning,
32+
isEnabledByDefault: true);
33+
34+
public void Initialize(IncrementalGeneratorInitializationContext context)
35+
{
36+
IncrementalValuesProvider<Output> outputs = context.CompilationProvider.SelectMany(
37+
(compilation, _) => CollectOutputs(compilation));
38+
39+
context.RegisterSourceOutput(outputs, Produce);
40+
}
41+
42+
private static List<Output> CollectOutputs(Compilation compilation)
43+
{
44+
var results = new List<Output>();
45+
46+
INamedTypeSymbol? iface = compilation.GetTypeByMetadataName(InterfaceFullyQualifiedName);
47+
if (iface is null)
48+
return results;
49+
50+
var seen = new HashSet<INamedTypeSymbol>(SymbolEqualityComparer.Default);
51+
52+
foreach (SyntaxTree tree in compilation.SyntaxTrees)
53+
{
54+
SemanticModel semantic = compilation.GetSemanticModel(tree);
55+
TypeDeclarationSyntax[] typeDecls = tree.GetRoot()
56+
.DescendantNodes()
57+
.OfType<TypeDeclarationSyntax>()
58+
.ToArray();
59+
60+
foreach (TypeDeclarationSyntax decl in typeDecls)
61+
{
62+
if (semantic.GetDeclaredSymbol(decl) is not { } symbol)
63+
continue;
64+
65+
if (symbol.TypeKind != TypeKind.Struct)
66+
continue;
67+
68+
if (!ImplementsInterface(symbol, iface))
69+
continue;
70+
71+
if (!seen.Add(symbol))
72+
continue;
73+
74+
if (decl.Modifiers.Any(SyntaxKind.FileKeyword))
75+
{
76+
results.Add(new Output(null, Diagnostic.Create(
77+
FileScopedTypeRule,
78+
symbol.Locations.Length > 0 ? symbol.Locations[0] : null,
79+
symbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat))));
80+
continue;
81+
}
82+
83+
if (symbol.IsUnboundGenericType || symbol.TypeParameters.Length > 0)
84+
{
85+
results.Add(new Output(null, Diagnostic.Create(
86+
OpenGenericTypeRule,
87+
symbol.Locations.Length > 0 ? symbol.Locations[0] : null,
88+
symbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat))));
89+
continue;
90+
}
91+
92+
results.Add(new Output(BuildModel(symbol), null));
93+
}
94+
}
95+
96+
return results;
97+
}
98+
99+
private static bool ImplementsInterface(INamedTypeSymbol type, INamedTypeSymbol iface)
100+
=> type.AllInterfaces.Any(i => SymbolEqualityComparer.Default.Equals(i, iface));
101+
102+
private static StructModel BuildModel(INamedTypeSymbol structSymbol)
103+
{
104+
MemberInfo[] members = CollectMembers(structSymbol);
105+
106+
string fullyQualifiedType = structSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
107+
string sanitized = SanitizeIdentifier(fullyQualifiedType);
108+
109+
var builder = new StringBuilder();
110+
foreach (MemberInfo member in members)
111+
{
112+
if (builder.Length > 0)
113+
builder.Append('\n');
114+
115+
builder.Append(" component.Member<")
116+
.Append(member.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat))
117+
.Append(">(\"")
118+
.Append(EscapeString(member.Name))
119+
.Append("\");");
120+
}
121+
122+
return new StructModel(
123+
fullyQualifiedType,
124+
$"StructReflection.{sanitized}.g.cs",
125+
$"__FlecsStructReflection_{sanitized}",
126+
builder.ToString());
127+
}
128+
129+
private static MemberInfo[] CollectMembers(INamedTypeSymbol structSymbol)
130+
{
131+
var fields = new List<MemberInfo>();
132+
var properties = new List<MemberInfo>();
133+
134+
foreach (ISymbol member in structSymbol.GetMembers())
135+
{
136+
if (member.DeclaredAccessibility != Accessibility.Public)
137+
continue;
138+
139+
switch (member)
140+
{
141+
case IFieldSymbol field when !field.IsStatic && !field.IsConst:
142+
fields.Add(new MemberInfo(field.Name, field.Type));
143+
break;
144+
case IPropertySymbol property when !property.IsStatic && property.Parameters.Length == 0:
145+
properties.Add(new MemberInfo(property.Name, property.Type));
146+
break;
147+
}
148+
}
149+
150+
return fields.Count > 0 ? fields.ToArray() : properties.ToArray();
151+
}
152+
153+
private static void Produce(SourceProductionContext spc, Output output)
154+
{
155+
if (output.Diagnostic is { } diagnostic)
156+
{
157+
spc.ReportDiagnostic(diagnostic);
158+
return;
159+
}
160+
161+
StructModel model = output.Model!.Value;
162+
163+
string source = $$"""
164+
// <auto-generated/>
165+
#nullable enable
166+
167+
using System.Runtime.CompilerServices;
168+
169+
namespace Flecs.NET.Generated.StructReflection;
170+
171+
internal static class {{model.GeneratedClassName}}
172+
{
173+
[ModuleInitializer]
174+
internal static void __Register()
175+
{
176+
global::Flecs.NET.Core.StructReflection.Register<{{model.FullyQualifiedTypeName}}>(
177+
static component =>
178+
{
179+
{{model.MemberStatement}}
180+
});
181+
}
182+
}
183+
""";
184+
185+
spc.AddSource(model.GeneratedFileName, source);
186+
}
187+
188+
private static string SanitizeIdentifier(string fullyQualifiedName)
189+
{
190+
var builder = new StringBuilder(fullyQualifiedName.Length);
191+
foreach (char c in fullyQualifiedName)
192+
{
193+
if (c == '_' || char.IsLetterOrDigit(c))
194+
builder.Append(c);
195+
else
196+
builder.Append('_');
197+
}
198+
199+
return builder.ToString();
200+
}
201+
202+
private static string EscapeString(string value)
203+
{
204+
var builder = new StringBuilder(value.Length);
205+
foreach (char c in value)
206+
{
207+
switch (c)
208+
{
209+
case '\\': builder.Append("\\\\"); break;
210+
case '"': builder.Append("\\\""); break;
211+
default: builder.Append(c); break;
212+
}
213+
}
214+
215+
return builder.ToString();
216+
}
217+
218+
private readonly struct Output
219+
{
220+
public StructModel? Model { get; }
221+
public Diagnostic? Diagnostic { get; }
222+
223+
public Output(StructModel? model, Diagnostic? diagnostic)
224+
{
225+
Model = model;
226+
Diagnostic = diagnostic;
227+
}
228+
}
229+
230+
private readonly struct StructModel
231+
{
232+
public string FullyQualifiedTypeName { get; }
233+
public string GeneratedFileName { get; }
234+
public string GeneratedClassName { get; }
235+
public string MemberStatement { get; }
236+
237+
public StructModel(string fullyQualifiedTypeName, string generatedFileName, string generatedClassName, string memberStatement)
238+
{
239+
FullyQualifiedTypeName = fullyQualifiedTypeName;
240+
GeneratedFileName = generatedFileName;
241+
GeneratedClassName = generatedClassName;
242+
MemberStatement = memberStatement;
243+
}
244+
}
245+
246+
private readonly struct MemberInfo
247+
{
248+
public string Name { get; }
249+
public ITypeSymbol Type { get; }
250+
251+
public MemberInfo(string name, ITypeSymbol type)
252+
{
253+
Name = name;
254+
Type = type;
255+
}
256+
}
257+
}

0 commit comments

Comments
 (0)