Skip to content

Commit 125469b

Browse files
Dangercoderdmiller
authored andcommitted
Fix assembly loading for .NET Core/5+ and add tests
This commit comprehensively fixes type resolution issues in .NET Core/.NET 5+ environments and adds tests to verify the functionality. Key improvements: 1. Type Resolution (classForName): - Add DependencyContext for runtime assembly discovery - Implement multi-tier resolution strategy with clear priorities - Fix loading of types from NuGet packages - Handle framework types in separate assemblies (e.g. System.Collections.Concurrent) - Load shared runtime libraries via TrustedPlatformAssemblies property 2. Shared Runtime Library Loading: - Use AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES") to access shared .NET runtime - Includes assemblies from shared frameworks 3. Fix duplicate type handling in CreateDefaultImportDictionary: - Handle cases where the same type name appears in multiple assemblies - Prevents ArgumentException during RT initialization in .NET Core/5+ 4. Dependencies: - Add Microsoft.Extensions.DependencyModel package for assembly discovery 5. Tests: - Verification of TRUSTED_PLATFORM_ASSEMBLIES availability - Loading types from shared runtime libraries (System.Collections.Concurrent, System.Text.Json, etc.) - Handling of duplicate type names across assemblies - Loading types from NuGet packages via DependencyContext This fixes long-standing issues with Clojure CLR not finding types in external assemblies.
1 parent 6773a46 commit 125469b

File tree

4 files changed

+283
-39
lines changed

4 files changed

+283
-39
lines changed

Clojure/Clojure/Clojure.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
<PackageReference Include="clojure.spec.alpha" Version="0.5.238" />
2323
<PackageReference Include="clojure.tools.reader" Version="1.5.0" />
2424
<PackageReference Include="DynamicLanguageRuntime" Version="1.3.5" />
25+
<PackageReference Include="Microsoft.Extensions.DependencyModel" Version="8.0.0" />
2526
</ItemGroup>
2627

2728

Clojure/Clojure/Lib/RT.cs

Lines changed: 172 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
using System.Text.RegularExpressions;
2626
using System.Threading;
2727
using RTProperties = clojure.runtime.Properties;
28+
using Microsoft.Extensions.DependencyModel;
2829

2930

3031
namespace clojure.lang
@@ -40,7 +41,19 @@ public static class RT
4041
static Dictionary<Symbol, Type> CreateDefaultImportDictionary()
4142
{
4243
var q = GetAllTypesInNamespace("System");
43-
var d = q.ToDictionary(keySelector: t => Symbol.intern(t.Name));
44+
// Handle duplicates by taking the first occurrence of each type name
45+
// This is necessary because in .NET Core/5+ the same type name can appear
46+
// in multiple assemblies (e.g., "Casing" appears in both System.Text.Json
47+
// and System.Private.CoreLib)
48+
var d = new Dictionary<Symbol, Type>();
49+
foreach (var type in q)
50+
{
51+
var symbol = Symbol.intern(type.Name);
52+
if (!d.ContainsKey(symbol))
53+
{
54+
d.Add(symbol, type);
55+
}
56+
}
4457

4558
// ADDED THESE TO SUPPORT THE BOOTSTRAPPING IN THE JAVA CORE.CLJ
4659
d.Add(Symbol.intern("StringBuilder"), typeof(StringBuilder));
@@ -2751,66 +2764,183 @@ public static string PrintToConsole(object x)
27512764

27522765
#region Locating types
27532766

2767+
// Cache for all runtime library assembly names, loaded once on demand.
2768+
private static readonly Lazy<List<AssemblyName>> _runtimeAssemblyNames = new Lazy<List<AssemblyName>>(() =>
2769+
{
2770+
var names = new List<AssemblyName>();
2771+
2772+
try
2773+
{
2774+
// DependencyContext.Default can be null in some scenarios (like unit tests or static initializers).
2775+
// Loading the context from a known assembly is more robust.
2776+
var entryAssembly = Assembly.GetEntryAssembly();
2777+
2778+
// If there's no entry assembly (e.g., when hosted in a non-standard way),
2779+
// fall back to the assembly that contains the RT class itself (Clojure.dll).
2780+
if (entryAssembly == null)
2781+
{
2782+
entryAssembly = typeof(RT).Assembly;
2783+
}
2784+
2785+
var context = Microsoft.Extensions.DependencyModel.DependencyContext.Load(entryAssembly);
2786+
2787+
if (context != null)
2788+
{
2789+
foreach (var lib in context.RuntimeLibraries)
2790+
{
2791+
foreach (var assembly in lib.RuntimeAssemblyGroups.SelectMany(g => g.AssetPaths))
2792+
{
2793+
try
2794+
{
2795+
var name = new AssemblyName(Path.GetFileNameWithoutExtension(assembly));
2796+
names.Add(name);
2797+
}
2798+
catch { }
2799+
}
2800+
}
2801+
}
2802+
}
2803+
catch { }
2804+
2805+
// Also include shared runtime libraries from TRUSTED_PLATFORM_ASSEMBLIES
2806+
try
2807+
{
2808+
var trustedAssemblies = AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES") as string;
2809+
if (!string.IsNullOrEmpty(trustedAssemblies))
2810+
{
2811+
var paths = trustedAssemblies.Split(Path.PathSeparator);
2812+
foreach (var path in paths)
2813+
{
2814+
try
2815+
{
2816+
var name = AssemblyName.GetAssemblyName(path);
2817+
if (!names.Any(n => n.Name == name.Name))
2818+
{
2819+
names.Add(name);
2820+
}
2821+
}
2822+
catch { }
2823+
}
2824+
}
2825+
}
2826+
catch { }
2827+
2828+
return names;
2829+
});
2830+
27542831
static readonly char[] _triggerTypeChars = new char[] { '`', ',', '[', '&' };
27552832

27562833
public static Type classForName(string p)
27572834
{
2758-
2759-
// This used to come later. Moved it up to the top for compiling definterface, e.g.
2760-
// (definterface IMyInterface ... )
2761-
// First compiled create IMyInterface classs, gets stored in the compiled-types map.
2762-
// Then eval'd so the we update the current environment and store the the eval-types map.
2763-
// However, definterface does an import, it picks up the version in the eval-types map.
2764-
// When a subsequent call tries to get IMyInterface, it was being found by Type.GetType.
2765-
// So code being compiled was picking up the eval'd version instead of the compiled version.
2766-
2835+
// First, check for types generated during the current compilation session.
27672836
Type t = Compiler.FindDuplicateType(p);
27682837
if (t != null)
2838+
{
27692839
return t;
2840+
}
27702841

2771-
// fastest path, will succeed for assembly qualified names (returned by Type.AssemblyQualifiedName)
2772-
// or namespace qualified names (returned by Type.FullName) in the executing assembly or mscorlib
2773-
// e.g. "UnityEngine.Transform, UnityEngine, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null"
2842+
// Try using Type.GetType which can find types with assembly-qualified names.
27742843
t = Type.GetType(p, false);
2775-
2776-
// Added the IsPublic check to deal with shadowed types in .Net Core,
2777-
// e.g. System.Environment in assemblies System.Private.CoreLib and System.Runtime.Exceptions.
2778-
// It is private in the former and public in the latter.
2779-
// Unfortunately, Type.GetType was finding the former.
27802844
if (t != null && (t.IsPublic || t.IsNestedPublic))
2845+
{
27812846
return t;
2847+
}
27822848

2849+
// Search through currently loaded assemblies.
2850+
AppDomain domain = AppDomain.CurrentDomain;
2851+
Assembly[] loadedAssemblies = domain.GetAssemblies();
2852+
2853+
// Search by namespace-qualified name in loaded assemblies.
2854+
foreach (Assembly assy in loadedAssemblies)
2855+
{
2856+
Type t1 = assy.GetType(p, false);
2857+
if (t1 != null && (t1.IsPublic || t1.IsNestedPublic))
2858+
{
2859+
return t1;
2860+
}
2861+
}
27832862

2863+
// Try to load from runtime libraries if we have DependencyContext.
2864+
var runtimeAssemblyNames = _runtimeAssemblyNames.Value;
2865+
if (runtimeAssemblyNames.Count > 0)
2866+
{
2867+
// Split the type name to identify the namespace and simple name.
2868+
string namespaceName = null;
2869+
string typeName = p;
2870+
int lastDot = p.LastIndexOf('.');
2871+
if (lastDot > 0)
2872+
{
2873+
namespaceName = p.Substring(0, lastDot);
2874+
typeName = p.Substring(lastDot + 1);
2875+
}
27842876

2877+
// Try to find and load the assembly that might contain this type.
2878+
foreach (var assemblyName in runtimeAssemblyNames)
2879+
{
2880+
// Skip if this assembly is already loaded.
2881+
if (loadedAssemblies.Any(a => a.GetName().Name == assemblyName.Name))
2882+
{
2883+
continue;
2884+
}
27852885

2786-
AppDomain domain = AppDomain.CurrentDomain;
2787-
Assembly[] assys = domain.GetAssemblies();
2788-
List<Type> candidateTypes = new();
2886+
// Try common patterns for framework assemblies.
2887+
if (namespaceName != null)
2888+
{
2889+
// Check if the assembly name matches the namespace pattern.
2890+
if (assemblyName.Name.StartsWith("System") && namespaceName.StartsWith("System"))
2891+
{
2892+
try
2893+
{
2894+
var assy = Assembly.Load(assemblyName);
2895+
var type = assy.GetType(p, false);
2896+
if (type != null && (type.IsPublic || type.IsNestedPublic))
2897+
{
2898+
return type;
2899+
}
2900+
}
2901+
catch { }
2902+
}
2903+
// Also try if the namespace directly matches the assembly name.
2904+
else if (assemblyName.Name.Equals(namespaceName, StringComparison.OrdinalIgnoreCase))
2905+
{
2906+
try
2907+
{
2908+
var assy = Assembly.Load(assemblyName);
2909+
var type = assy.GetType(p, false);
2910+
if (type != null && (type.IsPublic || type.IsNestedPublic))
2911+
{
2912+
return type;
2913+
}
2914+
}
2915+
catch { }
2916+
}
2917+
}
2918+
}
2919+
}
27892920

2790-
// fast path, will succeed for namespace qualified names (returned by Type.FullName)
2791-
// e.g. "UnityEngine.Transform"
2792-
foreach (Assembly assy in assys)
2921+
// Re-check loaded assemblies (some might have been loaded during the search).
2922+
loadedAssemblies = domain.GetAssemblies();
2923+
foreach (Assembly assy in loadedAssemblies)
27932924
{
27942925
Type t1 = assy.GetType(p, false);
27952926
if (t1 != null && (t1.IsPublic || t1.IsNestedPublic))
2927+
{
27962928
return t1;
2929+
}
27972930
}
27982931

2799-
// slow path, will succeed for display names (returned by Type.Name)
2800-
// e.g. "Transform"
2801-
foreach (Assembly assy1 in assys)
2932+
// Search by simple type name (slow path).
2933+
List<Type> candidateTypes = new List<Type>();
2934+
foreach (Assembly assy1 in loadedAssemblies)
28022935
{
28032936
Type t1 = null;
28042937

28052938
if (IsRunningOnMono)
28062939
{
2807-
// I do not know why Assembly.GetType fails to find types in our assemblies in Mono
2808-
28092940
if (!assy1.IsDynamic)
28102941
{
28112942
try
28122943
{
2813-
28142944
foreach (Type tt in assy1.GetTypes())
28152945
{
28162946
if (tt.Name.Equals(p))
@@ -2827,20 +2957,23 @@ public static Type classForName(string p)
28272957
}
28282958

28292959
if (t1 != null && !candidateTypes.Contains(t1))
2960+
{
28302961
candidateTypes.Add(t1);
2962+
}
28312963
}
28322964

2833-
if (candidateTypes.Count == 0)
2834-
t = null;
2835-
else if (candidateTypes.Count == 1)
2836-
t = candidateTypes[0];
2837-
else // multiple, ambiguous
2838-
t = null;
2965+
if (candidateTypes.Count == 1)
2966+
{
2967+
return candidateTypes[0];
2968+
}
28392969

2840-
if (t == null && p.IndexOfAny(_triggerTypeChars) != -1)
2841-
t = ClrTypeSpec.GetTypeFromName(p);
2970+
// Handle generic types and array types.
2971+
if (p.IndexOfAny(_triggerTypeChars) != -1)
2972+
{
2973+
return ClrTypeSpec.GetTypeFromName(p);
2974+
}
28422975

2843-
return t;
2976+
return null;
28442977
}
28452978

28462979

Clojure/Csharp.Tests/Csharp.Tests.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
<PropertyGroup>
44
<TargetFrameworks>net6.0;net8.0;net9.0</TargetFrameworks>
55
<LangVersion>12.0</LangVersion>
6+
<IsPackable>false</IsPackable>
7+
<IsTestProject>true</IsTestProject>
68
</PropertyGroup>
79

810
<ItemGroup>

0 commit comments

Comments
 (0)