如何让CLR插件DLL从非exe目录加载托管C# DLL?
Let's break down your scenario first to make sure we're on the same page:
- You have two C++ apps (
app1.exeinapp1/,app2.exeinapp2/) that load CLR plugins at runtime - Your CLR plugin DLL references a C# managed DLL (
PrintSample.dll) - All plugins and their dependencies live in a sibling
plugin/directory (not a subdirectory of either app) - The
plugin/directory is in%PATH%, so native DLLs load fine—but the managed DLL only loads if it's in the app's exe directory (which you want to avoid, to prevent duplication)
The core issue here is that CLR assembly loading doesn't respect the PATH environment variable like native DLLs do. By default, the CLR looks for managed assemblies in:
- The app's base directory (where the exe lives)
- Subdirectories specified in the app's
.configfile (via<probing>elements, which only support subdirectories of the base directory)
Since plugin/ is a sibling directory, the default rules don't cover it. Here are the most robust, no-duplication solutions:
1. Use AppDomain.AssemblyResolve Event in the CLR Plugin
This is the cleanest approach because it keeps all assembly loading logic contained within the CLR plugin, requiring no changes to your C++ host apps.
The idea is to register an event handler that tells the CLR where to look for missing managed assemblies when it fails to find them via default rules. We can derive the plugin/ path from the CLR plugin's own location (since it's already in plugin/), so this works for both app1 and app2 without hardcoding paths.
Modified clr_sample.cpp Code
#include <msclr/marshal.h> #include <msclr/auto_gcroot.h> #include <msclr/event.h> #include "clr_sample.h" using namespace System; using namespace System::Runtime::InteropServices; using namespace msclr::interop; using namespace PrintSample; using namespace System::Reflection; using namespace System::IO; // Event handler to resolve missing managed assemblies Assembly^ OnAssemblyResolve(Object^ sender, ResolveEventArgs^ args) { // Extract the simple assembly name (strip version/culture/public key token) String^ assemblyName = args->Name->Split(',')[0]; // Get the directory where our CLR plugin lives (the plugin/ directory) String^ pluginDir = Path::GetDirectoryName(Assembly::GetExecutingAssembly()->Location); // Build the full path to the missing assembly String^ assemblyPath = Path::Combine(pluginDir, assemblyName + ".dll"); // Load the assembly if it exists if (File::Exists(assemblyPath)) { return Assembly::LoadFrom(assemblyPath); } // Return null if we can't find it (let the CLR throw the original error) return nullptr; } class CClrPrintSample : public IPrintSample { msclr::auto_gcroot<PrintSample::CPrintSample^> bridge; public: CClrPrintSample () { // Register the resolve event ONCE per AppDomain static bool eventRegistered = false; if (!eventRegistered) { AppDomain::CurrentDomain->AssemblyResolve += gcnew ResolveEventHandler(OnAssemblyResolve); eventRegistered = true; } bridge = gcnew PrintSample::CPrintSample(); } ~CClrPrintSample () { delete bridge; GC::Collect(); } void Print() { bridge->print(); } }; IPrintSample* CLR_PS_API Factory() { return new CClrPrintSample(); }
How It Works:
- When the CLR fails to load
PrintSample.dllvia default paths, it triggers theAssemblyResolveevent - Our handler calculates the path to the
plugin/directory using the CLR plugin's own location - It then loads the missing assembly directly from that directory
- We register the event only once to avoid redundant handlers in the same AppDomain
2. Pre-Load the Managed Assembly Explicitly
If your managed DLL has no nested dependencies, you can simplify things by explicitly loading it when the plugin is initialized. This avoids needing to handle the AssemblyResolve event for all cases.
Modified Factory Function in clr_sample.cpp
IPrintSample* CLR_PS_API Factory() { // Get the plugin directory from the CLR plugin's location String^ pluginDir = Path::GetDirectoryName(Assembly::GetExecutingAssembly()->Location); String^ managedDllPath = Path::Combine(pluginDir, "PrintSample.dll"); // Pre-load the managed assembly if (File::Exists(managedDllPath)) { Assembly::LoadFrom(managedDllPath); } return new CClrPrintSample(); }
Caveat:
- This only works if your managed DLL doesn't reference other managed DLLs that also live in
plugin/. If it does, you'll still need theAssemblyResolvehandler from Solution 1 to catch those dependencies.
3. Create a Custom AppDomain (Advanced)
For more isolation between your plugin and the host app, you could create a separate AppDomain for the plugin and set its base directory to plugin/. This requires more work because you'll need to use MarshalByRefObject for cross-AppDomain calls, but it's useful if you want strict separation.
Key Steps:
- In the CLR plugin, create a wrapper class that inherits from
MarshalByRefObjectand wraps yourCPrintSamplelogic - When initializing the plugin, create a new AppDomain with
AppDomainSetupwhereApplicationBaseis set to theplugin/directory - Load the managed assembly into this new AppDomain
This is overkill for simple scenarios, but it's an option if you need strong isolation.
Testing the Solution
After applying either Solution 1 or 2:
- Ensure
clr_sample.dllandPrintSample.dllare in theplugin/directory - Confirm
plugin/is in%PATH% - Run
app1.exeorapp2.exefrom their respective directories—both should successfully load the managed DLL without any file duplication.
内容的提问来源于stack exchange,提问作者SHR




