web-dev-qa-db-fra.com

Comment puis-je spécifier un chemin [DllImport] au moment de l'exécution?

En fait, j'ai un C++ (travail) DLL que je veux importer dans mon projet C # pour appeler ses fonctions.

Cela fonctionne quand je spécifie le chemin complet de la DLL, comme ceci:

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

Le problème est que ce sera un projet installable, donc le dossier de l'utilisateur ne sera pas le même (ex: pierre, paul, jack, maman, papa, ...) en fonction de l'ordinateur/de la session où il sera exécuté.

J'aimerais donc que mon code soit un peu plus générique, comme ceci:

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

Le gros problème est que "DllImport" souhaite un paramètre "const string" pour le répertoire de la DLL.

Ma question est donc la suivante: que pourrait-on faire dans ce cas?

126
Jsncrdnl

Contrairement aux suggestions de certaines des autres réponses, utiliser l'attribut DllImport reste la bonne approche.

Honnêtement, je ne comprends pas pourquoi vous ne pouvez pas faire comme tout le monde et spécifier un chemin relatif vers votre DLL. Oui, le chemin d'installation de votre application diffère d'un ordinateur à l'autre, mais il s'agit en gros d'une règle universelle en matière de déploiement. Le mécanisme DllImport est conçu dans cet esprit.

En fait, ce n'est même pas DllImport qui le gère. Ce sont les règles de chargement natives Win32 DLL qui régissent les choses, que vous utilisiez ou non des wrappers gérés pratiques (le marshaller P/Invoke appelle simplement LoadLibrary ). Ces règles sont énumérées en détail ici , mais les plus importantes sont extraites ici:

Avant de rechercher une DLL, le système vérifie les éléments suivants:

  • Si un DLL portant le même nom de module est déjà chargé en mémoire, le système utilise la DLL chargée, quel que soit le répertoire dans lequel il se trouve. Le système ne recherche pas la DLL.
  • Si la DLL figure dans la liste des DLL connues de la version de Windows sur laquelle l'application est exécutée, le système utilise sa copie du DLL connu _ (et les DLL dépendantes de la DLL connue , si seulement). Le système ne recherche pas la DLL.

Si SafeDllSearchMode est activé (par défaut), l'ordre de recherche est le suivant:

  1. Le répertoire à partir duquel l'application a été chargée.
  2. Le répertoire système. Utilisez la fonction GetSystemDirectory pour obtenir le chemin de ce répertoire.
  3. Le répertoire système 16 bits. Aucune fonction n'obtient le chemin de ce répertoire, mais la recherche est effectuée.
  4. Le répertoire Windows. Utilisez la fonction GetWindowsDirectory pour obtenir le chemin de ce répertoire.
  5. Le répertoire en cours.
  6. Les répertoires répertoriés dans la variable d'environnement PATH. Notez que cela n'inclut pas le chemin d'accès par application spécifié par la clé de registre App Paths. La clé App Paths n'est pas utilisée lors du calcul du chemin de recherche DLL.

Donc, à moins que vous ne nommiez votre DLL comme un système DLL (ce que vous ne devriez évidemment pas faire, jamais, en aucune circonstance), l'ordre de recherche par défaut commencera en regardant dans le répertoire à partir duquel votre application a été chargée. Si vous placez le DLL lors de l'installation, il sera trouvé. Tous les problèmes compliqués disparaissent si vous utilisez uniquement des chemins relatifs.

Ecrivez:

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

Mais si ne fonctionne pas pour une raison quelconque et que vous devez forcer l'application à rechercher la DLL dans un autre répertoire, vous pouvez modifier le chemin de recherche par défaut à l'aide de - fonction SetDllDirectory .
Notez que, conformément à la documentation:

Après avoir appelé SetDllDirectory, le chemin de recherche standard DLL est le suivant:

  1. Le répertoire à partir duquel l'application a été chargée.
  2. Le répertoire spécifié par le paramètre lpPathName.
  3. Le répertoire système. Utilisez la fonction GetSystemDirectory pour obtenir le chemin de ce répertoire.
  4. Le répertoire système 16 bits. Aucune fonction n'obtient le chemin de ce répertoire, mais la recherche est effectuée.
  5. Le répertoire Windows. Utilisez la fonction GetWindowsDirectory pour obtenir le chemin de ce répertoire.
  6. Les répertoires répertoriés dans la variable d'environnement PATH.

Ainsi, tant que vous appelez cette fonction avant d'appeler la fonction importée de la DLL pour la première fois, vous pouvez modifier le chemin de recherche par défaut utilisé pour localiser les DLL. L’avantage, bien sûr, est que vous pouvez passer une valeur dynamic à cette fonction qui est calculée au moment de l’exécution. Cela n’est pas possible avec l’attribut DllImport, vous utiliserez donc toujours un chemin relatif (le nom du DLL uniquement) et vous ferez appel au nouvel ordre de recherche pour le trouver.

Vous devrez P/Invoke cette fonction. La déclaration ressemble à ceci:

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

Mieux encore que la suggestion de Ran d'utiliser GetProcAddress, il suffit d'appeler LoadLibrary avant tout appel aux fonctions DllImport (avec uniquement un nom de fichier sans chemin) et ils utiliseront la fonction chargée. module automatiquement.

J'ai utilisé cette méthode pour choisir, au moment de l'exécution, de charger un DLL natif 32 bits ou 64 bits sans avoir à modifier un tas de fonctions P/Invoke-d. Collez le code de chargement dans un constructeur statique pour le type contenant les fonctions importées et tout fonctionnera correctement.

33
MikeP

Si vous avez besoin d’un fichier .dll qui ne figure pas sur le chemin ou sur l’emplacement de l’application, je ne pense pas que vous puissiez le faire, car DllImport est un attribut et que les attributs ne sont que des métadonnées définies sur des types, des membres et des éléments. autres éléments de langage.

Une alternative qui peut vous aider à accomplir ce que je pense que vous essayez, est d'utiliser les noms LoadLibrary à travers P/Invoke, afin de charger un fichier .dll à partir du chemin souhaité, puis d'utiliser GetProcAddress pour obtenir une référence à la fonction. vous avez besoin de ce .dll. Ensuite, utilisez-les pour créer un délégué que vous pouvez appeler.

Pour faciliter son utilisation, vous pouvez ensuite définir ce délégué dans un champ de votre classe, de sorte que son utilisation ressemble à l'appel d'une méthode membre.

EDIT

Voici un extrait de code qui fonctionne et montre ce que je voulais 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);
    }
}

Remarque: je n'ai pas pris la peine d'utiliser FreeLibrary, ce code n'est donc pas complet. Dans une application réelle, vous devez prendre soin de libérer les modules chargés pour éviter une fuite de mémoire.

24
Ran

Si vous connaissez le répertoire dans lequel vos bibliothèques C++ pourraient être trouvées au moment de l'exécution, cela devrait être simple. Je peux clairement voir que c'est le cas dans votre code. Votre myDll.dll serait présent dans le répertoire myLibFolder du dossier temporaire de l'utilisateur actuel.

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

Maintenant, vous pouvez continuer à utiliser l'instruction DllImport en utilisant une chaîne const, comme indiqué ci-dessous:

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

Juste au moment de l'exécution avant d'appeler la fonction DLLFunction (présente dans la bibliothèque C++), ajoutez cette ligne de code dans le code C #:

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

Cela indique simplement au CLR de rechercher les bibliothèques C++ non gérées dans le chemin de répertoire que vous avez obtenu au moment de l'exécution de votre programme. L'appel Directory.SetCurrentDirectory définit le répertoire de travail actuel de l'application sur le répertoire spécifié. Si votre myDLL.dll est présent sur le chemin représenté par assemblyProbeDirectory chemin, il sera chargé et la fonction souhaitée sera appelée via p/invoke.

5
RBT

définir le chemin d'accès à la DLL dans le fichier de configuration

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

avant d'appeler la dll dans votre application, procédez comme suit

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

puis appelez le dll et vous pouvez utiliser comme ci-dessous

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

DllImport fonctionnera correctement sans le chemin complet spécifié tant que la dll se trouve quelque part sur le chemin du système. Vous pourrez peut-être ajouter temporairement le dossier de l'utilisateur au chemin.

0
Mike W