Jak mogę określić ścieżkę [DllImport] w czasie wykonywania?

141

W rzeczywistości mam C ++ (działającą) bibliotekę DLL, którą chcę zaimportować do mojego projektu C #, aby wywołać jej funkcje.

Działa, gdy określę pełną ścieżkę do biblioteki DLL, na przykład:

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

Problem polega na tym, że będzie to projekt do zainstalowania, więc folder użytkownika nie będzie taki sam (np. Pierre, paul, jack, mama, tata, ...) w zależności od komputera / sesji, na której zostanie uruchomiony.

Chciałbym więc, aby mój kod był trochę bardziej ogólny, na przykład:

/* 
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);

Problem polega na tym, że „DllImport” wymaga parametru „const string” dla katalogu biblioteki DLL.

Więc moje pytanie brzmi: co można by zrobić w tym przypadku?

Jsncrdnl
źródło
15
Wystarczy wdrożyć bibliotekę DLL w tym samym folderze, co plik EXE, aby nie musieć nic robić, poza określeniem nazwy DLL bez ścieżki. Możliwe są inne schematy, ale wszystkie są kłopotliwe.
Hans Passant
2
Chodzi o to, że będzie to dodatek do MS Office Excel, więc nie myślę, że umieszczenie biblioteki dll w katalogu exe byłoby najlepszym rozwiązaniem ...
Jsncrdnl
8
Twoje rozwiązanie jest złe. Nie umieszczaj plików w folderach systemu Windows lub systemowych. Wybrali te nazwy z jakiegoś powodu: ponieważ dotyczą plików systemowych Windows. Nie tworzysz żadnego z nich, ponieważ nie pracujesz dla firmy Microsoft w zespole Windows. Pamiętaj, czego nauczyłeś się w przedszkolu o używaniu rzeczy, które nie należą do Ciebie, bez pozwolenia, i przechowuj swoje pliki gdziekolwiek, tylko tam.
Cody Gray
Twoje rozwiązanie jest nadal złe. Dobrze działające aplikacje, które w rzeczywistości nie wykonują czynności administracyjnych, nie powinny wymagać dostępu administracyjnego. Innym problemem jest to, że nie wiem, aplikacja będzie faktycznie być instalowane w tym folderze. Mogę przenieść to gdzie indziej lub zmienić ścieżkę instalacji podczas instalacji (robię tego rodzaju rzeczy dla zabawy, aby zepsuć źle działające aplikacje). Zakodowane ścieżki są uosobieniem złego zachowania i są całkowicie niepotrzebne. Jeśli używasz folderu aplikacji, jest to pierwsza ścieżka w domyślnej kolejności wyszukiwania bibliotek DLL. Wszystko automatyczne.
Cody Gray
3
umieszczanie go w plikach programu NIE jest stałe. Maszyny 64-bitowe zamiast tego mają na przykład plik programu (x86).
Louis Kottmann

Odpowiedzi:

184

W przeciwieństwie do sugestii niektórych innych odpowiedzi, używanie DllImportatrybutu jest nadal właściwym podejściem.

Naprawdę nie rozumiem, dlaczego nie możesz zrobić tak jak wszyscy na świecie i określić krewnego ścieżki do swojej biblioteki DLL. Tak, ścieżka, w której aplikacja zostanie zainstalowana, różni się na komputerach różnych osób, ale w zasadzie jest to uniwersalna zasada dotycząca wdrażania. DllImportMechanizm został zaprojektowany z tą myślą.

W rzeczywistości nawet to nie DllImportrozwiązuje problemu. To natywne reguły ładowania Win32 DLL, które rządzą, niezależnie od tego, czy używasz poręcznych zarządzanych opakowań (po prostu wywołuje P / Invoke marshaller LoadLibrary). Zasady te są tutaj szczegółowo wyliczone , ale najważniejsze z nich wyszczególniono tutaj:

Zanim system wyszuka bibliotekę DLL, sprawdza następujące elementy:

  • Jeśli biblioteka DLL o tej samej nazwie modułu jest już załadowana do pamięci, system używa załadowanej biblioteki DLL, bez względu na katalog, w którym się ona znajduje. System nie wyszukuje biblioteki DLL.
  • Jeśli biblioteka DLL znajduje się na liście znanych bibliotek DLL dla wersji systemu Windows, w którym działa aplikacja, system używa swojej kopii znanej biblioteki DLL (i zależnych od niej bibliotek DLL, jeśli istnieją). System nie wyszukuje biblioteki DLL.

Jeśli SafeDllSearchModejest włączona (domyślnie), kolejność wyszukiwania jest następująca:

  1. Katalog, z którego aplikacja została załadowana.
  2. Katalog systemowy. Użyj GetSystemDirectoryfunkcji, aby uzyskać ścieżkę do tego katalogu.
  3. 16-bitowy katalog systemowy. Nie ma funkcji, która uzyskuje ścieżkę do tego katalogu, ale jest ona przeszukiwana.
  4. Katalog systemu Windows. Użyj GetWindowsDirectoryfunkcji, aby uzyskać ścieżkę do tego katalogu.
  5. Bieżący katalog.
  6. Katalogi wymienione w PATHzmiennej środowiskowej. Należy zauważyć, że nie obejmuje to ścieżki dla aplikacji określonej przez klucz rejestru App Paths. Klucz App Paths nie jest używany podczas obliczania ścieżki wyszukiwania DLL.

Tak więc, jeśli nie nazwiesz swojej biblioteki DLL tym samym, co systemowa biblioteka DLL (czego oczywiście nie powinieneś robić, nigdy, w żadnych okolicznościach), domyślna kolejność wyszukiwania rozpocznie wyszukiwanie w katalogu, z którego została załadowana aplikacja. Jeśli umieścisz tam bibliotekę DLL podczas instalacji, zostanie ona znaleziona. Wszystkie skomplikowane problemy znikną, jeśli użyjesz tylko ścieżek względnych.

Tylko napisz:

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

Ale jeśli to nie zadziała z jakiegokolwiek powodu i musisz zmusić aplikację do wyszukania biblioteki DLL w innym katalogu, możesz zmodyfikować domyślną ścieżkę wyszukiwania za pomocą SetDllDirectoryfunkcji .
Zauważ, że zgodnie z dokumentacją:

Po wywołaniu SetDllDirectorystandardowa ścieżka wyszukiwania DLL to:

  1. Katalog, z którego aplikacja została załadowana.
  2. Katalog określony przez lpPathName parametr.
  3. Katalog systemowy. UżyjGetSystemDirectoryfunkcji, aby uzyskać ścieżkę do tego katalogu.
  4. 16-bitowy katalog systemowy. Nie ma funkcji, która uzyskuje ścieżkę do tego katalogu, ale jest ona przeszukiwana.
  5. Katalog systemu Windows. UżyjGetWindowsDirectoryfunkcji, aby uzyskać ścieżkę do tego katalogu.
  6. Katalogi wymienione w PATHzmiennej środowiskowej.

Tak długo, jak wywołujesz tę funkcję przed wywołaniem funkcji zaimportowanej z biblioteki DLL po raz pierwszy, możesz zmodyfikować domyślną ścieżkę wyszukiwania używaną do lokalizowania bibliotek DLL. Zaletą jest oczywiście to, że do tej funkcji można przekazać wartość dynamiczną, która jest obliczana w czasie wykonywania. Nie jest to możliwe w przypadku DllImportatrybutu, więc nadal będziesz tam używać ścieżki względnej (tylko nazwa biblioteki DLL) i polegać na nowej kolejności wyszukiwania, aby ją znaleźć.

Będziesz musiał P / Wywołać tę funkcję. Deklaracja wygląda następująco:

[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
static extern bool SetDllDirectory(string lpPathName);
Cody Gray
źródło
16
Innym drobnym ulepszeniem może być usunięcie rozszerzenia z nazwy DLL. Windows doda automatycznie, .dlla inne systemy dodadzą odpowiednie rozszerzenie w Mono (np. W .soLinuksie). Może to pomóc, jeśli problemem jest przenośność.
jheddings
6
+1 dla SetDllDirectory. Możesz także po prostu zmienić, Environment.CurrentDirectorya wszystkie względne ścieżki zostaną ocenione z tej ścieżki!
GameScripting,
2
Jeszcze zanim to zostało opublikowane, OP wyjaśnił, że tworzy wtyczkę, więc umieszczenie bibliotek DLL w plikach programów Microsoftu jest czymś w rodzaju nieuruchomienia. Ponadto zmiana DllDirectory lub CWD procesu może nie być dobrym pomysłem, ponieważ może spowodować niepowodzenie procesu. Teraz AddDllDirectoryz drugiej strony ...
Mooing Duck
3
Poleganie na katalogu roboczym jest potencjalnie poważną luką w zabezpieczeniach @GameScripting, a szczególnie niezalecane w przypadku czegoś działającego z uprawnieniami superużytkownika. Warto napisać kod i wykonać prace projektowe, aby zrobić to dobrze.
Cody Gray
2
Zauważ, że DllImportto coś więcej niż tylko opakowanie LoadLibrary. Uwzględnia również katalog zestawu, w którym externmetoda jest zdefiniowana . Te DllImportścieżki wyszukiwania mogą być dodatkowo ograniczone użyciu DefaultDllImportSearchPath.
Mitch
38

Nawet lepiej niż sugestia Rana dotycząca użycia GetProcAddress, po prostu wykonaj wywołanie funkcji LoadLibraryprzed jakimkolwiek wywołaniem DllImportfunkcji (tylko z nazwą pliku bez ścieżki), a automatycznie użyją załadowanego modułu.

Użyłem tej metody, aby wybrać w czasie wykonywania, czy załadować 32-bitową, czy 64-bitową natywną bibliotekę DLL bez konieczności modyfikowania zestawu funkcji P / Invoke-d. Umieść kod ładujący w konstruktorze statycznym dla typu, który ma zaimportowane funkcje, a wszystko będzie działać poprawnie.

MikeP
źródło
1
Nie jestem pewien, czy to na pewno zadziała. Lub jeśli tak się po prostu dzieje w obecnej wersji frameworka.
CodesInChaos
3
@Kod: Wydaje mi się gwarantowane: Kolejność wyszukiwania w bibliotece Dynamic-Link . W szczególności „Czynniki wpływające na wyszukiwanie” punkt pierwszy.
Cody Grey
Miły. Cóż, moje rozwiązanie ma małą dodatkową zaletę, ponieważ nawet nazwa funkcji nie musi być statyczna i znana w czasie kompilacji. Jeśli masz 2 funkcje z tym samym podpisem i inną nazwą, możesz je wywołać za pomocą mojego FunctionLoaderkodu.
Biegał
To brzmi jak to, czego chcę. Miałem nadzieję, że użyję nazw plików, takich jak mylibrary32.dll i mylibrary64.dll, ale myślę, że mogę żyć z nimi o tej samej nazwie, ale w różnych folderach.
jojo
27

Jeśli potrzebujesz pliku .dll, który nie znajduje się na ścieżce ani w lokalizacji aplikacji, nie sądzę, że możesz to zrobić, ponieważ DllImportjest to atrybut, a atrybuty to tylko metadane, które są ustawione dla typów, członków i innych elementy językowe.

Alternatywą, która może pomóc ci osiągnąć to, co myślę, że próbujesz, jest użycie natywnego LoadLibraryprzez P / Invoke, w celu załadowania .dll ze ścieżki, której potrzebujesz, a następnie użyjGetProcAddress aby uzyskać odniesienie do potrzebnej funkcji z tego .dll. Następnie użyj ich, aby utworzyć delegata, którego możesz wywołać.

Aby ułatwić używanie, możesz następnie ustawić tego delegata na pole w swojej klasie, aby używanie go wyglądało jak wywołanie metody składowej.

EDYTOWAĆ

Oto fragment kodu, który działa i pokazuje, o co mi chodzi.

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);
    }
}

Uwaga: nie zawracałem sobie głowy używaniem FreeLibrary, więc ten kod nie jest kompletny. W prawdziwej aplikacji należy zadbać o zwolnienie załadowanych modułów, aby uniknąć wycieku pamięci.

Biegł
źródło
Istnieje zarządzany odpowiednik LoadLibrary (w klasie Assembly).
Luca
Gdybyś miał przykładowy kod, byłoby mi łatwiej to zrozumieć! ^^ (Właściwie to trochę mgliste)
Jsncrdnl
1
@Luca Piccioni: Jeśli chodziło Ci o Assembly.LoadFrom, ładuje to tylko zestawy .NET, a nie biblioteki natywne. Co miałeś na myśli?
Biegał
1
Miałem to na myśli, ale nie wiedziałem o tym ograniczeniu. Westchnienie.
Luca
1
Oczywiście nie. To był tylko przykład pokazujący, że można wywołać funkcję w natywnej bibliotece dll bez użycia P / Invoke, która wymaga statycznej ścieżki.
Biegał
5

Tak długo, jak znasz katalog, w którym można znaleźć biblioteki C ++ w czasie wykonywania, powinno to być proste. Wyraźnie widzę, że tak jest w twoim kodzie. Twój myDll.dllbyłby obecny w myLibFolderkatalogu wewnątrz folderu tymczasowego bieżącego użytkownika.

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

Teraz możesz kontynuować używanie instrukcji DllImport, używając ciągu const, jak pokazano poniżej:

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

Tuż w czasie wykonywania, zanim wywołasz DLLFunctionfunkcję (obecną w bibliotece C ++) dodaj następujący wiersz kodu w kodzie C #:

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

To po prostu instruuje środowisko CLR, aby szukało niezarządzanych bibliotek C ++ w ścieżce katalogu, którą otrzymałeś w czasie wykonywania programu. Directory.SetCurrentDirectorycall ustawia bieżący katalog roboczy aplikacji na określony katalog. Jeśli twoja myDLL.dlljest obecna na ścieżce reprezentowanej przez assemblyProbeDirectoryścieżkę, zostanie załadowana, a żądana funkcja zostanie wywołana przez p / invoke.

RBT
źródło
3
To zadziałało dla mnie. Mam folder „Moduły” znajdujący się w katalogu „bin” mojej uruchomionej aplikacji. Tam umieszczam zarządzaną bibliotekę dll i niektóre niezarządzane biblioteki dll, których wymaga zarządzana biblioteka dll. Korzystanie z tego rozwiązania ORAZ ustawienie ścieżki sondowania w pliku app.config umożliwia dynamiczne ładowanie wymaganych zestawów.
WBuck
Dla osób korzystających z Azure Functions: string workingDirectory = Path.GetFullPath (Path.Combine (ExecutionContext.FunctionDirectory, @ ".. \ bin"));
Czerwony Kapturek
4

ustaw ścieżkę dll w pliku konfiguracyjnym

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

przed wywołaniem biblioteki DLL w swojej aplikacji wykonaj następujące czynności

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

następnie wywołaj dll i możesz użyć jak poniżej

 [DllImport("myDLL.dll", CallingConvention = CallingConvention.Cdecl)]
public static extern int DLLFunction(int Number1, int Number2);
Sajithd
źródło
0

DllImport będzie działać poprawnie bez określonej ścieżki, o ile dll znajduje się gdzieś na ścieżce systemowej. Możesz tymczasowo dodać folder użytkownika do ścieżki.

Mike W.
źródło
Próbowałem umieścić go w systemie Zmienne środowiskowe, ALE nadal jest uważany za niestały (myślę, że logiczny)
Jsncrdnl
-14

Jeśli wszystko zawiedzie, po prostu umieść bibliotekę DLL w windows\system32folderze. Kompilator go znajdzie. Określ bibliotekę DLL do załadowania z:, DllImport("user32.dll"...ustaw, EntryPoint = "my_unmanaged_function"aby zaimportować żądaną niezarządzaną funkcję do aplikacji 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);    
   }
}

Źródło i jeszcze więcej DllImportprzykładów: http://msdn.microsoft.com/en-us/library/aa288468(v=vs.71).aspx

Twórca oprogramowania
źródło
Ok, zgadzam się z Twoim rozwiązaniem polegającym na użyciu folderu win32 (najprostszy sposób), ale jak przyznać dostęp do tego folderu debugerowi Visual Studio (a także skompilowanej aplikacji)? (Z wyjątkiem ręcznego uruchomienia go jako admin)
Jsncrdnl
Jeśli jest to używane do czegoś więcej niż pomoc w debugowaniu, przejdzie przez każdą recenzję (bezpieczeństwo lub inne) w mojej książce.
Christian.K
21
To dość okropne rozwiązanie. Folder systemowy jest przeznaczony dla systemowych bibliotek DLL. Teraz potrzebujesz uprawnień administratora i polegasz na złych praktykach tylko dlatego, że jesteś leniwy.
MikeP
5
+1 dla MikeP, -1 dla tej odpowiedzi. To straszne rozwiązanie, każdy, kto to robi, powinien być wielokrotnie chłostany, zmuszony do czytania The Old New Thing . Tak jak nauczyłeś się w przedszkolu: folder systemowy nie należy do ciebie, więc nie powinieneś go używać bez pozwolenia.
Cody Gray
Okok, zgadzam się z tobą, ale mój problem nie został rozwiązany, więc ... Którą lokalizację byś mi wtedy polecił? (Wiedząc, że nie mogę użyć zmiennych do ustawienia - ponieważ czeka na ciągły ciąg znaków - więc że MUSZĘ użyć lokalizacji, która będzie taka sama na każdym komputerze?) (Czy jest jakiś sposób na użycie zmiennych zamiast stałej, aby to wykonać?)
Jsncrdnl