Como posso especificar um caminho [DllImport] em tempo de execução?

141

Na verdade, eu tenho uma DLL C ++ (funcionando) que quero importar para o meu projeto C # para chamar suas funções.

Ele funciona quando eu especifico o caminho completo para a DLL, assim:

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

O problema é que ele será um projeto instalável, portanto a pasta do usuário não será a mesma (por exemplo: pierre, paul, jack, mãe, pai, ...) dependendo do computador / sessão em que seria executado.

Então, eu gostaria que meu código fosse um pouco mais genérico, assim:

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

O grande problema é que "DllImport" deseja um parâmetro "const string" para o diretório da DLL.

Então, minha pergunta é: O que poderia ser feito nesse caso?

Jsncrdnl
fonte
15
Basta implantar a DLL na mesma pasta que o EXE, para que você não precise fazer nada além de especificar o nome da DLL sem o caminho. Outros esquemas são possíveis, mas são todos problemáticos.
Hans Passant
2
A coisa é que ele vai ser um MS Office Excel Add In, então eu não coisa de colocar a dll no diretório do exe seria a melhor solução ...
Jsncrdnl
8
Sua solução é a errada. Não coloque arquivos nas pastas do Windows ou do sistema. Eles escolheram esses nomes por um motivo: porque são para arquivos de sistema do Windows. Você não está criando um desses porque não trabalha para a Microsoft na equipe do Windows. Lembre-se do que aprendeu no jardim de infância sobre o uso de coisas que não lhe pertencem sem permissão e coloque seus arquivos em qualquer lugar, menos lá.
Cody Gray
Sua solução ainda está errada. Aplicativos bem comportados que realmente não executam coisas administrativas não devem exigir acesso administrativo. A outra questão é que você não sabe a sua aplicação vai realmente ser instalado nessa pasta. Eu posso movê-lo para outro lugar ou alterar o caminho da instalação durante a instalação (faço esse tipo de coisa por diversão, apenas para quebrar aplicativos mal comportados). Caminhos codificados são o epítome do mau comportamento e são completamente desnecessários. Se você estiver usando a pasta do seu aplicativo, esse é o primeiro caminho na ordem de pesquisa padrão para DLLs. Tudo automático.
Cody Gray
3
colocá-lo em arquivos de programa NÃO é constante. Máquinas de 64 bits têm o arquivo de programa (x86), por exemplo.
Louis Kottmann

Respostas:

184

Ao contrário das sugestões de algumas das outras respostas, o uso do DllImportatributo ainda é a abordagem correta.

Sinceramente, não entendo por que você não pode fazer como todos os outros no mundo e especificar um caminho relativo para sua DLL. Sim, o caminho em que seu aplicativo será instalado difere nos computadores de outras pessoas, mas isso é basicamente uma regra universal quando se trata de implantação. oDllImport mecanismo é projetado com isso em mente.

De fato, nem isso é o DllImportque lida com isso. São as regras nativas de carregamento da DLL do Win32 que governam as coisas, independentemente de você estar usando os úteis wrappers gerenciados (o empacotador P / Invoke apenas chama LoadLibrary). Essas regras são enumeradas em grandes detalhes aqui , mas as importantes são extraídas aqui:

Antes de o sistema procurar uma DLL, ele verifica o seguinte:

  • Se uma DLL com o mesmo nome de módulo já estiver carregada na memória, o sistema utilizará a DLL carregada, independentemente do diretório em que esteja. O sistema não procura a DLL.
  • Se a DLL estiver na lista de DLLs conhecidas para a versão do Windows em que o aplicativo está sendo executado, o sistema utilizará sua cópia da DLL conhecida (e as DLLs dependentes da DLL conhecida, se houver). O sistema não procura a DLL.

Se SafeDllSearchModeestiver ativado (o padrão), a ordem de pesquisa será a seguinte:

  1. O diretório a partir do qual o aplicativo foi carregado.
  2. O diretório do sistema. Use oGetSystemDirectory função para obter o caminho deste diretório.
  3. O diretório do sistema de 16 bits. Não há função que obtenha o caminho desse diretório, mas ele é pesquisado.
  4. O diretório do Windows. Use oGetWindowsDirectory função para obter o caminho deste diretório.
  5. O diretório atual.
  6. Os diretórios listados na PATHvariável de ambiente. Observe que isso não inclui o caminho por aplicativo especificado pela chave do Registro App Paths. A chave Caminhos do aplicativo não é usada ao calcular o caminho de pesquisa da DLL.

Portanto, a menos que você esteja nomeando sua DLL da mesma forma que uma DLL do sistema (o que você obviamente nunca deveria fazer, em nenhuma circunstância), a ordem de pesquisa padrão começará a procurar no diretório em que seu aplicativo foi carregado. Se você colocar a DLL lá durante a instalação, ela será encontrada. Todos os problemas complicados desaparecem se você apenas usar caminhos relativos.

Apenas escreva:

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

Mas se isso não faz o trabalho por qualquer motivo, e você precisa forçar o aplicativo a olhar em um diretório diferente para o DLL, você pode modificar o caminho de busca padrão usando a SetDllDirectoryfunção .
Observe que, conforme a documentação:

Após a chamada SetDllDirectory, o caminho de pesquisa DLL padrão é:

  1. O diretório a partir do qual o aplicativo foi carregado.
  2. O diretório especificado pelo lpPathNameparâmetro
  3. O diretório do sistema. Use aGetSystemDirectory função para obter o caminho deste diretório.
  4. O diretório do sistema de 16 bits. Não há função que obtenha o caminho desse diretório, mas ele é pesquisado.
  5. O diretório do Windows. Use aGetWindowsDirectory função para obter o caminho deste diretório.
  6. Os diretórios listados na PATHvariável de ambiente.

Portanto, desde que você chame essa função antes de chamar a função importada da DLL pela primeira vez, poderá modificar o caminho de pesquisa padrão usado para localizar DLLs. O benefício, é claro, é que você pode passar um valor dinâmico para essa função que é calculada em tempo de execução. Isso não é possível com o DllImportatributo; portanto, você ainda usará um caminho relativo (apenas o nome da DLL) e contará com a nova ordem de pesquisa para encontrá-lo.

Você terá que P / Invocar esta função. A declaração é assim:

[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
static extern bool SetDllDirectory(string lpPathName);
Cody Gray
fonte
16
Outro aprimoramento menor sobre isso pode ser remover a extensão do nome da DLL. O Windows adiciona automaticamente .dlle outros sistemas adicionam a extensão apropriada em Mono (por exemplo, .sono Linux). Isso pode ajudar se a portabilidade for uma preocupação.
perfil completo de jheddings
6
+1 para o SetDllDirectory. Você também pode mudar Environment.CurrentDirectorye todos os caminhos relativos serão avaliados a partir desse caminho!
GameScripting 23/07
2
Mesmo antes de isso ser publicado, o OP esclareceu que ele está criando um plug-in, portanto, colocar as DLLs nos arquivos de programa da Microsoft é uma espécie de iniciante. Além disso, alterar o processo DllDirectory ou CWD pode não ser uma boa ideia, pois pode causar falhas no processo. Agora, AddDllDirectorypor outro lado ...
Mooing Duck
3
Confiar no diretório de trabalho é uma vulnerabilidade de segurança potencialmente séria, @GameScripting, e especialmente desaconselhada para algo executando com permissões de superusuário. Vale a pena escrever o código e fazer o trabalho de design para acertar.
Cody Gray
2
Observe que DllImporté mais do que apenas um invólucro LoadLibrary. Ele também considera o diretório do assembly no qual o externmétodo está definido . Os DllImportcaminhos de pesquisa podem ser adicionalmente limitados usando DefaultDllImportSearchPath.
Mitch
38

Ainda melhor do que a sugestão de Ran de usar GetProcAddress, basta fazer a chamada LoadLibraryantes de qualquer chamada para as DllImportfunções (com apenas um nome de arquivo sem um caminho) e elas usarão o módulo carregado automaticamente.

Usei esse método para escolher, em tempo de execução, se é necessário carregar uma DLL nativa de 32 ou 64 bits sem precisar modificar várias funções de P / Invoke-d. Cole o código de carregamento em um construtor estático para o tipo que possui as funções importadas e tudo funcionará bem.

MikeP
fonte
1
Não tenho certeza se isso está garantido para o trabalho. Ou se isso acontecer na versão atual da estrutura.
CodesInChaos
3
Código: Parece-me garantido: Ordem de Pesquisa na Biblioteca de Links Dinâmicos . Especificamente, "Fatores que afetam a pesquisa", ponto um.
Cody Gray
Agradável. Bem, minha solução tem uma pequena vantagem adicional, pois mesmo o nome da função não precisa ser estático e conhecido em tempo de compilação. Se você tiver 2 funções com a mesma assinatura e um nome diferente, poderá invocá-las usando meu FunctionLoadercódigo.
Ran
Parece o que eu quero. Eu esperava usar nomes de arquivos como mylibrary32.dll e mylibrary64.dll, mas acho que posso viver com eles com o mesmo nome, mas em pastas diferentes.
yoyo
27

Se você precisar de um arquivo .dll que não esteja no caminho ou no local do aplicativo, não acho que você possa fazer exatamente isso, porque DllImporté um atributo, e os atributos são apenas metadados definidos em tipos, membros e outros elementos de linguagem.

Uma alternativa que pode ajudá-lo a realizar o que acho que está tentando é usar o nativo LoadLibrarypor meio do P / Invoke, para carregar uma .dll do caminho que você precisa e, em seguida, usar GetProcAddresspara obter uma referência à função que você precisa a partir desse .dll. Em seguida, use-os para criar um delegado que você pode chamar.

Para facilitar o uso, você pode definir esse delegado como um campo da sua classe, para que pareça chamar um método de membro.

EDITAR

Aqui está um trecho de código que funciona e mostra o que eu quis dizer.

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: Eu não me incomodei em usar FreeLibrary, portanto este código não está completo. Em um aplicativo real, você deve ter o cuidado de liberar os módulos carregados para evitar um vazamento de memória.

Correu
fonte
Há contraparte gerenciada para LoadLibrary (na classe Assembly).
Luca
Se você tivesse algum exemplo de código, seria mais fácil para mim entender! ^^ (Na verdade, é um pouco nebuloso)
Jsncrdnl
1
@ Lucas Piccioni: Se você quis dizer Assembly.LoadFrom, isso carrega apenas assemblies .NET, não bibliotecas nativas. O que você quis dizer?
Ran
1
Eu quis dizer isso, mas não sabia dessa limitação. Suspiro.
Luca
1
Claro que não. Isso foi apenas um exemplo para mostrar que você pode chamar uma função em uma dll nativa sem usar P / Invoke, o que requer um caminho estático.
Ran
5

Desde que você saiba o diretório em que suas bibliotecas C ++ podem ser encontradas em tempo de execução, isso deve ser simples. Eu posso ver claramente que esse é o caso no seu código. Você myDll.dllestaria presente dentro do myLibFolderdiretório dentro da pasta temporária do usuário atual.

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

Agora você pode continuar usando a instrução DllImport usando uma string const, como mostrado abaixo:

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

Apenas em tempo de execução, antes de chamar a DLLFunctionfunção (presente na biblioteca C ++), adicione esta linha de código no código C #:

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

Isso simplesmente instrui o CLR a procurar as bibliotecas C ++ não gerenciadas no caminho do diretório que você obteve no tempo de execução do seu programa. Directory.SetCurrentDirectorychamada define o diretório de trabalho atual do aplicativo para o diretório especificado. Se você myDLL.dllestiver presente no caminho representado pelo assemblyProbeDirectorycaminho, ele será carregado e a função desejada será chamada através de p / invoke.

RBT
fonte
3
Isso funcionou para mim. Eu tenho uma pasta "Módulos" localizada no diretório "bin" do meu aplicativo em execução. Lá estou colocando uma DLL gerenciada e algumas DLLs não gerenciadas que a DLL gerenciada exige. Usar esta solução E definir o caminho de investigação no meu app.config permite carregar dinamicamente os assemblies necessários.
WBuck
Para pessoas que usam o Azure Functions: string workingDirectory = Path.GetFullPath (Path.Combine (ExecutionContext.FunctionDirectory, @ ".. \ bin"));
Chapeuzinho Vermelho
4

defina o caminho da DLL no arquivo de configuração

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

antes de chamar a dll no seu aplicativo, faça o seguinte

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

então chame a dll e você pode usar como abaixo

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

DllImport funcionará bem sem o caminho completo especificado, desde que a dll esteja localizada em algum lugar no caminho do sistema. Você pode adicionar temporariamente a pasta do usuário ao caminho.

Mike W
fonte
Eu tentei colocá-lo nas variáveis de ambiente do sistema, mas ainda é considerado como não constante (lógico, eu acho)
Jsncrdnl
-14

Se tudo falhar, basta colocar a DLL na windows\system32pasta. O compilador irá encontrá-lo. Especifique a DLL para carregar de:, DllImport("user32.dll"...defina EntryPoint = "my_unmanaged_function"para importar a função não gerenciada desejada para o aplicativo 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 ainda mais DllImportexemplos: http://msdn.microsoft.com/en-us/library/aa288468(v=vs.71).aspx

Designer de software
fonte
Ok, concordo com a sua solução de usar a pasta win32 (maneira mais simples de fazer isso), mas como você concede acesso a essa pasta ao depurador do Visual Studio (e também ao aplicativo compilado)? (Exceto manualmente executá-lo como admin)
Jsncrdnl
Se isso for usado para algo mais do que um auxílio à depuração, ele passará por qualquer revisão (de segurança ou não) no meu livro.
Christian.K
21
Esta é uma solução bastante terrível. A pasta do sistema é para DLLs do sistema . Agora você precisa de privilégios de administrador e depende de práticas ruins apenas porque é preguiçoso.
Mikep
5
+1 para MikeP, -1 para esta resposta. Esta é uma solução terrível, qualquer pessoa que faça isso deve ser açoitada repetidamente enquanto forçada a ler A Velha Nova Coisa . Assim como você aprendeu no jardim de infância: a pasta do sistema não pertence a você, portanto você não deve usá-la sem permissão.
Cody Grey
Okok, eu concordo com você, mas meu problema não está resolvido, então ... Qual local você me recomendaria então? (Sabendo que não posso usar variáveis ​​para configurá-lo, porque ele está esperando por uma string constante), então ? que eu devo usar um local que vai ser o mesmo em todos os computadores) (Ou há alguma maneira de usar um variáveis, em vez de uma constante, para realizá-lo)?
Jsncrdnl