Source Generator Development
This module covers building Roslyn source generators for compile-time GPU kernel code generation.
Why Source Generators?
Source generators provide:
- Compile-time code generation - No runtime reflection
- Native AOT compatibility - Essential for DotCompute
- IDE integration - IntelliSense for generated code
- Build-time validation - Catch errors early
Source Generator Basics
Generator Structure
using Microsoft.CodeAnalysis;
[Generator]
public class MyGenerator : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
// 1. Find relevant syntax nodes
var candidates = context.SyntaxProvider
.CreateSyntaxProvider(
predicate: IsCandidateSyntax,
transform: GetSemanticTarget)
.Where(m => m is not null);
// 2. Combine with compilation
var compilation = context.CompilationProvider.Combine(candidates.Collect());
// 3. Generate source
context.RegisterSourceOutput(compilation, GenerateSource);
}
private static bool IsCandidateSyntax(SyntaxNode node, CancellationToken ct)
{
// Quick syntactic check
return node is MethodDeclarationSyntax method &&
method.AttributeLists.Count > 0;
}
private static MethodInfo? GetSemanticTarget(
GeneratorSyntaxContext context,
CancellationToken ct)
{
// Semantic analysis
var method = (MethodDeclarationSyntax)context.Node;
var symbol = context.SemanticModel.GetDeclaredSymbol(method, ct);
if (symbol?.GetAttributes().Any(a =>
a.AttributeClass?.Name == "KernelAttribute") == true)
{
return ExtractMethodInfo(symbol);
}
return null;
}
private static void GenerateSource(
SourceProductionContext context,
(Compilation, ImmutableArray<MethodInfo?>) input)
{
var (compilation, methods) = input;
foreach (var method in methods.Where(m => m != null))
{
var source = GenerateKernelWrapper(method!);
context.AddSource($"{method!.Name}_Generated.g.cs", source);
}
}
}
Project Configuration
<!-- MyGenerator.csproj -->
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.8.0" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" PrivateAssets="all" />
</ItemGroup>
</Project>
Kernel Source Generator
Discovering Kernels
public class KernelInfo
{
public string Name { get; set; } = "";
public string Namespace { get; set; } = "";
public string ContainingClass { get; set; } = "";
public List<ParameterInfo> Parameters { get; set; } = new();
public KernelOptions Options { get; set; } = new();
}
public class ParameterInfo
{
public string Name { get; set; } = "";
public string Type { get; set; } = "";
public bool IsReadOnly { get; set; }
public bool IsOutput { get; set; }
}
private static KernelInfo? ExtractKernelInfo(
GeneratorSyntaxContext context,
CancellationToken ct)
{
var method = (MethodDeclarationSyntax)context.Node;
var symbol = context.SemanticModel.GetDeclaredSymbol(method, ct);
if (symbol == null) return null;
// Check for [Kernel] attribute
var kernelAttr = symbol.GetAttributes()
.FirstOrDefault(a => a.AttributeClass?.Name == "KernelAttribute");
if (kernelAttr == null) return null;
// Extract kernel info
var info = new KernelInfo
{
Name = symbol.Name,
Namespace = symbol.ContainingNamespace.ToDisplayString(),
ContainingClass = symbol.ContainingType.Name,
Options = ExtractOptions(kernelAttr)
};
// Extract parameters
foreach (var param in symbol.Parameters)
{
info.Parameters.Add(new ParameterInfo
{
Name = param.Name,
Type = param.Type.ToDisplayString(),
IsReadOnly = param.Type.Name.StartsWith("ReadOnlySpan"),
IsOutput = param.Type.Name == "Span"
});
}
return info;
}
Generating Wrapper Code
private static string GenerateKernelWrapper(KernelInfo kernel)
{
var sb = new StringBuilder();
sb.AppendLine("// <auto-generated />");
sb.AppendLine("#nullable enable");
sb.AppendLine();
sb.AppendLine($"namespace {kernel.Namespace};");
sb.AppendLine();
sb.AppendLine($"partial class {kernel.ContainingClass}");
sb.AppendLine("{");
// Generate kernel definition
sb.AppendLine($" public static readonly KernelDefinition {kernel.Name}Definition = new()");
sb.AppendLine(" {");
sb.AppendLine($" Name = \"{kernel.Name}\",");
sb.AppendLine($" Parameters = new[]");
sb.AppendLine(" {");
foreach (var param in kernel.Parameters)
{
var direction = param.IsReadOnly ? "Input" : (param.IsOutput ? "Output" : "InOut");
sb.AppendLine($" new ParameterDefinition(\"{param.Name}\", typeof({param.Type}), ParameterDirection.{direction}),");
}
sb.AppendLine(" },");
sb.AppendLine($" Options = new KernelOptions {{ /* ... */ }}");
sb.AppendLine(" };");
// Generate execution helper
sb.AppendLine();
sb.AppendLine($" public static async Task Execute{kernel.Name}Async(");
sb.AppendLine(" IComputeOrchestrator orchestrator,");
sb.AppendLine(" KernelConfig config,");
for (int i = 0; i < kernel.Parameters.Count; i++)
{
var param = kernel.Parameters[i];
var comma = i < kernel.Parameters.Count - 1 ? "," : ")";
sb.AppendLine($" IBuffer<{GetElementType(param.Type)}> {param.Name}{comma}");
}
sb.AppendLine(" {");
sb.AppendLine($" await orchestrator.ExecuteKernelAsync({kernel.Name}, config,");
var bufferArgs = string.Join(", ", kernel.Parameters.Select(p => p.Name));
sb.AppendLine($" {bufferArgs});");
sb.AppendLine(" }");
sb.AppendLine("}");
return sb.ToString();
}
Ring Kernel Generator
Discovering Ring Kernels
public class RingKernelInfo
{
public string Name { get; set; } = "";
public string KernelId { get; set; } = "";
public string InputMessageType { get; set; } = "";
public string OutputMessageType { get; set; } = "";
public int Capacity { get; set; } = 1024;
public RingProcessingMode ProcessingMode { get; set; }
}
private static RingKernelInfo? ExtractRingKernelInfo(
IMethodSymbol method,
AttributeData attr)
{
var info = new RingKernelInfo
{
Name = method.Name
};
// Extract attribute arguments
foreach (var arg in attr.NamedArguments)
{
switch (arg.Key)
{
case "KernelId":
info.KernelId = arg.Value.Value?.ToString() ?? method.Name;
break;
case "InputMessageType":
info.InputMessageType = ((INamedTypeSymbol)arg.Value.Value!).ToDisplayString();
break;
case "OutputMessageType":
info.OutputMessageType = ((INamedTypeSymbol)arg.Value.Value!).ToDisplayString();
break;
case "Capacity":
info.Capacity = (int)arg.Value.Value!;
break;
case "ProcessingMode":
info.ProcessingMode = (RingProcessingMode)(int)arg.Value.Value!;
break;
}
}
return info;
}
Generating Message Serialization
private static string GenerateMessageSerializer(string messageType, INamedTypeSymbol typeSymbol)
{
var sb = new StringBuilder();
sb.AppendLine($"// Serializer for {messageType}");
sb.AppendLine($"public static class {typeSymbol.Name}Serializer");
sb.AppendLine("{");
// Generate serialize method
sb.AppendLine($" public static int Serialize({messageType} message, Span<byte> buffer)");
sb.AppendLine(" {");
sb.AppendLine(" int offset = 0;");
foreach (var member in typeSymbol.GetMembers().OfType<IPropertySymbol>())
{
var size = GetTypeSize(member.Type);
sb.AppendLine($" // {member.Name}: {member.Type}");
sb.AppendLine($" MemoryMarshal.Write(buffer.Slice(offset), ref message.{member.Name});");
sb.AppendLine($" offset += {size};");
}
sb.AppendLine(" return offset;");
sb.AppendLine(" }");
// Generate deserialize method
sb.AppendLine();
sb.AppendLine($" public static {messageType} Deserialize(ReadOnlySpan<byte> buffer)");
sb.AppendLine(" {");
sb.AppendLine($" var message = new {messageType}();");
sb.AppendLine(" int offset = 0;");
foreach (var member in typeSymbol.GetMembers().OfType<IPropertySymbol>())
{
var size = GetTypeSize(member.Type);
sb.AppendLine($" message.{member.Name} = MemoryMarshal.Read<{member.Type}>(buffer.Slice(offset));");
sb.AppendLine($" offset += {size};");
}
sb.AppendLine(" return message;");
sb.AppendLine(" }");
sb.AppendLine("}");
return sb.ToString();
}
Testing Source Generators
Unit Testing with CSharpGeneratorDriver
public class KernelGeneratorTests
{
[Fact]
public void GeneratesWrapperForKernelMethod()
{
// Arrange
var source = @"
using DotCompute.Generators.Kernel.Attributes;
namespace Test;
public static partial class MyKernels
{
[Kernel]
public static void VectorAdd(
ReadOnlySpan<float> a,
ReadOnlySpan<float> b,
Span<float> result)
{
int idx = Kernel.ThreadId.X;
if (idx < result.Length)
result[idx] = a[idx] + b[idx];
}
}";
// Act
var result = GenerateAndCompile(source);
// Assert
Assert.Empty(result.Diagnostics.Where(d => d.Severity == DiagnosticSeverity.Error));
Assert.Contains(result.GeneratedSources, s => s.HintName.Contains("VectorAdd"));
var generatedCode = result.GeneratedSources
.First(s => s.HintName.Contains("VectorAdd"))
.SourceText.ToString();
Assert.Contains("VectorAddDefinition", generatedCode);
Assert.Contains("ExecuteVectorAddAsync", generatedCode);
}
private static GeneratorDriverRunResult GenerateAndCompile(string source)
{
var syntaxTree = CSharpSyntaxTree.ParseText(source);
var references = new[]
{
MetadataReference.CreateFromFile(typeof(object).Assembly.Location),
MetadataReference.CreateFromFile(typeof(Span<>).Assembly.Location),
// Add DotCompute references
};
var compilation = CSharpCompilation.Create(
"TestAssembly",
new[] { syntaxTree },
references,
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
var generator = new KernelSourceGenerator();
var driver = CSharpGeneratorDriver.Create(generator);
return driver.RunGenerators(compilation).GetRunResult();
}
}
Integration Testing
[Fact]
public async Task GeneratedKernelExecutesCorrectly()
{
// Generated code should compile and execute
var services = new ServiceCollection();
services.AddDotComputeRuntime();
var provider = services.BuildServiceProvider();
var orchestrator = provider.GetRequiredService<IComputeOrchestrator>();
var a = new float[] { 1, 2, 3 };
var b = new float[] { 4, 5, 6 };
var result = new float[3];
using var bufferA = orchestrator.CreateBuffer<float>(3);
using var bufferB = orchestrator.CreateBuffer<float>(3);
using var bufferResult = orchestrator.CreateBuffer<float>(3);
await bufferA.CopyFromAsync(a);
await bufferB.CopyFromAsync(b);
// Use generated execution helper
await MyKernels.ExecuteVectorAddAsync(
orchestrator,
new KernelConfig { BlockSize = 256, GridSize = 1 },
bufferA, bufferB, bufferResult);
await bufferResult.CopyToAsync(result);
Assert.Equal(5, result[0]);
Assert.Equal(7, result[1]);
Assert.Equal(9, result[2]);
}
Debugging Source Generators
Enable Generator Debugging
<!-- In your .csproj -->
<PropertyGroup>
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
<CompilerGeneratedFilesOutputPath>$(BaseIntermediateOutputPath)\GeneratedFiles</CompilerGeneratedFilesOutputPath>
</PropertyGroup>
Debug in Visual Studio
- Set breakpoint in generator code
- Right-click generator project → Debug → Start New Instance
- Open project that uses the generator
- Build to hit breakpoints
Logging During Generation
private static void GenerateSource(
SourceProductionContext context,
(Compilation, ImmutableArray<KernelInfo?>) input)
{
var (compilation, kernels) = input;
// Report diagnostic for debugging
foreach (var kernel in kernels.Where(k => k != null))
{
context.ReportDiagnostic(Diagnostic.Create(
new DiagnosticDescriptor(
"DCDEBUG001",
"Generator Debug",
$"Processing kernel: {kernel!.Name}",
"Debug",
DiagnosticSeverity.Info,
true),
Location.None));
}
}
Performance Optimization
Incremental Generation
public void Initialize(IncrementalGeneratorInitializationContext context)
{
// Use value providers for efficient caching
var kernels = context.SyntaxProvider
.ForAttributeWithMetadataName(
"DotCompute.Generators.Kernel.Attributes.KernelAttribute",
predicate: (node, _) => node is MethodDeclarationSyntax,
transform: (ctx, ct) => ExtractKernelInfo(ctx, ct))
.Where(k => k is not null)
.Select((k, _) => k!);
// Register output with value equality for caching
context.RegisterSourceOutput(kernels, (ctx, kernel) =>
{
var source = GenerateKernelWrapper(kernel);
ctx.AddSource($"{kernel.Name}.g.cs", source);
});
}
Minimize Allocations
// Use pooled string builders
private static readonly ObjectPool<StringBuilder> StringBuilderPool =
new DefaultObjectPoolProvider().CreateStringBuilderPool();
private static string GenerateCode(KernelInfo kernel)
{
var sb = StringBuilderPool.Get();
try
{
// Generate code using sb
return sb.ToString();
}
finally
{
StringBuilderPool.Return(sb);
}
}
Exercises
Exercise 1: Custom Attribute Generator
Create a generator for a [CachedKernel] attribute that generates caching logic.
Exercise 2: Validation Generator
Generate compile-time validation for kernel parameter constraints.
Exercise 3: Documentation Generator
Generate XML documentation from kernel attributes.
Key Takeaways
- Incremental generators provide efficient, cached code generation
- Semantic analysis extracts type information accurately
- Testing is essential - use CSharpGeneratorDriver
- Debug with diagnostics when IDE debugging isn't available
- Cache generated output to improve build performance
Next Module
Learn to create Roslyn analyzers for GPU code validation.
Further Reading
- Source Generators Architecture - Technical details
- Roslyn Documentation - Official docs