3

... where by any assembly I mean also assemblies that depend on assemblies which are wrappers of native libraries.

The Minimal, Reproducible Example for this question is here:

https://gitlab.com/mikenakis-public/dotnet/playground/loadingnativelibraries

I have a library called MyLib. It exports MyType, which contains one method: void DoYourThing(). This method uses some type from the LibGit2Sharp package, so for this reason, MyLib.csproj contains <PackageReference Include="LibGit2Sharp" Version="0.29.0" />. Not that it matters, but here is the code:

public static void DoYourThing() { string currentDirectory = SysIo.Directory.GetCurrentDirectory(); Sys.Console.WriteLine( $"Current Directory: {currentDirectory}" ); string repositoryDirectory = LibGit2Sharp.Repository.Discover( currentDirectory ); Sys.Console.WriteLine( $"Repository Directory: {repositoryDirectory}" ); bool valid = LibGit2Sharp.Repository.IsValid( repositoryDirectory ); Sys.Console.WriteLine( $"Is a valid repository: {valid.ToString().ToUpperInvariant()}" ); } 

It just so happens that the LibGit2Sharp package is a managed wrapper around a native library, but I have no say on this and no control over this, and I would like my code to not have the slightest knowledge of this.

Then I have an application called DirectDependencyApp which has a dependency on MyLib, and contains the statement MyType.DoYourThing();, proving that the whole thing works.

Now, I have an application called DynamicLoadingApp which depends on nothing. Instead, it accepts a DLL path-name as a command-line argument, it loads it as an assembly, it finds a type with a DoYourThing method, and it invokes that method. Not that it matters, but here is the code:

string assemblyFilePath = SysIo.Path.GetFullPath( arguments.Single() ); SysReflect.Assembly assembly = SysReflect.Assembly.LoadFrom( assemblyFilePath ); SysReflect.MethodInfo methodInfo = assembly.GetTypes().Single().GetMethod( "DoYourThing" ) ?? throw new Sys.Exception(); methodInfo.Invoke( null, null ); 

(Disclosure: what I am actually doing is writing a test-runner like VSTest.Console.exe: it discovers all projects within a solution that produce VisualStudio-UnitTesting-compatible test assemblies, and runs them. DynamicLoadingApp corresponds to the test-runner, MyLib corresponds to a test assembly, and LibGit2Sharp corresponds to an assembly-under-test, or a library that the assembly-under-test depends on. It is crucial to note that the test-runner wants to know nothing specific about each test assembly, and nothing at all about each assembly-under-test. Furthermore, the test assemblies also want to know absolutely nothing about any particular test-runner.)

For DynamicLoadingApp to be able to load MyLib, I already had to do something that I am not quite happy with: I had to add <CopyLocalLockFileAssemblies>true to MyLib.csproj, or else DynamicLoadingApp would fail while trying to load MyLib because LibGit2Sharp.dll could not be found. I am not happy with this, but let's ignore that for now, it might be the subject of another question. The fact is that by adding <CopyLocalLockFileAssemblies>true I got past that hurdle. Unfortunately, that's when I arrived at the main hurdle.

When DynamicLoadingApp runs, it successfully finds MyLib, and it successfully loads it, (which means that LibGit2Sharp gets successfully loaded,) and it successfully invokes the DoYourThing method, but as soon as the method tries to actually invoke any method of LibGit2Sharp, the whole thing blows up with:

System.TypeInitializationException: 'The type initializer for 'LibGit2Sharp.Core.NativeMethods' threw an exception.' 

The inner exception is:

DllNotFoundException: Unable to load DLL 'git2-a2bde63' or one of its dependencies: The specified module could not be found. (0x8007007E) 

This exception was originally thrown at this call stack:

LibGit2Sharp.Core.NativeMethods.InitializeNativeLibrary() LibGit2Sharp.Core.NativeMethods.NativeMethods() 

The call stack shown in Visual Studio is this:

LibGit2Sharp.Core.Proxy.git_repository_discover.AnonymousMethod__0({LibGit2Sharp.Core.Handles.GitBuf}) Unknown LibGit2Sharp.Core.Proxy.ConvertPath({Method = {System.Reflection.RuntimeMethodInfo}}) Unknown LibGit2Sharp.Core.Proxy.git_repository_discover({LibGit2Sharp.Core.FilePath}) Unknown LibGit2Sharp.Repository.Discover("D:\\Personal\\LoadingNativeLibraries") Unknown MyLib.MyType.DoYourThing() C# 

So, what is happening is that GitLib2Sharp, which is a library that I have absolutely no control over, is failing to locate its own native libraries. Of course this problem is not specific to GitLib2Sharp, it pertains to any wrapper of native libraries.

I know there are workarounds for fixing the problem by copying the native binaries from bin/Debug/net8.0/runtimes/win-x64 to bin/Debug/net8.0, or by using some MSBuild property which will put them directly there, but I want a proper solution so that I can give DynamicLoadingApp to people and tell them "here, use it" without having to tell them "before you can use it you have to modify each and every one of your MyLibs to accommodate DynamicLoadingApp's quirks".

So:

How can I get DynamicLoadingApp to load MyLib and have MyLib successfully invoke Gitlib2Sharp?

The solution must involve modifications only to DynamicLoadingApp. (Not modifications to MyLib, which works as it is.)

7
  • Cant you just add the files in the runtimes folder also? So bin/Debug/net8.0 and all sub folder. Commented Feb 29, 2024 at 15:06
  • @Magnus I do not understand your question. Add the files where? <CopyLocalLockFileAssemblies>true in MyLib's project file causes all binaries (including those under runtimes) to be emitted into MyLib's bin directory. And although MyLib succeeds in loading GitLib2Sharp from that bin directory, it fails to locate the stuff under runtimes. Plus, I have already stated that I did not really want to be doing even this. In any case, I added a link to a cloneable MRE, so you can try any idea with it. Commented Feb 29, 2024 at 15:22
  • Since it is MyLib dependent on LibGit2Sharp, how to ensure that LibGit2Sharp can be loaded successfully should be a concern for MyLib (or for DirectDependencyApp). Alternatively, your test runner may accept some search path like options to complete this task. Commented Feb 29, 2024 at 15:26
  • @shingo MyLib works fine, as DirectDependencyApp proves. Things get weird when it is dynamically loaded by DynamicLoadingApp, so it is DynamicLoadingApp that must take whatever measures are necessary in order to load it properly. Commented Feb 29, 2024 at 15:30
  • @shingo it is possible to pass search-path-like options to the test runner, and it is possible to discover those options. My question then is what is the test runner going to do with these options. How it is going to put them to use. Commented Feb 29, 2024 at 15:39

1 Answer 1

2

So, I found the answer. It required creating my own AssemblyLoadContext which makes use of an AssemblyDependencyResolver. Here is the full code:

namespace DynamicLoadingApp; using System.Linq; using SysLoader = System.Runtime.Loader; using Sys = System; using SysIo = System.IO; using SysReflect = System.Reflection; sealed class DynamicLoadingAppMain { static void Main( string[] arguments ) { Sys.Console.WriteLine( nameof( DynamicLoadingAppMain ) ); string assemblyFilePath = SysIo.Path.GetFullPath( arguments.Single() ); SysLoader.AssemblyLoadContext assemblyLoadContext = new ResolvingAssemblyLoadContext( assemblyFilePath ); SysReflect.Assembly assembly = assemblyLoadContext.LoadFromAssemblyPath( assemblyFilePath ); SysReflect.MethodInfo methodInfo = assembly.GetTypes().Single().GetMethod( "DoYourThing" ) ?? throw new Sys.Exception(); methodInfo.Invoke( null, null ); Sys.Console.Write( "Press [Enter] to continue: " ); Sys.Console.ReadLine(); } sealed class ResolvingAssemblyLoadContext : SysLoader.AssemblyLoadContext { readonly SysLoader.AssemblyDependencyResolver resolver; public ResolvingAssemblyLoadContext( string assemblyFilePath ) { resolver = new SysLoader.AssemblyDependencyResolver( assemblyFilePath ); } protected override SysReflect.Assembly? Load( SysReflect.AssemblyName assemblyName ) { string? assemblyPath = resolver.ResolveAssemblyToPath( assemblyName ); if( assemblyPath != null ) return LoadFromAssemblyPath( assemblyPath ); return null; } protected override nint LoadUnmanagedDll( string unmanagedDllName ) { string? libraryPath = resolver.ResolveUnmanagedDllToPath( unmanagedDllName ); if( libraryPath != null ) return LoadUnmanagedDllFromPath( libraryPath ); return 0; } } } 

And here is the (glorious!) output:

DynamicLoadingAppMain Current Directory: D:\Personal\LoadingNativeLibraries Repository Directory: D:\Personal\LoadingNativeLibraries\.git\ Is a valid repository: TRUE Press [Enter] to continue: 
Sign up to request clarification or add additional context in comments.

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.