Come posso specificare un percorso [DllImport] in fase di esecuzione?


141

In effetti, ho una DLL C ++ (funzionante) che voglio importare nel mio progetto C # per chiamarne le funzioni.

Funziona quando specifico il percorso completo della DLL, in questo modo:

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

Il problema è che sarà un progetto installabile, quindi la cartella dell'utente non sarà la stessa (es: pierre, paul, jack, mamma, papà, ...) a seconda del computer / sessione in cui verrebbe eseguito.

Quindi vorrei che il mio codice fosse un po 'più generico, in questo modo:

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

Il grosso problema è che "DllImport" desidera un parametro "const string" per la directory della DLL.

Quindi la mia domanda è: cosa si potrebbe fare in questo caso?


15
Distribuisci la DLL nella stessa cartella di EXE in modo da non dover fare altro che specificare il nome DLL senza il percorso. Altri schemi sono possibili ma sono tutti problematici.
Hans Passant,

2
Il fatto è che sarà un componente aggiuntivo MS Office Excel, quindi non credo che mettere la dll nella directory
dell'exe

8
La tua soluzione è quella sbagliata. Non posizionare i file nelle cartelle di Windows o di sistema. Hanno scelto quei nomi per un motivo: perché sono per i file di sistema di Windows. Non stai creando uno di questi perché non lavori per Microsoft nel team di Windows. Ricorda cosa hai imparato all'asilo sull'uso di cose che non ti appartengono senza permesso e metti i tuoi file ovunque ma lì.
Cody Grey

La tua soluzione è ancora sbagliata. Le applicazioni ben educate che in realtà non svolgono attività amministrative non dovrebbero richiedere l'accesso amministrativo. L'altra questione è che non si conosce la vostra applicazione sarà effettivamente essere installato in quella cartella. Potrei spostarlo da qualche altra parte o cambiare il percorso di installazione durante l'installazione (faccio quel genere di cose per divertimento, solo per rompere le applicazioni maleducate). I percorsi hard-coding sono l'epitome di comportamenti scorretti ed è completamente inutile. Se stai utilizzando la cartella dell'applicazione, questo è il primo percorso nell'ordine di ricerca predefinito per le DLL. Tutto automatico.
Cody Grey

3
metterlo nei file di programma NON è costante. Le macchine a 64 bit hanno invece File di programma (x86), ad esempio.
Louis Kottmann,

Risposte:


184

Contrariamente ai suggerimenti di alcune delle altre risposte, l'utilizzo DllImportdell'attributo è ancora l'approccio corretto.

Onestamente non capisco perché non puoi fare come tutti gli altri nel mondo e specificare un percorso relativo alla tua DLL. Sì, il percorso in cui verrà installata l'applicazione differisce sui computer di persone diverse, ma questa è fondamentalmente una regola universale quando si tratta di distribuzione. Il DllImportmeccanismo è progettato pensando a questo.

In realtà, non è nemmeno DllImportquello che lo gestisce. Sono le regole di caricamento della DLL Win32 native che governano le cose, indipendentemente dal fatto che tu stia utilizzando i pratici wrapper gestiti (il marshaller P / Invoke chiama semplicemente LoadLibrary). Queste regole sono elencate qui in dettaglio , ma quelle importanti sono tratte qui:

Prima che il sistema cerchi una DLL, controlla quanto segue:

  • Se una DLL con lo stesso nome di modulo è già caricata in memoria, il sistema utilizza la DLL caricata, indipendentemente dalla directory in cui si trova. Il sistema non cerca la DLL.
  • Se la DLL è nell'elenco delle DLL conosciute per la versione di Windows su cui è in esecuzione l'applicazione, il sistema utilizza la sua copia della DLL nota (e delle eventuali DLL dipendenti della DLL nota). Il sistema non cerca la DLL.

Se SafeDllSearchModeè abilitato (impostazione predefinita), l'ordine di ricerca è il seguente:

  1. La directory da cui è stata caricata l'applicazione.
  2. La directory di sistema. Utilizzare la GetSystemDirectoryfunzione per ottenere il percorso di questa directory.
  3. La directory di sistema a 16 bit. Non esiste alcuna funzione che ottiene il percorso di questa directory, ma viene cercata.
  4. La directory di Windows Utilizzare la GetWindowsDirectoryfunzione per ottenere il percorso di questa directory.
  5. La directory corrente.
  6. Le directory elencate nella PATHvariabile di ambiente. Si noti che ciò non include il percorso per applicazione specificato dalla chiave del Registro di sistema Percorsi app. La chiave Percorsi app non viene utilizzata durante il calcolo del percorso di ricerca DLL.

Quindi, a meno che tu non stia nominando la tua DLL come una DLL di sistema (cosa che ovviamente non dovresti mai fare, in nessun caso), l'ordine di ricerca predefinito inizierà a cercare nella directory da cui è stata caricata la tua applicazione. Se si inserisce la DLL lì durante l'installazione, verrà trovata. Tutti i complicati problemi scompaiono se si utilizzano solo percorsi relativi.

Scrivi e basta:

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

Ma se ciò non funziona per qualsiasi motivo, ed è necessario forzare l'applicazione a cercare una directory diversa per la DLL, è possibile modificare il percorso di ricerca predefinito utilizzando la SetDllDirectoryfunzione .
Si noti che, come da documentazione:

Dopo aver chiamato SetDllDirectory, il percorso di ricerca DLL standard è:

  1. La directory da cui è stata caricata l'applicazione.
  2. La directory specificata dal lpPathNameparametro
  3. La directory di sistema. Utilizzare la GetSystemDirectoryfunzione per ottenere il percorso di questa directory.
  4. La directory di sistema a 16 bit. Non esiste alcuna funzione che ottiene il percorso di questa directory, ma viene cercata.
  5. La directory di Windows Utilizzare la GetWindowsDirectoryfunzione per ottenere il percorso di questa directory.
  6. Le directory elencate nella PATHvariabile di ambiente.

Quindi, finché si chiama questa funzione prima di chiamare la funzione importata dalla DLL per la prima volta, è possibile modificare il percorso di ricerca predefinito utilizzato per individuare le DLL. Il vantaggio, ovviamente, è che puoi trasferire un valore dinamico a questa funzione che viene calcolata in fase di esecuzione. Ciò non è possibile con l' DllImportattributo, quindi dovrai comunque utilizzare un percorso relativo (solo il nome della DLL) e fare affidamento sul nuovo ordine di ricerca per trovarlo.

Dovrai P / Invocare questa funzione. La dichiarazione è simile alla seguente:

[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
static extern bool SetDllDirectory(string lpPathName);

16
Un altro piccolo miglioramento su questo potrebbe essere quello di eliminare l'estensione dal nome DLL. Windows verrà aggiunto automaticamente .dlle altri sistemi aggiungeranno l'estensione appropriata in Mono (ad es. .soSu Linux). Questo può aiutare se la portabilità è un problema.
jheddings,

6
+1 per il SetDllDirectory. Puoi anche solo cambiare Environment.CurrentDirectorye tutti i percorsi relativi verranno valutati da quel percorso!
GameScript

2
Ancor prima che questo fosse pubblicato, l'OP ha chiarito che sta realizzando un plugin, quindi inserire le DLL nei file di programma di Microsoft è una specie di non avviatore. Inoltre, alterare il processo DllDirectory o CWD potrebbe non essere una buona idea, potrebbero causare il fallimento del processo. Ora AddDllDirectoryd'altra parte ...
Mooing anatra

3
Affidarsi alla directory di lavoro è una vulnerabilità di sicurezza potenzialmente grave, @GameScripting, e particolarmente sconsigliata per qualcosa che funziona con autorizzazioni di superutente. Vale la pena scrivere il codice e fare il lavoro di progettazione per farlo bene.
Cody Grey

2
Nota che DllImportè molto più di un semplice wrapper LoadLibrary. Consideraextern anche la directory dell'assembly in cui è definito il metodo . I DllImportpercorsi di ricerca possono essere ulteriormente limitati utilizzando DefaultDllImportSearchPath.
Mitch,

38

Anche meglio del suggerimento di Ran di utilizzare GetProcAddress, è sufficiente effettuare la chiamata LoadLibraryprima di qualsiasi chiamata alle DllImportfunzioni (con solo un nome file senza percorso) e utilizzeranno automaticamente il modulo caricato.

Ho usato questo metodo per scegliere in fase di esecuzione se caricare una DLL nativa a 32 o 64 bit senza dover modificare un gruppo di funzioni P / Invoke-d. Inserisci il codice di caricamento in un costruttore statico per il tipo che ha le funzioni importate e tutto funzionerà perfettamente.


1
Non sono sicuro se questo è garantito per funzionare. O se succede solo sulla versione corrente del framework.
CodesInChaos,

3
@Code: Mi sembra garantito: Ordine di ricerca della libreria Dynamic-Link . In particolare, "Fattori che influenzano la ricerca", punto uno.
Cody Grey

Bello. Bene, la mia soluzione ha un piccolo vantaggio aggiuntivo, poiché anche il nome della funzione non deve essere statico e noto al momento della compilazione. Se hai 2 funzioni con la stessa firma e un nome diverso, puoi invocarle usando il mio FunctionLoadercodice.
Ha funzionato il

Sembra quello che voglio. Speravo di usare nomi di file come mylibrary32.dll e mylibrary64.dll, ma immagino di poter vivere con loro con lo stesso nome ma in cartelle diverse.
yoyo

27

Se hai bisogno di un file DLL che non si trova sul percorso o sul percorso dell'applicazione, non penso che tu possa fare proprio questo, perché DllImportè un attributo e gli attributi sono solo metadati impostati su tipi, membri e altri elementi del linguaggio.

Un'alternativa che può aiutarti a realizzare quello che penso tu stia provando, è usare il nativo LoadLibrarytramite P / Invoke, al fine di caricare una DLL dal percorso che ti serve, e quindi usare GetProcAddressper ottenere un riferimento alla funzione che ti serve da quel .dll. Quindi utilizzare questi per creare un delegato che è possibile richiamare.

Per facilitarne l'utilizzo, puoi quindi impostare questo delegato su un campo della tua classe, in modo che utilizzarlo sia come chiamare un metodo membro.

MODIFICARE

Ecco un frammento di codice che funziona e mostra cosa intendevo dire.

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

Nota: non mi sono preoccupato di usare FreeLibrary, quindi questo codice non è completo. In un'applicazione reale, prestare attenzione a rilasciare i moduli caricati per evitare perdite di memoria.


Esiste una controparte gestita per LoadLibrary (nella classe Assembly).
Luca,

Se avessi qualche esempio di codice, sarebbe più facile per me capire! ^^ (In realtà, è un po 'nebbioso)
Jsncrdnl,

1
@Luca Piccioni: se intendevi Assembly.LoadFrom, questo carica solo assembly .NET, non librerie native. Cosa intendevi?
Ran

1
Lo intendevo, ma non sapevo di questa limitazione. Sospiro.
Luca,

1
Ovviamente no. Quello era solo un esempio per dimostrare che è possibile chiamare una funzione in una dll nativa senza usare P / Invoke che richiede un percorso statico.
Ha funzionato il

5

Se conosci la directory in cui è possibile trovare le tue librerie C ++ in fase di esecuzione, questo dovrebbe essere semplice. Posso vedere chiaramente che questo è il caso nel tuo codice. Il tuo myDll.dllsarebbe presente all'interno della myLibFolderdirectory all'interno della cartella temporanea dell'utente corrente.

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

Ora puoi continuare a usare l'istruzione DllImport usando una stringa const come mostrato di seguito:

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

Proprio in fase di esecuzione prima di chiamare la DLLFunctionfunzione (presente nella libreria C ++) aggiungere questa riga di codice nel codice C #:

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

Ciò indica semplicemente al CLR di cercare le librerie C ++ non gestite nel percorso della directory ottenuto in fase di esecuzione del programma. Directory.SetCurrentDirectorycall imposta la directory di lavoro corrente dell'applicazione sulla directory specificata. Se il tuo myDLL.dllè presente nel percorso rappresentato dal assemblyProbeDirectorypercorso, verrà caricato e la funzione desiderata verrà chiamata tramite p / invoke.


3
Questo ha funzionato per me. Ho una cartella "Moduli" situata nella directory "bin" della mia applicazione in esecuzione. Lì sto inserendo una DLL gestita e alcune DLL non gestite richieste dalla DLL gestita. Usando questa soluzione E impostando il percorso di sondaggio in my app.config mi permette di caricare dinamicamente gli assembly richiesti.
WBuck

Per le persone che usano Funzioni di Azure: string workingDirectory = Path.GetFullPath (Path.Combine (executionContext.FunctionDirectory, @ ".. \ bin"));
Cappuccetto rosso,

4

imposta il percorso dll nel file di configurazione

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

prima di chiamare la DLL nella tua app, procedi come segue

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

quindi chiama la dll e puoi usarla come di seguito

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

0

DllImport funzionerà bene senza il percorso completo specificato purché la dll si trovi da qualche parte sul percorso del sistema. Potrebbe essere possibile aggiungere temporaneamente la cartella dell'utente al percorso.


Ho provato a inserirlo nelle variabili di ambiente di sistema MA è ancora considerato non costante (logico, credo)
Jsncrdnl,

-14

Se tutto fallisce, basta inserire la DLL nella windows\system32cartella. Il compilatore lo troverà. Specificare la DLL da cui caricare:, DllImport("user32.dll"...impostare EntryPoint = "my_unmanaged_function"per importare la funzione non gestita desiderata nell'app 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);    
   }
}

Fonte e ancora altri DllImportesempi: http://msdn.microsoft.com/en-us/library/aa288468(v=vs.71).aspx


Ok, sono d'accordo con la tua soluzione di utilizzo della cartella win32 (il modo più semplice per farlo) ma come concedi l'accesso a quella cartella al debugger di Visual Studio (e anche all'applicazione compilata)? (Tranne eseguirlo manualmente come amministratore)
Jsncrdnl,

Se viene utilizzato per qualcosa di più di un aiuto per il debug, verrebbe sottoposto a qualsiasi recensione (sicurezza o altro) nel mio libro.
Christian.K,

21
Questa è una soluzione piuttosto terribile. La cartella di sistema è per le DLL di sistema . Ora hai bisogno dei privilegi di amministratore e fai affidamento su cattive pratiche solo perché sei pigro.
MikeP,

5
+1 per MikeP, -1 per questa risposta. Questa è una soluzione terribile, chiunque lo faccia dovrebbe essere frustato ripetutamente mentre è costretto a leggere The Old New Thing . Proprio come hai imparato all'asilo: la cartella di sistema non ti appartiene, quindi non dovresti usarla senza permesso.
Cody Gray

Ok, sono d'accordo con te, ma il mio problema non è stato risolto quindi ... Quale posizione mi consiglieresti allora? (Sapendo che non posso usare le variabili per configurarlo -Perché è in attesa di una stringa costante-, quindi che DEVO usare una posizione che sarà la stessa su tutti i computer?) (O c'è un modo per usare una variabile, invece che una costante, per eseguirla?)
Jsncrdnl,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.