3F / DllExport

.NET DllExport with .NET Core support (aka 3F/DllExport aka DllExport.bat)
MIT License
940 stars 131 forks source link

Calling referenced .NET assemblies: Could not load file or assembly #86

Closed bertstomphorst closed 5 years ago

bertstomphorst commented 5 years ago

Using an DllExported-dll as a broker between VBA and other .NET code (separate dll's) doesn't seem to work.

How to reproduce

See attached project DllExportTest.zip

Build de project. In a VBA-module (I used Access):


Declare Function CreateBroker Lib "C:\source\repos\DllExportTest\ClassLibrary1\bin\Debug\ClassLibrary1.dll" () As Object

Public Sub RunTest()
  Dim class1 As Object

  Set class1 = CreateBroker()

  class1.DoNothingOnClass2

  Set class1 = Nothing
End Sub

(be sure to correct the path in the Declare Function to point to the built dll)

Execute the RunTest()

VBA gives "'Could not load file or assembly 'ClassLibrary2, version=1.0.0.0, Culture=neutral, PublicKeyToken=null' or one of iets dependencies. The system cannot find the file specified."

3F commented 5 years ago

@bertstomphorst It looks like you're trying to use COM. But DllExport works for P/Invoke

[return: MarshalAs(UnmanagedType.IDispatch)] // <<< a COM IDispatch pointer that you're trying for
static object CreateBroker()

Here's about default marshaling and data types: https://github.com/3F/DllExport/wiki/Quick-start#about-data-types

bertstomphorst commented 5 years ago

Hi Denis, I don't think that's the issue. The Class1 is correctly recognized in VBA. The variable 'class1' represents the object. When I add a string and int-property returning a fixed value, I can see these values in the local variable inspector in VBA. This usageof IDispatch is as mentioned by Robert Giesecke on Stackoverflow: https://stackoverflow.com/a/6340601/10205407

The issue is when ClassLibrary1.Class1 does it's first call to ClassLibrary2.Class2 (pointing to referenced dll), it can't find the dll. But it's compiled at the same time, and both dll's are in the same folder. So what can be the reason that ClassLibrary1.dll can't find ClassLibrary2.dll?

3F commented 5 years ago

The Class1 is correctly recognized in VBA. When I add a string and int-property returning a fixed value, I can see these values in the local variable inspector in VBA.

Marshaling for IDispatch works basically as a pointer(that I already mentioned above) to the object on the heap. I'm not using VBA, but if you have only problem for types from different assemblies, then you need to view loading of domain and its types. Or try native usage for intptr.

For domains example:

As you should understand, your ClassLibrary1 assembly does not contain Class2 type. Now CLR will try to load this into used domain by information about referenced assemblies.

it can't find the dll. But it's compiled at the same time, and both dll's are in the same folder.

Therefore, you can also try with AssemblyResolve or even with additional domain with unwrapping types.

Here, I have old code for some related loader, try to experiment:

upd.: forgot to say,

both dll's are in the same folder

You need to focus on host side and its searching for dependencies. Usually this is the application's directory only. I'm not sure how VBA implements loading, but if you will use LoadLibraryEx for SEARCH_DLL_LOAD_DIR it will also add the directory that contains the loaded dll to the beginning of the list of directories that are searched for the dependencies. So it can also affect for loading in CLR.

bertstomphorst commented 5 years ago

Thank you so far Denis, I will dive into it and share my experience here

bertstomphorst commented 5 years ago

Thanx Denis, got it work with following change: In Class1.DoNothingOnClass2 (where Class2 was initialized), change body of DoNothingOnClass2() from

        public void DoNothingOnClass2()
        {
            new Class2().DoNothing();
        }

to

        public void DoNothingOnClass2()
        {
            string path = new FileInfo(Assembly.GetExecutingAssembly().Location).DirectoryName;
            var assemblyname = "ClassLibrary2";
            var typename = "Class2";

            dynamic class2 = AppDomain.CurrentDomain.CreateInstanceFromAndUnwrap($@"{path}\{assemblyname}.dll", $"{assemblyname}.{typename}");
            class2.DoNothing();
        }

After that, found that AssemblyResolve works better, because it loads all types from referenced assembly, and in ClassLibrary1 everything is typed. Solution (for others reference): static class UnmanagedExport (from sample project) should be:

    static class UnmanagedExport
    {
        static UnmanagedExport()
            => AppDomain.CurrentDomain.AssemblyResolve += CurrentDomain_AssemblyResolve;

        static Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args)
            => Assembly.LoadFrom($@"{new FileInfo(args.RequestingAssembly.Location).DirectoryName}\{args.Name.Split(',')[0]}.dll");

        [DllExport]
        [return: MarshalAs(UnmanagedType.IDispatch)]
        static object CreateBroker()
            => new Class1();
    }