Wie kann ich zur Laufzeit einen [DllImport] -Pfad angeben?

141

Tatsächlich habe ich eine C ++ (funktionierende) DLL erhalten, die ich in mein C # -Projekt importieren möchte, um dessen Funktionen aufzurufen.

Es funktioniert, wenn ich den vollständigen Pfad zur DLL wie folgt spezifiziere:

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

Das Problem ist, dass es sich um ein installierbares Projekt handelt, sodass der Ordner des Benutzers nicht derselbe ist (z. B. Pierre, Paul, Jack, Mama, Papa, ...), je nachdem, auf welchem ​​Computer / in welcher Sitzung er ausgeführt wird.

Ich möchte, dass mein Code etwas allgemeiner ist, wie folgt:

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

Die große Sache ist, dass "DllImport" einen "const string" -Parameter für das DLL-Verzeichnis wünscht.

Meine Frage lautet also: Was könnte in diesem Fall getan werden?

Jsncrdnl
quelle
15
Stellen Sie die DLL einfach im selben Ordner wie die EXE-Datei bereit, damit Sie nichts anderes tun müssen, als den DLL-Namen ohne den Pfad anzugeben. Andere Schemata sind möglich, aber alle problematisch.
Hans Passant
2
Die Sache ist, dass es ein MS Office Excel Add In sein wird, also wäre es nicht die beste Lösung, die DLL in das Verzeichnis der Exe zu stellen ...
Jsncrdnl
8
Ihre Lösung ist die falsche. Legen Sie keine Dateien in den Windows- oder Systemordnern ab. Sie haben diese Namen aus einem Grund gewählt: weil sie für Windows-Systemdateien sind. Sie erstellen keine davon, weil Sie im Windows-Team nicht für Microsoft arbeiten. Denken Sie daran, was Sie im Kindergarten über die Verwendung von Dingen gelernt haben, die Ihnen nicht ohne Erlaubnis gehören, und legen Sie Ihre Dateien an einer anderen Stelle als dort ab.
Cody Gray
Ihre Lösung ist immer noch falsch. Gut erzogene Anwendungen, die eigentlich keine administrativen Aufgaben ausführen, sollten keinen administrativen Zugriff erfordern. Das andere Problem ist , dass Sie nicht wissen , Ihre Anwendung tatsächlich wird in diesem Ordner installiert. Ich könnte es an einen anderen Ort verschieben oder den Installationspfad während des Setups ändern (ich mache solche Sachen zum Spaß, nur um schlecht benommene Anwendungen zu brechen). Das Hardcodieren von Pfaden ist der Inbegriff für schlechtes Verhalten und völlig unnötig. Wenn Sie den Ordner Ihrer Anwendung verwenden, ist dies der erste Pfad in der Standardsuchreihenfolge für DLLs. Alles automatisch.
Cody Gray
3
Das Einfügen in Programmdateien ist NICHT konstant. 64-Bit-Computer verfügen beispielsweise stattdessen über eine Programmdatei (x86).
Louis Kottmann

Antworten:

184

Im Gegensatz zu den Vorschlägen einiger anderer Antworten ist die Verwendung des DllImportAttributs immer noch der richtige Ansatz.

Ich verstehe ehrlich gesagt nicht, warum Sie nicht wie alle anderen auf der Welt einen relativen Pfad zu Ihrer DLL angeben können . Ja, der Pfad, in dem Ihre Anwendung installiert wird, unterscheidet sich auf den Computern verschiedener Personen. Dies ist jedoch im Grunde eine universelle Regel für die Bereitstellung. Der DllImportMechanismus ist in diesem Sinne ausgelegt.

Tatsächlich ist es nicht einmal das DllImport, was damit umgeht. Es sind die nativen Win32-DLL-Laderegeln, die die Dinge regeln, unabhängig davon, ob Sie die handlichen verwalteten Wrapper verwenden (der P / Invoke-Marshaller ruft nur auf LoadLibrary). Diese Regeln werden hier sehr detailliert aufgeführt , aber die wichtigsten sind hier auszugsweise:

Bevor das System nach einer DLL sucht, überprüft es Folgendes:

  • Wenn bereits eine DLL mit demselben Modulnamen im Speicher geladen ist, verwendet das System die geladene DLL, unabhängig davon, in welchem ​​Verzeichnis sie sich befindet. Das System sucht nicht nach der DLL.
  • Befindet sich die DLL in der Liste der bekannten DLLs für die Windows-Version, unter der die Anwendung ausgeführt wird, verwendet das System die Kopie der bekannten DLL (und gegebenenfalls die abhängigen DLLs der bekannten DLL). Das System sucht nicht nach der DLL.

Wenn SafeDllSearchModeaktiviert (Standardeinstellung), lautet die Suchreihenfolge wie folgt:

  1. Das Verzeichnis, aus dem die Anwendung geladen wurde.
  2. Das Systemverzeichnis. Verwenden Sie die GetSystemDirectoryFunktion, um den Pfad dieses Verzeichnisses abzurufen.
  3. Das 16-Bit-Systemverzeichnis. Es gibt keine Funktion, die den Pfad dieses Verzeichnisses abruft, aber es wird gesucht.
  4. Das Windows-Verzeichnis. Verwenden Sie die GetWindowsDirectoryFunktion, um den Pfad dieses Verzeichnisses abzurufen.
  5. Das aktuelle Verzeichnis.
  6. Die Verzeichnisse, die in der PATHUmgebungsvariablen aufgeführt sind. Beachten Sie, dass dies nicht den Anwendungspfad enthält, der im Registrierungsschlüssel für Anwendungspfade angegeben ist. Der Schlüssel "App-Pfade" wird bei der Berechnung des DLL-Suchpfads nicht verwendet.

Sofern Sie Ihre DLL nicht wie eine System-DLL benennen (was Sie unter keinen Umständen tun sollten), wird die Standardsuchreihenfolge in dem Verzeichnis angezeigt, aus dem Ihre Anwendung geladen wurde. Wenn Sie die DLL während der Installation dort platzieren, wird sie gefunden. Alle komplizierten Probleme verschwinden, wenn Sie nur relative Pfade verwenden.

Einfach schreiben:

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

Wenn dies jedoch aus irgendeinem Grund nicht funktioniert und Sie die Anwendung zwingen müssen, in einem anderen Verzeichnis nach der DLL zu suchen, können Sie den Standardsuchpfad mithilfe der SetDllDirectoryFunktion ändern .
Beachten Sie, dass gemäß der Dokumentation:

Nach dem Aufruf SetDllDirectorylautet der Standard-DLL-Suchpfad:

  1. Das Verzeichnis, aus dem die Anwendung geladen wurde.
  2. Das durch den lpPathNameParameter angegebene Verzeichnis .
  3. Das Systemverzeichnis. Verwenden Sie die GetSystemDirectoryFunktion, um den Pfad dieses Verzeichnisses abzurufen.
  4. Das 16-Bit-Systemverzeichnis. Es gibt keine Funktion, die den Pfad dieses Verzeichnisses abruft, aber es wird gesucht.
  5. Das Windows-Verzeichnis. Verwenden Sie die GetWindowsDirectoryFunktion, um den Pfad dieses Verzeichnisses abzurufen.
  6. Die Verzeichnisse, die in der PATHUmgebungsvariablen aufgeführt sind.

Solange Sie diese Funktion aufrufen, bevor Sie die aus der DLL importierte Funktion zum ersten Mal aufrufen, können Sie den Standardsuchpfad zum Suchen von DLLs ändern. Der Vorteil ist natürlich, dass Sie dieser Funktion, die zur Laufzeit berechnet wird, einen dynamischen Wert übergeben können. Dies ist mit dem DllImportAttribut nicht möglich. Daher verwenden Sie dort immer noch einen relativen Pfad (nur den Namen der DLL) und verlassen sich auf die neue Suchreihenfolge, um ihn für Sie zu finden.

Sie müssen diese Funktion aufrufen. Die Erklärung sieht folgendermaßen aus:

[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
static extern bool SetDllDirectory(string lpPathName);
Cody Grey
quelle
16
Eine weitere geringfügige Verbesserung besteht möglicherweise darin, die Erweiterung aus dem DLL-Namen zu entfernen. Windows fügt automatisch hinzu .dllund andere Systeme fügen die entsprechende Erweiterung unter Mono hinzu (z .so. B. unter Linux). Dies kann hilfreich sein, wenn die Portabilität ein Problem darstellt.
jheddings
6
+1 für die SetDllDirectory. Sie können auch einfach ändern Environment.CurrentDirectoryund alle relativen Pfade werden von diesem Pfad ausgewertet!
GameScripting
2
Noch bevor dies veröffentlicht wurde, stellte das OP klar, dass er ein Plugin erstellt, sodass das Einfügen der DLLs in die Programmdateien von Microsoft eine Art Nichtstarter ist. Das Ändern des Prozess-DllDirectory oder des CWD ist möglicherweise keine gute Idee. Sie können dazu führen, dass der Prozess fehlschlägt. Jetzt AddDllDirectoryauf der anderen Seite ...
Mooing Duck
3
Das Verlassen auf das Arbeitsverzeichnis ist eine potenziell schwerwiegende Sicherheitslücke, @GameScripting, und besonders schlecht beraten für etwas, das mit Superuser-Berechtigungen ausgeführt wird. Es lohnt sich, den Code zu schreiben und die Entwurfsarbeit zu erledigen, um ihn richtig zu machen.
Cody Gray
2
Beachten Sie, dass dies DllImportmehr als nur ein Wrapper ist LoadLibrary. Es berücksichtigt auch das Verzeichnis der Assembly, in der die externMethode definiert ist . Die DllImportSuchpfade können zusätzlich mit eingeschränkt werden DefaultDllImportSearchPath.
Mitch
38

Noch besser als Rans Vorschlag GetProcAddress, einfach die Funktionen aufzurufen, LoadLibrarybevor die DllImportFunktionen aufgerufen werden (nur mit einem Dateinamen ohne Pfad), und das geladene Modul wird automatisch verwendet.

Ich habe diese Methode verwendet, um zur Laufzeit zu wählen, ob eine native 32-Bit- oder 64-Bit-DLL geladen werden soll, ohne eine Reihe von P / Invoke-d-Funktionen ändern zu müssen. Stecken Sie den Ladecode in einen statischen Konstruktor für den Typ, der die importierten Funktionen hat, und alles wird gut funktionieren.

MikeP
quelle
1
Ich bin mir nicht sicher, ob dies garantiert funktioniert. Oder wenn es nur bei der aktuellen Version des Frameworks passiert.
CodesInChaos
3
@Code: Scheint mir garantiert: Dynamic-Link Library Search Order . Insbesondere "Faktoren, die die Suche beeinflussen", Punkt eins.
Cody Gray
Nett. Nun, meine Lösung hat einen kleinen zusätzlichen Vorteil, da selbst der Funktionsname beim Kompilieren nicht statisch und bekannt sein muss. Wenn Sie zwei Funktionen mit derselben Signatur und einem anderen Namen haben, können Sie diese mit meinem FunctionLoaderCode aufrufen .
Ran
Das klingt nach dem, was ich will. Ich hatte gehofft, Dateinamen wie mylibrary32.dll und mylibrary64.dll verwenden zu können, aber ich denke, ich kann damit leben, dass sie denselben Namen haben, aber in verschiedenen Ordnern.
Yooy
27

Wenn Sie eine DLL-Datei benötigen, die sich nicht im Pfad oder am Speicherort der Anwendung befindet, können Sie dies meines Erachtens nicht tun, da DllImportes sich um ein Attribut handelt und Attribute nur Metadaten sind, die für Typen, Mitglieder und andere festgelegt sind Sprachelemente.

Eine Alternative, die Ihnen dabei helfen kann, das zu erreichen, was Sie meiner Meinung nach versuchen, besteht darin, das native LoadLibraryüber P / Invoke zu verwenden, um eine DLL aus dem von Ihnen benötigten Pfad zu laden und dann GetProcAddresseinen Verweis auf die von Ihnen benötigte Funktion zu erhalten von dieser .dll. Verwenden Sie diese dann, um einen Delegaten zu erstellen, den Sie aufrufen können.

Um die Verwendung zu vereinfachen, können Sie diesen Delegaten dann auf ein Feld in Ihrer Klasse festlegen, sodass die Verwendung so aussieht, als würde eine Mitgliedsmethode aufgerufen.

BEARBEITEN

Hier ist ein Code-Snippet, das funktioniert und zeigt, was ich meinte.

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

Hinweis: Ich habe mich nicht um die Verwendung gekümmert FreeLibrary, daher ist dieser Code nicht vollständig. In einer realen Anwendung sollten Sie darauf achten, die geladenen Module freizugeben, um einen Speicherverlust zu vermeiden.

Ran
quelle
Es gibt ein verwaltetes Gegenstück für LoadLibrary (in der Assembly-Klasse).
Luca
Wenn Sie ein Codebeispiel hätten, wäre es für mich einfacher zu verstehen! ^^ (Eigentlich ist es ein bisschen neblig)
Jsncrdnl
1
@Luca Piccioni: Wenn Sie Assembly.LoadFrom gemeint haben, werden nur .NET-Assemblys geladen, keine nativen Bibliotheken. Was hast du gemeint?
Ran
1
Ich meinte das, aber ich wusste nichts über diese Einschränkung. Seufzer.
Luca
1
Natürlich nicht. Dies war nur ein Beispiel, um zu zeigen, dass Sie eine Funktion in einer nativen DLL aufrufen können, ohne P / Invoke zu verwenden, für das ein statischer Pfad erforderlich ist.
Ran
5

Solange Sie das Verzeichnis kennen, in dem sich Ihre C ++ - Bibliotheken zur Laufzeit befinden, sollte dies einfach sein. Ich kann deutlich sehen, dass dies in Ihrem Code der Fall ist. Sie befinden myDll.dllsich im myLibFolderVerzeichnis des temporären Ordners des aktuellen Benutzers.

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

Jetzt können Sie die DllImport-Anweisung mit einer const-Zeichenfolge wie unten gezeigt weiter verwenden:

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

Fügen Sie zur Laufzeit vor dem Aufrufen der DLLFunctionFunktion (in der C ++ - Bibliothek vorhanden) diese Codezeile in C # -Code hinzu:

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

Dadurch wird die CLR lediglich angewiesen, im Verzeichnispfad, den Sie zur Laufzeit Ihres Programms erhalten haben, nach nicht verwalteten C ++ - Bibliotheken zu suchen. Directory.SetCurrentDirectorycall setzt das aktuelle Arbeitsverzeichnis der Anwendung auf das angegebene Verzeichnis. Wenn Ihr myDLL.dllam Pfad vorhanden ist, der durch den assemblyProbeDirectoryPfad dargestellt wird, wird er geladen und die gewünschte Funktion wird über p / invoke aufgerufen.

RBT
quelle
3
Das hat bei mir funktioniert. Ich habe einen Ordner "Module" im Verzeichnis "bin" meiner ausführenden Anwendung. Dort platziere ich eine verwaltete DLL und einige nicht verwaltete DLLs, die für die verwaltete DLL erforderlich sind. Mit dieser Lösung UND dem Festlegen des Prüfpfads in meiner app.config kann ich die erforderlichen Assemblys dynamisch laden.
WBuck
Für Benutzer von Azure-Funktionen: Zeichenfolge workingDirectory = Path.GetFullPath (Path.Combine (executeContext.FunctionDirectory, @ ".. \ bin"));
Rotkäppchen
4

Legen Sie den DLL-Pfad in der Konfigurationsdatei fest

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

Gehen Sie wie folgt vor, bevor Sie die DLL in Ihrer App aufrufen

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

Rufen Sie dann die DLL auf und Sie können wie unten verwenden

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

DllImport funktioniert ohne den angegebenen vollständigen Pfad einwandfrei, solange sich die DLL irgendwo im Systempfad befindet. Möglicherweise können Sie den Ordner des Benutzers vorübergehend zum Pfad hinzufügen.

Mike W.
quelle
Ich habe versucht, es in das System Umgebungsvariablen zu platzieren, aber es wird immer noch als nicht konstant angesehen (logisch, denke ich)
Jsncrdnl
-14

Wenn alles fehlschlägt, legen Sie einfach die DLL in den windows\system32Ordner. Der Compiler wird es finden. Geben Sie die DLL zu laden aus mit: DllImport("user32.dll"..., Satz EntryPoint = "my_unmanaged_function"Ihre gewünschte nicht verwaltete Funktion zu C # app zu importieren:

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

Quelle und noch mehr DllImportBeispiele: http://msdn.microsoft.com/en-us/library/aa288468(v=vs.71).aspx

Software Designer
quelle
Ok, ich stimme Ihrer Lösung zur Verwendung des Win32-Ordners zu (einfachste Methode), aber wie gewähren Sie dem Visual Studio-Debugger (und auch der kompilierten Anwendung) Zugriff auf diesen Ordner? (Außer es manuell als Admin
auszuführen
Wenn dies für mehr als eine Debugging-Hilfe verwendet wird, würde es durch jede Überprüfung (Sicherheit oder auf andere Weise) in meinem Buch fallen.
Christian.K
21
Dies ist eine ziemlich schreckliche Lösung. Der Systemordner ist für System- DLLs. Jetzt benötigen Sie Administratorrechte und verlassen sich auf schlechte Praktiken, nur weil Sie faul sind.
MikeP
5
+1 für MikeP, -1 für diese Antwort. Dies ist eine schreckliche Lösung. Jeder, der dies tut, sollte wiederholt ausgepeitscht werden, während er gezwungen wird, The Old New Thing zu lesen . Genau wie Sie im Kindergarten gelernt haben: Der Systemordner gehört Ihnen nicht, deshalb sollten Sie ihn nicht ohne Erlaubnis verwenden.
Cody Gray
Okok, ich stimme Ihnen zu, aber mein Problem ist nicht gelöst, also ... Welchen Speicherort würden Sie mir dann empfehlen? (Da Sie wissen, dass ich keine Variablen zum Einrichten verwenden kann - weil es auf eine konstante Zeichenfolge wartet -, also dass ich einen Ort verwenden MUSS, der auf jedem Computer gleich sein wird?) (Oder gibt es eine Möglichkeit, Variablen anstelle einer Konstanten zu verwenden, um sie auszuführen?)
Jsncrdnl