Skip to content

Commit 3455e67

Browse files
committed
Merge branch 'palliate-assembly-loading-errors' into 'main'
Improvements to extension loading See merge request Sharpmake/sharpmake!545
2 parents 7a5e318 + 3d5d3ba commit 3455e67

File tree

4 files changed

+75
-43
lines changed

4 files changed

+75
-43
lines changed

Sharpmake.Application/Program.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
using System.Linq;
1111
using System.Reflection;
1212
using System.Runtime.InteropServices;
13+
using System.Runtime.Loader;
1314
using System.Threading;
1415
using System.Threading.Tasks;
1516
using Sharpmake.Generators;
@@ -438,7 +439,7 @@ private static void LogSharpmakeExtensionLoaded(Assembly extensionAssembly)
438439
return;
439440

440441
GetAssemblyInfo(extensionAssembly, out var extensionName, out var _, out var extensionVersion, out var extensionLocation);
441-
LogWriteLine(" {0} {1} loaded from '{2}'", extensionName, extensionVersion, extensionLocation);
442+
LogWriteLine(" {0} {1} loaded from '{2}' in assembly load context '{3}'", extensionName, extensionVersion, extensionLocation, AssemblyLoadContext.GetLoadContext(extensionAssembly).Name);
442443
}
443444

444445
private static void CreateBuilderAndGenerate(BuildContext.BaseBuildContext buildContext, Argument parameters, bool generateDebugSolution)

Sharpmake/BuilderExtension.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System;
55
using System.Linq;
66
using System.Reflection;
7+
using System.Runtime.Loader;
78

89
namespace Sharpmake
910
{
@@ -40,7 +41,7 @@ private void AppDomain_AssemblyLoad(object sender, AssemblyLoadEventArgs args)
4041
/// <param name="extensionAssembly">The <see cref="Assembly"/> to scan.</param>
4142
private void RegisterExtensionAssembly(Assembly extensionAssembly)
4243
{
43-
if (extensionAssembly.ReflectionOnly)
44+
if (ExtensionLoader.IsTempAssembly(extensionAssembly))
4445
return;
4546

4647
// Ignores if the assembly does not declare itself as a Sharpmake extension.

Sharpmake/ExtensionLoader.cs

Lines changed: 70 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@
22
// Licensed under the Apache 2.0 License. See LICENSE.md in the project root for license information.
33

44
using System;
5+
using System.Collections.Concurrent;
56
using System.Collections.Generic;
67
using System.IO;
8+
using System.Linq;
79
using System.Reflection;
8-
using Microsoft.CodeAnalysis.CSharp.Syntax;
10+
using System.Runtime.Loader;
911

1012
namespace Sharpmake
1113
{
@@ -19,44 +21,14 @@ namespace Sharpmake
1921
/// </remarks>
2022
public class ExtensionLoader : IDisposable
2123
{
24+
public const string TempContextNamePrefix = "Temp extension check AssemblyLoadContext";
2225
public class ExtensionChecker : MarshalByRefObject
2326
{
24-
private readonly Dictionary<string, bool> _loadedAssemblies = new Dictionary<string, bool>(StringComparer.OrdinalIgnoreCase);
25-
2627
public bool IsSharpmakeExtension(string assemblyPath)
2728
{
28-
if (assemblyPath == null)
29-
throw new ArgumentNullException(nameof(assemblyPath));
30-
if (!File.Exists(assemblyPath))
31-
throw new FileNotFoundException("Cannot find the assembly DLL.", assemblyPath);
32-
33-
lock (_loadedAssemblies)
34-
{
35-
// If the assembly has already been loaded, check if it is.
36-
bool result;
37-
if (_loadedAssemblies.TryGetValue(assemblyPath, out result))
38-
return result;
39-
40-
try
41-
{
42-
Assembly assembly = Assembly.LoadFrom(assemblyPath);
43-
result = IsSharpmakeExtension(assembly);
44-
}
45-
catch (BadImageFormatException)
46-
{
47-
// This is either a native C/C++ assembly, or there is a x86/x64 mismatch
48-
// that prevents it to load. Sharpmake platforms have no reason to not be
49-
// AnyCPU so just assume that it's not a Sharpmake extension.
50-
result = false;
51-
}
52-
catch (Exception ex)
53-
{
54-
throw new Error("An unexpected error has occurred while loading a potential Sharpmake extension assembly: {0}", ex.Message);
55-
}
56-
57-
_loadedAssemblies.Add(assemblyPath, result);
58-
return result;
59-
}
29+
ExtensionLoader extensionLoader = new();
30+
bool result = extensionLoader.LoadExtension(assemblyPath) != null;
31+
return result;
6032
}
6133

6234
public static bool IsSharpmakeExtension(Assembly assembly)
@@ -68,6 +40,12 @@ public static bool IsSharpmakeExtension(Assembly assembly)
6840
}
6941
}
7042

43+
public static bool IsTempAssembly(Assembly assembly)
44+
{
45+
return AssemblyLoadContext.GetLoadContext(assembly)?.Name?.StartsWith(ExtensionLoader.TempContextNamePrefix) ?? false;
46+
}
47+
48+
private static ConcurrentDictionary<string, byte> _nonExtensionAssemblyPaths = new();
7149
/// <summary>
7250
/// Releases the remote <see cref="AppDomain"/> if one was created.
7351
/// </summary>
@@ -92,23 +70,75 @@ public Assembly LoadExtension(string assemblyPath, bool fastLoad)
9270
/// Loads a Sharpmake extension assembly.
9371
/// </summary>
9472
/// <param name="assemblyPath">The path of the assembly that contains the Sharpmake extension.</param>
95-
/// <returns>The loaded extension's <see cref="Assembly"/>.</returns>
73+
/// <returns>The loaded extension's <see cref="Assembly"/> or null if it's not a Sharpmake extension.</returns>
9674
public Assembly LoadExtension(string assemblyPath)
9775
{
9876
if (assemblyPath == null)
9977
throw new ArgumentNullException(nameof(assemblyPath));
10078

101-
try
79+
if (!File.Exists(assemblyPath))
80+
throw new FileNotFoundException("Cannot find the assembly DLL.", assemblyPath);
81+
82+
if (_nonExtensionAssemblyPaths.Keys.Contains(assemblyPath))
83+
return null;
84+
85+
// If we've already loaded the assembly in some loading context, check the attributes without loading it again
86+
foreach (AssemblyLoadContext alc in AssemblyLoadContext.All)
10287
{
103-
Assembly assembly = Assembly.LoadFrom(assemblyPath);
104-
if (!ExtensionChecker.IsSharpmakeExtension(assembly))
88+
Assembly assembly = alc.Assemblies.FirstOrDefault(a => AssemblyHasSamePath(a, assemblyPath));
89+
if (assembly != null)
90+
{
91+
if (ExtensionChecker.IsSharpmakeExtension(assembly))
92+
return assembly;
93+
94+
_nonExtensionAssemblyPaths.TryAdd(assemblyPath, 0);
95+
10596
return null;
106-
return assembly;
97+
}
98+
}
99+
100+
// Check the attributes via an unloadable context (reflexion only is not a thing anymore in .net 6)
101+
var contextName = $"{TempContextNamePrefix} - {Path.GetFileNameWithoutExtension(assemblyPath)}";
102+
AssemblyLoadContext tempLoadContext = new(contextName, true);
103+
104+
bool isSharpmakeExtension;
105+
106+
try
107+
{
108+
var tempAssembly = tempLoadContext.LoadFromAssemblyPath(assemblyPath);
109+
isSharpmakeExtension = ExtensionChecker.IsSharpmakeExtension(tempAssembly);
110+
// log unloading of contexts, only log info for Sharpmake extensions
111+
Action<string, object[]> logger = isSharpmakeExtension ? Builder.Instance.LogWriteLine : Builder.Instance.DebugWriteLine;
112+
tempLoadContext.Unloading += (context) => logger($" [ExtensionLoader] Attempting to unload temporary context {context.Name}", new object[]{});
107113
}
108114
catch (BadImageFormatException)
109115
{
116+
// This is either a native C/C++ assembly, or there is a x86/x64 mismatch
117+
// that prevents it to load. Sharpmake platforms have no reason to not be
118+
// AnyCPU so just assume that it's not a Sharpmake extension.
110119
return null;
111120
}
121+
finally
122+
{
123+
// Unload the AssemblyLoadContext
124+
// NOTE: the unloading event is fired on calling unload for the context, the assembly might not be unloaded
125+
// if there are references to it (make sure it is not used on temp assembly loading)
126+
tempLoadContext.Unload();
127+
}
128+
129+
if (isSharpmakeExtension)
130+
return AssemblyLoadContext.Default.LoadFromAssemblyPath(assemblyPath);
131+
132+
_nonExtensionAssemblyPaths.TryAdd(assemblyPath, 0);
133+
134+
return null;
135+
136+
static bool AssemblyHasSamePath(Assembly assembly, string assemblyPath)
137+
{
138+
return assembly != null
139+
&& !string.IsNullOrEmpty(assemblyPath) && !assembly.IsDynamic &&
140+
string.Equals(assembly.Location, assemblyPath, StringComparison.Ordinal);
141+
}
112142
}
113143

114144
[Obsolete("This can't work on .net 6. Dead code", error: true)]

Sharpmake/PlatformRegistry.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ public static void RegisterExtensionAssembly(Assembly extensionAssembly)
104104
if (extensionAssembly == null)
105105
throw new ArgumentNullException(nameof(extensionAssembly));
106106

107-
if (extensionAssembly.ReflectionOnly)
107+
if (ExtensionLoader.IsTempAssembly(extensionAssembly))
108108
return;
109109

110110
// Don't support loading dynamically compiled assemblies

0 commit comments

Comments
 (0)