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

141

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

Cela fonctionne lorsque 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 / session sur lequel il sera exécuté.

Alors j'aimerais 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: que pourrait-on faire dans ce cas?

Jsncrdnl
la source
15
Déployez simplement la DLL dans le même dossier que le fichier EXE pour ne rien faire d'autre que spécifier le nom de la DLL sans le chemin. D'autres schémas sont possibles mais sont tous gênants.
Hans Passant
2
Le fait est que ce sera un complément MS Office Excel, donc je ne pense pas que mettre la dll dans le répertoire de l'exe serait la meilleure solution ...
Jsncrdnl
8
Votre solution est la mauvaise. Ne placez pas de fichiers dans les dossiers Windows ou système. Ils ont choisi ces noms pour une raison: parce qu'ils sont destinés aux fichiers système Windows. Vous n'en créez pas un car vous ne travaillez pas pour Microsoft dans l'équipe Windows. Souvenez-vous de ce que vous avez appris à la maternelle sur l'utilisation d'objets qui ne vous appartiennent pas sans autorisation et placez vos fichiers ailleurs que là-bas.
Cody Gray
Votre solution est toujours erronée. Les applications bien conduites qui ne font pas réellement de tâches administratives ne devraient pas nécessiter d'accès administratif. L'autre problème est que vous ne savez pas votre application sera effectivement être installé dans ce dossier. Je pourrais le déplacer ailleurs, ou changer le chemin d'installation pendant l'installation (je fais ce genre de choses pour le plaisir, juste pour casser des applications mal conduites). Le codage en dur des chemins est la quintessence du mauvais comportement, et c'est complètement inutile. Si vous utilisez le dossier de votre application, il s'agit du premier chemin dans l'ordre de recherche par défaut des DLL. Tout automatique.
Cody Gray
3
le mettre dans des fichiers programme n'est PAS constant. Les machines 64 bits ont par exemple un fichier programme (x86).
Louis Kottmann le

Réponses:

184

Contrairement aux suggestions de certaines des autres réponses, l'utilisation de l' DllImportattribut reste la bonne approche.

Honnêtement, je ne comprends pas pourquoi vous ne pouvez pas faire comme tout le monde dans le monde et spécifier un chemin relatif vers votre DLL. Oui, le chemin dans lequel votre application sera installée diffère sur les ordinateurs de différentes personnes, mais c'est fondamentalement une règle universelle en matière de déploiement. Le DllImportmécanisme est conçu dans cet esprit.

En fait, ce n'est même pas cela DllImportqui le gère. Ce sont les règles de chargement de la DLL Win32 native qui régissent les choses, que vous utilisiez ou non les 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 que le système recherche une DLL, il vérifie les éléments suivants:

  • Si une DLL portant le même nom de module est déjà chargée en mémoire, le système utilise la DLL chargée, quel que soit le répertoire dans lequel elle se trouve. Le système ne recherche pas la DLL.
  • Si la DLL figure dans la liste des DLL connues pour la version de Windows sur laquelle l'application s'exécute, le système utilise sa copie de la DLL connue (et les DLL dépendantes de la DLL connue, le cas échéant). Le système ne recherche pas la DLL.

Si SafeDllSearchModeest 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 GetSystemDirectoryfonction 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 il est recherché.
  4. Le répertoire Windows. Utilisez la GetWindowsDirectoryfonction pour obtenir le chemin de ce répertoire.
  5. Le répertoire actuel.
  6. Les répertoires répertoriés dans la PATHvariable d'environnement. 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.

Ainsi, à moins que vous ne nommiez votre DLL de la même manière qu'une DLL système (ce que vous ne devriez évidemment pas faire, jamais, en aucune circonstance), l'ordre de recherche par défaut commencera à chercher dans le répertoire à partir duquel votre application a été chargée. Si vous placez la DLL là pendant l'installation, elle sera trouvée. Tous les problèmes compliqués disparaissent si vous utilisez simplement 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 cela 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 la SetDllDirectoryfonction .
Notez que, selon la documentation:

Après l'appel SetDllDirectory, le chemin de recherche standard de la DLL est:

  1. Le répertoire à partir duquel l'application a été chargée.
  2. Le répertoire spécifié par le lpPathNameparamètre.
  3. Le répertoire système. Utilisez la GetSystemDirectoryfonction 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 il est recherché.
  5. Le répertoire Windows. Utilisez la GetWindowsDirectoryfonction pour obtenir le chemin de ce répertoire.
  6. Les répertoires répertoriés dans la PATHvariable d'environnement.

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 transmettre une valeur dynamique à cette fonction qui est calculée au moment de l'exécution. Ce n'est pas possible avec l' DllImportattribut, donc vous utiliserez toujours un chemin relatif (le nom de la DLL uniquement) là-bas, et comptez sur le nouvel ordre de recherche pour le trouver pour vous.

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

[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
static extern bool SetDllDirectory(string lpPathName);
Cody Gray
la source
16
Une autre amélioration mineure à ce sujet peut être de supprimer l'extension du nom de la DLL. Windows ajoutera automatiquement .dllet d'autres systèmes ajouteront l'extension appropriée sous Mono (par exemple .sosous Linux). Cela peut aider si la portabilité est un problème.
jheddings
6
+1 pour le SetDllDirectory. Vous pouvez également simplement changer Environment.CurrentDirectoryet tous les chemins relatifs seront évalués à partir de ce chemin!
GameScripting
2
Avant même que cela ne soit publié, l'OP a précisé qu'il était en train de créer un plugin, donc mettre les DLL dans les fichiers de programme de Microsoft est en quelque sorte un non-démarreur. De plus, modifier le processus DllDirectory ou CWD peut ne pas être une bonne idée, cela pourrait entraîner l'échec du processus. Maintenant AddDllDirectoryd'un autre côté ...
Mooing Duck
3
S'appuyer sur le répertoire de travail est une vulnérabilité de sécurité potentiellement sérieuse, @GameScripting, et particulièrement déconseillée pour quelque chose fonctionnant avec des autorisations de super-utilisateur. Cela vaut la peine d'écrire le code et de faire le travail de conception pour le faire correctement.
Cody Gray
2
Notez que DllImportc'est plus qu'un simple wrapper LoadLibrary. Il considèreextern également le répertoire de l'assembly dans lequel la méthode est définie . Les DllImportchemins de recherche peuvent être en outre limités en utilisant DefaultDllImportSearchPath.
Mitch
38

Mieux encore que la suggestion d'utilisation de Ran GetProcAddress, faites simplement l'appel à LoadLibraryavant tout appel aux DllImportfonctions (avec seulement un nom de fichier sans chemin) et ils utiliseront automatiquement le module chargé.

J'ai utilisé cette méthode pour choisir au moment de l'exécution de charger une DLL native 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.

MikeP
la source
1
Je ne suis pas sûr que cela fonctionne. Ou si cela se produit uniquement sur la version actuelle du framework.
CodesInChaos
3
@Code: Cela me semble garanti: Ordre de recherche de la bibliothèque de liens dynamiques . Plus précisément, «Facteurs qui affectent la recherche», point un.
Cody Gray
Agréable. Eh bien, ma solution a un petit avantage supplémentaire, car même le nom de la fonction n'a pas besoin d'être statique et connu au moment de la compilation. Si vous avez 2 fonctions avec la même signature et un nom différent, vous pouvez les appeler à l'aide de mon FunctionLoadercode.
Ran le
Cela ressemble à ce que je veux. J'espérais utiliser des noms de fichiers comme mylibrary32.dll et mylibrary64.dll, mais je suppose que je peux vivre avec eux ayant le même nom mais dans des dossiers différents.
yoyo
27

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

Une alternative qui peut vous aider à accomplir ce que je pense que vous essayez est d'utiliser le natif LoadLibraryvia P / Invoke, afin de charger un .dll à partir du chemin dont vous avez besoin, puis de l'utiliser GetProcAddresspour obtenir une référence à la fonction dont vous avez besoin à partir de ce .dll. Utilisez ensuite ces derniers pour créer un délégué que vous pouvez appeler.

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

ÉDITER

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 de l'utiliser FreeLibrary, donc ce code n'est 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.

Ran
la source
Il existe un équivalent géré pour LoadLibrary (dans la classe Assembly).
Luca
Si vous aviez un exemple de code, ce serait plus facile pour moi de comprendre! ^^ (En fait, c'est un peu brumeux)
Jsncrdnl
1
@Luca Piccioni: si vous vouliez dire Assembly.LoadFrom, cela ne charge que les assemblys .NET, pas les bibliothèques natives. Que voulais-tu dire?
Ran
1
Je voulais dire cela, mais je ne connaissais pas cette limitation. Soupir.
Luca
1
Bien sûr que non. C'était juste un exemple pour montrer que vous pouvez appeler une fonction dans une dll native sans utiliser P / Invoke qui nécessite un chemin statique.
Ran le
5

Tant que vous connaissez le répertoire dans lequel vos bibliothèques C ++ peuvent être trouvées au moment de l'exécution, cela devrait être simple. Je vois clairement que c'est le cas dans votre code. Votre myDll.dllserait présent à l'intérieur du myLibFolderrépertoire dans le dossier temporaire de l'utilisateur actuel.

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

Vous pouvez maintenant 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 DLLFunctionfonction (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 demande simplement au CLR de rechercher les bibliothèques C ++ non gérées dans le chemin du répertoire que vous avez obtenu au moment de l'exécution de votre programme. Directory.SetCurrentDirectorycall définit le répertoire de travail actuel de l'application sur le répertoire spécifié. Si votre myDLL.dllest présent sur le chemin représenté par assemblyProbeDirectorychemin, il sera chargé et la fonction souhaitée sera appelée via p / invoke.

RBT
la source
3
Cela a fonctionné pour moi. J'ai un dossier "Modules" situé dans le répertoire "bin" de mon application en cours d'exécution. Là, je place une dll gérée et quelques dll non gérées dont la dll gérée a besoin. L'utilisation de cette solution ET la définition du chemin de sondage dans mon app.config me permet de charger dynamiquement les assemblys requis.
WBuck
Pour les personnes utilisant Azure Functions: string workingDirectory = Path.GetFullPath (Path.Combine (executionContext.FunctionDirectory, @ ".. \ bin"));
Red Riding Hood
4

définir le chemin 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 la dll et vous pouvez utiliser comme ci-dessous

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

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.

Mike W
la source
J'ai essayé de le placer dans les variables d'environnement du système MAIS il est toujours considéré comme non constant (logique, je pense)
Jsncrdnl
-14

Si tout échoue, placez simplement la DLL dans le windows\system32dossier. Le compilateur le trouvera. Spécifiez la DLL à partir de laquelle charger avec:, DllImport("user32.dll"...définissez EntryPoint = "my_unmanaged_function"pour importer la fonction non gérée souhaitée dans votre application 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);    
   }
}

Source et encore plus d' DllImportexemples: http://msdn.microsoft.com/en-us/library/aa288468(v=vs.71).aspx

Concepteur de logiciels
la source
Ok, je suis d'accord avec votre solution d'utiliser le dossier win32 (moyen le plus simple de le faire) mais comment accordez-vous l'accès à ce dossier au débogueur Visual Studio (et également à l'application compilée)? (Sauf en l'exécutant manuellement en tant qu'administrateur)
Jsncrdnl
Si cela est utilisé pour autre chose qu'une aide au débogage, cela passerait par n'importe quelle critique (sécurité ou autre) de mon livre.
Christian.K
21
C'est une solution assez terrible. Le dossier système est destiné aux DLL système . Maintenant, vous avez besoin de privilèges d'administrateur et vous vous fiez à de mauvaises pratiques simplement parce que vous êtes paresseux.
MikeP
5
+1 pour MikeP, -1 pour cette réponse. C'est une solution terrible, quiconque fait cela devrait être fouetté à plusieurs reprises tout en étant forcé de lire The Old New Thing . Tout comme vous l'avez appris à la maternelle: le dossier système ne vous appartient pas, vous ne devez donc pas l'utiliser sans autorisation.
Cody Gray
Okok, je suis d'accord avec vous, mais mon problème n'est pas résolu alors ... Quel emplacement me recommanderiez-vous alors? (Sachant que je ne peux pas utiliser de variables pour le configurer -Parce qu'il attend une chaîne constante-, donc que je DOIS utiliser un emplacement qui sera le même sur tous les ordinateurs?) (Ou y a-t-il un moyen d'utiliser une variable, au lieu d'une constante, pour l'exécuter?)
Jsncrdnl