为Unity参考程序集添加实现和注释

前言

在Unity开发过程中,经常会有需要查看其他程序集的源码。但是如果使用VSCode作为外部IDE,按下F12跳转后看到的结果是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#region Assembly mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
// F:\Program Files\Unity\2022.3.62f2c1\Editor\Data\UnityReferenceAssemblies\unity-4.8-api\mscorlib.dll
#endregion

using System.Collections.Generic;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Runtime.Serialization;
using System.Security;

namespace System.Text
{
[ComVisible(true)]
[DefaultMember("Chars")]
public sealed class StringBuilder : ISerializable
{
public StringBuilder();
public StringBuilder(int capacity);
public StringBuilder(string value);
...

可以看到,只有公开的API而没有真正的实现,并且没有接口注释,即使在设置中开启了 Enable Decompilation Support选项也是如此。该如何可以直接通过VSCode查看实现呢?

解决

替换实现

可以注意到,文件顶部有一个链接: F:\Program Files\Unity\2022.3.62f2c1\Editor\Data\UnityReferenceAssemblies\unity-4.8-api\mscorlib.dll,说明这是反编译所使用的dll,而这个dll路径与VSCode插件所生成的 .csproj中引用项路径是一致的。

根据Reference assemblies - .NET | Microsoft Learn可以得知,这应该就是一个参考程序集(Reference Assembly)。在编译过程中使用参考程序集可以不依赖于具体的实现,只需要API一致即可编译(比如多平台的可能实现会有所不同),同时还能加快编译速度。

至于为什么Visual Studio可以正确查看实现,猜测是VSTU对此做了适配处理从而可以查看相应的实现。可惜VSCode上的Unity插件还有很多功能都没有适配,于是一种可行的方案就是直接把参考程序集替换成实际的实现,反正在开发时,编辑器本身所在的平台肯定是确定的。通过如下代码可以找到编辑器中编译所使用的真实程序集路径:

1
Debug.Log($"Assembly Location: {typeof(System.Object).Assembly.Location}");

打印结果如下:

1
Assembly Location: F:\Program Files\Unity\2022.3.62f2c1\Editor\Data\MonoBleedingEdge\lib\mono\unityjit-win32\mscorlib.dll

理论上我们只需要找到 {Unity安装路径}\Assembly Location: F:\Program Files\Unity\2022.3.62f2c1\Editor\Data\MonoBleedingEdge\lib\mono\unityjit-win32\路径下的所有dll,然后直接覆盖 {Unity安装路径}\Editor\Data\UnityReferenceAssemblies\unity-4.8-api\里的所有dll就可以反编译看到实现。

我们试一下替换掉 mscorlib.dll(需要关闭VSCode),然后F12看看:

1
2
3
4
5
6
7
8
9
10
11
12
//
// Summary:
// Represents a mutable string of characters. This class cannot be inherited.
[Serializable]
[StructLayout(LayoutKind.Sequential)]
public sealed class StringBuilder : ISerializable
{
internal char[] m_ChunkChars;

internal StringBuilder m_ChunkPrevious;

internal int m_ChunkLength;

实践符合理论,现在可以看到API注释和私有的实现了。

补充注释

现在还有一个小问题,比如下面这个方法却没有API的注释了:

1
public bool Equals(ReadOnlySpan<char> span)

Unity文档可以了解到,所谓.Net Framework其实是 .NET Framework 4.8.NET Standard 2.1的并集,而此项目Api Compatibilty Level设置的就是 .NET Framework

关于参考程序集的生成

Editor\Data\UnityReferenceAssemblies文件夹里有个README.txt,其中这样写道:

These are the reference assemblies used for the .NET Framework API Compatibility Level option in Unity.

They consist of the union of the .NET Framework 4.8 API and the .NET Standard 2.1 API.

我们可以从 netstandard.xml中找到这个方法的注释:

1
2
3
4
5
6
<member name="M:System.Text.StringBuilder.Equals(System.ReadOnlySpan{System.Char})">
<summary>Returns a value indicating whether the characters in this instance are equal to the characters in a specified read-only character span.</summary>
<param name="span">The character span to compare with the current instance.</param>
<returns>
<see langword="true" /> if the characters in this instance and <paramref name="span" /> are the same; otherwise, <see langword="false" />.</returns>
</member>

于是我们还需要合并一下xml注释文件:

  • .NET Framework 4.8的注释文件位于 C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.8(需要下载.NET Framework 4.8 Developer Pack
  • .NET Standard 2.1的注释文件位于 C:\Program Files (x86)\dotnet\packs\NETStandard.Library.Ref\2.1.0\ref\netstandard2.1\netstandard.xml(安装3.0以上的.NET Core就有)

合并步骤很简单:

  1. 加载所有 .NET Framework 4.8.NET Standard 2.1注释文件
  2. 遍历 mscorlib.dllSystem开头的dll,如果发现缺少 .NET Framework 4.8注释,则使用 .NET Standard 2.1中的注释补充

用C#进行实现时会发现加载程序集不能直接加载,否则会被自动重定向到已经加载的程序集,从而无法正确获取其中的成员。理论上可以使用 Assembly.ReflectionOnlyLoad,但是此接口已经被废弃,可以使用 System.Reflection.MetadataLoadContext包的 MetadataLoadContext进行加载。

完整代码

注意:项目Api Compatibilty Level设置的是 .NET Framework,如果是 .NET Standard 2.1的话,会发现参考程序集路径是 {Unity安装路径}\Editor\Data\NetStandard\ref\2.1.0\。其中只有一个 netstandard.dll,其具体实现与.NET平台有关,比如.NET Framework平台是 mscorlib.dll,而.NET Core平台则是 System.Private.CoreLib.dll。这样就不能通过简单的替换文件来查看实现了,目前还没找到可行的方案。

下面的代码依赖于System.Reflection.MetadataLoadContext包。

AI给出的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
using System;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Xml.Linq;
using System.Collections.Generic;

namespace UnityBCLGen
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Unity BCL Generator");
Console.WriteLine("==================");
Console.WriteLine();

// 1. 输入Unity安装路径
Console.Write("请输入Unity安装路径 (例如: F:\\Program Files\\Unity\\2022.3.62f2c1): ");
string unityPath = Console.ReadLine();

if (string.IsNullOrWhiteSpace(unityPath))
{
Console.WriteLine("错误: Unity路径不能为空");
return;
}

// 移除末尾的反斜杠
unityPath = unityPath.TrimEnd('\\', '/');

// 验证路径是否存在
if (!Directory.Exists(unityPath))
{
Console.WriteLine($"错误: Unity路径不存在: {unityPath}");
return;
}

string editorDataPath = Path.Combine(unityPath, "Editor", "Data");
if (!Directory.Exists(editorDataPath))
{
Console.WriteLine($"错误: 未找到Editor\\Data文件夹: {editorDataPath}");
return;
}

Console.WriteLine($"Unity路径: {unityPath}");
Console.WriteLine();

try
{
// 2. 备份参考api文件夹
BackupReferenceAssemblies(editorDataPath);

// 3. 替换参考程序集为实际程序集
ReplaceReferenceAssemblies(editorDataPath);

// 4. 添加.NET Framework 4.8程序集注释xml
AddDotNetFrameworkXmlDocs(editorDataPath);

// 5. 补充netstandard 2.1程序集注释xml
SupplementNetStandardXmlDocs(editorDataPath);

// 6. 完成提示
Console.WriteLine();
Console.WriteLine("==================");
Console.WriteLine("完成,请重新启动IDE");
Console.WriteLine("==================");
}
catch (Exception ex)
{
Console.WriteLine($"错误: {ex.Message}");
Console.WriteLine($"详细信息: {ex.StackTrace}");
}
}

static void BackupReferenceAssemblies(string editorDataPath)
{
Console.WriteLine("步骤 1/4: 备份参考api文件夹...");

string apiPath = Path.Combine(editorDataPath, "UnityReferenceAssemblies", "unity-4.8-api");
string backupPath = Path.Combine(editorDataPath, "UnityReferenceAssemblies", "unity-4.8-api - 副本");

if (!Directory.Exists(apiPath))
{
throw new DirectoryNotFoundException($"未找到unity-4.8-api文件夹: {apiPath}");
}

// 如果备份已存在,使用备份恢复原文件夹
if (Directory.Exists(backupPath))
{
Console.WriteLine(" 检测到已存在备份,正在用备份恢复原文件夹...");
Directory.Delete(apiPath, true);
CopyDirectory(backupPath, apiPath);
Console.WriteLine(" 已从备份恢复原文件夹");
}
else
{
Console.WriteLine($" 正在备份到: {backupPath}");
CopyDirectory(apiPath, backupPath);
Console.WriteLine(" 备份完成");
}
Console.WriteLine();
}

static void ReplaceReferenceAssemblies(string editorDataPath)
{
Console.WriteLine("步骤 2/4: 替换参考程序集为实际程序集...");

string sourcePath = Path.Combine(editorDataPath, "MonoBleedingEdge", "lib", "mono", "unityjit-win32");
string targetPath = Path.Combine(editorDataPath, "UnityReferenceAssemblies", "unity-4.8-api");

if (!Directory.Exists(sourcePath))
{
throw new DirectoryNotFoundException($"未找到unityjit-win32文件夹: {sourcePath}");
}

Console.WriteLine($" 源路径: {sourcePath}");
Console.WriteLine($" 目标路径: {targetPath}");

// 计算总文件数
int totalFiles = Directory.GetFiles(sourcePath, "*", SearchOption.AllDirectories).Length;
Console.WriteLine($" 正在复制 {totalFiles} 个文件(包含子文件夹)...");

// 递归复制所有文件和子文件夹
CopyDirectoryContents(sourcePath, targetPath);

Console.WriteLine(" 替换完成");
Console.WriteLine();
}

static void CopyDirectoryContents(string sourceDir, string targetDir)
{
// 复制当前目录的所有文件
foreach (var file in Directory.GetFiles(sourceDir))
{
string fileName = Path.GetFileName(file);
string destFile = Path.Combine(targetDir, fileName);
File.Copy(file, destFile, true);
}

// 递归复制所有子目录
foreach (var directory in Directory.GetDirectories(sourceDir))
{
string dirName = Path.GetFileName(directory);
string destDir = Path.Combine(targetDir, dirName);

// 确保目标目录存在
Directory.CreateDirectory(destDir);

// 递归复制子目录内容
CopyDirectoryContents(directory, destDir);
}
}

static void AddDotNetFrameworkXmlDocs(string editorDataPath)
{
Console.WriteLine("步骤 3/4: 添加.NET Framework 4.8程序集注释xml...");

string dotNetPath = @"C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.8";
string targetPath = Path.Combine(editorDataPath, "UnityReferenceAssemblies", "unity-4.8-api");

if (!Directory.Exists(dotNetPath))
{
Console.WriteLine(" 警告: 未找到.NET Framework 4.8路径");
Console.WriteLine(" 请安装.NET Framework 4.8");
Console.WriteLine(" 跳过此步骤");
Console.WriteLine();
return;
}

var xmlFiles = Directory.GetFiles(dotNetPath, "*.xml");
Console.WriteLine($" 找到 {xmlFiles.Length} 个XML文件");

foreach (var xmlFile in xmlFiles)
{
string fileName = Path.GetFileName(xmlFile);
string destFile = Path.Combine(targetPath, fileName);
File.Copy(xmlFile, destFile, true);
}

Console.WriteLine(" XML文件复制完成");
Console.WriteLine();
}

static void SupplementNetStandardXmlDocs(string editorDataPath)
{
Console.WriteLine("步骤 4/4: 补充netstandard 2.1程序集注释xml...");

string netStandardXmlPath = Path.Combine(editorDataPath, "NetStandard", "ref", "2.1.0", "netstandard.xml");
string apiPath = Path.Combine(editorDataPath, "UnityReferenceAssemblies", "unity-4.8-api");

// 如果Unity安装路径中没有netstandard.xml,尝试使用dotnet SDK中的版本
if (!File.Exists(netStandardXmlPath))
{
Console.WriteLine($" Unity路径中未找到netstandard.xml,尝试使用dotnet SDK版本...");
netStandardXmlPath = @"C:\Program Files (x86)\dotnet\packs\NETStandard.Library.Ref\2.1.0\ref\netstandard2.1\netstandard.xml";

if (!File.Exists(netStandardXmlPath))
{
Console.WriteLine($" 警告: 未找到netstandard.xml");
Console.WriteLine($" 已尝试路径:");
Console.WriteLine($" - {Path.Combine(editorDataPath, "NetStandard", "ref", "2.1.0", "netstandard.xml")}");
Console.WriteLine($" - {netStandardXmlPath}");
Console.WriteLine(" 跳过此步骤");
Console.WriteLine();
return;
}

Console.WriteLine($" 找到dotnet SDK中的netstandard.xml");
}

Console.WriteLine(" 正在加载netstandard.xml...");
XDocument netStandardDoc = XDocument.Load(netStandardXmlPath);
var netStandardMembers = ParseXmlMembers(netStandardDoc);
Console.WriteLine($" netstandard.xml中找到 {netStandardMembers.Count} 个成员注释");

// 处理mscorlib.dll和所有System开头的dll
var dllFiles = Directory.GetFiles(apiPath, "*.dll")
.Where(f =>
{
string name = Path.GetFileNameWithoutExtension(f);
return name == "mscorlib" || name.StartsWith("System");
})
.ToList();

Console.WriteLine($" 找到 {dllFiles.Count} 个需要处理的程序集");

int totalSupplemented = 0;

// 使用MetadataLoadContext来加载所有程序集
try
{
// 创建MetadataLoadContext,使用api路径下的所有dll作为解析器
var allDlls = Directory.GetFiles(apiPath, "*.dll");
var resolver = new PathAssemblyResolver(allDlls);
using var mlc = new MetadataLoadContext(resolver);

foreach (var dllFile in dllFiles)
{
string dllName = Path.GetFileNameWithoutExtension(dllFile);
string xmlFile = Path.Combine(apiPath, dllName + ".xml");

Console.WriteLine($" 处理: {dllName}.dll");

try
{
int supplemented = SupplementXmlForAssembly(mlc, dllFile, xmlFile, netStandardMembers);
totalSupplemented += supplemented;
if (supplemented > 0)
{
Console.WriteLine($" 补充了 {supplemented} 个成员注释");
}
}
catch (Exception ex)
{
Console.WriteLine($" 警告: 处理失败 - {ex.Message}");
}
}
}
catch (Exception ex)
{
Console.WriteLine($" 错误: 无法创建MetadataLoadContext - {ex.Message}");
}

Console.WriteLine($" 总计补充了 {totalSupplemented} 个成员注释");
Console.WriteLine();
}

static Dictionary<string, XElement> ParseXmlMembers(XDocument xmlDoc)
{
var members = new Dictionary<string, XElement>();
var membersElement = xmlDoc.Root?.Element("members");

if (membersElement != null)
{
foreach (var member in membersElement.Elements("member"))
{
var nameAttr = member.Attribute("name");
if (nameAttr != null)
{
members[nameAttr.Value] = member;
}
}
}

return members;
}

static int SupplementXmlForAssembly(MetadataLoadContext mlc, string dllFile, string xmlFile, Dictionary<string, XElement> netStandardMembers)
{
// 使用MetadataLoadContext加载程序集
Assembly assembly;
try
{
assembly = mlc.LoadFromAssemblyPath(dllFile);
}
catch (Exception ex)
{
Console.WriteLine($" 警告: 无法加载 {Path.GetFileName(dllFile)} - {ex.Message}");
return 0;
}

// 加载或创建XML文档
XDocument xmlDoc;
if (File.Exists(xmlFile))
{
xmlDoc = XDocument.Load(xmlFile);
}
else
{
xmlDoc = new XDocument(
new XElement("doc",
new XElement("assembly",
new XElement("name", Path.GetFileNameWithoutExtension(dllFile))
),
new XElement("members")
)
);
}

var existingMembers = ParseXmlMembers(xmlDoc);
var membersElement = xmlDoc.Root?.Element("members");
if (membersElement == null)
{
return 0;
}

int supplementedCount = 0;

// 遍历程序集中的所有类型
try
{
foreach (var type in assembly.GetTypes())
{
supplementedCount += SupplementTypeMembers(type, existingMembers, netStandardMembers, membersElement);
}
}
catch (ReflectionTypeLoadException ex)
{
// 有些类型可能无法加载,但我们可以处理已加载的类型
if (ex.Types != null)
{
foreach (var type in ex.Types)
{
if (type != null)
{
try
{
supplementedCount += SupplementTypeMembers(type, existingMembers, netStandardMembers, membersElement);
}
catch { }
}
}
}
}

// 保存XML文档
if (supplementedCount > 0)
{
xmlDoc.Save(xmlFile);
}

return supplementedCount;
}

static int SupplementTypeMembers(Type type, Dictionary<string, XElement> existingMembers,
Dictionary<string, XElement> netStandardMembers, XElement membersElement)
{
int count = 0;

// 类型本身
count += SupplementMember("T:" + type.FullName, existingMembers, netStandardMembers, membersElement);

// 字段
foreach (var field in type.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance))
{
string memberId = "F:" + type.FullName + "." + field.Name;
count += SupplementMember(memberId, existingMembers, netStandardMembers, membersElement);
}

// 属性
foreach (var property in type.GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance))
{
string memberId = "P:" + type.FullName + "." + property.Name;
count += SupplementMember(memberId, existingMembers, netStandardMembers, membersElement);
}

// 方法
foreach (var method in type.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance))
{
if (method.IsSpecialName) continue; // 跳过属性访问器等特殊方法

string memberId = GetMethodMemberId(method);
count += SupplementMember(memberId, existingMembers, netStandardMembers, membersElement);
}

// 构造函数
foreach (var constructor in type.GetConstructors(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance))
{
string memberId = GetConstructorMemberId(constructor);
count += SupplementMember(memberId, existingMembers, netStandardMembers, membersElement);
}

// 事件
foreach (var evt in type.GetEvents(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance))
{
string memberId = "E:" + type.FullName + "." + evt.Name;
count += SupplementMember(memberId, existingMembers, netStandardMembers, membersElement);
}

return count;
}

static int SupplementMember(string memberId, Dictionary<string, XElement> existingMembers,
Dictionary<string, XElement> netStandardMembers, XElement membersElement)
{
// 如果已存在注释,不需要补充
if (existingMembers.ContainsKey(memberId))
{
return 0;
}

// 如果netstandard中有这个成员的注释,则添加
if (netStandardMembers.TryGetValue(memberId, out var memberElement))
{
var newMember = new XElement(memberElement);
membersElement.Add(newMember);
existingMembers[memberId] = newMember;
return 1;
}

return 0;
}

static string GetMethodMemberId(MethodInfo method)
{
string memberId = "M:" + method.DeclaringType?.FullName + "." + method.Name;

var parameters = method.GetParameters();
if (parameters.Length > 0)
{
memberId += "(" + string.Join(",", parameters.Select(p => GetTypeName(p.ParameterType))) + ")";
}

return memberId;
}

static string GetConstructorMemberId(ConstructorInfo constructor)
{
string memberId = "M:" + constructor.DeclaringType?.FullName + ".#ctor";

var parameters = constructor.GetParameters();
if (parameters.Length > 0)
{
memberId += "(" + string.Join(",", parameters.Select(p => GetTypeName(p.ParameterType))) + ")";
}

return memberId;
}

static string GetTypeName(Type type)
{
if (type.IsByRef)
{
return GetTypeName(type.GetElementType()!) + "@";
}

if (type.IsGenericType)
{
string name = type.GetGenericTypeDefinition().FullName;
int backtickIndex = name.IndexOf('`');
if (backtickIndex > 0)
{
name = name.Substring(0, backtickIndex);
}
name += "{" + string.Join(",", type.GetGenericArguments().Select(GetTypeName)) + "}";
return name;
}

if (type.IsArray)
{
return GetTypeName(type.GetElementType()!) + "[]";
}

return type.FullName ?? type.Name;
}

static void CopyDirectory(string sourceDir, string targetDir)
{
Directory.CreateDirectory(targetDir);

// 复制所有文件
foreach (var file in Directory.GetFiles(sourceDir))
{
string fileName = Path.GetFileName(file);
string destFile = Path.Combine(targetDir, fileName);
File.Copy(file, destFile, true);
}

// 递归复制所有子目录
foreach (var directory in Directory.GetDirectories(sourceDir))
{
string dirName = Path.GetFileName(directory);
string destDir = Path.Combine(targetDir, dirName);
CopyDirectory(directory, destDir);
}
}
}
}