¿Cómo puedo especificar una ruta [DllImport] en tiempo de ejecución?


141

De hecho, obtuve una DLL de C ++ (en funcionamiento) que quiero importar a mi proyecto C # para llamar a sus funciones.

Funciona cuando especifico la ruta completa a la DLL, así:

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

El problema es que será un proyecto instalable, por lo que la carpeta del usuario no será la misma (por ejemplo, Pierre, Paul, Jack, Mum, Dad, ...) dependiendo de la computadora / sesión donde se ejecute.

Entonces me gustaría que mi código sea un poco más genérico, como este:

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

El gran problema es que "DllImport" desea un parámetro de "cadena constante" para el directorio de la DLL.

Entonces mi pregunta es: ¿Qué podría hacerse en este caso?


15
Simplemente implemente la DLL en la misma carpeta que el EXE para que no tenga que hacer nada más que especificar el nombre de la DLL sin la ruta. Otros esquemas son posibles pero todos son problemáticos.
Hans Passant

2
La cosa es que va a ser un MS Office Excel Add In, así que no lo pone la DLL en el directorio del exe sería la mejor solución ...
Jsncrdnl

8
Tu solución es la incorrecta. No coloque archivos en las carpetas de Windows o del sistema. Eligieron esos nombres por una razón: porque son para archivos del sistema de Windows. No estás creando uno de esos porque no trabajas para Microsoft en el equipo de Windows. Recuerde lo que aprendió en el jardín de niños sobre el uso de cosas que no le pertenecen sin permiso, y guarde sus archivos en cualquier lugar que no sea allí.
Cody Gray

Tu solución aún está mal. Las aplicaciones con buen comportamiento que en realidad no hacen cosas administrativas no deberían requerir acceso administrativo. El otro problema es que no sabe que su aplicación realmente se instalará en esa carpeta. Podría moverlo a otro lugar, o cambiar la ruta de instalación durante la configuración (hago ese tipo de cosas por diversión, solo para romper las aplicaciones con mal comportamiento). Las rutas de codificación rígida son el epítome del mal comportamiento y son completamente innecesarias. Si está utilizando la carpeta de su aplicación, entonces esa es la primera ruta en el orden de búsqueda predeterminado para las DLL. Todo automático.
Cody Gray

3
ponerlo en archivos de programa NO es constante. En cambio, las máquinas de 64 bits tienen un archivo de programa (x86), por ejemplo.
Louis Kottmann

Respuestas:


184

Contrariamente a las sugerencias de algunas de las otras respuestas, el uso del DllImportatributo sigue siendo el enfoque correcto.

Sinceramente, no entiendo por qué no puedes hacer lo mismo que todos los demás en el mundo y especificar una ruta relativa a tu DLL. Sí, la ruta en la que se instalará su aplicación difiere en las computadoras de diferentes personas, pero esa es básicamente una regla universal cuando se trata de la implementación. El DllImportmecanismo está diseñado con esto en mente.

De hecho, ni siquiera DllImporteso lo maneja. Son las reglas de carga de DLL de Win32 nativas las que gobiernan las cosas, independientemente de si está utilizando los prácticos contenedores administrados (el encargado de invocación P / Invoke solo llama LoadLibrary). Estas reglas se enumeran con gran detalle aquí , pero las importantes se extraen aquí:

Antes de que el sistema busque una DLL, verifica lo siguiente:

  • Si una DLL con el mismo nombre de módulo ya está cargada en la memoria, el sistema usa la DLL cargada, sin importar en qué directorio se encuentre. El sistema no busca la DLL.
  • Si la DLL está en la lista de DLL conocidas para la versión de Windows en la que se ejecuta la aplicación, el sistema usa su copia de la DLL conocida (y las DLL dependientes de la DLL conocida, si las hay). El sistema no busca la DLL.

Si SafeDllSearchModeestá habilitado (el valor predeterminado), el orden de búsqueda es el siguiente:

  1. El directorio desde el que se cargó la aplicación.
  2. El directorio del sistema. Use la GetSystemDirectoryfunción para obtener la ruta de este directorio.
  3. El directorio del sistema de 16 bits. No hay ninguna función que obtenga la ruta de este directorio, pero se busca.
  4. El directorio de Windows. Use la GetWindowsDirectoryfunción para obtener la ruta de este directorio.
  5. El directorio actual.
  6. Los directorios que se enumeran en la PATHvariable de entorno. Tenga en cuenta que esto no incluye la ruta por aplicación especificada por la clave de registro de rutas de aplicaciones. La clave de Rutas de aplicación no se usa al calcular la ruta de búsqueda de DLL.

Entonces, a menos que esté nombrando su DLL de la misma manera que una DLL del sistema (lo que obviamente no debería hacer, nunca, bajo ninguna circunstancia), el orden de búsqueda predeterminado comenzará a buscar en el directorio desde el que se cargó su aplicación. Si coloca la DLL allí durante la instalación, se encontrará. Todos los problemas complicados desaparecen si solo usa rutas relativas.

Solo escribe:

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

Pero si eso no funciona por alguna razón, y necesita forzar a la aplicación a buscar en la DLL un directorio diferente, puede modificar la ruta de búsqueda predeterminada utilizando la SetDllDirectoryfunción .
Tenga en cuenta que, según la documentación:

Después de llamar SetDllDirectory, la ruta de búsqueda estándar de DLL es:

  1. El directorio desde el que se cargó la aplicación.
  2. El directorio especificado por el lpPathNameparámetro.
  3. El directorio del sistema. Use la GetSystemDirectoryfunción para obtener la ruta de este directorio.
  4. El directorio del sistema de 16 bits. No hay ninguna función que obtenga la ruta de este directorio, pero se busca.
  5. El directorio de Windows. Use la GetWindowsDirectoryfunción para obtener la ruta de este directorio.
  6. Los directorios que se enumeran en la PATHvariable de entorno.

Por lo tanto, siempre que llame a esta función antes de llamar a la función importada de la DLL por primera vez, puede modificar la ruta de búsqueda predeterminada utilizada para ubicar las DLL. El beneficio, por supuesto, es que puede pasar un valor dinámico a esta función que se calcula en tiempo de ejecución. Eso no es posible con el DllImportatributo, por lo que aún usará una ruta relativa (solo el nombre de la DLL) allí, y dependerá del nuevo orden de búsqueda para encontrarlo.

Tendrás que P / Invocar esta función. La declaración se ve así:

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

16
Otra mejora menor en esto puede ser eliminar la extensión del nombre de la DLL. Windows agregará automáticamente .dlly otros sistemas agregarán la extensión apropiada en Mono (por ejemplo, .soen Linux). Esto puede ayudar si la portabilidad es una preocupación.
jheddings

66
+1 para el SetDllDirectory. ¡También puede cambiar Environment.CurrentDirectoryy todas las rutas relativas se evaluarán desde esa ruta!
GameScripting

2
Incluso antes de que esto se publicara, el OP aclaró que está haciendo un complemento, por lo que poner los archivos DLL en los archivos de programa de Microsoft es una especie de no inicio. Además, alterar el proceso DllDirectory o CWD podría no ser una buena idea, ya que podrían hacer que el proceso falle. Ahora, AddDllDirectorypor otro lado ...
Mooing Duck

3
Confiar en el directorio de trabajo es una vulnerabilidad de seguridad potencialmente grave, @GameScripting, y especialmente desaconsejada para algo que se ejecuta con permisos de superusuario. Vale la pena escribir el código y hacer el trabajo de diseño para hacerlo bien.
Cody Gray

2
Tenga en cuenta que DllImportes más que solo un contenedor LoadLibrary. También considera el directorio del ensamblaje en el externque se define el método . Las DllImportrutas de búsqueda se pueden limitar adicionalmente usando DefaultDllImportSearchPath.
Mitch

38

Incluso mejor que la sugerencia de uso de Ran GetProcAddress, simplemente llame LoadLibraryantes de cualquier llamada a las DllImportfunciones (con solo un nombre de archivo sin ruta) y usarán el módulo cargado automáticamente.

He usado este método para elegir en tiempo de ejecución si cargar una DLL nativa de 32 o 64 bits sin tener que modificar un montón de funciones P / Invoke-d. Pegue el código de carga en un constructor estático para el tipo que tiene las funciones importadas y todo funcionará bien.


1
No estoy seguro de si esto está garantizado para funcionar. O si solo sucede en la versión actual del marco.
CodesInChaos

3
@Code: Parece garantizado para mí: Orden de búsqueda de la biblioteca Dynamic-Link . Específicamente, "Factores que afectan la búsqueda", punto uno.
Cody Gray

Agradable. Bueno, mi solución tiene una pequeña ventaja adicional, ya que incluso el nombre de la función no tiene que ser estático y conocido en el momento de la compilación. Si tiene 2 funciones con la misma firma y un nombre diferente, puede invocarlas usando mi FunctionLoadercódigo.
Corrió el

Esto suena como lo que quiero. Esperaba usar nombres de archivo como mylibrary32.dll y mylibrary64.dll, pero creo que puedo vivir con ellos con el mismo nombre pero en diferentes carpetas.
yoyo

27

Si necesita un archivo .dll que no está en la ruta o en la ubicación de la aplicación, entonces no creo que pueda hacer eso, porque DllImportes un atributo, y los atributos son solo metadatos que se establecen en tipos, miembros y otros elementos del lenguaje

Una alternativa que puede ayudarlo a lograr lo que creo que está intentando es usar el nativo a LoadLibrarytravés de P / Invoke, para cargar un .dll de la ruta que necesita, y luego usarlo GetProcAddresspara obtener una referencia de la función que necesita de ese .dll. Luego, utilícelos para crear un delegado que pueda invocar.

Para que sea más fácil de usar, puede configurar este delegado en un campo en su clase, de modo que usarlo parezca llamar a un método miembro.

EDITAR

Aquí hay un fragmento de código que funciona y muestra lo que quise decir.

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: No me molesté en usarlo FreeLibrary, por lo que este código no está completo. En una aplicación real, debe tener cuidado al liberar los módulos cargados para evitar una pérdida de memoria.


Hay una contraparte administrada para LoadLibrary (en la clase Assembly).
Luca

Si tuviera algún ejemplo de código, ¡me sería más fácil de entender! ^^ (En realidad, es un poco brumoso)
Jsncrdnl

1
@Luca Piccioni: Si se refería a Assembly.LoadFrom, esto solo carga ensamblados .NET, no bibliotecas nativas. ¿Qué querías decir?
Corrió el

1
Quería decir eso, pero no sabía sobre esta limitación. Suspiro.
Luca

1
Por supuesto no. Eso fue solo una muestra para mostrar que puede llamar a una función en un dll nativo sin usar P / Invoke, que requiere una ruta estática.
Corrió el

5

Siempre que conozca el directorio donde se pueden encontrar sus bibliotecas de C ++ en tiempo de ejecución, esto debería ser simple. Puedo ver claramente que este es el caso en su código. Su myDll.dllestaría presente en el interior myLibFolderdel directorio dentro de la carpeta temporal del usuario actual.

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

Ahora puede continuar usando la instrucción DllImport usando una cadena constante como se muestra a continuación:

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

Justo en tiempo de ejecución antes de llamar a la DLLFunctionfunción (presente en la biblioteca de C ++) agregue esta línea de código en el código C #:

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

Esto simplemente le indica al CLR que busque las bibliotecas C ++ no administradas en la ruta del directorio que obtuvo en el tiempo de ejecución de su programa. Directory.SetCurrentDirectoryLa llamada establece el directorio de trabajo actual de la aplicación en el directorio especificado. Si su myDLL.dllestá presente en la ruta representada por la assemblyProbeDirectoryruta, entonces se cargará y la función deseada se llamará a través de p / invoke.


3
Esto funcionó para mí. Tengo una carpeta "Módulos" ubicada en el directorio "bin" de mi aplicación en ejecución. Allí estoy colocando un dll administrado y algunos dll no administrados que requiere el dll administrado. Usar esta solución Y establecer la ruta de prueba en mi app.config me permite cargar dinámicamente los ensamblajes necesarios.
WBuck

Para las personas que usan Azure Functions: string workingDirectory = Path.GetFullPath (Path.Combine (executeContext.FunctionDirectory, @ ".. \ bin"));
Caperucita Roja

4

establecer la ruta dll en el archivo de configuración

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

antes de llamar al dll en tu aplicación, haz lo siguiente

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

luego llame al dll y puede usar como a continuación

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

0

DllImport funcionará bien sin la ruta completa especificada siempre que la dll esté ubicada en algún lugar de la ruta del sistema. Es posible que pueda agregar temporalmente la carpeta del usuario a la ruta.


Intenté colocarlo en el sistema Variables de entorno PERO todavía se considera no constante (lógico, creo)
Jsncrdnl

-14

Si todo falla, simplemente coloque la DLL en la windows\system32carpeta. El compilador lo encontrará. Especifique la DLL desde la cual cargar: DllImport("user32.dll"...configure EntryPoint = "my_unmanaged_function"para importar la función no administrada que desee a su aplicación 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);    
   }
}

Fuente e incluso más DllImportejemplos: http://msdn.microsoft.com/en-us/library/aa288468(v=vs.71).aspx


Ok, estoy de acuerdo con su solución de usar la carpeta win32 (la forma más sencilla de hacerlo), pero ¿cómo concede acceso a esa carpeta al depurador de Visual Studio (y también a la aplicación compilada)? (Excepto ejecutarlo manualmente como administrador)
Jsncrdnl

Si eso se usa para algo más que una ayuda de depuración, pasaría por cualquier revisión (de seguridad o de otro tipo) en mi libro.
Christian.K

21
Esta es una solución bastante terrible. La carpeta del sistema es para las DLL del sistema . Ahora necesita privilegios de administrador y confía en las malas prácticas solo porque es perezoso.
MikeP

55
+1 para MikeP, -1 para esta respuesta. Esta es una solución terrible, cualquiera que haga esto debe ser azotado repetidamente mientras se ve obligado a leer The Old New Thing . Tal como aprendiste en el jardín de infantes: la carpeta del sistema no te pertenece, por lo que no debes usarla sin permiso.
Cody Gray

Okok, estoy de acuerdo contigo, pero mi problema no está resuelto, así que ... ¿qué ubicación me recomendarías entonces? (Sabiendo que no puedo usar variables para configurarlo -Porque está esperando una cadena constante-, entonces que DEBO usar una ubicación que sea la misma en todas las computadoras?) (¿O hay alguna forma de usar variables, en lugar de una constante, para realizarla?)
Jsncrdnl
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.