在本文,笔者将带大家从 EditorUtility.CompileCsharp API 出发,看一看Unity 是怎么使用 带参数命令行调用软件的。行文仓促内容繁杂故而较为佛系,另笔者技术有限难免疏漏,见谅~
前言
昨天看到一篇 博客 ,讲述了如何将Unity 编辑器之外的脚本通过 EditorUtility.CompileCsharp 在编辑器下编译成 Dll 。
这引起了笔者的不少兴趣,于是拿起 Dnspy 沿着这个方法名 CompileCsharp
就追了进去...
这些与脚本编译相关的逻辑居然不是调用的 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 常规数据和异常输出 的方法。
其他
- 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
啦。这些都可以直接拿来用,当然也很值得借鉴一哦。
- 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("\"");
}
}
- 上述脚本的使用场景: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**
杂项
-
可以发现在 Windows 下Unity 是调用了 mcs.exe 进行的脚本编译。
- 并不是所有遇到的 API 都能在官方 Manual 找到,譬如这个 EditorUtility.CompileCsharp
- 博客中提供的 Demo 现在测试时会报错 【CS0518 】:
- 经过谷歌,找到解决方案:Compiler Error CS0518 | Microsoft Docs
亦即是:添加上对 mscorlib.dll 的引用即可。 -
经过追源码,这个 EditorUtility.CompileCsharp 中ApiCompatibilityLevel
固化为ApiCompatibilityLevel.NET_2_0_Subset,这一点需要留意,因为笔者目前遇到的情况表明子集缺少一些API,包含但可能不限于部分 IO 和 Regex 相关的API。
扩展阅读
- Unity 使用 Unity 直接编译外部 DLL - 无幻 - CSDN博客
- [Unity 3d] uDllExporter (Dll导出工具) - GitHub -
- 利用Unity3D 打包dll工具箱,只需一步~ -