Search Unity

C# Source Generators?

Discussion in 'Scripting' started by rohan-zatun, Apr 14, 2021.

  1. rohan-zatun

    rohan-zatun

    Joined:
    Jan 13, 2021
    Posts:
    14
    I was trying to look up information regarding C# source generators, and I read somewhere that source generators are technically just analyzers and so Unity should support them, and I wanted to know if this is true.

    Can C# source generators be used in Unity?
     
  2. koirat

    koirat

    Joined:
    Jul 7, 2012
    Posts:
    2,074
    I was also searching for this once.
    What I have found is T4 Text Templates, but obviously it is not going to work as automatic pre-compilation.

    If you find a solution like the old c++ kind of Meta Programming or similar than don't forget to share.
     
  3. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    38,745
    Any code that is compatible with Unity's level of C# might work.

    When you generate source in Unity you need to tell Unity to reimport the files so they are compiled.

    You can see a complete example of this in my Datasacks package, where I have a "CODEGEN" button.

    https://github.com/kurtdekker/datas...cks/Assets/Datasack/Core/DatasackInspector.cs

    Around line 242 it writes the source file.

    Around line 288 it tells it to reimport.
     
  4. rohan-zatun

    rohan-zatun

    Joined:
    Jan 13, 2021
    Posts:
    14
    This is not at all the same thing.

    C# source generators are not triggered manually, but Visual Studio rather generates the code on the fly, while you are typing.

     
  5. rohan-zatun

    rohan-zatun

    Joined:
    Jan 13, 2021
    Posts:
    14
  6. PraetorBlue

    PraetorBlue

    Joined:
    Dec 13, 2012
    Posts:
    7,911
  7. rohan-zatun

    rohan-zatun

    Joined:
    Jan 13, 2021
    Posts:
    14
    Dammit... It's a shame.

    Imo one of the major shortcomings of Unity Visual Scripting is its reliance on reflection, causing significant runtime overhead. Code generation and a stronger interpreter could solve this problem quite easily, as UHT and Script VM do in UE4.
     
  8. PraetorBlue

    PraetorBlue

    Joined:
    Dec 13, 2012
    Posts:
    7,911
    I would assume/hope most or all of the reflection is done at startup, rather than as needed but dunno for sure.
     
  9. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    38,745
    Perhaps, but don't lose sight of the fact that Unity is used first and foremost to make small compact mobile-targeted games and to do so with maximum cross-target portability.

    Mobile targets don't get to play with 90% of the yummy goodness the average business app running on a webserver gets to play with.

    With mobile all code must be compiled AOT and digitally signed, the memory footprint is constrained and lacks virtualizing, and the distribution package must be complete and compact, and there's plenty more limitations too.

    I remember way back in 2015 when all of our games were so close to the 100mb download limit that selecting full .NET 2.0 rather than .NET 2.0 subset would put us over the limit and fail our app submissions.

    Best to think in the mobile space when working in the mobile space, the old "When in Rome..." adage.
     
  10. rohan-zatun

    rohan-zatun

    Joined:
    Jan 13, 2021
    Posts:
    14
    Bolt does have its own optimizations for this obviously: it was one of their store page images, that they're up to six times faster than Unity's SendMessage or C# Reflection, but that advantage is per-call.

    If you're interested in more theory (that I personally find fascinating), it's like this. Your code has two parts, data, and instructions. Sometimes, the instructions are dependent on the input data, such as a boolean that you use to branch. Sometimes, the data itself is an instruction, and this is what a virtual machine is. With that in mind, think of every blueprint node in UE4 as "an instruction" that is run on ScriptVM (I think that's what Unreal calls it). Running data as instruction in this manner means you're invoking the runtime overhead of your virtual machine, and there's no way around it.

    Bolt does not generate C# code: instead, it works on top of C#, meaning under the hood, it is getting converted to some data (during initialization, or maybe during a separate compile step when it's saved) which is then interpreted by more custom runtime C# code. This is the actual reason you can make changes to a Bolt script during play mode. It's like when you make a Player script, and change the health from the inspector while testing. You're basically just changing the data, while the runtime remains the same, and just processes the new data you provided.

    C# itself is an interpreted language in a similar way, running on top of the .NET runtime, which is like C/C++ code. When you use Roslyn, C# gets converted to IL bytecode which is interpreted by the .NET runtime, which is what makes raw C++ code slightly faster than C# (generally speaking).

    My point being, Bolt generates its own data which is then interpreted, and its runtime engine is based on reflection, which is taking a fairly tough toll on the runtime. There are several problems with invoking a method through reflection, here's one:
    Code (CSharp):
    1.  
    2. public class MyType
    3. {
    4.     public static void DoSomething(float a, float b)
    5.     {
    6.         UnityEngine.Debug.Log($"The addition of {a} and {b} is {(a + b)}.");
    7.     }
    8. }
    9.  
    10. public static class ReflectionInvoker
    11. {
    12.     public static void DoSomething()
    13.     {
    14.         // gets public static methods of name DoSomething in MyType
    15.         var methodInfo = typeof(MyType).GetMethod("DoSomething", BindingFlags.Static | BindingFlags.Public);
    16.         // this search is quite expensive, and here's the point where you can perform some caching
    17.         // to store the methodInfo.Invoke method as a delegate that takes in an object array
    18.  
    19.         methodInfo.Invoke(new object[]{21, 21});
    20.         // here's where the problem arises, firstly you're boxing a struct by storing it as an object reference
    21.         // secondly, you're creating an object array, which is going to generate some garbage
    22.         // more arguments mean more garbage, struct arguments mean boxing
    23.         // moreover, this is the ACTUAL INVOCATION of the method, and cannot be cached in some way
    24.     }
    25. }
    26.  
    I don't consider myself completely an expert, but based on my personal experience with virtual machines, I don't see any way around this. Even if they are somehow caching argument data, there's no way to actually convert it into a strongly typed method (which can only be defined at compile-time), which is gonna always carry garbage collection overhead.

    This might have been why they moved to code generation in Bolt 2 IMO, but that has its own set of problems and is akin to providing a GUI to write C# code, which is quite limiting in its own way, as you are bound by the feature-set that the underlying language provides.

    Fair point, I'm not very familiar with dealing with app sizes, but I imagine generated code to be more performant overall than say runtime reflection. I don't imagine it would bloat up sizes the way using entire .NET 2.0 feature set would. But then all of this is just conjecture, and you probably know better than I do.

    My point is just that Bolt performance could be significantly better, and source generators would help quite a bit in this regard.

    The main problem source generators target is one of having to use runtime reflection, and my dev-console plugin and my virtual machine plugin would benefit a lot from such a setup.
     
  11. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    38,745
    While I can't speak to Bolt specifically, modern mobile CPUs are incredibly performant and can breeze through an awful lot of very poorly-written user scripts in the little milliseconds between Unity doing all the heavy game engine logic.

    Mobile targets at least are almost always pixel fill bound, rarely CPU bound.

    Again, lacking Bolt experience I cannot speak to this, but obviously any reduction in CPU use will result in less battery use, and that's a Good Thing(tm).

    It's just an engineering economy choice of spending the effort and incurring the technical liability and complexity of integrating with a whole new foreign library and workflow, and maintaining that in to the full future lifecycle of the product.