Since nobody else seems to have an answer I'll document what I ended up doing.
When native code calls a dotnet DLL the CLR starts up behind the scenes to run it. Although the native code can search for the DLL in a variety of places- including that specified by SetDllDirectory- the CLR will only look in the directory from which the initial executable has been started, and in the Global Assembly Cache. To access assemblies that your DLL has been linked to by adding references to them in Visual Studio, they have to be in one of these two locations.
Since I didn't want to do either, what's needed is to make a .net DLL that is directly dependent only on framework assemblies, with no references to any of my own. This then gets the CLR up and running my code. I can then load the assembly I want to use via Assembly::LoadFrom()
and invoke the method I want to use as documented here.
Of course, loading the assembly this way will still cause any other dependent assemblies to be searched for in the executable dir or in the GAC, if they're not already loaded, and in all but the most trivial case it's too complicated to bother explicitly loading each assembly in order from the most fundamental upwards. So the AssemblyResolve event is registered first. When the CLR can't find an assembly in its two search locations, it raises this event and lets me determine the full path of the assembly and load it, again using Assembly::LoadFrom()
.
Of course, LoadFrom needs to know the base path- the only information that seems to be available concerns the executable's directory, but there's plenty of ways to resolve that.