MerlinVR / UdonSharp

An experimental compiler for compiling C# to Udon assembly
MIT License
679 stars 89 forks source link

Added pre and post compiler callbacks for compiling UdonSharpProgramAsset #64

Closed NGenesis closed 4 years ago

NGenesis commented 4 years ago

This pull request adds support for beforeProgramAssetCompile and afterProgramAssetCompile callbacks to UdonSharpCompiler which allows for additional pre/post pipelining steps such as method stub generation, source file overrides, cache updates, etc when an UdonSharpProgramAsset is to be compiled by UdonSharp.

An example use case:

using System;
using UdonSharp;
using UnityEngine;
using VRC.SDKBase;
using VRC.Udon;

public class MyUdonSharpBehaviour : UdonSharpBehaviour {
    public void OnCustomEvent(string eventName) {
        Debug.Log(eventName);
    }

    // Custom event stubs in this section are automatically generated before compile time and will be overwritten.
    /*<#UDONSHARP_CUSTOMEVENTS#>*/
    // These methods would be listed in the source file after compilation has completed.  Alternative implementations could temporarily override the original source file during compilation.
    /*public void MyFirstCustomEvent() {
        OnCustomEvent("MyFirstCustomEvent");
    }
    public void MySecondCustomEvent() {
        OnCustomEvent("MySecondCustomEvent");
    }
    public void MyThirdCustomEvent() {
        OnCustomEvent("MyThirdCustomEvent");
    }*/
    /*<#!UDONSHARP_CUSTOMEVENTS!#>*/
}

#if !COMPILER_UDONSHARP && UNITY_EDITOR
using UdonSharpEditor;
using UnityEditor;
using System.IO;
using System.Collections;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using System.Linq;

[CustomEditor(typeof(MyUdonSharpBehaviour))]
public class MyUdonSharpBehaviourInspectorEditor : Editor {
    public override void OnInspectorGUI() {
        UdonSharpCustomEventUtil.CustomEventNames.Add("MyFirstCustomEvent");
        UdonSharpCustomEventUtil.CustomEventNames.Add("MySecondCustomEvent");
        UdonSharpCustomEventUtil.CustomEventNames.Add("MyThirdCustomEvent");
    }
}

[InitializeOnLoad]
public static class UdonSharpCustomEventUtil {
    public static readonly string CustomEventStubMarker = "/*<#UDONSHARP_CUSTOMEVENTS#>*/";
    public static readonly string CustomEventStubMarkerEnd = "/*<#!UDONSHARP_CUSTOMEVENTS!#>*/";
    public static readonly string CustomEventStubMethodName = "OnCustomEvent";

    private static HashSet<string> CustomEventNames = new HashSet<string>();

    static UdonSharpCustomEventUtil() {
        UdonSharpCompiler.beforeProgramAssetCompile += OnUdonSharpCompile;
    }

    private static void OnUdonSharpCompile(UdonSharpProgramAsset programAsset) {
        var program = UdonSharpEditorUtility.GetUdonSharpProgramAsset(typeof(MyUdonSharpBehaviour));
        if(programAsset != null && programAsset.sourceCsScript != null && programAsset == program) BuildCustomEvents();
    }

    private static void BuildCustomEvents() {
        var orderedEventNames = CustomEventNames.OrderBy(x => x).ToList();

        // Generate stub code
        var customEventStubs = CustomEventStubMarker;
        foreach(var eventName in orderedEventNames) customEventStubs += BuildCustomEventMethodStub(eventName, CustomEventStubMethodName);
        customEventStubs += "\n\t" + CustomEventStubMarkerEnd;

        var program = UdonSharpEditorUtility.GetUdonSharpProgramAsset(typeof(MyUdonSharpBehaviour));
        var sourceFileName = AssetDatabase.GetAssetPath(program.sourceCsScript);
        var sourceCode = File.ReadAllText(sourceFileName);

        var expression = String.Format("{0}(.*){1}", Regex.Escape(CustomEventStubMarker), Regex.Escape(CustomEventStubMarkerEnd));
        var newSourceCode = Regex.Replace(sourceCode, expression, customEventStubs, RegexOptions.Singleline);
        if(newSourceCode != sourceCode) File.WriteAllText(sourceFileName, newSourceCode);
    }

    private static string BuildCustomEventMethodStub(string eventName, string methodName) {
        return "\n\tpublic void " + eventName + "() {\n\t\t" + methodName + "(\"" + eventName + "\");\n\t}";
    }
}
#endif
MerlinVR commented 4 years ago

The callbacks currently aren't doing quite what they're advertised to do. The callback will run on all program assets regardless of what's actually being compiled. It'd be more accurate to use the programs in the compilation modules since that is what is actually getting compiled at a given moment. I can merge and fix this if you'd like.