Existe alguma maneira de criar uma biblioteca .Net para Windows e Mac, com referências dependentes da plataforma?

12

Estamos tentando desenvolver um aplicativo de plataforma cruzada para desktops (Windows e Mac) usando C #. O aplicativo contém uma quantidade enorme de coisas dependentes da plataforma e nosso líder de equipe deseja que escrevamos todo esse código em C #. A maneira mais fácil de fazer isso seria escrever wrappers em C ++ e apenas fazer referência às bibliotecas no código C # e deixar que as bibliotecas C ++ lidassem com o material da plataforma ... infelizmente, isso não deve acontecer.

Estou tentando configurar uma biblioteca usando o Visual Studio para Mac. Espero poder fornecer uma única interface em uma única biblioteca para conceder acesso à biblioteca de texto nativo para fala na plataforma atual - de preferência sem fazer dois assemblies. A biblioteca de conversão de texto em fala em C # para Windows não está disponível no Mac; portanto, não temos realmente uma opção alternativa do que fornecer uma ponte.

Estou feliz em ter a biblioteca pesquisando o sistema operacional para determinar em qual sistema operacional está sendo executado e / ou tentando carregar um monte de bibliotecas nativas e apenas usando qual biblioteca será carregada.

Se eu precisar, ter um único projeto que me permita escrever duas implementações terá que fazer. Não encontrei uma maneira de criar um projeto de biblioteca no Visual Studio para Mac, o que me permitirá fazê-lo. O único tipo de projeto que permitirá várias implementações parece exigir que as implementações sejam para iOS ou Android.

Mais claro
fonte
Qual tempo de execução do .NET você vai usar?
Euphoric
2
Xamarin faz algo semelhante. Talvez com configurações de compilação?
Ewan
2
Vale a pena notar que o Visual Studio para mac não é realmente um estúdio visual, é apenas um estúdio Xamarin reformulado. Talvez haja algo nos documentos do Xamarin?
richzilla
3
A melhor abordagem será os diferentes assemblies ... Um para as interfaces e o código comum e um para a plataforma de destino. No entanto, você pode segmentar várias vezes e usar as instruções #if para trocar as implementações. A desvantagem é que qualquer coisa que não esteja ativada não é avaliada para que possa ficar desatualizada com facilidade.
Berin Loritsch
Quando você diz referências dependentes da plataforma, está falando de código nativo ou todo o código C #?
Berin Loritsch

Respostas:

4

Você tem uma boa pergunta. Provavelmente existem algumas vantagens e desvantagens com sua solução. A resposta final realmente depende do que você quer dizer com dependente da plataforma. Por exemplo, se você está iniciando um processo para iniciar aplicativos externos e está simplesmente alternando entre um aplicativo e outro, provavelmente poderá lidar com isso sem muita complicação. Se você está falando de P / Invoke com bibliotecas nativas, há um pouco mais a ser feito. No entanto, se você estiver vinculando com bibliotecas que existem apenas em uma plataforma, provavelmente precisará usar vários assemblies.

Aplicativos externos

Você provavelmente não precisará usar #ifinstruções nessa situação. Basta configurar algumas interfaces e ter uma implementação por plataforma. Use uma fábrica para detectar a plataforma e fornecer a instância correta.

Em alguns casos, é apenas um binário compilado para uma plataforma específica, mas o nome do executável e todos os parâmetros são definidos da mesma forma. Nesse caso, é uma questão de resolver o executável correto. Para um aplicativo conversor de áudio em massa que poderia ser executado no Windows e Linux, solicitei que um inicializador estático resolvesse o nome binário.

public class AudioProcessor
{
    private static readonly string AppName = "lame";
    private static readonly string FullAppPath;

    static AudioProcessor()
    {
        var platform = DetectPlatform();
        var architecture = Detect64or32Bits();

        FullAppPath = Path.combine(platform, architecture, AppName);
    }
}

Nada extravagante aqui. Apenas boas aulas antigas.

P / Invoke

P / Invoke é um pouco mais complicado. A linha inferior é que você precisa garantir que a versão correta da biblioteca nativa seja carregada. Nas janelas, você faria P / Invoke SetDllDirectory(). Plataformas diferentes podem não precisar dessa etapa. Então é aqui que as coisas podem ficar confusas. Pode ser necessário usar #ifinstruções para controlar qual chamada é usada para controlar a resolução do caminho da biblioteca - principalmente se você a incluir no pacote de distribuição.

Vinculando a Bibliotecas Dependentes da Plataforma completamente Diferentes

A abordagem de segmentação antiga à moda antiga pode ser útil aqui. No entanto, ele vem com muita feiura. Nos dias em que alguns projetos tentavam ter o mesmo destino de DLL do Silverlight, WPF e potencialmente UAP, seria necessário compilar o aplicativo várias vezes com diferentes marcas de compilação. O desafio de cada uma das plataformas acima é que, embora elas compartilhem os mesmos conceitos, as plataformas são suficientemente diferentes para que você precise solucionar essas diferenças. É aqui que entramos no inferno #if.

Essa abordagem também requer edição manual do .csprojarquivo para lidar com referências dependentes da plataforma. Como seu .csprojarquivo é um arquivo MSBuild, é totalmente possível fazer isso de maneira conhecida e previsível.

#se inferno

Você pode ativar e desativar seções de código usando #ifinstruções para que seja eficaz no tratamento de pequenas diferenças entre os aplicativos. Na superfície, parece uma boa ideia. Eu até o usei como um meio de ativar e desativar a visualização da caixa delimitadora para depurar o código de desenho.

O problema número 1 #ifé que nenhum código desativado é avaliado pelo analisador. Você pode ter erros de sintaxe latentes ou, pior, erros de lógica esperando que você recompile a biblioteca. Isso se torna ainda mais problemático com o código de refatoração. Algo tão simples como renomear um método ou alterar a ordem dos parâmetros normalmente seria tratado como OK, mas como o analisador nunca avalia nada desativado pela #ifinstrução, você repentinamente quebrou o código que não verá até recompilar.

Todo o meu código de depuração que foi escrito dessa maneira teve que ser reescrito depois que uma série de refatorações o quebrou. Durante a reescrita, usei uma classe de configuração global para ativar e desativar esses recursos. Isso fez com que refatorasse a prova de ferramentas, mas essa solução não ajuda quando a API é completamente diferente.

Meu método preferido

Meu método preferido, com base em muitas lições dolorosas aprendidas, e mesmo com base no próprio exemplo da Microsoft, é usar várias montagens.

Um assembly NetStandard principal definiria todas as interfaces e conteria todo o código comum. As implementações dependentes da plataforma estariam em um assembly separado que adicionaria recursos quando incluídos.

Essa abordagem é exemplificada pela nova API de configuração e pela arquitetura de identidade atual. Como você precisa de integrações mais específicas, basta adicionar esses novos assemblies. Esses conjuntos também fornecem funções de extensão para incorporar-se à sua configuração. Se você estiver usando uma abordagem de injeção de dependência, esses métodos de extensão permitirão que a biblioteca registre seus serviços.

Essa é a única maneira que conheço de evitar o #ifinferno e satisfazer um ambiente substancialmente diferente.

Berin Loritsch
fonte
Eu sou péssima em C # (usei C ++ por cerca de 18 anos e C # por cerca de meio dia, isso é de se esperar). Esta nova API de configuração ... você poderia fornecer alguns links para isso. Parece que não consigo encontrá-lo sozinho.
Clearer
2
docs.microsoft.com/en-us/aspnet/core/fundamentals/configuration/… Existem vários pacotes Nuget para adicionar fontes de configuração adicionais. Por exemplo, a partir de variáveis de ambiente, arquivos JSON, etc.
Berin Loritsch
1
É o Microsoft.Extensions.Configurationconjunto de APIs.
Berin Loritsch
1
Aqui está o projeto github root: github.com/aspnet/Configuration
Berin Loritsch