[Unity 3d] 读源码,看 Unity 对带参数命令行启动逻辑的封装

在本文,笔者将带大家从 EditorUtility.CompileCsharp API 出发,看一看Unity 是怎么使用 带参数命令行调用软件的。行文仓促内容繁杂故而较为佛系,另笔者技术有限难免疏漏,见谅~

前言

昨天看到一篇 博客 ,讲述了如何将Unity 编辑器之外的脚本通过 EditorUtility.CompileCsharp 在编辑器下编译成 Dll 。
这引起了笔者的不少兴趣,于是拿起 Dnspy 沿着这个方法名 CompileCsharp 就追了进去...

[Unity 3d] 读源码,看 Unity 对带参数命令行启动逻辑的封装_第1张图片

这些与脚本编译相关的逻辑居然不是调用的 Native dll的方法,这很 nice!

发现

经过不停的点击跟进,笔者发现了一个 Program 类型,也就是本文的主角,它将我们常规的 软件命令行调用 进行了封装,感觉到了满满的专业级别的严谨:

using System;
using System.Collections;
using System.Diagnostics;
using System.IO;
using System.Text;

namespace UnityEditor.Utils
{
    internal class Program : IDisposable
    {
        protected Program()
        {
            this._process = new Process();
        }
        public Program(ProcessStartInfo si) : this()
        {
            this._process.StartInfo = si;
        }
        public void Start()
        {
            this.Start(null);
        }
        public void Start(EventHandler exitCallback) 
        {
            if (exitCallback != null)
            {
                this._process.EnableRaisingEvents = true;
                this._process.Exited += exitCallback; //监听软件退出事件
            }
            this._process.StartInfo.RedirectStandardInput = true;
            this._process.StartInfo.RedirectStandardError = true;
            this._process.StartInfo.RedirectStandardOutput = true;
            this._process.StartInfo.UseShellExecute = false;
            this._process.Start();
            this._stdout = new ProcessOutputStreamReader(this._process, this._process.StandardOutput); //使用异步读取软件常规输出
            this._stderr = new ProcessOutputStreamReader(this._process, this._process.StandardError);//使用异步读取软件异常
            this._stdin = this._process.StandardInput.BaseStream;
        }
        public ProcessStartInfo GetProcessStartInfo()
        {
            return this._process.StartInfo;
        }
        public void LogProcessStartInfo() 
        {
            if (this._process != null)
            {
                Program.LogProcessStartInfo(this._process.StartInfo);
            }
            else
            {
                Console.WriteLine("Failed to retrieve process startInfo");
            }
        }
        private static void LogProcessStartInfo(ProcessStartInfo si) //实现被启动的进程的 文件名+命令行参数的输出
        {
            Console.WriteLine("Filename: " + si.FileName);
            Console.WriteLine("Arguments: " + si.Arguments);
            IEnumerator enumerator = si.EnvironmentVariables.GetEnumerator();
            try
            {
                while (enumerator.MoveNext())
                {
                    object obj = enumerator.Current;
                    DictionaryEntry dictionaryEntry = (DictionaryEntry)obj;
                    if (dictionaryEntry.Key.ToString().StartsWith("MONO"))
                    {
                        Console.WriteLine("{0}: {1}", dictionaryEntry.Key, dictionaryEntry.Value);
                    }
                }
            }
            finally
            {
                IDisposable disposable;
                if ((disposable = (enumerator as IDisposable)) != null)
                {
                    disposable.Dispose();
                }
            }
            int num = si.Arguments.IndexOf("Temp/UnityTempFile");
            Console.WriteLine("index: " + num);
            if (num > 0)
            {
                string text = si.Arguments.Substring(num);
                Console.WriteLine("Responsefile: " + text + " Contents: ");
                Console.WriteLine(File.ReadAllText(text));
            }
        }
        public string GetAllOutput() //实现进程的所有标准输出和异常输出
        {
            StringBuilder stringBuilder = new StringBuilder();
            stringBuilder.AppendLine("stdout:");
            foreach (string value in this.GetStandardOutput())
            {
                stringBuilder.AppendLine(value);
            }
            stringBuilder.AppendLine("stderr:");
            foreach (string value2 in this.GetErrorOutput())
            {
                stringBuilder.AppendLine(value2);
            }
            return stringBuilder.ToString();
        }
        public bool HasExited
        {
            get
            {
                if (this._process == null)
                {
                    throw new InvalidOperationException("You cannot call HasExited before calling Start");
                }
                bool result;
                try
                {
                    result = this._process.HasExited;
                }
                catch (InvalidOperationException)
                {
                    result = true;
                }
                return result;
            }
        }
        public int ExitCode
        {
            get
            {
                return this._process.ExitCode;
            }
        }
        public int Id
        {
            get
            {
                return this._process.Id;
            }
        }
        public void Dispose()
        {
            this.Kill();
            this._process.Dispose();
        }
        public void Kill()
        {
            if (!this.HasExited)
            {
                this._process.Kill();
                this._process.WaitForExit();
            }
        }
        public Stream GetStandardInput()
        {
            return this._stdin;
        }
        public string[] GetStandardOutput()
        {
            return this._stdout.GetOutput();
        }
        public string GetStandardOutputAsString()
        {
            string[] standardOutput = this.GetStandardOutput();
            return Program.GetOutputAsString(standardOutput);
        }
        public string[] GetErrorOutput()
        {
            return this._stderr.GetOutput();
        }
        public string GetErrorOutputAsString()
        {
            string[] errorOutput = this.GetErrorOutput();
            return Program.GetOutputAsString(errorOutput);
        }
        private static string GetOutputAsString(string[] output)
        {
            StringBuilder stringBuilder = new StringBuilder();
            foreach (string value in output)
            {
                stringBuilder.AppendLine(value);
            }
            return stringBuilder.ToString();
        }
        public void WaitForExit()
        {
            this._process.WaitForExit();
        }
        public bool WaitForExit(int milliseconds)
        {
            return this._process.WaitForExit(milliseconds);
        }
        private ProcessOutputStreamReader _stdout;
            private ProcessOutputStreamReader _stderr;
        private Stream _stdin;
        public Process _process;
    }
}

其中,它把 ProcessInfo 进行了包装,完善了命令行执行软件运行的流程:Start 、Kill 、Dispose 一个都不少。
再次,它把 标准输出(StandardOuput) 和 标准错误输出 (StandardOuput)使用了专门封装的 流读写器中,开启线程进行读取,提高了性能,也避免了卡调用 Process 的线程。
最后,为了方便调试,实现了非常完善的 命令行参数调试输出,Process 常规数据和异常输出 的方法。

其他

  1. ProcessOutputStreamReader
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Threading;

namespace UnityEditor.Utils
{
    internal class ProcessOutputStreamReader
    {
        internal ProcessOutputStreamReader(Process p, StreamReader stream) : this(() => p.HasExited, stream){}
        internal ProcessOutputStreamReader(Func hostProcessExited, StreamReader stream)
        {
            this.hostProcessExited = hostProcessExited;
            this.stream = stream;
            this.lines = new List();
            this.thread = new Thread(new ThreadStart(this.ThreadFunc));
            this.thread.Start();
        }
        private void ThreadFunc()
        {
            if (!this.hostProcessExited())
            {
                try
                {
                    while (this.stream.BaseStream != null)
                    {
                        string text = this.stream.ReadLine();
                        if (text == null)
                        {
                            break;
                        }
                        object obj = this.lines;
                        lock (obj)
                        {
                            this.lines.Add(text);
                        }
                    }
                }
                catch (ObjectDisposedException)
                {
                    object obj2 = this.lines;
                    lock (obj2)
                    {
                        this.lines.Add("Could not read output because an ObjectDisposedException was thrown.");
                    }
                }
                catch (IOException)
                {
                }
            }
        }

        internal string[] GetOutput()
        {
            if (this.hostProcessExited())
            {
                this.thread.Join();
            }
            object obj = this.lines;
            string[] result;
            lock (obj)
            {
                result = this.lines.ToArray();
            }
            return result;
        }
        private readonly Func hostProcessExited;
        private readonly StreamReader stream;
        internal List lines;
        private Thread thread;
    }
}

当然了,里面还有很多令人印象深刻的工具类,比如对Unity Mono 库的查找辅助脚本 MonoInstallationFinder啦,对命令行参数进行进行修剪/格式化的脚本:CommandLineFormatter啦。这些都可以直接拿来用,当然也很值得借鉴一哦。

  1. CommandLineFormatter
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using UnityEngine;

namespace UnityEditor.Scripting.Compilers
{
    internal static class CommandLineFormatter
    {
        public static string EscapeCharsQuote(string input)
        {
            string result;
            if (input.IndexOf('\'') == -1)
            {
                result = "'" + input + "'";
            }
            else if (input.IndexOf('"') == -1)
            {
                result = "\"" + input + "\"";
            }
            else
            {
                result = null;
            }
            return result;
        }

        public static string PrepareFileName(string input)
        {
            input = FileUtil.ResolveSymlinks(input);
            string result;
            if (Application.platform == RuntimePlatform.WindowsEditor)
            {
                result = CommandLineFormatter.EscapeCharsWindows(input);
            }
            else
            {
                result = CommandLineFormatter.EscapeCharsQuote(input);
            }
            return result;
        }

        public static string EscapeCharsWindows(string input)
        {
            string result;
            if (input.Length == 0)
            {
                result = "\"\"";
            }
            else if (CommandLineFormatter.UnescapeableChars.IsMatch(input))
            {
                Debug.LogWarning("Cannot escape control characters in string");
                result = "\"\"";
            }
            else if (CommandLineFormatter.UnsafeCharsWindows.IsMatch(input))
            {
                result = "\"" + CommandLineFormatter.Quotes.Replace(input, "\"\"") + "\"";
            }
            else
            {
                result = input;
            }
            return result;
        }

        internal static string GenerateResponseFile(IEnumerable arguments)
        {
            string uniqueTempPathInProject = FileUtil.GetUniqueTempPathInProject();
            using (StreamWriter streamWriter = new StreamWriter(uniqueTempPathInProject))
            {
                foreach (string value in from a in arguments
                where a != null
                select a)
                {
                    streamWriter.WriteLine(value);
                }
            }
            return uniqueTempPathInProject;
        }

        private static readonly Regex UnsafeCharsWindows = new Regex("[^A-Za-z0-9_\\-\\.\\:\\,\\/\\@\\\\]");

        private static readonly Regex UnescapeableChars = new Regex("[\\x00-\\x08\\x10-\\x1a\\x1c-\\x1f\\x7f\\xff]");

        private static readonly Regex Quotes = new Regex("\"");
    }
}
  1. 上述脚本的使用场景:ManagedProgram
using System;
using System.Diagnostics;
using System.IO;
using UnityEditor.Scripting.Compilers;
using UnityEditor.Utils;
using UnityEngine;

namespace UnityEditor.Scripting
{
    internal class ManagedProgram : Program //继承 Program
    {
        public ManagedProgram(string monodistribution, string profile, string executable, string arguments, Action setupStartInfo) : this(monodistribution, profile, executable, arguments, true, setupStartInfo){}
        public ManagedProgram(string monodistribution, string profile, string executable, string arguments, bool setMonoEnvironmentVariables, Action setupStartInfo)
        {
            string text = ManagedProgram.PathCombine(new string[]
            {
                monodistribution,
                "bin",
                "mono"
            });
            if (Application.platform == RuntimePlatform.WindowsEditor)
            {
                text = CommandLineFormatter.PrepareFileName(text + ".exe"); //装载 exe 程序路径
            }
            ProcessStartInfo processStartInfo = new ProcessStartInfo 
            {
                                //配置带参数启动的命令行参数
                Arguments = CommandLineFormatter.PrepareFileName(executable) + " " + arguments, 
                CreateNoWindow = true,
                FileName = text, 
                RedirectStandardError = true,
                RedirectStandardOutput = true,
                WorkingDirectory = Application.dataPath + "/..", //设置工作目录
                UseShellExecute = false
            };
            if (setMonoEnvironmentVariables)
            {
                string value = ManagedProgram.PathCombine(new string[]
                {
                    monodistribution,
                    "lib",
                    "mono",
                    profile
                });
                processStartInfo.EnvironmentVariables["MONO_PATH"] = value; //按需设置环境变量
                processStartInfo.EnvironmentVariables["MONO_CFG_DIR"] = ManagedProgram.PathCombine(new string[]
                {
                    monodistribution,
                    "etc"
                });
            }
            if (setupStartInfo != null)
            {
                setupStartInfo(processStartInfo); // 进程信息配置完成的回调
            }
            this._process.StartInfo = processStartInfo;
        }

        private static string PathCombine(params string[] parts)
        {
            string text = parts[0];
            for (int i = 1; i < parts.Length; i++)
            {
                text = Path.Combine(text, parts[i]);
            }
            return text;
        }
    }
}
Tips : 更上层的调用见:**UnityEditor.Scripting.Compilers.ScriptCompilerBase**

杂项

  1. 可以发现在 Windows 下Unity 是调用了 mcs.exe 进行的脚本编译。


    [Unity 3d] 读源码,看 Unity 对带参数命令行启动逻辑的封装_第2张图片
  2. 并不是所有遇到的 API 都能在官方 Manual 找到,譬如这个 EditorUtility.CompileCsharp
  3. 博客中提供的 Demo 现在测试时会报错 【CS0518 】:
    [Unity 3d] 读源码,看 Unity 对带参数命令行启动逻辑的封装_第3张图片
  4. 经过谷歌,找到解决方案:Compiler Error CS0518 | Microsoft Docs
    亦即是:添加上对 mscorlib.dll 的引用即可。
  5. 经过追源码,这个 EditorUtility.CompileCsharp 中ApiCompatibilityLevel
    固化为ApiCompatibilityLevel.NET_2_0_Subset,这一点需要留意,因为笔者目前遇到的情况表明子集缺少一些API,包含但可能不限于部分 IO 和 Regex 相关的API。


    [Unity 3d] 读源码,看 Unity 对带参数命令行启动逻辑的封装_第4张图片

扩展阅读

  • Unity 使用 Unity 直接编译外部 DLL - 无幻 - CSDN博客
  • [Unity 3d] uDllExporter (Dll导出工具) - GitHub -
  • 利用Unity3D 打包dll工具箱,只需一步~ -

你可能感兴趣的:([Unity 3d] 读源码,看 Unity 对带参数命令行启动逻辑的封装)