4

We currently build an ODBC driver as 4 DLLs:

  1. A DLL implementing the ODBC (C) API, mostly implemented in C++, with some 'glue' code written in C++/CLI, used to interact with #2, #3, & #4

  2. A DLL containing a managed assembly (written in C#) which defines the 'base' interfaces used for #1 & #4 to talk to each other.

  3. Another DLL containing a managed assembly that depends on #2, and defines some extensions to the classes in #2
  4. Yet another DLL containing a managed assembly, which contains the 'business logic' for the driver, which depends on #2 & #3

To deploy the driver, we configure a DSN to point to #1, and put #2, #3 & #4 into the GAC.

We have a customer who wants to avoid the GAC entirely. I know that putting #2, #3, and #4 into the same directory as the application which loads #1 'works', but that's not a good solution, because many different applications might use the driver.

How can we set it up so that the dependencies can be resolved without the GAC? I've tried creating manifest files (based on https://learn.microsoft.com/en-us/windows/win32/sbscs/assembly-manifests), but that didn't seem to work (EEFileLoadException exception gets thrown because it can't find the managed assemblies, same thing that happens as soon as I remove the dependencies from the GAC). I put the manifest files, and all of the .DLL's into the same directory.

I couldn't find any good documentation/examples for this case with some (perhaps not enough) googling.

Bwmat
  • 4,314
  • 3
  • 27
  • 42
  • You'd have to give up on C++/CLI and host the CLR yourself so you can initialize the primary appdomain before it gets used. Big rewrite. Quote the right price and its a problem that solves itself. – Hans Passant Sep 26 '19 at 18:03
  • There's really no 'manifest-file-like' way of doing it? You have to actually call CLR APIs at runtime? – Bwmat Sep 26 '19 at 20:13
  • We're currently using https://learn.microsoft.com/en-us/cpp/dotnet/call-in-appdomain-function?view=vs-2019 in our entry points, with a hard-coded appdomain of 1 (meant to be the 'default' appdomain). Maybe we could create our own appdomain and use that instead... A coworker was testing out trying to configure how the CLR was loading assemblies, and noticed that it delayed loading the managed assemblies until the first time managed code was run, but only in 32-bit, not in 64-bit (not sure why) – Bwmat Sep 26 '19 at 21:18
  • What's the real problem if you ship the #2 #3 and #4 with each application? What do you mean by "many different applications might use the driver"? – Simon Mourier Oct 02 '19 at 20:56
  • When you install an ODBC driver on a machine, any application can use it. Since ODBC is a standard API, there's no way to list all the possible applications which would need to be considered. – Bwmat Oct 02 '19 at 21:19
  • So the dll that's initially loaded by the "windows" is #1 right? Then what piece of code loads these #2 #3 #4? where is it located? PS: add @loginname so we're aware you answered comment. – Simon Mourier Oct 03 '19 at 07:05
  • @SimonMourier Forgot about that, thanks. Yes, #1 is the 'ODBC Driver' (from the point of view of the driver manager), that's the DLL which gets loaded directly. We have no control over the application whatsoever. – Bwmat Oct 04 '19 at 00:11
  • @SimonMourier Right now we just rely on the 'default search path' (I'm not actually familiar with the details, perhaps why I needed to ask this question) & having the dependencies in the GAC. We didn't do anything explicit to find the dependencies. – Bwmat Oct 04 '19 at 00:12
  • But what triggers #2 3 4 dlls load? do you reference them from #1? how exactly? whats the loading flow? – Simon Mourier Oct 04 '19 at 06:36
  • @SimonMourier #1's entry points are to native code. The native code is statically linked to some C++/CLI code which implements some interfaces from the native code. The project which builds the C++/CLI code has 'references' (in the visual studio IDE sense, not sure about the details) on #2 & #3. There's another C++/CLI file which implements a static method which returns an instance of an interface defined in #2, where the implementation is from #4. The project that builds _this_ file has 'references' on #2 & #4 The project that builds #4 has a reference to #2 – Bwmat Oct 04 '19 at 20:31
  • Have you tried Arie's answer suggestion? Hooking AssemblyResolve could work. – Simon Mourier Oct 05 '19 at 06:38
  • @SimonMourier My coworker tried it, and he was able to create a PoC that worked, but for some reason when we try to do it in our ODBC Driver, it attempts to resolve #2/#3/#4 as soon as any managed code is run, before we could add a resolver – Bwmat Oct 05 '19 at 16:33
  • Hooking AppDomain.CurrentDomain.AssemblyResolve should be the first managed code you call in C+/CLI – Simon Mourier Oct 05 '19 at 17:26
  • @SimonMourier I'm not sure exactly what he tried, I was thinking maybe the constructor of some global object might be the cause. – Bwmat Oct 05 '19 at 22:26
  • @SimonMourier - He showed me it failing, and somewhere in the CLR stack frames in between the caller of the managed code, and where the 'can't load dependency' EEFileLoadException exception gets thrown, it was calling some 'initterm' function, and iterating over an array of function pointers (forgot to note the exact name), supports my idea that it's a constructor of a static/global object (I think, looks similar to native static initialization) – Bwmat Oct 07 '19 at 23:35

2 Answers2

2

I'm not sure that I entirely understand your question, but I'll try to answer:

While trying to solve this, we should remember the following:

  1. Fusion doesn't kick in (and thus dependency resolution), until you are in a stack frame, where the first appearance of the unresolved type is located.
  2. We can assume that the CLR is already loaded in the host process, and we have at least one Appdomain. (Otherwise, you have to think about both scenarios - especially being consumed by a process, which doesn't load CLR by default. What do you want to do in such case? Load CLR yourself? Implicitly? Explicitly? If so, how do you configure your Appdomain? Where is your App Root? What are your probing paths?)

Given these two points, you can subscribe to AppDomain.AssemblyResolve Event. But you should refactor your code in such a manner, so fusion doesn't try to resolve your assemblies before you've managed to subscribe. In the handler, handle only the events in which YOUR assemblies are being resolved, and load them from any location you want - for example, from the same path, in which your native library resides.

Additional reading:

Aryéh Radlé
  • 1,310
  • 14
  • 31
0

Updated based on comments...

I found an interesting point in Microsoft's documentation here: learn.microsoft.com/en-us/windows/win32/dlls/… -- "If a DLL has dependencies, the system searches for the dependent DLLs as if they were loaded with just their module names. This is true even if the first DLL was loaded by specifying a full path." This means if those dlls were loaded by any other process the version in memory with that name is what will be used.

Essentially what microsoft's doc is saying is:

You have nothing to worry about unless there are other dlls on the computer with the same module names as #2-#4. Regardless of whether you put those dlls into the GAC. #1 will be loaded by the application using whatever path you specify, #2-#4 will be loaded by module name only.

Your only problem is if your modules #2-#4 do not have good distinctive names and there could exist others with the same name and different definitions. This is because they will be loaded by name only.

As far as order of execution...#1 is your entry point, if it has dependencies on #2 it will be loaded before any execution, and so on down the line. Unless you did something explicit with your build directives, but you would have run into that already with your current implementations as well.

  • Haven't finished reading your answer yet, just wanted to clarify that #1 is _not_ the application, 'the application' is any old program that uses the ODBC API and happens to load our driver. – Bwmat Oct 07 '19 at 23:28
  • "Secondly, including local copies of referenced libraries with a deployed application is pretty easy to set up in almost any development environment." - I think this would work fine if the driver was an .exe. Since it's a DLL that's loaded by some other application, it doesn't get it's own 'activation context' I think? (I'm reusing a term a colleague used, I don't fully understand the details) – Bwmat Oct 07 '19 at 23:31
  • Normally this would not be a problem. However I found an interesting point in Microsoft's documentation here: https://learn.microsoft.com/en-us/windows/win32/dlls/dynamic-link-library-search-order -- "If a DLL has dependencies, the system searches for the dependent DLLs as if they were loaded with just their module names. This is true even if the first DLL was loaded by specifying a full path." This means if those dlls were loaded by any other process the version in memory with that name is what will be used. – user11953043 Oct 07 '19 at 23:42
  • Given that your dlls #1-#4 will almost certainly have references to other Microsoft assemblies, you cannot reasonably know which versions (GAC or explicit local copies) of those will be loaded when your driver is loaded, unless you know for sure they do not exist anywhere else on the machine. – user11953043 Oct 07 '19 at 23:47
  • The customer was only worried about putting *their* dependencies in the GAC, they don't care about system libraries – Bwmat Oct 08 '19 at 00:53
  • By that I mean dependencies of #4, specific to their implementation of it – Bwmat Oct 08 '19 at 00:53