You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

如何让CLR插件DLL从非exe目录加载托管C# DLL?

Solution for Loading Managed DLLs from a Non-Exe Subdirectory in C++ Host Apps with CLR Plugins

Let's break down your scenario first to make sure we're on the same page:

  • You have two C++ apps (app1.exe in app1/, app2.exe in app2/) 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:

  1. The app's base directory (where the exe lives)
  2. Subdirectories specified in the app's .config file (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.dll via default paths, it triggers the AssemblyResolve event
  • 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 the AssemblyResolve handler 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:

  1. In the CLR plugin, create a wrapper class that inherits from MarshalByRefObject and wraps your CPrintSample logic
  2. When initializing the plugin, create a new AppDomain with AppDomainSetup where ApplicationBase is set to the plugin/ directory
  3. 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:

  1. Ensure clr_sample.dll and PrintSample.dll are in the plugin/ directory
  2. Confirm plugin/ is in %PATH%
  3. Run app1.exe or app2.exe from their respective directories—both should successfully load the managed DLL without any file duplication.

内容的提问来源于stack exchange,提问作者SHR

火山引擎 最新活动