Powershell: O carregamento de bibliotecas nativas está quebrado

Criado em 27 abr. 2019  ·  70Comentários  ·  Fonte: PowerShell/PowerShell

A tentativa de carregar um módulo que requer p / invoke de uma biblioteca nativa de terceiros falha ao carregar a biblioteca da mesma pasta que a DLL que está fazendo p / invoke.

Isso _usou funcionar_ no PowerShell Core 6.1.0, mas regrediu entre 6.2.0-preview1 e 6.2.0-preview2. Ele ainda funciona no Windows PowerShell.

A biblioteca nativa que ele não conseguiu localizar está localizada na mesma pasta do SkiaSharp.dll que está tentando localizá-la. Consulte https://github.com/PowerShell/PowerShell/issues/8861#issuecomment -462362391 e esse tópico de problemas para saber por que foi feito dessa maneira.

Passos para reproduzir

Install-Module PSWordCloud
New-WordCloud

Comportamento esperado

cmdlet New-WordCloud at command pipeline position 1
Supply values for the following parameters:
InputObject:

Comportamento real

new-wordcloud : The type initializer for 'PSWordCloud.WCUtils' threw an exception.
At line:1 char:1
+ new-wordcloud
+ ~~~~~~~~~~~~~
+ CategoryInfo          : NotSpecified: (:) [], TypeInitializationException
+ FullyQualifiedErrorId : System.TypeInitializationException

Consultar a exceção de base com $Error[0].Exception.GetBaseException() | Format-List * -F produz o seguinte:

Message        : Unable to load DLL 'libSkiaSharp' or one of its dependencies: The specified module could not be
                 found. (Exception from HRESULT: 0x8007007E)
TypeName       :
Data           : {}
InnerException :
TargetSite     : IntPtr sk_fontmgr_ref_default()
StackTrace     :    at SkiaSharp.SkiaApi.sk_fontmgr_ref_default()
                    at SkiaSharp.SKFontManager.get_Default()
                    at PSWordCloud.WCUtils..cctor()
HelpLink       :
Source         : SkiaSharp
HResult        : -2146233052

Dados ambientais

Name                           Value
----                           -----
PSVersion                      6.2.0
PSEdition                      Core
GitCommitId                    6.2.0
OS                             Microsoft Windows 10.0.17763
Platform                       Win32NT
PSCompatibleVersions           {1.0, 2.0, 3.0, 4.0…}
PSRemotingProtocolVersion      2.3
SerializationVersion           1.1.0.1
WSManStackVersion              3.0

_Nota: _ O mesmo foi testado em um ambiente Mac (obrigado @steviecoaster!) E também falhou da mesma forma. Suspeito que também será idêntico nas distros Linux.

/ cc @ SteveL-MSFT

Issue-Question Resolution-Fixed

Comentários muito úteis

@rkeithhill

Eu realmente gostaria de não ter que recompilar isso para cada plataforma porque este é um assembly netstandard2. Mas AFAICT, o valor do nome lib fornecido ao atributo DllImport deve ser uma constante de tempo de compilação. :-(

FWIW você pode omitir a extensão para DllImport . Fazemos isso para a dependência nativa não-Windows do PSES. Não ajuda se os nomes básicos não forem os mesmos.

Provavelmente, você também pode pular as diretivas do compilador para as chamadas LoadLibrary / dlopen armazenando-as em implementações de interface separadas. Contanto que o método não seja JIT, ele não deve ser lançado em tempo de execução.

Todos 70 comentários

@adityapatwardhan você pode dar uma olhada nisso?

Você pode procurar caminhos usados ​​com o utilitário procmon e, em seguida, tentar consertar com HintPath

Vou dar uma olhada nisso, mas dado que não é uma biblioteca que estou carregando diretamente (ela é carregada pelo próprio SkiaSharp.dll principal), não estou muito confiante em fazer muito. O fato de haver quatro arquivos de biblioteca possíveis para carregar dependendo da plataforma também complica as coisas.

Mas, independentemente disso, isso não parece uma mudança intencional, então a regressão provavelmente deve ser tratada.

A equipe da MSFT trabalha muito no vNext p / invokes no .Net Core 3.0 e talvez tenha feito back-port de algo para 2.1. Não me lembro de termos mudado nada no carregamento de dll relacionado ao PowerShell.

Houve # 8073 e várias alterações marcadas com Internal em 6.2.0-preview2. Não tenho ideia do que mais pode ter mudado durante esses. 😕

A mudança de PR é carregar dll do módulo da pasta do módulo primeiro e depois do GAC. O problema é que a dll do módulo faz referência à segunda dll e o carregamento automático da dll (faz Core, não PowerShell) não encontrou a dll. Minha sugestão é adicionar explicitamente hintpath à dll no arquivo de projeto.

@iSazonov Eu adicionei algumas referências HintPath ao meu csproj, mas tudo que recebo é um aviso quando eu dotnet publish . Não há mudança no comportamento do módulo depois de carregado.

Depois de ler um pouco sobre ele, parece que o HintPath é usado para fazer referência a assemblies externos que _não estão incluídos_ na própria construção do projeto. Os assemblies SkiaSharp estão incluídos, portanto, adicionar um HintPath para compilar / construir não afeta as DLLs do Skia ou os caminhos que procuram no tempo de execução.

Para referência, este é meu csproj:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>netstandard2.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="PowerShellStandard.Library" Version="5.1.1" PrivateAssets="All" />
    <PackageReference Include="SkiaSharp" Version="1.68.0" />
    <PackageReference Include="SkiaSharp.NativeAssets.Linux" Version="1.68.0" />
  </ItemGroup>

</Project>

Eu adicionei as seguintes linhas sem efeito (exceto um aviso de tempo de construção / publicação de que elas não podem ser encontradas, porque, bem, elas não existem até que a publicação seja concluída):

  <ItemGroup>
    <Reference Include="libSkiaSharp">
        <HintPath>win-x86/libSkiaSharp.dll</HintPath>
        <HintPath>win-x64/libSkiaSharp.dll</HintPath>
        <HintPath>osx/libSkiaSharp.dylib</HintPath>
        <HintPath>linux-x64/libSkiaSharp.dylib</HintPath>
    </Reference>
</ItemGroup>

Pelo que li no HintPath, não parece ter nenhum efeito sobre quais caminhos são pesquisados ​​ao carregar uma DLL, ou que a DLL tenta carregar outras DLLs para fins de p / invocação.

_Isso_ HintPath é uma coisa apenas em tempo de compilação.

A maneira como você diz isso faz parecer que perdi outro?

@ vexx32 Se você tiver um código de repo simples, pode perguntar no repositório CoreCLR.

Não tenho certeza se posso descartar uma alteração específica do Powershell ainda. Posso ter que fazer um pequeno teste para um aplicativo de console e ver como isso funciona.

Graças a uma dica de @Jaykul , consegui encontrar uma solução

Add-Type -TypeDefinition @"
    using System.Runtime.InteropServices;

    public class DllLoadPath
    {
        [DllImport("kernel32", CharSet=CharSet.Unicode)]
        public static extern int SetDllDirectory(string NewDirectory);
    }
"@

# ...

[DllLoadPath]::SetDllDirectory($NativeRuntimeFolder)

No entanto, o fato de que _requer_ p / invoke adicional para fazer este trabalho é meio ridículo. Também estou muito confuso quanto ao que mudou e removeu o caminho do diretório do SkiaSharp.dll carregado dos locais de pesquisa padrão, conforme documentado aqui .

@ vexx32 Não consegui reproduzir o problema. Perdi algo na reprodução? Talvez uma versão específica se PSWordCloud?

PS> Install-Module PSWordCloud                                                               
Untrusted repository
You are installing the modules from an untrusted repository. If you trust this repository, change its
InstallationPolicy value by running the Set-PSRepository cmdlet. Are you sure you want to install the modules from
'PSGallery'?
[Y] Yes  [A] Yes to All  [N] No  [L] No to All  [S] Suspend  [?] Help (default is "N"): y

PS> New-WordCloud                                                                            
cmdlet New-WordCloud at command pipeline position 1
Supply values for the following parameters:
InputObject:
Path:

PS> $PSVersionTable                                                                          
Name                           Value
----                           -----
PSVersion                      6.2.0
PSEdition                      Core
GitCommitId                    6.2.0
OS                             Microsoft Windows 10.0.18885
Platform                       Win32NT
PSCompatibleVersions           {1.0, 2.0, 3.0, 4.0…}
PSRemotingProtocolVersion      2.3
SerializationVersion           1.1.0.1
WSManStackVersion              3.0

Desculpe, como mencionei, retifiquei em 2.1.1.

Se você instalar o 2.1.0 especificamente, verá a falha, pois falta a solução alternativa p / invoke acima.

Ok, posso reproduzir agora. Vou dar uma olhada.

@ vexx32 Parece que carregamos todos os arquivos listados em FileList de psd1 . Você precisa ter isso? Quando removi todas as entradas de FileList não consigo mais reproduzir o problema.

É assim mesmo? Isso é muito estranho. Não, eu particularmente não preciso disso e provavelmente irei removê-lo de qualquer maneira, pois eu realmente não quero ter que atualizá-lo constantemente, mas isso está lá por algumas versões.

Parece estranho que isso afetaria o carregamento do DLL de repente.

Parece que carregamos todos os arquivos listados em FileList do psd1.

Temos um problema para oferecer suporte a "if" em arquivos .psd1. Assim, poderíamos carregar dlls específicos da plataforma.

@iSazonov é # 5541 . Eu vou te dar uma atualização em breve.

Essa é uma possibilidade. O que estou me perguntando _agora_ é se simplesmente adicionar o caminho para as bibliotecas nativas (todas elas) na FileList é suficiente aqui ... talvez eu nem precise do p / invoke. Vou fazer alguns testes! 😊

EDIT: Não, não estou vendo que tenha qualquer efeito. :(

Chamamos Assembly.Load() que carrega apenas assemblies gerenciados.

Fizemos uma alteração em 6.2.0-preview.2, onde começamos a carregar DLLs da pasta do módulo antes de pesquisar no GAC. O efeito colateral dessa mudança é que também carregamos DLLs em FileList.

Isso faz sentido. Temos um problema de doc aberto para isso? Parece que esse tipo de mudança será muito difícil de rastrear se alguém se deparar com ela e não tivermos documentado. Provavelmente devemos documentar que você não deseja colocar assemblies nativos na lista de arquivos por esse motivo.

Agradeço a ajuda para rastreá-lo! Por enquanto, a solução P / invoke que mencionei me dá mais flexibilidade, mas pode ser sensato adicionar manipulação para essas coisas no carregamento do módulo.

Por exemplo, digamos que em PrivateData adicionemos uma entrada chamada RuntimeLibPaths, que é uma tabela de hash em que as chaves correspondem aos nomes / arquiteturas de plataforma suportadas e os valores são o caminho para a pasta que contém a biblioteca nativa. As pastas apropriadas são adicionadas aos caminhos de pesquisa dll na importação pelo PowerShell internamente, em vez de precisarem ser adicionadas pelo próprio módulo na importação.

Temos um problema de doc aberto para isso?

Não temos.

@adityapatwardhan @ SteveL-MSFT, obrigado a @TylerLeonhardt (não intencionalmente) lembrando-me de que isso também deve funcionar em coisas que não sejam do Windows se eu quiser ser minucioso ... Preciso de uma solução mais completa.

Atualmente, não parece haver nenhuma documentação clara sobre como a interoperabilidade nativa deve ser tratada no próprio .NET Core. Tudo o que posso encontrar é esta página para .NET Framework em documentos do MS.

É tratado via Mono ou algo assim? Se for via Mono, olhando para esta página, há realmente um _bug no Mono_ no Mac OS que impede que os caminhos de carregamento da biblioteca sejam devidamente respeitados ...

PowerShell ... precisa lidar com isso. Não tenho certeza se _existe_ uma maneira de lidar com isso de forma adequada em todas as plataformas do próprio módulo.

@ vexx32 não conhece os detalhes, se você pode produzir um repro em C # que não inclui PowerShell, eu sugeriria abrir um problema no repositório CoreCLR

@steveL-msft esse é o problema, eu acho. Parece funcionar bem em outro lugar, com algumas ressalvas ainda. Por exemplo, carregar a lib nativa funciona se estiver na mesma pasta do SkiaSharp.dll principal, mas para PS isso não funciona devido à mudança que @adityapatwardhan menciona. Isso só multiplica a dificuldade no caso de trabalhar com PowerShell e como ele lida com módulos.

Na maioria das situações, ao trabalhar com tais bibliotecas nativas, ele pode ser abstraído, porque você pode apenas empacotá-lo para cada plataforma suportada de qualquer maneira. Simplesmente não temos esse luxo para módulos PS, a menos que eu queira empacotar e manter três ou mais módulos de galeria completamente separados, o que infelizmente é muito ruim para a descoberta e a experiência do usuário final.

Dado que o PS é uma ferramenta de plataforma cruzada, precisamos de alguma forma de lidar com bibliotecas nativas em módulos, ou precisamos de alguma forma para que o módulo PackageManagement reconheça que é necessário haver bibliotecas separadas extraídas do nupkg por plataforma.

Estou aberto a soluções alternativas, mas parece que o baralho aqui é bem escasso. :confuso:

@ vexx32 Acho que você quer algo assim https://github.com/PowerShell/PowerShellGet/issues/273

No CoreFX 3.0, obtemos algumas novas APIs para bibliotecas nativas pinoke. Eles estão apenas no começo da estrada, mas querem fazer algo ainda melhor do que o Mono.

Atualmente, minha solução alternativa é parecida com esta:

  1. Bibliotecas nativas para Mac e Linux estão na raiz do módulo, pois parece ser o único lugar onde posso colocá-las e carregá-las de forma confiável no momento.
  2. Um ScriptsToProcess lida com p / invoke na importação de módulo para adicionar a versão de 32 ou 64 bits da biblioteca nativa do Windows, que deve ser armazenada em subpastas, pois os nomes dos arquivos são, infelizmente, idênticos.

Não é o ideal e, como @TylerLeonhardt pode atestar por meio de seu fluxo hoje, isso ainda não funciona por algum motivo ao executar no Azure Functions no momento.

@ vexx32 não conhece os detalhes, se você pode produzir um repro em C # que não inclui PowerShell, eu sugeriria abrir um problema no repositório CoreCLR

Não: em C #, isso não é um problema

Em C #, você:

  1. Desenvolva um aplicativo e libere _instaladores separados_ por plataforma e tenha apenas _um caminho de pesquisa de biblioteca_ para se preocupar.
  2. Desenvolva uma biblioteca e libere os pacotes _NuGet_, e o nuget fornece um padrão de caminho informando onde colocar suas bibliotecas e, em seguida, o studio / msbuild cuida de tudo para você.

@ vexx32 Acho que você quer algo como este PowerShell / PowerShellGet # 273

Não: este não é um problema de plataforma cruzada

É por isso que esse bug foi arquivado. Desde PowerShell 6.2 @ vexx32 não consegue nem obter uma versão que funcione _apenas Windows_ porque ele não consegue descobrir como fazer o PowerShell usar o caminho de pesquisa de biblioteca correto.

No Windows, pelo menos, há um ap / invoke disponível que me permite mexer com ele. Essa é uma _solução_, mas realmente precisamos de uma solução melhor.

Em sistemas Unix, no entanto, _não existe tal p / invoke_ disponível, pelo que fui capaz de encontrar, e por alguma razão o PowerShell não está consultando as variáveis ​​de ambiente que normalmente indicam quais caminhos pesquisar quando você tenta DllImport de uma montagem carregada pelo PS.

Basicamente, em todas as plataformas isso é ... extremamente hostil para fazer qualquer coisa. 😅

Como está atualmente, parece não haver outra opção além de incluir qualquer e todos os assemblies nativos Unix diretamente na pasta raiz do módulo (o que limita as plataformas que você pode suportar, pois muitas vezes pode haver colisões de nomes), e então ter cuidado com quais pasta que você adiciona aos caminhos de pesquisa no Windows com o p / invoke (se você se preocupa em oferecer suporte ao Windows x86 - e @TylerLeonhardt me diz que o Azure Functions é executado em x86 por algum motivo estranho, portanto, é uma necessidade se você quiser trabalhar com Essa.)

Isso é o que eu uso em meu arquivo psm1 para pré-carregar o binário nativo apropriado para a plataforma. Nota: Eu apenas direcionei / testei isso no Linux e no Windows:

# Module PSM1 File
$binPath = Join-Path $PSScriptRoot bin $(if ($IsWindows) {"Windows"} else {"Linux"})
Add-Type -Path $binPath\Acme.Api.dll

E este é o código C # que vai em Acme.Api.dll

[Flags]
public enum DLOpenFlags
{
    RTLD_LAZY = 1,
    RTLD_NOW = 2,
    RTLD_LOCAL = 4,
    RTLD_GLOBAL = 8,
}

public static class LinuxNativeMethods
{
    [DllImport("libdl.so", CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]
    public static extern IntPtr dlopen(
        string dlToOpen, 
        DLOpenFlags flags);
}

public static class Win32NativeMethods
{
    [DllImport("kernel32.dll", EntryPoint="LoadLibraryW")]
    public static extern IntPtr LoadLibrary(
        [InAttribute, MarshalAs(UnmanagedType.LPWStr)] string dllToLoad);
}

public static class FooNativeMethods
{
    // On Linux/macOS the shared lib must be named Foo.so and not libFoo.so
    public const string LibName = "Foo";

    static FooNativeMethods()
    {
        string assemblyDir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);

        if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) 
        {
            string nativeDllPath = Path.Combine(assemblyDir, "Windows", $"{LibName}.dll");
            LibHandle = Win32NativeMethods.LoadLibrary(nativeDllPath);
        }
        else
        {
            string nativeSOPath = Path.Combine(assemblyDir, "Linux", $"{LibName}.so");
            LibHandle = LinuxNativeMethods.dlopen(nativeSOPath, DLOpenFlags.RTLD_LAZY);
        }
    }

    public static IntPtr LibHandle { get; private set; }

    [DllImport(LibName, CallingConvention = CallingConvention.Cdecl)]
    public static extern int getLastError(
        StringBuilder buffer,
        ref int bufferLength);

    ...
}

@ vexx32 Você pode abrir o problema de consultoria no repositório CoreCLR. Acho que eles têm isso a dizer.

@rkeithhill isso só funciona para binários .NET. Eu preciso carregar um binário .NET que por si só precisa usar P / invoke para carregar um assembly nativo. É aí que ele falha e desmorona completamente. Não tenho controle sobre esta biblioteca, é uma biblioteca de terceiros (SkiaSharp, neste caso). Não consigo carregar esta biblioteca no meu módulo binário, pois ela precisa ser carregada ao mesmo tempo que o código é carregado do meu módulo, pois meu módulo depende dos tipos presentes e funcionais apenas com o SkiaSharp carregado.

@isazonov, como já foi abordado algumas vezes neste problema, não é um problema no .net Core porque você pode simplesmente gerar pacotes separados por plataforma para a maioria dos aplicativos.

O PowerShell não tem nenhuma maneira possível de fazer isso e, incidentalmente, remove alguns caminhos padrão dos caminhos de pesquisa da biblioteca, tornando literalmente impossível trabalhar com assemblies nativos em alguns contextos e muito frágil e particular naqueles que acontecem trabalhos.

@ vexx32

Eu preciso carregar um binário .NET que por si só precisa usar P / invoke para carregar um assembly nativo. É aí que falha e desmorona completamente.

Uh, isso é exatamente o que este código faz. Já faz algum tempo que o uso com sucesso no 6.2 no Windows e no Linux.

Emendado, desculpe, não fui claro. Não posso fazer isso em meu próprio binário, os tipos precisam estar presentes enquanto meu próprio módulo é carregado; Não posso fazer com que seja importado durante a instanciação do comando ... E não deveria.

O PowerShell não deve ser menos funcional do que o núcleo .net quando se trata de carregar binários.

Dito isso, verei se consigo adaptar algumas coisas e melhorar minhas soluções alternativas. Aprecie os exemplos!

Mas, realmente ... Não vejo por que deveria ir tão longe para contornar isso. Deve funcionar e até que o PowerShell seja envolvido ... Funciona.

Concordo que os aros são uma dor. Estava apenas tentando mostrar como o carregamento da lib nativa pode ser feito via p / invoke no Windows e Linux. :-)

@ vexx32 vejo divisão na discussão. Você poderia esclarecer que é um problema - você tem um módulo com bibliotecas nativas e precisa carregar a biblioteca certa com dependências em uma plataforma específica?

Tipo de. Este é o layout que tenho que funciona atualmente, mas está bagunçado como tudo. Qualquer coisa que eu não consiga definir um caminho via p / invoke deve estar na raiz do módulo (que causa problemas de colisão de nomes em algumas plataformas).

PSWordCloud/
|- PSWordCloudCmdlet.dll
|- SkiaSharp.dll
|- libSkiaSharp.so
|- libSkiaSharp.dylib
|- win-x86/
  |- libSkiaSharp.dll
|- win-x64/
  |- libSkiaSharp.dll

SkiaSharp.dll é .NET e é um assembly referenciado para minha dll de cmdlet principal. Os quatro arquivos libSkiaSharp são DLLs por plataforma nativos. Eu não controlo o código no SkiaSharp.dll, então não posso forçar essa DLL a atualizar os caminhos antes de p / invocar.

Então ... eu tenho um módulo com uma dependência .NET. Essa dependência depende de uma biblioteca nativa que está carregando com DllImport; o arquivo exato é diferente por plataforma / arquitetura (mas vários dos arquivos têm colisões de nomes, de modo que restringe as plataformas que posso realmente suportar).

Usando o código de @rkeithhill , acho que também posso mover a biblioteca nativa do Linux para uma subpasta. Não tenho certeza sobre o MacOS, provavelmente há algo semelhante, mas ainda não encontrei nenhuma documentação sobre métodos para isso.

_Idealmente_, TODOS esses arquivos devem estar, na pior das hipóteses, em uma subpasta fora do caminho, algo como /PSWordCloud/runtime/<platform>/ - infelizmente, devido a como o PowerShell alterou as especificações de carregamento do caminho das configurações padrão, apenas os arquivos na raiz pasta do módulo pode ser carregada via DllImport. Outros locais podem funcionar, mas (pelo menos no Mac / Linux) _todas_ as variáveis ​​de ambiente nessas plataformas que deveriam definir caminhos de carregamento de tempo de execução nativos são completamente ignoradas pelo PowerShell.

Eu acredito que $env:Path ainda funciona (embora não devesse estar usando isso em plataformas Unix, e eu não testei isso desde a versão 6.2, então isso pode estar quebrado também), mas francamente, não acho que é uma boa ideia adicionar uma tonelada de caminhos a essa variável, isso torna a experiência do usuário realmente terrível se o usuário precisar usar essa variável de ambiente. Você carrega um módulo e, de repente, sua variável de caminho tem dados extras inesperados que provavelmente não deveriam estar lá, mas _têm_ que estar lá para que as coisas funcionem corretamente.

Acho que isso não deve ser problema; O PowerShell deve ter um método mais robusto de lidar com os caminhos de carregamento da biblioteca nativa se for substituir os métodos predefinidos do .NET Core. Eu sugeriria ter alguma maneira de especificar um caminho de carregamento de biblioteca nativa no manifesto do módulo ou algo nesse sentido, alguma maneira de definir onde deve estar procurando por plataforma quando algo precisa p / invocar algum código nativo.

Tenho o mesmo problema e o relatório de @ vexx32 contém todos os elementos da cadeia.
Concordo que o PowerShell deve ter um método mais robusto de manipulação de biblioteca nativa.

Não acho uma boa ideia adicionar uma tonelada de caminhos a essa variável

Concordo e da mesma forma, não estou interessado em usar SetDllDirectory porque isso também abrange (pwsh) o processo. O manifesto do módulo pode ser atualizado para permitir que os autores do módulo forneçam uma lista de dlls nativos por combo os-arch e então o pwsh pré-carregaria essas dlls nativas antes de pré-carregar quaisquer assemblies. Dito isso, se alguém vai usar seu assembly de outra linguagem, o assembly realmente deve pré-carregar suas próprias dlls nativas. O usuário da montagem não deveria ter que fazer isso IMO.

@ vexx32 "Acho" que dlopen funciona no macOS.

Vou tentar quando puder (as VMs do mac são um pouco ... estranhas heh), mas de memória, o Mac não usa arquivos .so então eu definitivamente terei que tentar - - a maioria das libs nativas que vejo referências em Macs parecem ser .dylib .

A montagem faz seu próprio pré-carregamento aqui (pelo menos, tenho certeza que sim ...), eu só tenho que fazer um caso especial de tudo porque a montagem não sabe para onde olhar com a forma como o PS lida com caminhos. : /

Acho que o macOS usa .dylib então você precisaria compilar esse assembly três vezes com macros diferentes definidas, por exemplo:

public static class FooNativeMethods
{
    // This unfortunately requires that you compile this assembly twice - once with WIN32 defined and again with
    // it not defined.
#if WIN32        
    public const string LibName = "Foo";
#elif MACOS
    public const string LibName = "libFoo.dylib";
#else
    public const string LibName = "libFoo.so";
#endif      

Eu realmente gostaria de não ter que recompilar isso para cada plataforma porque este é um assembly netstandard2. Mas AFAICT, o valor do nome lib fornecido ao atributo DllImport deve ser uma constante de tempo de compilação. :-(

Sim. Como este é um módulo do PowerShell, o que eu realmente acabaria fazendo é uma compilação dinâmica na importação com Add-Type -TypeDefinition $srcString com uma verificação de plataforma e, em seguida, aponto para o diretório correto. Você pode usar ScriptsToProcess em um manifesto do módulo para isso, felizmente.

Além disso, libdl.so uma biblioteca válida e utilizável para fazer isso no MacOS?

Mas sim, precisamos de uma solução melhor. 😄

@ vexx32 Você olhou Mono MapDll? O recurso permite o mapeamento automático para a biblioteca nativa apropriada com base nas condições (arco, plataforma e etc.). Eu espero que seja isso que você precisa. Talvez SkiaSharp.dll deva ser recompilado, mas pode ser configurado em um arquivo de configuração externo. Algo semelhante será implementado no Core.

Se bem me lembro, o .NET Core não usa Mono quando executado no Windows, então, novamente, seria apenas uma solução parcial.

Agradeço muito todas as opções oferecidas aqui e examinarei minuciosamente quais são minhas opções atuais - mas, no final do dia, o PowerShell precisa ter algo em vigor para os módulos de plataforma cruzada usarem para este propósito no futuro . 🙂

Se bem me lembro, o .NET Core não usa Mono quando executado no Windows, então, novamente, seria apenas uma solução parcial.

Eu digo sobre novos recursos. Vejo
Documento de design do Dllmap https://github.com/dotnet/coreclr/blob/c3e1bd5ccc482c9a7670762676bda95ebd34707d/Documentation/design-docs/dllmap.md
e a versão anterior do documento
https://github.com/dotnet/coreclr/blob/6a48f05c77e80b604d71a9b84cc6b014306e849e/Documentation/design-docs/dllmap.md#

Atualização: CurrentContextualReflectionContext
https://github.com/dotnet/designs/blob/master/accepted/runtime-binding.md#rollforward

@rkeithhill

Eu realmente gostaria de não ter que recompilar isso para cada plataforma porque este é um assembly netstandard2. Mas AFAICT, o valor do nome lib fornecido ao atributo DllImport deve ser uma constante de tempo de compilação. :-(

FWIW você pode omitir a extensão para DllImport . Fazemos isso para a dependência nativa não-Windows do PSES. Não ajuda se os nomes básicos não forem os mesmos.

Provavelmente, você também pode pular as diretivas do compilador para as chamadas LoadLibrary / dlopen armazenando-as em implementações de interface separadas. Contanto que o método não seja JIT, ele não deve ser lançado em tempo de execução.

Eu encontrei uma possível solução para este problema. Eu teria que experimentar. No momento, estou ocupado trabalhando em alguns lançamentos, então provavelmente chegaremos a isso no início da próxima semana. Vou atualizar esse problema com minhas descobertas.

https://docs.microsoft.com/en-us/dotnet/api/system.runtime.loader.assemblyloadcontext.loadunmanageddll?view=netcore-2.2#System_Runtime_Loader_AssemblyLoadContext_LoadUnmanagedDll_System_String_

@adityapatwardhan Isso se parece com um wrapper de plataforma cruzada em torno de LoadLibrary / dlopen. Belo achado!

@adityapatwardhan Eu encontrei esse problema em LoadUnmanagedDll. Talvez irrelevante para o nosso caso.

Devemos considerar a adição de um retorno de chamada que permita às pessoas fornecer uma política de resolução personalizada para bibliotecas não gerenciadas. Pretendemos fazer isso com AssemblyLoadContext.LoadUnmanagedDll. No final, ele abordou apenas uma pequena fração do problema, porque não funciona para o contexto de carregamento padrão. Quando introduzimos AssemblyLoadContext.LoadUnmanagedDll originalmente, permitimos substituir o contexto de carregamento padrão também, mas removemos esse recurso e não pensamos no impacto em todos os cenários.

Fonte: LoadUnmanagedDllFromPath / LoadUnmanagedDll não está funcionando no linux se o arquivo .so estiver presente em qualquer outro caminho diferente do diretório de saída do aplicativo

Acho que meu link acima sobre APIs NativeLibrary nos dá uma solução de plataforma cruzada (depois de mudar para .Net Core 3.0).

CurrentContextualReflectionContext https://github.com/dotnet/coreclr/blob/master/Documentation/design-docs/AssemblyLoadContext.ContextualReflection.md

É para modelos de plug-in. Isso pode resolver o problema # 6724, # 6426 e alguns outros.
/ cc @ daxian-dbw

@adityapatwardhan alguma atualização desde o mês passado? Queremos publicar a orientação correta do documento sobre este

Não fui capaz de me concentrar nisso, no momento estou ocupado com outros itens de alta prioridade. Vou atualizar na próxima semana.

@adamdriscoll pode ser capaz de dar alguns conselhos, já que ele provavelmente teve que fazer algumas coisas criativas no UniversalDashboard e no AvaloniaUI + PowerShell.

FWIW, o código que apresentei acima é o que usamos para um módulo interno que usa uma lib nativa. Funciona sem problemas no Windows e Linux. Não testei no macOS, mas além dos ajustes no pinvoke dlopen e alterando o nome da biblioteca compartilhada no mac, ele deve funcionar. Isso está no .NET Core 2.2.

UD está fazendo praticamente a mesma coisa que a solução de @rkeithhill . Isso funciona no MacOSX, bem como no Linux e no Windows. https://github.com/ironmansoftware/universal-dashboard/blob/master/src/UniversalDashboard/Server/CustomAssemblyLoadContext.cs

O problema é que pode haver muito mais variações dos caminhos de pasta para carregar os vários binários, então é um pouco complicado. Se um novo tipo binário é gerado com base em uma nova dependência, uma nova pasta pode aparecer e o UD não carregará o assembly.

@rkeithhill, você está mesclando todos os binários do WindowsLinux em uma única pasta? (EDITAR: Na verdade, entendi. Você tem um assembly DLL wrapper que carrega apenas o assembly nativo de destino. Você faz isso para todos os seus assemblies nativos?)

O UD apenas mantém a pasta de tempo de execução de publicação da mesma forma de quando foi produzida com o dotnet publish e, em seguida, a classe referenciada tenta carregar todos os binários nativos com base na arquitetura de plataforma correta.

Funciona, mas gostaria que o PS pudesse apenas olhar para uma pasta de tempo de execução e carregar os binários corretos. Parece que o dotnet deve estar fazendo isso quando é iniciado, então talvez haja algum código para capturar lá.

Eu mudo onde procuro por assemblies nativos com base no resultado RuntimeInformation.IsOSPlatform(OSPlatform.Windows) . Se isso retornar verdadeiro, carrego da pasta Windows do diretório em que o assembly C # está localizado. Caso contrário, carrego da pasta Linux .

Você faz isso para todos os seus assemblies nativos?)

Sim, mas só temos um e não depende de nada além de libs de sistema (msvcrt, etc).

Podemos ter alguma estrutura de arquivo padrão (talvez baseada em nomes de tempo de execução)? Isso nos permitiria aprimorar o resolvedor dll nativo.

O UD apenas mantém a pasta de tempo de execução de publicação da mesma forma de quando foi produzida com o dotnet publish e, em seguida, a classe referenciada tenta carregar todos os binários nativos com base na arquitetura de plataforma correta.

Então, se estou entendendo corretamente, você está evitando usar o PowerShell para carregar os binários nativos apenas fazendo com que o .NET os carregue por baixo do capô quando você carrega um binário gerenciado que depende dos binários nativos específicos de RID?

Funciona, mas gostaria que o PS pudesse apenas olhar para uma pasta de tempo de execução e carregar os binários corretos. Parece que o dotnet deve estar fazendo isso quando é iniciado, então talvez haja algum código para capturar lá.

Sim, eu suspeito que haja algum reconhecimento de RID acontecendo no próprio runtime do .NET. Tenho tentado evitar que tenhamos que seguir esse caminho, mas parece que vamos precisar em algum momento.

@joeyaiello

Então, se estou entendendo corretamente, você está evitando usar o PowerShell para carregar os binários nativos apenas fazendo com que o .NET os carregue por baixo do capô quando você carrega um binário gerenciado que depende dos binários nativos específicos de RID?

Sim, exatamente. O UD simplesmente carrega tudo para o RID de destino. Portanto, não é muito inteligente quais são as dependências reais. Não tenho certeza se existe uma boa maneira de detectar isso.

@ vexx32 Instalei o PSWordCloud. A estrutura da pasta é geral? Nesse caso, podemos tornar nosso resolvedor mais inteligente. No caso, eu gostaria de perguntar a você - você poderia preparar um novo (s) teste (s) de Pester com base no PSWordCloud simplificado (é suficiente para nós obter algo simples das bibliotecas nativas como "Hello World")? Nós mesclaríamos isso como pendente e isso abrirá o caminho para trabalhar em nosso resolvedor. Obrigado!

Atualmente a estrutura de pastas do PSWordCloud não é geral, não. Isso pode ser mudado, é claro.

Sim, posso montar uma cama de teste simples para isso. Pester é um pouco complicado de fazer, apenas com base na natureza da saída, suponho, mas _cheio_ que quando ele falha, ele produz uma exceção que você pode detectar, embora essa exceção seja lançada a partir do inicializador de tipo, se bem me lembro.

Seria apropriado que o próprio teste extraísse as DLLs do Skia via dotnet ou seria melhor empacotá-las no repo? Eu acho que se puxá-los todas as vezes não é um grande problema se for sinalizado como um teste de recurso quando não estiver mais pendente? :pensando:

Se você pudesse implementar um nódulo de teste com dependência indireta com dlls Skia, seria bom. E sim, podemos instalar o Skia do nuget. Embora o teste seja mais confiável se colocarmos diretamente as dlls com o módulo de teste em ~ nossa pasta de módulo de teste ~ em uma pasta assert.
Parece que a prática recomendada do Nuget é colocar dlls nativos em pastas nomeadas RID. Sim?

Sim, eu posso fazer isso! Vou dar uma olhada nisso amanhã. :corar:

Eu atualizei meu comentário - por favor, coloque o módulo de teste em uma pasta test assert.

@rjmholt Fiz algumas tentativas de usar o código que você forneceu para fazê-lo funcionar no PSWordCloud, mas ainda não está funcionando. A operação de carregamento é concluída com êxito, mas o carregamento do SkiaSharp.dll gerenciado após a biblioteca não gerenciada ainda não consegue usar a DLL carregada em plataformas Unix. Ele consegue funcionar para o Windows, de alguma forma.

Veja meu branch WIP aqui https://github.com/vexx32/PSWordCloud/tree/FixPInvokes - execute build.ps1 para construir o módulo e colocar todos os arquivos nos lugares certos. Ele importará o módulo para você também. Chamar New-WordCloud após executar a compilação funciona no Windows, mas falha completamente no Mac OS.

@SeeminglyScience, você viu alguma coisa que eu fiz de errado aí? : /

GitHub
Crie lindas nuvens de palavras com o PowerShell! Contribua com o desenvolvimento do vexx32 / PSWordCloud criando uma conta no GitHub.

@ SteveL-MSFT qual é o plano para lidar com libs nativas no momento? 😕

: tada: Este problema foi resolvido em # 11032, que agora foi lançado com sucesso como v7.0.0-rc.1 .: tada:

Links úteis:

Esta página foi útil?
0 / 5 - 0 avaliações