From f58e574cc7b1041f06b9607fb55761e96da79300 Mon Sep 17 00:00:00 2001 From: Mohamed Koubaa Date: Wed, 23 Jul 2025 08:20:33 -0500 Subject: [PATCH 1/3] load the assembly and get function address inside the domain When using domain.Load for an assembly, the assembly resolution rules are awkward Even if the full path is given to the AssemblyName, when the domain tries to load the assembly, it does not use that context and tries to resolve the assembly using normal domain resolution rules, which would require an assembly resolver to be installed. However, the assembly resolver that is actually used at runtime is the one installed to the main appdomain. This prevents a library like Python.Runtime.dll (used by pythonnet) which is not installed to the application base directory to be loaded by clr_loader. To fix this issue, the assembly resolver of the main appdomain is lazily extending to include paths needed for libraries passed into GetFunction, and GetFunction internally uses AppDomain.DoCallBack() to marshal the function pointer inside the target app domain, using global domain data to access the function pointer and return it to the user of clr_loader. --- netfx_loader/ClrLoader.cs | 23 ++++++++++--- netfx_loader/DomainData.cs | 70 ++++++++++++++++++++++++++++++-------- 2 files changed, 73 insertions(+), 20 deletions(-) diff --git a/netfx_loader/ClrLoader.cs b/netfx_loader/ClrLoader.cs index 32b4c01..29826b9 100644 --- a/netfx_loader/ClrLoader.cs +++ b/netfx_loader/ClrLoader.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Reflection; using System.Runtime.InteropServices; using NXPorts.Attributes; @@ -21,6 +22,17 @@ public static void Initialize() } } + private static string AssemblyDirectory + { + get + { + string codeBase = Assembly.GetExecutingAssembly().CodeBase; + UriBuilder uri = new UriBuilder(codeBase); + string path = Uri.UnescapeDataString(uri.Path); + return Path.GetDirectoryName(path); + } + } + [DllExport("pyclr_create_appdomain", CallingConvention.Cdecl)] public static IntPtr CreateAppDomain( [MarshalAs(UnmanagedType.LPUTF8Str)] string name, @@ -28,16 +40,17 @@ public static IntPtr CreateAppDomain( ) { Print($"Creating AppDomain {name} with {configFile}"); + + var clrLoaderDir = AssemblyDirectory; if (!string.IsNullOrEmpty(name)) { var setup = new AppDomainSetup { - ApplicationBase = AppDomain.CurrentDomain.BaseDirectory, + ApplicationBase = clrLoaderDir, ConfigurationFile = configFile }; - Print($"Base: {AppDomain.CurrentDomain.BaseDirectory}"); + Print($"Base: {clrLoaderDir}"); var domain = AppDomain.CreateDomain(name, null, setup); - Print($"Located domain {domain}"); var domainData = new DomainData(domain); @@ -61,8 +74,8 @@ public static IntPtr GetFunction( try { var domainData = _domains[(int)domain]; - var deleg = domainData.GetEntryPoint(assemblyPath, typeName, function); - return Marshal.GetFunctionPointerForDelegate(deleg); + Print($"Getting functor for function {function} of type {typeName} in assembly {assemblyPath}"); + return domainData.GetFunctor(assemblyPath, typeName, function); } catch (Exception exc) { diff --git a/netfx_loader/DomainData.cs b/netfx_loader/DomainData.cs index 3a17d7a..fbf5ad6 100644 --- a/netfx_loader/DomainData.cs +++ b/netfx_loader/DomainData.cs @@ -1,46 +1,86 @@ using System; using System.Collections.Generic; using System.Reflection; +using System.Runtime.InteropServices; namespace ClrLoader { using static ClrLoader; - class DomainData : IDisposable + public static class DomainSetup { public delegate int EntryPoint(IntPtr buffer, int size); + public static void StoreFunctorFromDomainData() + { + var domain = AppDomain.CurrentDomain; + var assemblyPath = (string)domain.GetData("_assemblyPath"); + var typeName = (string)domain.GetData("_typeName"); + var function = (string)domain.GetData("_function"); + var functor = GetFunctor(domain, assemblyPath, typeName, function); + domain.SetData("_thisFunctor", functor); + } + + private static IntPtr GetFunctor(AppDomain domain, string assemblyPath, string typeName, string function) + { + var assemblyName = AssemblyName.GetAssemblyName(assemblyPath).Name; + var assembly = domain.Load(AssemblyName.GetAssemblyName(assemblyPath)); + var type = assembly.GetType(typeName, throwOnError: true); + var deleg = Delegate.CreateDelegate(typeof(EntryPoint), type, function); + IntPtr result = Marshal.GetFunctionPointerForDelegate(deleg); + return result; + } + } + class DomainData : IDisposable + { bool _disposed = false; public AppDomain Domain { get; } - public Dictionary<(string, string, string), EntryPoint> _delegates; + public Dictionary<(string, string, string), IntPtr> _functors; + public HashSet _resolvedAssemblies; public DomainData(AppDomain domain) { Domain = domain; - _delegates = new Dictionary<(string, string, string), EntryPoint>(); + _functors = new Dictionary<(string, string, string), IntPtr>(); + _resolvedAssemblies = new HashSet(); } - public EntryPoint GetEntryPoint(string assemblyPath, string typeName, string function) + private void installResolver(string assemblyPath) + { + var assemblyName = AssemblyName.GetAssemblyName(assemblyPath).Name; + if (_resolvedAssemblies.Contains(assemblyName)) + return; + _resolvedAssemblies.Add(assemblyName); + + AppDomain.CurrentDomain.AssemblyResolve += (sender, args) => + { + if (args.Name.Contains(assemblyName)) + return Assembly.LoadFrom(assemblyPath); + return null; + }; + } + + public IntPtr GetFunctor(string assemblyPath, string typeName, string function) { if (_disposed) throw new InvalidOperationException("Domain is already disposed"); - var key = (assemblyPath, typeName, function); + installResolver(assemblyPath); - EntryPoint result; + var key = (assemblyPath, typeName, function); - if (!_delegates.TryGetValue(key, out result)) + IntPtr result; + if (!_functors.TryGetValue(key, out result)) { - var assembly = Domain.Load(AssemblyName.GetAssemblyName(assemblyPath)); - var type = assembly.GetType(typeName, throwOnError: true); + Domain.SetData("_assemblyPath", assemblyPath); + Domain.SetData("_typeName", typeName); + Domain.SetData("_function", function); - Print($"Loaded type {type}"); - result = (EntryPoint)Delegate.CreateDelegate(typeof(EntryPoint), type, function); - - _delegates[key] = result; + Domain.DoCallBack(new CrossAppDomainDelegate(DomainSetup.StoreFunctorFromDomainData)); + result = (IntPtr)Domain.GetData("_thisFunctor"); + _functors[key] = result; } - return result; } @@ -48,7 +88,7 @@ public void Dispose() { if (!_disposed) { - _delegates.Clear(); + _functors.Clear(); if (Domain != AppDomain.CurrentDomain) AppDomain.Unload(Domain); From 1761cb0a2e64c890cd4666793b302b6ebdb4562c Mon Sep 17 00:00:00 2001 From: Mohamed Koubaa Date: Wed, 30 Jul 2025 14:43:49 -0500 Subject: [PATCH 2/3] Add comment --- netfx_loader/ClrLoader.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/netfx_loader/ClrLoader.cs b/netfx_loader/ClrLoader.cs index 29826b9..2a065bb 100644 --- a/netfx_loader/ClrLoader.cs +++ b/netfx_loader/ClrLoader.cs @@ -26,6 +26,8 @@ private static string AssemblyDirectory { get { + // This is needed in case the DLL was shadow-copied + // (Otherwise .Location would work) string codeBase = Assembly.GetExecutingAssembly().CodeBase; UriBuilder uri = new UriBuilder(codeBase); string path = Uri.UnescapeDataString(uri.Path); From bbb9bdd3e18c5a5dfb9376b9f329957f95cee14e Mon Sep 17 00:00:00 2001 From: Mohamed Koubaa Date: Wed, 30 Jul 2025 14:49:00 -0500 Subject: [PATCH 3/3] PR review feedback --- netfx_loader/DomainData.cs | 61 +++++++++++++++++++++++++------------- 1 file changed, 41 insertions(+), 20 deletions(-) diff --git a/netfx_loader/DomainData.cs b/netfx_loader/DomainData.cs index fbf5ad6..35c3363 100644 --- a/netfx_loader/DomainData.cs +++ b/netfx_loader/DomainData.cs @@ -10,24 +10,26 @@ namespace ClrLoader public static class DomainSetup { public delegate int EntryPoint(IntPtr buffer, int size); + public static void StoreFunctorFromDomainData() { var domain = AppDomain.CurrentDomain; var assemblyPath = (string)domain.GetData("_assemblyPath"); var typeName = (string)domain.GetData("_typeName"); var function = (string)domain.GetData("_function"); - var functor = GetFunctor(domain, assemblyPath, typeName, function); + var deleg = GetDelegate(domain, assemblyPath, typeName, function); + var functor = Marshal.GetFunctionPointerForDelegate(deleg); + domain.SetData("_thisDelegate", deleg); domain.SetData("_thisFunctor", functor); } - private static IntPtr GetFunctor(AppDomain domain, string assemblyPath, string typeName, string function) + private static Delegate GetDelegate(AppDomain domain, string assemblyPath, string typeName, string function) { - var assemblyName = AssemblyName.GetAssemblyName(assemblyPath).Name; - var assembly = domain.Load(AssemblyName.GetAssemblyName(assemblyPath)); + var assemblyName = AssemblyName.GetAssemblyName(assemblyPath); + var assembly = domain.Load(assemblyName); var type = assembly.GetType(typeName, throwOnError: true); var deleg = Delegate.CreateDelegate(typeof(EntryPoint), type, function); - IntPtr result = Marshal.GetFunctionPointerForDelegate(deleg); - return result; + return deleg; } } @@ -61,27 +63,46 @@ private void installResolver(string assemblyPath) }; } + private static readonly object _lockObj = new object(); + public IntPtr GetFunctor(string assemblyPath, string typeName, string function) { if (_disposed) throw new InvalidOperationException("Domain is already disposed"); - installResolver(assemblyPath); - - var key = (assemblyPath, typeName, function); - - IntPtr result; - if (!_functors.TryGetValue(key, out result)) + // neither the domain data nor the _functors dictionary is threadsafe + lock (_lockObj) { - Domain.SetData("_assemblyPath", assemblyPath); - Domain.SetData("_typeName", typeName); - Domain.SetData("_function", function); - - Domain.DoCallBack(new CrossAppDomainDelegate(DomainSetup.StoreFunctorFromDomainData)); - result = (IntPtr)Domain.GetData("_thisFunctor"); - _functors[key] = result; + installResolver(assemblyPath); + var assemblyName = AssemblyName.GetAssemblyName(assemblyPath).Name; + + var key = (assemblyName, typeName, function); + + IntPtr result; + if (!_functors.TryGetValue(key, out result)) + { + Domain.SetData("_assemblyPath", assemblyPath); + Domain.SetData("_typeName", typeName); + Domain.SetData("_function", function); + + Domain.DoCallBack(new CrossAppDomainDelegate(DomainSetup.StoreFunctorFromDomainData)); + result = (IntPtr)Domain.GetData("_thisFunctor"); + if (result == IntPtr.Zero) + throw new Exception($"Unable to get functor for {assemblyName}, {typeName}, {function}"); + + // set inputs to StoreFunctorFromDomainData to null. + // (There's no method to explicitly clear domain data) + Domain.SetData("_assemblyPath", null); + Domain.SetData("_typeName", null); + Domain.SetData("_function", null); + + // the result has to remain in the domain data because we don't know when the + // client of pyclr_get_function will actually invoke the functor, and if we + // remove it from the domain data after returning it may get collected too early. + _functors[key] = result; + } + return result; } - return result; } public void Dispose()