Как я могу указать путь [DllImport] во время выполнения?

145

Фактически, у меня есть C ++ (рабочая) DLL, которую я хочу импортировать в свой проект C # для вызова ее функций.

Это работает, когда я указываю полный путь к DLL, например:

string str = "C:\\Users\\userName\\AppData\\Local\\myLibFolder\\myDLL.dll";
[DllImport(str, CallingConvention = CallingConvention.Cdecl)]
public static extern int DLLFunction(int Number1, int Number2);

Проблема в том, что это будет устанавливаемый проект, поэтому папка пользователя не будет одинаковой (например, Пьер, Пол, Джек, мама, папа, ...) в зависимости от компьютера / сеанса, на котором он будет запущен.

Поэтому я бы хотел, чтобы мой код был немного более общим, например:

/* 
goes right to the temp folder of the user 
    "C:\\Users\\userName\\AppData\\Local\\temp"
then go to parent folder
    "C:\\Users\\userName\\AppData\\Local"
and finally go to the DLL's folder
    "C:\\Users\\userName\\AppData\\Local\\temp\\myLibFolder"
*/

string str = Path.GetTempPath() + "..\\myLibFolder\\myDLL.dll"; 
[DllImport(str, CallingConvention = CallingConvention.Cdecl)]
public static extern int DLLFunction(int Number1, int Number2);

Дело в том, что "DllImport" требует параметр "const string" для каталога DLL.

Итак, мой вопрос: что можно сделать в этом случае?

Jsncrdnl
источник
16
Просто разверните DLL в той же папке, что и EXE, поэтому вам не нужно ничего делать, кроме как указать имя DLL без пути. Возможны и другие схемы, но все они неудобны.
Hans Passant
2
Дело в том, что это будет надстройка MS Office Excel, поэтому я не думаю, что размещение dll в каталоге exe было бы лучшим решением ...
Jsncrdnl
8
Ваше решение неверное. Не помещайте файлы в Windows или системные папки. Они выбрали эти имена не просто так: потому что они предназначены для системных файлов Windows. Вы создаете один из них не потому, что не работаете в Microsoft в команде Windows. Вспомните, что вы узнали в детском саду об использовании вещей, которые вам не принадлежат, без разрешения, и кладите свои файлы куда угодно, только не туда.
Коди Грей
Ваше решение по-прежнему неверно. Приложения с хорошим поведением, которые на самом деле не выполняют административных функций, не должны требовать административного доступа. Другая проблема заключается в том, что вы не знаете, что ваше приложение действительно будет установлено в этой папке. Я мог бы переместить его в другое место или изменить путь установки во время установки (я делаю такие вещи для удовольствия, просто чтобы сломать плохо работающие приложения). Пути жесткого кодирования - это воплощение плохого поведения, и в этом нет необходимости. Если вы используете папку своего приложения, это первый путь в порядке поиска по умолчанию для библиотек DLL. Все автомат.
Коди Грей
3
помещение его в программные файлы НЕ является постоянным. Например, 64-битные машины имеют Program File (x86).
Луи Коттманн,

Ответы:

187

Вопреки предложениям некоторых других ответов, использование DllImportатрибута по-прежнему является правильным подходом.

Я, честно говоря, не понимаю, почему вы не можете поступить так же, как все в мире, и указать относительный путь к своей DLL. Да, путь, по которому ваше приложение будет установлено, различается на компьютерах разных людей, но в основном это универсальное правило, когда дело доходит до развертывания. DllImportМеханизм разработан с учетом этого.

На самом деле, дело даже не в DllImportэтом. Это собственные правила загрузки Win32 DLL, которые управляют вещами, независимо от того, используете ли вы удобные управляемые оболочки (маршаллер P / Invoke просто вызывает LoadLibrary). Эти правила перечислены здесь очень подробно , но здесь приведены выдержки из наиболее важных:

Прежде чем система выполнит поиск DLL, она проверяет следующее:

  • Если DLL с тем же именем модуля уже загружена в память, система использует загруженную DLL, независимо от того, в каком каталоге она находится. Система не ищет DLL.
  • Если DLL находится в списке известных DLL для версии Windows, в которой запущено приложение, система использует свою копию известной DLL (и зависимые библиотеки DLL известной DLL, если таковые имеются). Система не ищет DLL.

Если SafeDllSearchModeон включен (по умолчанию), порядок поиска следующий:

  1. Каталог, из которого загружено приложение.
  2. Системный каталог. Используйте GetSystemDirectoryфункцию, чтобы получить путь к этому каталогу.
  3. 16-битный системный каталог. Нет функции, которая получает путь к этому каталогу, но он ищется.
  4. Каталог Windows. Используйте GetWindowsDirectoryфункцию, чтобы получить путь к этому каталогу.
  5. Текущий каталог.
  6. Каталоги, перечисленные в PATHпеременной среды. Обратите внимание, что это не включает путь для каждого приложения, указанный в разделе реестра App Paths. Ключ App Paths не используется при вычислении пути поиска DLL.

Итак, если вы не называете свою DLL так же, как системная DLL (чего, очевидно, не следует делать ни при каких обстоятельствах), порядок поиска по умолчанию начнет поиск в каталоге, из которого было загружено ваше приложение. Если вы поместите туда DLL во время установки, она будет найдена. Все сложные проблемы исчезнут, если вы просто используете относительные пути.

Просто пиши:

[DllImport("MyAppDll.dll")] // relative path; just give the DLL's name
static extern bool MyGreatFunction(int myFirstParam, int mySecondParam);

Но если это не сработает по какой-либо причине и вам нужно заставить приложение искать DLL в другом каталоге, вы можете изменить путь поиска по умолчанию с помощью этой SetDllDirectoryфункции .
Обратите внимание, что согласно документации:

После вызова SetDllDirectoryстандартный путь поиска DLL:

  1. Каталог, из которого загружено приложение.
  2. Каталог, указанный lpPathNameпараметром.
  3. Системный каталог. Используйте GetSystemDirectoryфункцию, чтобы получить путь к этому каталогу.
  4. 16-битный системный каталог. Нет функции, которая получает путь к этому каталогу, но он ищется.
  5. Каталог Windows. Используйте GetWindowsDirectoryфункцию, чтобы получить путь к этому каталогу.
  6. Каталоги, перечисленные в PATHпеременной среды.

Итак, если вы вызываете эту функцию перед вызовом функции, импортированной из DLL, в первый раз, вы можете изменить путь поиска по умолчанию, используемый для поиска DLL. Преимущество, конечно же, в том, что вы можете передать этой функции динамическое значение, которое вычисляется во время выполнения. Это невозможно с DllImportатрибутом, поэтому вы все равно будете использовать относительный путь (только имя DLL) и полагаться на новый порядок поиска, чтобы найти его за вас.

Вам нужно будет P / Invoke этой функции. Объявление выглядит так:

[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
static extern bool SetDllDirectory(string lpPathName);
Коди Грей
источник
16
Еще одно незначительное улучшение может заключаться в удалении расширения из имени DLL. Windows автоматически добавит, .dllа другие системы добавят соответствующее расширение в Mono (например, .soв Linux). Это может помочь, если переносимость вызывает беспокойство.
jheddings
7
+1 за SetDllDirectory. Вы также можете просто изменить, Environment.CurrentDirectoryи все относительные пути будут оцениваться с этого пути!
GameScripting
2
Еще до того, как это было опубликовано, ОП пояснил, что он делает плагин, поэтому размещение библиотек DLL в программных файлах Microsoft - это своего рода неудача. Кроме того, изменение процесса DllDirectory или CWD может быть не очень хорошей идеей, они могут привести к сбою процесса. Теперь AddDllDirectoryс другой стороны ...
Mooing Duck
3
Использование рабочего каталога является потенциально серьезной уязвимостью безопасности, @GameScripting, и особенно не рекомендуется для чего-то, работающего с разрешениями суперпользователя. Стоит написать код и поработать над дизайном, чтобы все получилось правильно.
Коди Грей
2
Обратите внимание, что DllImportэто больше, чем просто оболочка LoadLibrary. Также учитывается каталог сборки, в externкоторой определен метод . На DllImportпути поиска может быть дополнительно ограничен с помощью DefaultDllImportSearchPath.
Митч
39

Даже лучше, чем предложение Рана об использовании GetProcAddress, просто сделайте вызов LoadLibraryперед любыми вызовами DllImportфункций (только с именем файла без пути), и они будут использовать загруженный модуль автоматически.

Я использовал этот метод, чтобы выбрать во время выполнения, загружать ли 32-разрядную или 64-разрядную собственную DLL без необходимости изменять кучу функций P / Invoke-d. Вставьте код загрузки в статический конструктор для типа, который имеет импортированные функции, и все будет работать нормально.

MikeP
источник
1
Я не уверен, что это сработает гарантированно. Или, если это происходит в текущей версии фреймворка.
CodesInChaos
3
@Code: Мне кажется гарантированным: порядок поиска в динамически подключаемой библиотеке . В частности, «Факторы, влияющие на поиск», точка первый.
Коди Грей
Ницца. Что ж, у моего решения есть небольшое дополнительное преимущество, поскольку даже имя функции не обязательно должно быть статическим и известным во время компиляции. Если у вас есть 2 функции с одинаковой подписью и другим именем, вы можете вызывать их, используя мой FunctionLoaderкод.
Ран
Это похоже на то, что я хочу. Я надеялся использовать такие имена файлов, как mylibrary32.dll и mylibrary64.dll, но думаю, я могу жить с ними с одинаковыми именами, но в разных папках.
йойо
28

Если вам нужен файл .dll, которого нет на пути или в расположении приложения, то я не думаю, что вы можете это сделать, потому что DllImportэто атрибут, а атрибуты - это только метаданные, которые установлены для типов, членов и других языковые элементы.

Альтернатива, которая может помочь вам выполнить то, что, как я думаю, вы пытаетесь, - использовать собственный LoadLibraryчерез P / Invoke, чтобы загрузить .dll по нужному вам пути, а затем использовать GetProcAddressдля получения ссылки на нужную функцию из этого .dll. Затем используйте их для создания делегата, который вы можете вызвать.

Чтобы упростить использование, вы можете затем установить этот делегат в поле в вашем классе, чтобы его использование выглядело как вызов метода члена.

РЕДАКТИРОВАТЬ

Вот фрагмент кода, который работает и показывает, что я имел в виду.

class Program
{
    static void Main(string[] args)
    {
        var a = new MyClass();
        var result = a.ShowMessage();
    }
}

class FunctionLoader
{
    [DllImport("Kernel32.dll")]
    private static extern IntPtr LoadLibrary(string path);

    [DllImport("Kernel32.dll")]
    private static extern IntPtr GetProcAddress(IntPtr hModule, string procName);

    public static Delegate LoadFunction<T>(string dllPath, string functionName)
    {
        var hModule = LoadLibrary(dllPath);
        var functionAddress = GetProcAddress(hModule, functionName);
        return Marshal.GetDelegateForFunctionPointer(functionAddress, typeof (T));
    }
}

public class MyClass
{
    static MyClass()
    {
        // Load functions and set them up as delegates
        // This is just an example - you could load the .dll from any path,
        // and you could even determine the file location at runtime.
        MessageBox = (MessageBoxDelegate) 
            FunctionLoader.LoadFunction<MessageBoxDelegate>(
                @"c:\windows\system32\user32.dll", "MessageBoxA");
    }

    private delegate int MessageBoxDelegate(
        IntPtr hwnd, string title, string message, int buttons); 

    /// <summary>
    /// This is the dynamic P/Invoke alternative
    /// </summary>
    static private MessageBoxDelegate MessageBox;

    /// <summary>
    /// Example for a method that uses the "dynamic P/Invoke"
    /// </summary>
    public int ShowMessage()
    {
        // 3 means "yes/no/cancel" buttons, just to show that it works...
        return MessageBox(IntPtr.Zero, "Hello world", "Loaded dynamically", 3);
    }
}

Примечание: я не стал использовать FreeLibrary, поэтому этот код не полный. В реальном приложении вы должны освободить загруженные модули, чтобы избежать утечки памяти.

Ран
источник
Есть управляемый аналог для LoadLibrary (в классе Assembly).
Лука
Если бы у вас был пример кода, мне было бы легче понять! ^^ (На самом деле, немного туманно)
Jsncrdnl
1
@Luca Piccioni: Если вы имели в виду Assembly.LoadFrom, это загружает только сборки .NET, а не собственные библиотеки. Что ты имел в виду?
Ран
1
Я имел в виду это, но не знал об этом ограничении. Вздох.
Luca
1
Конечно, нет. Это был всего лишь пример, чтобы показать, что вы можете вызывать функцию в собственной dll без использования P / Invoke, для которого требуется статический путь.
Ран
5

Если вы знаете каталог, в котором можно найти ваши библиотеки C ++ во время выполнения, это должно быть просто. Я ясно вижу, что это так в вашем коде. Ваш myDll.dllбудет присутствовать внутри myLibFolderкаталога во временной папке текущего пользователя.

string str = Path.GetTempPath() + "..\\myLibFolder\\myDLL.dll"; 

Теперь вы можете продолжить использование оператора DllImport, используя константную строку, как показано ниже:

[DllImport("myDLL.dll", CallingConvention = CallingConvention.Cdecl)]
public static extern int DLLFunction(int Number1, int Number2);

Непосредственно во время выполнения перед вызовом DLLFunctionфункции (присутствующей в библиотеке C ++) добавьте эту строку кода в код C #:

string assemblyProbeDirectory = Path.GetTempPath() + "..\\myLibFolder\\myDLL.dll"; 
Directory.SetCurrentDirectory(assemblyProbeDirectory);

Это просто указывает CLR искать неуправляемые библиотеки C ++ в пути к каталогу, который вы получили во время выполнения вашей программы. Directory.SetCurrentDirectorycall устанавливает текущий рабочий каталог приложения в указанный каталог. Если ваш myDLL.dllприсутствует на пути, представленном assemblyProbeDirectorypath, он будет загружен, и нужная функция будет вызвана через p / invoke.

RBT
источник
3
Это сработало для меня. У меня есть папка «Модули», расположенная в каталоге «bin» моего исполняемого приложения. Сюда я помещаю управляемую DLL и некоторые неуправляемые библиотеки DLL, которые требуются управляемой DLL. Использование этого решения И установка пути зондирования в моем app.config позволяет мне динамически загружать необходимые сборки.
WBuck
Для людей, использующих Функции Azure: string workingDirectory = Path.GetFullPath (Path.Combine (executionContext.FunctionDirectory, @ ".. \ bin"));
Red Riding Hood
4

установить путь к dll в файле конфигурации

<add key="dllPath" value="C:\Users\UserName\YourApp\myLibFolder\myDLL.dll" />

перед вызовом dll в вашем приложении сделайте следующее

string dllPath= ConfigurationManager.AppSettings["dllPath"];    
   string appDirectory = Path.GetDirectoryName(dllPath);
   Directory.SetCurrentDirectory(appDirectory);

затем вызовите dll, и вы можете использовать, как показано ниже

 [DllImport("myDLL.dll", CallingConvention = CallingConvention.Cdecl)]
public static extern int DLLFunction(int Number1, int Number2);
Саджитд
источник
0

DllImport будет работать нормально без указания полного пути, если dll находится где-то на системном пути. Вы можете временно добавить в путь папку пользователя.

Майк В.
источник
Я попытался поместить его в системные переменные среды, НО он все еще считается непостоянным (я думаю, логичным)
Jsncrdnl
-14

Если все не удается, просто поместите DLL в windows\system32папку. Компилятор его найдет. Укажите DLL для загрузки с помощью:, DllImport("user32.dll"...установите EntryPoint = "my_unmanaged_function"для импорта желаемой неуправляемой функции в ваше приложение C #:

 using System;
using System.Runtime.InteropServices;

class Example
{
   // Use DllImport to import the Win32 MessageBox function.

   [DllImport ("user32.dll", CharSet = CharSet.Auto)]
   public static extern int MessageBox 
      (IntPtr hWnd, String text, String caption, uint type);

   static void Main()
   {
      // Call the MessageBox function using platform invoke.
      MessageBox (new IntPtr(0), "Hello, World!", "Hello Dialog", 0);    
   }
}

Источник и другие DllImportпримеры: http://msdn.microsoft.com/en-us/library/aa288468(v=vs.71).aspx

Разработчик программного обеспечения
источник
Хорошо, я согласен с вашим решением об использовании папки win32 (самый простой способ сделать это), но как предоставить доступ к этой папке отладчику Visual Studio (а также скомпилированному приложению)? (Кроме ручного запуска от имени администратора)
Jsncrdnl
Если это используется для чего-то большего, чем помощь при отладке, он не выдержит ни одного обзора (безопасности или иного) в моей книге.
Christian.K
21
Это довольно ужасное решение. Системная папка предназначена для системных библиотек DLL. Теперь вам требуются права администратора, и вы полагаетесь на плохие методы только потому, что ленивы.
MikeP
5
+1 за MikeP, -1 за этот ответ. Это ужасное решение, любого, кто это делает, следует многократно пороть, заставляя читать «Старое новое» . Как вы узнали в детском саду: системная папка вам не принадлежит, поэтому вы не должны использовать ее без разрешения.
Коди Грей
Окок, я согласен с тобой, но моя проблема не решена, так что ... Какое место вы бы мне тогда порекомендовали? (Зная, что я не могу использовать переменные для его настройки, потому что он ждет постоянной строки, поэтому что я ДОЛЖЕН использовать место, которое будет одинаковым на каждом компьютере?) (Или есть какой-нибудь способ использовать переменные вместо константы для этого?)
Jsncrdnl