Як я можу вказати шлях [DllImport] під час виконання?


141

Насправді я отримав 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);

Проблема полягає в тому, що це буде інстальований проект, тому папка користувача не буде такою самою (наприклад: pierre, paul, jack, mum, dad, ...) залежно від комп'ютера / сесії, де вона буде запущена.

Тож я хотів би, щоб мій код був трохи більш загальним, як ось такий:

/* 
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.

Отже, моє питання: Що можна зробити в цьому випадку?


15
Просто розгорніть DLL в тій же папці, що і EXE, тому вам не доведеться нічого робити, окрім вказати ім'я DLL без шляху. Можливі й інші схеми, але всі вони складні.
Ганс Пасант

2
Річ у тім, що це буде MS Office Excel Add In, тому я не хочу, щоб введення dll у каталог EXE було б найкращим рішенням ...
Jsncrdnl

8
Ваше рішення - неправильне. Не розміщуйте файли у папках Windows або системних папках. Вони вибрали ці імена з причини: адже вони для системних файлів Windows. Ви не створюєте одного з таких, оскільки ви не працюєте для Microsoft у команді Windows. Згадайте, що ви дізналися в дитячому садку про використання речей, які вам не належать без дозволу, і покладіть свої файли кудись, але там.
Коді Грей

Ваше рішення все ще неправильне. Добре сприйняті програми, які насправді не виконують адміністративні послуги, не повинні вимагати адміністративного доступу. Інша проблема полягає в тому, що ви не знаєте, що ваша програма насправді буде встановлена ​​у цій папці. Я можу перенести його кудись інше або змінити шлях встановлення під час налаштування (я роблю такі речі для розваги, просто щоб зламати недоброзичливі програми). Шляхи жорсткого кодування є втіленням поганої поведінки, і це зовсім непотрібно. Якщо ви використовуєте папку вашої програми, то це перший шлях у порядку пошуку за замовчуванням для DLL. Усі автоматичні.
Коді Грей

3
розміщення його в програмних файлах НЕ є постійним. Наприклад, 64-бітні машини мають програмовий файл (x86).
Луї Котманн

Відповіді:


184

На відміну від пропозицій деяких інших відповідей, використання DllImportатрибута все-таки є правильним підходом.

Я, чесно кажучи, не розумію, чому ви не можете робити так, як усі в світі, і вказати відносний шлях до DLL. Так, шлях, по якому буде встановлено ваш додаток, відрізняється на комп'ютерах різних людей, але це в основному універсальне правило, що стосується розгортання. DllImportМеханізм розроблений з урахуванням цього.

Насправді це навіть не з DllImportцим справляється. Це рідні правила завантаження DLL Win32, які керують речами, незалежно від того, використовуєте ви зручні керовані обгортки (маршалок 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. Клавіша Шлях до програми не використовується при обчисленні шляху пошуку 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

6
+1 для SetDllDirectory. Ви також можете просто змінити, Environment.CurrentDirectoryі всі відносні патчі будуть оцінені з цього шляху!
GameScripting

2
Ще до того, як це було розміщено, ОП уточнила, що він робить плагін, тому введення DLL-файлів у програмні файли Microsoft - це щось на кшталт не-стартера. Крім того, зміна процесу DllDirectory або CWD не може бути гарною ідеєю, вони можуть спричинити збій процесу. Тепер AddDllDirectoryз іншого боку ...
Mooing Duck

3
Покладатися на робочий каталог - це потенційно серйозна вразливість безпеки, @GameScripting, і особливо нераціональна порада щодо того, що працює з дозволами суперпользователя. Варто написати код і виконати проектні роботи, щоб його правильно.
Коді Грей

2
Зауважте, що DllImportце більше, ніж просто обгортка LoadLibrary. Він також розглядає каталог збірки, в якому externвизначений метод . На DllImportшляху пошуку може бути додатково обмежений з допомогою DefaultDllImportSearchPath.
Мітч

38

Навіть краще, ніж пропозиція Рана використовувати GetProcAddress, просто зателефонуйте LoadLibraryперед будь-якими дзвінками до DllImportфункцій (лише ім'я файлу без шляху), і вони автоматично використовуватимуть завантажений модуль.

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


1
Я не впевнений, чи гарантовано це буде працювати. Або якщо це просто трапляється в поточній версії фреймворку.
CodesInChaos

3
@Code: Здається, гарантовано мені: Замовлення на пошук у бібліотеці Dynamic-Link . Зокрема, пункт "Фактори, які впливають на пошук".
Коді Грей

Приємно. Моє рішення має невелику додаткову перевагу, оскільки навіть ім'я функції не повинно бути статичним і відомим під час компіляції. Якщо у вас є дві функції з однаковою підписом та іншим ім'ям, ви можете викликати їх за допомогою мого FunctionLoaderкоду.
Побіг

Це звучить як те, що я хочу. Я сподівався використати назви файлів, такі як mylibrary32.dll та mylibrary64.dll, але, мабуть, я можу жити з ними з однаковою назвою, але в різних папках.
yoyo

27

Якщо вам потрібен файл .dll, який не стоїть на шляху чи в розташуванні програми, я не думаю, що ви можете це зробити, тому що DllImportце атрибут, а атрибути - це лише метадані, встановлені на типи, члени та інші мовні елементи.

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

Щоб полегшити його використання, ви можете встановити цього делегата на поле свого класу, щоб його використання було схожим на виклик методу-члена.

EDIT

Ось фрагмент коду, який працює, і показує, що я мав на увазі.

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 (у класі асамблеї).
Лука

Якби у вас був приклад коду, мені було б простіше зрозуміти! ^^ (Насправді, це трохи туманно)
Jsncrdnl

1
@Luca Piccioni: Якщо ви мали на увазі Assembly.LoadFrom, це завантажує лише .NET збірки, а не рідні бібліотеки. Що ти маєш на увазі?
Ран

1
Я мав на увазі це, але я не знав про це обмеження. Зітхнути.
Лука

1
Звичайно, ні. Це був лише зразок, який показує, що ви можете викликати функцію в рідному dll, не використовуючи P / Invoke, який вимагає статичного шляху.
Побіг

5

Поки ви знаєте каталог, де можна було знайти ваші бібліотеки C ++ під час виконання, це повинно бути простим. Я чітко бачу, що це так у вашому коді. Ви myDll.dllбудете знаходитись у myLibFolderкаталозі всередині тимчасової папки поточного користувача.

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

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

[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присутній на шляху, представленому шляхом, assemblyProbeDirectoryвін завантажиться, а потрібна функція буде викликана через p / invoke.


3
Це працювало для мене. У мене в папці "Модулі" розміщена в каталозі "бін" моєї програми виконання. Там я розміщую керований DLL та деякі керовані DLL, які потрібен керований DLL. Використання цього рішення І встановлення зондуючого шляху в моєму app.config дозволяє мені динамічно завантажувати необхідні збірки.
WBuck

Для людей, які використовують функції Azure: string workingDirectory = Path.GetFullPath (Path.Combine (ExecutionContext.FunctionDirectory, @ ".. \ bin"));
Червона Шапочка

4

встановити шлях dll у файлі config

<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

Якщо це буде використано для чого-небудь іншого, ніж налагодження для налагодження, воно потрапить через будь-який огляд (безпеку чи інше) моєї книги.
Крістіан.К

21
Це досить жахливе рішення. Системна папка призначена для системних DLL. Тепер вам потрібні права адміністратора і ви покладаєтесь на погану практику лише тому, що ви ліниві.
MikeP

5
+1 для MikeP, -1 за цю відповідь. Це жахливе рішення, кожен, хто робить це, повинен неодноразово лускати, примушуючи читати Стару Нову річ . Так само, як ви дізналися в дитячому садку: системна папка не належить вам, тому не слід використовувати її без дозволу.
Коді Грей

Okok, я згоден з вами, але моя проблема не вирішена так ... Яке місце ви б тоді мені не рекомендували? (Знаючи, що я не можу використовувати змінні для його налаштування - Тому що він чекає постійної рядки - так що я ОБОВ'ЯЗКОВО використовувати місце, яке буде однаковим на кожному комп'ютері?) (Чи є спосіб використовувати змінні, а не константу, замість константи?)
Jsncrdnl
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.