├── .dockerignore ├── .github └── workflows │ └── dotnet.yml ├── .gitignore ├── CompileTimeMethodExecutionGenerator.Example ├── CompileTimeMethodExecutionGenerator.Example.csproj └── Program.cs ├── CompileTimeMethodExecutionGenerator.Generator ├── CompileTimeMethodExecutionGenerator.Generator.csproj └── Generator.cs ├── CompileTimeMethodExecutionGenerator.sln ├── Dockerfile ├── LICENSE ├── README.md └── docker-compose.yml /.dockerignore: -------------------------------------------------------------------------------- 1 | bin/ 2 | obj/ 3 | **/bin/ 4 | **/obj/ 5 | Dockerfile 6 | docker-compose.yml 7 | README.md -------------------------------------------------------------------------------- /.github/workflows/dotnet.yml: -------------------------------------------------------------------------------- 1 | name: dotnet 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | workflow_dispatch: 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Setup .NET 17 | uses: actions/setup-dotnet@v1 18 | with: 19 | dotnet-version: 5.0.100 20 | - name: Install dependencies 21 | run: dotnet restore 22 | - name: Build 23 | run: dotnet build --no-restore 24 | - name: Run ======================> check the logs here 25 | run: dotnet run --project ./CompileTimeMethodExecutionGenerator.Example/CompileTimeMethodExecutionGenerator.Example.csproj 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Ww][Ii][Nn]32/ 27 | [Aa][Rr][Mm]/ 28 | [Aa][Rr][Mm]64/ 29 | bld/ 30 | [Bb]in/ 31 | [Oo]bj/ 32 | [Ll]og/ 33 | [Ll]ogs/ 34 | 35 | # Visual Studio 2015/2017 cache/options directory 36 | .vs/ 37 | # Uncomment if you have tasks that create the project's static files in wwwroot 38 | #wwwroot/ 39 | 40 | # Visual Studio 2017 auto generated files 41 | Generated\ Files/ 42 | 43 | # MSTest test Results 44 | [Tt]est[Rr]esult*/ 45 | [Bb]uild[Ll]og.* 46 | 47 | # NUnit 48 | *.VisualState.xml 49 | TestResult.xml 50 | nunit-*.xml 51 | 52 | # Build Results of an ATL Project 53 | [Dd]ebugPS/ 54 | [Rr]eleasePS/ 55 | dlldata.c 56 | 57 | # Benchmark Results 58 | BenchmarkDotNet.Artifacts/ 59 | 60 | # .NET Core 61 | project.lock.json 62 | project.fragment.lock.json 63 | artifacts/ 64 | 65 | # ASP.NET Scaffolding 66 | ScaffoldingReadMe.txt 67 | 68 | # StyleCop 69 | StyleCopReport.xml 70 | 71 | # Files built by Visual Studio 72 | *_i.c 73 | *_p.c 74 | *_h.h 75 | *.ilk 76 | *.meta 77 | *.obj 78 | *.iobj 79 | *.pch 80 | *.pdb 81 | *.ipdb 82 | *.pgc 83 | *.pgd 84 | *.rsp 85 | *.sbr 86 | *.tlb 87 | *.tli 88 | *.tlh 89 | *.tmp 90 | *.tmp_proj 91 | *_wpftmp.csproj 92 | *.log 93 | *.vspscc 94 | *.vssscc 95 | .builds 96 | *.pidb 97 | *.svclog 98 | *.scc 99 | 100 | # Chutzpah Test files 101 | _Chutzpah* 102 | 103 | # Visual C++ cache files 104 | ipch/ 105 | *.aps 106 | *.ncb 107 | *.opendb 108 | *.opensdf 109 | *.sdf 110 | *.cachefile 111 | *.VC.db 112 | *.VC.VC.opendb 113 | 114 | # Visual Studio profiler 115 | *.psess 116 | *.vsp 117 | *.vspx 118 | *.sap 119 | 120 | # Visual Studio Trace Files 121 | *.e2e 122 | 123 | # TFS 2012 Local Workspace 124 | $tf/ 125 | 126 | # Guidance Automation Toolkit 127 | *.gpState 128 | 129 | # ReSharper is a .NET coding add-in 130 | _ReSharper*/ 131 | *.[Rr]e[Ss]harper 132 | *.DotSettings.user 133 | 134 | # TeamCity is a build add-in 135 | _TeamCity* 136 | 137 | # DotCover is a Code Coverage Tool 138 | *.dotCover 139 | 140 | # AxoCover is a Code Coverage Tool 141 | .axoCover/* 142 | !.axoCover/settings.json 143 | 144 | # Coverlet is a free, cross platform Code Coverage Tool 145 | coverage*.json 146 | coverage*.xml 147 | coverage*.info 148 | 149 | # Visual Studio code coverage results 150 | *.coverage 151 | *.coveragexml 152 | 153 | # NCrunch 154 | _NCrunch_* 155 | .*crunch*.local.xml 156 | nCrunchTemp_* 157 | 158 | # MightyMoose 159 | *.mm.* 160 | AutoTest.Net/ 161 | 162 | # Web workbench (sass) 163 | .sass-cache/ 164 | 165 | # Installshield output folder 166 | [Ee]xpress/ 167 | 168 | # DocProject is a documentation generator add-in 169 | DocProject/buildhelp/ 170 | DocProject/Help/*.HxT 171 | DocProject/Help/*.HxC 172 | DocProject/Help/*.hhc 173 | DocProject/Help/*.hhk 174 | DocProject/Help/*.hhp 175 | DocProject/Help/Html2 176 | DocProject/Help/html 177 | 178 | # Click-Once directory 179 | publish/ 180 | 181 | # Publish Web Output 182 | *.[Pp]ublish.xml 183 | *.azurePubxml 184 | # Note: Comment the next line if you want to checkin your web deploy settings, 185 | # but database connection strings (with potential passwords) will be unencrypted 186 | *.pubxml 187 | *.publishproj 188 | 189 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 190 | # checkin your Azure Web App publish settings, but sensitive information contained 191 | # in these scripts will be unencrypted 192 | PublishScripts/ 193 | 194 | # NuGet Packages 195 | *.nupkg 196 | # NuGet Symbol Packages 197 | *.snupkg 198 | # The packages folder can be ignored because of Package Restore 199 | **/[Pp]ackages/* 200 | # except build/, which is used as an MSBuild target. 201 | !**/[Pp]ackages/build/ 202 | # Uncomment if necessary however generally it will be regenerated when needed 203 | #!**/[Pp]ackages/repositories.config 204 | # NuGet v3's project.json files produces more ignorable files 205 | *.nuget.props 206 | *.nuget.targets 207 | 208 | # Microsoft Azure Build Output 209 | csx/ 210 | *.build.csdef 211 | 212 | # Microsoft Azure Emulator 213 | ecf/ 214 | rcf/ 215 | 216 | # Windows Store app package directories and files 217 | AppPackages/ 218 | BundleArtifacts/ 219 | Package.StoreAssociation.xml 220 | _pkginfo.txt 221 | *.appx 222 | *.appxbundle 223 | *.appxupload 224 | 225 | # Visual Studio cache files 226 | # files ending in .cache can be ignored 227 | *.[Cc]ache 228 | # but keep track of directories ending in .cache 229 | !?*.[Cc]ache/ 230 | 231 | # Others 232 | ClientBin/ 233 | ~$* 234 | *~ 235 | *.dbmdl 236 | *.dbproj.schemaview 237 | *.jfm 238 | *.pfx 239 | *.publishsettings 240 | orleans.codegen.cs 241 | 242 | # Including strong name files can present a security risk 243 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 244 | #*.snk 245 | 246 | # Since there are multiple workflows, uncomment next line to ignore bower_components 247 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 248 | #bower_components/ 249 | 250 | # RIA/Silverlight projects 251 | Generated_Code/ 252 | 253 | # Backup & report files from converting an old project file 254 | # to a newer Visual Studio version. Backup files are not needed, 255 | # because we have git ;-) 256 | _UpgradeReport_Files/ 257 | Backup*/ 258 | UpgradeLog*.XML 259 | UpgradeLog*.htm 260 | ServiceFabricBackup/ 261 | *.rptproj.bak 262 | 263 | # SQL Server files 264 | *.mdf 265 | *.ldf 266 | *.ndf 267 | 268 | # Business Intelligence projects 269 | *.rdl.data 270 | *.bim.layout 271 | *.bim_*.settings 272 | *.rptproj.rsuser 273 | *- [Bb]ackup.rdl 274 | *- [Bb]ackup ([0-9]).rdl 275 | *- [Bb]ackup ([0-9][0-9]).rdl 276 | 277 | # Microsoft Fakes 278 | FakesAssemblies/ 279 | 280 | # GhostDoc plugin setting file 281 | *.GhostDoc.xml 282 | 283 | # Node.js Tools for Visual Studio 284 | .ntvs_analysis.dat 285 | node_modules/ 286 | 287 | # Visual Studio 6 build log 288 | *.plg 289 | 290 | # Visual Studio 6 workspace options file 291 | *.opt 292 | 293 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 294 | *.vbw 295 | 296 | # Visual Studio LightSwitch build output 297 | **/*.HTMLClient/GeneratedArtifacts 298 | **/*.DesktopClient/GeneratedArtifacts 299 | **/*.DesktopClient/ModelManifest.xml 300 | **/*.Server/GeneratedArtifacts 301 | **/*.Server/ModelManifest.xml 302 | _Pvt_Extensions 303 | 304 | # Paket dependency manager 305 | .paket/paket.exe 306 | paket-files/ 307 | 308 | # FAKE - F# Make 309 | .fake/ 310 | 311 | # CodeRush personal settings 312 | .cr/personal 313 | 314 | # Python Tools for Visual Studio (PTVS) 315 | __pycache__/ 316 | *.pyc 317 | 318 | # Cake - Uncomment if you are using it 319 | # tools/** 320 | # !tools/packages.config 321 | 322 | # Tabs Studio 323 | *.tss 324 | 325 | # Telerik's JustMock configuration file 326 | *.jmconfig 327 | 328 | # BizTalk build output 329 | *.btp.cs 330 | *.btm.cs 331 | *.odx.cs 332 | *.xsd.cs 333 | 334 | # OpenCover UI analysis results 335 | OpenCover/ 336 | 337 | # Azure Stream Analytics local run output 338 | ASALocalRun/ 339 | 340 | # MSBuild Binary and Structured Log 341 | *.binlog 342 | 343 | # NVidia Nsight GPU debugger configuration file 344 | *.nvuser 345 | 346 | # MFractors (Xamarin productivity tool) working folder 347 | .mfractor/ 348 | 349 | # Local History for Visual Studio 350 | .localhistory/ 351 | 352 | # BeatPulse healthcheck temp database 353 | healthchecksdb 354 | 355 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 356 | MigrationBackup/ 357 | 358 | # Ionide (cross platform F# VS Code tools) working folder 359 | .ionide/ 360 | 361 | # Fody - auto-generated XML schema 362 | FodyWeavers.xsd -------------------------------------------------------------------------------- /CompileTimeMethodExecutionGenerator.Example/CompileTimeMethodExecutionGenerator.Example.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net5.0 6 | 7 | 8 | 9 | 10 | 12 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /CompileTimeMethodExecutionGenerator.Example/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using CompileTimeMethodExecutionGenerator; 4 | 5 | namespace CompileTimeMethodExecutionGenerator.Example 6 | { 7 | /// 8 | /// The following class will be extended during compilation by the source generator "CompileTimeMethodExecutionGenerator". 9 | /// To allow for that to happen, it has to be made partial. 10 | /// 11 | public partial class Calculator 12 | { 13 | /// 14 | /// This method does something that is cpu intensive and should always have the same result; it calculates pi in 20000 digits. 15 | /// 16 | /// Of course, you could just run it once and place the resulting string in the method body to achieve the same result. 17 | /// But that would be more difficult to maintain. 18 | /// 19 | /// Also, there is System.Math.PI. You should obviously use that if you want to do something with pi. 20 | /// But that's not the point here; this is a proof of concept example. 21 | /// 22 | [CompileTimeExecutor] 23 | public string Pi() { 24 | // Code derived from: https://stackoverflow.com/a/11679007/4624255 25 | const int digits = 20000; 26 | 27 | uint[] x = new uint[digits*10/3+2]; 28 | uint[] r = new uint[digits*10/3+2]; 29 | 30 | uint[] pi = new uint[digits]; 31 | 32 | for (int j = 0; j < x.Length; j++) 33 | x[j] = 20; 34 | 35 | for (int i = 0; i < digits; i++) 36 | { 37 | uint carry = 0; 38 | for (int j = 0; j < x.Length; j++) 39 | { 40 | uint num = (uint)(x.Length - j - 1); 41 | uint dem = num * 2 + 1; 42 | 43 | x[j] += carry; 44 | 45 | uint q = x[j] / dem; 46 | r[j] = x[j] % dem; 47 | 48 | carry = q * num; 49 | } 50 | 51 | 52 | pi[i] = (x[x.Length-1] / 10); 53 | 54 | 55 | r[x.Length - 1] = x[x.Length - 1] % 10; ; 56 | 57 | for (int j = 0; j < x.Length; j++) 58 | x[j] = r[j] * 10; 59 | } 60 | 61 | var result = ""; 62 | 63 | uint c = 0; 64 | 65 | for(int i = pi.Length - 1; i >=0; i--) 66 | { 67 | pi[i] += c; 68 | c = pi[i] / 10; 69 | 70 | result = (pi[i] % 10).ToString() + result; 71 | } 72 | 73 | return result; 74 | } 75 | } 76 | 77 | class Program 78 | { 79 | static void Main(string[] args) 80 | { 81 | var calculator = new Calculator(); 82 | 83 | // Execute the method like you normally would first and measure the time it takes 84 | Stopwatch stopWatch = new Stopwatch(); 85 | stopWatch.Start(); 86 | Console.WriteLine($"Pi calculated with {calculator.Pi().Length} digits"); 87 | stopWatch.Stop(); 88 | Console.WriteLine($"Execution took {stopWatch.Elapsed.TotalMilliseconds}ms"); 89 | 90 | // Now execute the compile time generated version of the same method 91 | stopWatch.Reset(); 92 | stopWatch.Start(); 93 | Console.WriteLine($"Pi calculated with {calculator.PiCompileTime().Length} digits (but performed calculation during compilation)"); 94 | stopWatch.Stop(); 95 | Console.WriteLine($"Execution took {stopWatch.Elapsed.TotalMilliseconds}ms"); 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /CompileTimeMethodExecutionGenerator.Generator/CompileTimeMethodExecutionGenerator.Generator.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | 8.0 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /CompileTimeMethodExecutionGenerator.Generator/Generator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using Microsoft.CodeAnalysis; 6 | using Microsoft.CodeAnalysis.CSharp; 7 | using Microsoft.CodeAnalysis.CSharp.Syntax; 8 | using Microsoft.CodeAnalysis.Text; 9 | using Microsoft.CodeAnalysis.Emit; 10 | using System.IO; 11 | using System.Reflection; 12 | 13 | namespace CompileTimeMethodExecutionGenerator.Generator 14 | { 15 | [Generator] 16 | public class Generator : ISourceGenerator 17 | { 18 | // The attribute that allows decorating methods with [CompileTimeExecutor] can be added to the compilation 19 | private const string attributeText = @" 20 | using System; 21 | namespace CompileTimeMethodExecutionGenerator 22 | { 23 | [AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = false)] 24 | sealed class CompileTimeExecutorAttribute : Attribute 25 | { 26 | public CompileTimeExecutorAttribute() 27 | { 28 | } 29 | } 30 | }"; 31 | 32 | public void Execute(GeneratorExecutionContext context) 33 | { 34 | // Always add the attribute itself to the compilation 35 | context.AddSource("CompileTimeExecutorAttribute", SourceText.From(attributeText, Encoding.UTF8)); 36 | 37 | // retreive the populated receiver 38 | if (!(context.SyntaxReceiver is SyntaxReceiver receiver)) 39 | return; 40 | 41 | // we're going to create a new compilation that contains the attribute. 42 | // TODO: we should allow source generators to provide source during initialize, so that this step isn't required. 43 | CSharpParseOptions options = (context.Compilation as CSharpCompilation).SyntaxTrees[0].Options as CSharpParseOptions; 44 | Compilation compilation = context.Compilation.AddSyntaxTrees(CSharpSyntaxTree.ParseText(SourceText.From(attributeText, Encoding.UTF8), options)); 45 | 46 | // get the newly bound attribute, and INotifyPropertyChanged 47 | INamedTypeSymbol attributeSymbol = compilation.GetTypeByMetadataName("CompileTimeMethodExecutionGenerator.CompileTimeExecutorAttribute"); 48 | 49 | foreach (MethodDeclarationSyntax method in receiver.CandidateMethods) 50 | { 51 | SemanticModel model = compilation.GetSemanticModel(method.SyntaxTree); 52 | 53 | var methodSymbol = model.GetDeclaredSymbol(method) as IMethodSymbol; 54 | 55 | foreach (AttributeSyntax attribute in method.AttributeLists 56 | .SelectMany(al => al.Attributes) 57 | .Where(at => string.Equals("CompileTimeExecutor", at.Name.ToString()))) 58 | { 59 | string calculatedResult = CalculateResult(method, methodSymbol); 60 | 61 | context.AddSource($"{methodSymbol.ContainingNamespace.ToDisplayString()}_{methodSymbol.ContainingType.Name}_{method.Identifier.Text}.gen.cs", 62 | SourceText.From($@"namespace {methodSymbol.ContainingNamespace.ToDisplayString()} 63 | {{ 64 | public partial class {methodSymbol.ContainingType.Name} 65 | {{ 66 | public string {method.Identifier.Text}CompileTime() 67 | {{ 68 | return ""{calculatedResult}""; 69 | }} 70 | }} 71 | }}", Encoding.UTF8)); 72 | } 73 | } 74 | } 75 | 76 | private static string CalculateResult(MethodDeclarationSyntax method, IMethodSymbol methodSymbol) 77 | { 78 | string assemblyName = Path.GetRandomFileName(); 79 | MetadataReference[] references = new MetadataReference[] 80 | { 81 | MetadataReference.CreateFromFile(typeof(object).Assembly.Location), 82 | MetadataReference.CreateFromFile(typeof(Enumerable).Assembly.Location) 83 | }; 84 | 85 | CSharpParseOptions options = method.SyntaxTree.Options as CSharpParseOptions; 86 | 87 | CSharpCompilation compilation = CSharpCompilation.Create( 88 | assemblyName, 89 | syntaxTrees: new[] { CSharpSyntaxTree.ParseText( 90 | SourceText.From( 91 | $"public class C {{ public {method.ReturnType.ToString()} M() {method.Body.ToFullString()} }}", Encoding.UTF8), 92 | options) }, 93 | references: references, 94 | options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); 95 | 96 | try 97 | { 98 | using (var ms = new MemoryStream()) 99 | { 100 | EmitResult result = compilation.Emit(ms); 101 | 102 | if (!result.Success) 103 | { 104 | IEnumerable failures = result.Diagnostics.Where(diagnostic => 105 | diagnostic.IsWarningAsError || 106 | diagnostic.Severity == DiagnosticSeverity.Error); 107 | 108 | throw new Exception(string.Join("\r\n", failures.Select(f => $"{f.Id} {f.GetMessage()}"))); 109 | } 110 | else 111 | { 112 | ms.Seek(0, SeekOrigin.Begin); 113 | Assembly assembly = Assembly.Load(ms.ToArray()); 114 | 115 | Type type = assembly.GetType("C"); 116 | object obj = Activator.CreateInstance(type); 117 | var res = type.InvokeMember("M", 118 | BindingFlags.Default | BindingFlags.InvokeMethod, 119 | null, 120 | obj, 121 | new object[0])?.ToString(); 122 | 123 | return res; 124 | } 125 | } 126 | } 127 | catch(Exception ex) 128 | { 129 | return $"Exception when executing method for injecting into compilation {ex.Message}"; 130 | } 131 | } 132 | 133 | public void Initialize(GeneratorInitializationContext context) 134 | { 135 | // Register a syntax receiver that will be created for each generation pass 136 | context.RegisterForSyntaxNotifications(() => new SyntaxReceiver()); 137 | } 138 | 139 | /// 140 | /// Created on demand before each generation pass 141 | /// 142 | class SyntaxReceiver : ISyntaxReceiver 143 | { 144 | public List CandidateMethods { get; } = new List(); 145 | 146 | /// 147 | /// Called for every syntax node in the compilation, we can inspect the nodes and save any information useful for generation 148 | /// 149 | public void OnVisitSyntaxNode(SyntaxNode syntaxNode) 150 | { 151 | // any method with at least one attribute is a candidate 152 | if (syntaxNode is MethodDeclarationSyntax methodDeclarationSyntax 153 | && methodDeclarationSyntax.AttributeLists.Count > 0) 154 | { 155 | CandidateMethods.Add(methodDeclarationSyntax); 156 | } 157 | } 158 | } 159 | } 160 | } -------------------------------------------------------------------------------- /CompileTimeMethodExecutionGenerator.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.26124.0 5 | MinimumVisualStudioVersion = 15.0.26124.0 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CompileTimeMethodExecutionGenerator.Generator", "CompileTimeMethodExecutionGenerator.Generator\CompileTimeMethodExecutionGenerator.Generator.csproj", "{1C805869-A87F-4F87-81E6-72B81E752F1C}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CompileTimeMethodExecutionGenerator.Example", "CompileTimeMethodExecutionGenerator.Example\CompileTimeMethodExecutionGenerator.Example.csproj", "{FDF39D05-995E-47C3-B100-84EAA1DD53F5}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Debug|x64 = Debug|x64 14 | Debug|x86 = Debug|x86 15 | Release|Any CPU = Release|Any CPU 16 | Release|x64 = Release|x64 17 | Release|x86 = Release|x86 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 23 | {1C805869-A87F-4F87-81E6-72B81E752F1C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 24 | {1C805869-A87F-4F87-81E6-72B81E752F1C}.Debug|Any CPU.Build.0 = Debug|Any CPU 25 | {1C805869-A87F-4F87-81E6-72B81E752F1C}.Debug|x64.ActiveCfg = Debug|Any CPU 26 | {1C805869-A87F-4F87-81E6-72B81E752F1C}.Debug|x64.Build.0 = Debug|Any CPU 27 | {1C805869-A87F-4F87-81E6-72B81E752F1C}.Debug|x86.ActiveCfg = Debug|Any CPU 28 | {1C805869-A87F-4F87-81E6-72B81E752F1C}.Debug|x86.Build.0 = Debug|Any CPU 29 | {1C805869-A87F-4F87-81E6-72B81E752F1C}.Release|Any CPU.ActiveCfg = Release|Any CPU 30 | {1C805869-A87F-4F87-81E6-72B81E752F1C}.Release|Any CPU.Build.0 = Release|Any CPU 31 | {1C805869-A87F-4F87-81E6-72B81E752F1C}.Release|x64.ActiveCfg = Release|Any CPU 32 | {1C805869-A87F-4F87-81E6-72B81E752F1C}.Release|x64.Build.0 = Release|Any CPU 33 | {1C805869-A87F-4F87-81E6-72B81E752F1C}.Release|x86.ActiveCfg = Release|Any CPU 34 | {1C805869-A87F-4F87-81E6-72B81E752F1C}.Release|x86.Build.0 = Release|Any CPU 35 | {FDF39D05-995E-47C3-B100-84EAA1DD53F5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 36 | {FDF39D05-995E-47C3-B100-84EAA1DD53F5}.Debug|Any CPU.Build.0 = Debug|Any CPU 37 | {FDF39D05-995E-47C3-B100-84EAA1DD53F5}.Debug|x64.ActiveCfg = Debug|Any CPU 38 | {FDF39D05-995E-47C3-B100-84EAA1DD53F5}.Debug|x64.Build.0 = Debug|Any CPU 39 | {FDF39D05-995E-47C3-B100-84EAA1DD53F5}.Debug|x86.ActiveCfg = Debug|Any CPU 40 | {FDF39D05-995E-47C3-B100-84EAA1DD53F5}.Debug|x86.Build.0 = Debug|Any CPU 41 | {FDF39D05-995E-47C3-B100-84EAA1DD53F5}.Release|Any CPU.ActiveCfg = Release|Any CPU 42 | {FDF39D05-995E-47C3-B100-84EAA1DD53F5}.Release|Any CPU.Build.0 = Release|Any CPU 43 | {FDF39D05-995E-47C3-B100-84EAA1DD53F5}.Release|x64.ActiveCfg = Release|Any CPU 44 | {FDF39D05-995E-47C3-B100-84EAA1DD53F5}.Release|x64.Build.0 = Release|Any CPU 45 | {FDF39D05-995E-47C3-B100-84EAA1DD53F5}.Release|x86.ActiveCfg = Release|Any CPU 46 | {FDF39D05-995E-47C3-B100-84EAA1DD53F5}.Release|x86.Build.0 = Release|Any CPU 47 | EndGlobalSection 48 | EndGlobal 49 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mcr.microsoft.com/dotnet/sdk:5.0 2 | 3 | WORKDIR /app/CompileTimeMethodExecutionGenerator.Generator 4 | COPY ./CompileTimeMethodExecutionGenerator.Generator/CompileTimeMethodExecutionGenerator.Generator.csproj ./ 5 | RUN dotnet restore 6 | 7 | WORKDIR /app/CompileTimeMethodExecutionGenerator.Example 8 | COPY ./CompileTimeMethodExecutionGenerator.Example/CompileTimeMethodExecutionGenerator.Example.csproj ./ 9 | RUN dotnet restore 10 | 11 | WORKDIR /app 12 | 13 | COPY . ./ 14 | 15 | RUN dotnet build ./CompileTimeMethodExecutionGenerator.Example/CompileTimeMethodExecutionGenerator.Example.csproj --no-restore 16 | 17 | CMD dotnet run --project ./CompileTimeMethodExecutionGenerator.Example/CompileTimeMethodExecutionGenerator.Example.csproj -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Robin Hermanussen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![dotnet](https://github.com/hermanussen/CompileTimeMethodExecutionGenerator/workflows/dotnet/badge.svg) 2 | 3 | # Compile Time Method Execution Generator 4 | 5 | A ".NET 5" source generator proof of concept that allows executing a method during compilation, so that it can be really fast during runtime. 6 | 7 | ## What does it do? 8 | 9 | [This blogpost](https://devblogs.microsoft.com/dotnet/introducing-c-source-generators/) describes how C# source generators work. In short, C# source generators allow code to be generated and added to the compilation while the compilation is running. 10 | 11 | This particular generator looks for a method that is decorated with the `[CompileTimeExecutor]` attribute, and then adds the same method with a "CompileTime" postfix in its name to the same class. This method returns the result immediately without having to execute the logic. 12 | 13 | This is possible, because the source generator compiles and executes the original method and then has the new "CompileTime" version of the method return the result immediately. 14 | 15 | ## But why? 16 | 17 | I'm not sure if there are that many real world use cases, but please let me know if you have one ([Send me a Tweet](https://twitter.com/knifecore/)). 18 | 19 | I just thought it would be fun. And potentially, it could be useful if you have a method that: 20 | - Always returns the same result 21 | - The implementation is slow 22 | - You may want to change the implementation of the method during development 23 | - You don't mind if this slightly slows down your project's compilation 24 | 25 | It may actually be a really bad idea to use this. I could imagine that visual studio will be very slow because of this (because the live compilation will also be very slow). 26 | 27 | ## How does it work? 28 | 29 | Well, take a look for yourself! The "CompileTimeMethodExecutionGenerator.Example" shows how you could use the generator. A method that calculates pi in 20000 digits is decorated with the "CompileTime" attribute. The program benchmarks running the method during runtime as well as compile time. 30 | 31 | If you want to understand the inner workings, then take a look at the "CompileTimeMethodExecutionGenerator.Generator" project, which contains the actual generator. 32 | 33 | ## How can I see the result? 34 | 35 | Just take a look at the output of [the latest run here](https://github.com/hermanussen/CompileTimeMethodExecutionGenerator/actions?query=workflow%3Adotnet). In the build log, expand the "Run" section and you'll find something like this: 36 | ``` 37 | Pi calculated with 20000 digits 38 | Execution took 26482.0309ms 39 | Pi calculated with 20000 digits (but performed calculation during compilation) 40 | Execution took 0.2029ms 41 | ``` 42 | 43 | ## How can I run it myself? 44 | 45 | I've rolled the compilation and running of the example in a Docker that is included here. The easiest way to run it is by running `docker-compose up --build` and looking at the output. Example output: 46 | ``` 47 | Attaching to compiletimemethodexecutiongenerator_cg_1 48 | cg_1 | Pi calculated with 20000 digits 49 | cg_1 | Execution took 22455.9488ms 50 | cg_1 | Pi calculated with 20000 digits (but performed calculation during compilation) 51 | cg_1 | Execution took 0.1939ms 52 | compiletimemethodexecutiongenerator_cg_1 exited with code 0 53 | ``` 54 | 55 | As you can see, there are significant performance gains possible. Even though this is a very contrived example, obviously. 56 | 57 | ## Is there a future for this? 58 | 59 | I don't know. It's just something that I thought was interesting. There are many limitations at this point, that could be addressed: 60 | - I've only tested with `string` and `int` as the return type. Potentially, the generator could support any serializable type. 61 | - The methods can not have any parameters at this time. The parameter values should be known at compile time for this to work. Maybe adding the values to the attribute or looking at the syntax trees could be used to determine these values. 62 | - There's probably a million ways in which using this could go wrong at this point. I should add some unit tests and handle many of the edge cases. 63 | - ... send me your ideas and opinions ([Send me a Tweet](https://twitter.com/knifecore/))! 64 | 65 | I may invest some time in this if there are more people interested. 66 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | cg: 4 | build: . --------------------------------------------------------------------------------