Usar bibliotecas C/C++ com O Xamarin

Visão geral

O Xamarin permite que os desenvolvedores criem aplicativos móveis nativos multiplataforma com o Visual Studio. Geralmente, as associações C# são usadas para expor componentes de plataforma existentes aos desenvolvedores. No entanto, há momentos em que os aplicativos Xamarin precisam trabalhar com bases de código existentes. Às vezes, as equipes simplesmente não têm tempo, orçamento ou recursos para portar uma base de código grande, bem testada e altamente otimizada para C#.

O Visual C++ para desenvolvimento móvel multiplataforma permite que o código C/C++ e C# sejam criados como parte da mesma solução, oferecendo muitas vantagens, incluindo uma experiência unificada de depuração. A Microsoft usou C/C++ e Xamarin dessa forma para fornecer aplicativos como Hyperlapse Mobile e Pix Camera.

No entanto, em alguns casos, há um desejo (ou requisito) de manter as ferramentas e processos C/C++ existentes em vigor e manter o código da biblioteca dissociado do aplicativo, tratando a biblioteca como se fosse semelhante a um componente de terceiros. Nessas situações, o desafio não é apenas expor os membros relevantes ao C#, mas gerenciar a biblioteca como uma dependência. E, claro, automatizar o máximo possível desse processo.

Esta postagem descreve uma abordagem de alto nível para esse cenário e percorre um exemplo simples.

Tela de fundo

O C/C++ é considerado uma linguagem multiplataforma, mas é necessário ter muito cuidado para garantir que o código-fonte seja de fato multiplataforma, usando apenas O C/C++ compatível com todos os compiladores de destino e contendo pouco ou nenhum código específico de plataforma ou compilador incluído condicionalmente.

Por fim, o código deve ser compilado e executado com êxito em todas as plataformas de destino, portanto, isso se resume à semelhança entre as plataformas (e os compiladores) que estão sendo direcionadas. Problemas ainda podem surgir de pequenas diferenças entre compiladores e, portanto, testes completos (preferencialmente automatizados) em cada plataforma de destino se tornam cada vez mais importantes.

Abordagem de alto nível

A ilustração a seguir representa a abordagem de quatro estágios usada para transformar o código-fonte C/C++ em uma biblioteca Xamarin multiplataforma que é compartilhada por meio do NuGet e, em seguida, é consumida em um aplicativo Xamarin.Forms.

Abordagem de alto nível para usar C/C++ com Xamarin

Os quatro estágios são:

  1. Compilando o código-fonte C/C++ em bibliotecas nativas específicas da plataforma.
  2. Encapsulando as bibliotecas nativas com uma solução do Visual Studio.
  3. Empacotar e enviar por push um pacote NuGet para o wrapper do .NET.
  4. Consumindo o pacote NuGet de um aplicativo Xamarin.

Estágio 1: compilando o código-fonte C/C++ em bibliotecas nativas específicas da plataforma

A meta desse estágio é criar bibliotecas nativas que podem ser chamadas pelo wrapper C#. Isso pode ou não ser relevante dependendo da sua situação. As muitas ferramentas e processos que podem ser trazidos para suportar neste cenário comum estão além do escopo deste artigo. As principais considerações são manter a base de código C/C++ em sincronia com qualquer código wrapper nativo, teste de unidade suficiente e automação de build.

As bibliotecas no passo a passo foram criadas usando Visual Studio Code com um script de shell que acompanha. Uma versão estendida deste passo a passo pode ser encontrada no repositório GitHub do Mobile CAT que discute essa parte do exemplo com mais profundidade. As bibliotecas nativas estão sendo tratadas como uma dependência de terceiros nesse caso, no entanto, esse estágio é ilustrado para contexto.

Para simplificar, o passo a passo tem como alvo apenas um subconjunto de arquiteturas. Para iOS, ele usa o utilitário lipo para criar um binário de gordura único dos binários específicos da arquitetura individual. O Android usará binários dinâmicos com uma extensão .so e o iOS usará um binário de gordura estático com uma extensão .a.

Estágio 2: Encapsulando as bibliotecas nativas com uma solução do Visual Studio

O próximo estágio é encapsular as bibliotecas nativas para que elas sejam facilmente usadas do .NET. Isso é feito com uma solução do Visual Studio com quatro projetos. Um projeto compartilhado contém o código comum. Projetos direcionados a cada Xamarin.Android, Xamarin.iOS e .NET Standard permitem que a biblioteca seja referenciada de maneira independente da plataforma.

O wrapper usa "a isca e o truque de troca", . Essa não é a única maneira, mas facilita a referência à biblioteca e evita a necessidade de gerenciar explicitamente implementações específicas da plataforma dentro do próprio aplicativo consumidor. O truque é essencialmente garantir que os destinos (.NET Standard, Android, iOS) compartilhem o mesmo namespace, nome do assembly e estrutura de classe. Como o NuGet sempre preferirá uma biblioteca específica da plataforma, a versão do .NET Standard nunca é usada em runtime.

A maior parte do trabalho nesta etapa se concentrará em usar P/Invoke para chamar os métodos de biblioteca nativa e gerenciar as referências aos objetos subjacentes. O objetivo é expor a funcionalidade da biblioteca ao consumidor ao abstrair qualquer complexidade. Os desenvolvedores do Xamarin.Forms não precisam ter conhecimento de trabalho sobre o funcionamento interno da biblioteca não gerenciada. Deve parecer que eles estão usando qualquer outra biblioteca C# gerenciada.

Por fim, a saída desse estágio é um conjunto de bibliotecas .NET, uma por destino, juntamente com um documento nuspec que contém as informações necessárias para compilar o pacote na próxima etapa.

Estágio 3: Empacotar e enviar por push um pacote NuGet para o wrapper do .NET

O terceiro estágio é criar um pacote NuGet usando os artefatos de build da etapa anterior. O resultado dessa etapa é um pacote NuGet que pode ser consumido de um aplicativo Xamarin. O passo a passo usa um diretório local para servir como o feed do NuGet. Em produção, essa etapa deve publicar um pacote em um feed NuGet público ou privado e deve ser totalmente automatizada.

Estágio 4: consumindo o pacote NuGet de um aplicativo Xamarin.Forms

A etapa final é referenciar e usar o pacote NuGet de um aplicativo Xamarin.Forms. Isso requer a configuração do feed do NuGet no Visual Studio para usar o feed definido na etapa anterior.

Depois que o feed é configurado, o pacote precisa ser referenciado de cada projeto no aplicativo Xamarin.Forms multiplataforma. 'O truque de isca e comutador' fornece interfaces idênticas, de modo que a funcionalidade da biblioteca nativa pode ser chamada usando o código definido em um único local.

O repositório de código-fonte contém uma lista de leitura adicional que inclui artigos sobre como configurar um feed NuGet privado no Azure DevOps e como enviar o pacote por push para esse feed. Embora exija um pouco mais de tempo de instalação do que um diretório local, esse tipo de feed é melhor em um ambiente de desenvolvimento em equipe.

Passo a passo

As etapas fornecidas são específicas para Visual Studio para Mac, mas a estrutura também funciona no Visual Studio 2017.

Pré-requisitos

Para acompanhar, o desenvolvedor precisará:

Observação

Uma conta de desenvolvedor ativa da Apple é necessária para implantar aplicativos em um iPhone.

Criando as bibliotecas nativas (Estágio 1)

A funcionalidade da biblioteca nativa baseia-se no exemplo de Passo a passo: Criando e usando uma biblioteca estática (C++).

Este passo a passo ignora o primeiro estágio, criando as bibliotecas nativas, pois a biblioteca é fornecida como uma dependência de terceiros nesse cenário. As bibliotecas nativas pré-compiladas são incluídas junto com o código de exemplo ou podem ser baixadas diretamente.

Trabalhando com a biblioteca nativa

O exemplo mathfuncsLib original inclui uma única classe chamada MyMathFuncs com a seguinte definição:

namespace MathFuncs
{
    class MyMathFuncs
    {
    public:
        double Add(double a, double b);
        double Subtract(double a, double b);
        double Multiply(double a, double b);
        double Divide(double a, double b);
    };
}

Uma classe adicional define funções wrapper que permitem que um consumidor do .NET crie, descarte e interaja com a classe nativa MyMathFuncs subjacente.

#include "MyMathFuncs.h"
using namespace MathFuncs;

extern "C" {
    MyMathFuncs* CreateMyMathFuncsClass();
    void DisposeMyMathFuncsClass(MyMathFuncs* ptr);
    double MyMathFuncsAdd(MyMathFuncs *ptr, double a, double b);
    double MyMathFuncsSubtract(MyMathFuncs *ptr, double a, double b);
    double MyMathFuncsMultiply(MyMathFuncs *ptr, double a, double b);
    double MyMathFuncsDivide(MyMathFuncs *ptr, double a, double b);
}

Serão essas funções wrapper usadas no lado do Xamarin .

Encapsulando a biblioteca nativa (Estágio 2)

Essa fase requer as bibliotecas pré-compiladas descritas na seção anterior.

Criando a solução do Visual Studio

  1. Em Visual Studio para Mac, clique em Novo Projeto (na Página de Boas-vindas) ou Em Nova Solução (no menu Arquivo).

  2. Na janela Novo Projeto, escolha Projeto Compartilhado (na Biblioteca Multiplataforma>) e clique em Avançar.

  3. Atualize os seguintes campos e clique em Criar:

    • Nome do Projeto: MathFuncs.Shared
    • Nome da solução: MathFuncs
    • Localização: Usar o local de salvamento padrão (ou escolher uma alternativa)
    • Crie um projeto no diretório da solução: Defina isso como verificado
  4. Em Gerenciador de Soluções, clique duas vezes no projeto MathFuncs.Shared e navegue até Configurações Principais.

  5. Remova . Compartilhado do Namespace Padrão para que ele seja definido apenas como MathFuncs e clique em OK.

  6. Abra MyClass.cs (criado pelo modelo), renomeie a classe e o nome do arquivo como MyMathFuncsWrapper e altere o namespace para MathFuncs.

  7. CONTROL + CLIQUE na solução MathFuncs e escolha Adicionar Novo Projeto... no menu Adicionar .

  8. Na janela Novo Projeto, escolha Biblioteca .NET Standard (de dentro da Biblioteca Multiplataforma>) e clique em Avançar.

  9. Escolha .NET Standard 2.0 e clique em Avançar.

  10. Atualize os seguintes campos e clique em Criar:

    • Nome do Projeto: MathFuncs.Standard
    • Localização: Usar o mesmo local de salvamento que o projeto compartilhado
  11. Em Gerenciador de Soluções, clique duas vezes no projeto MathFuncs.Standard.

  12. Navegue até Configurações Principais e atualize o Namespace Padrão para MathFuncs.

  13. Navegue até as configurações de Saída e atualize o nome do assembly para MathFuncs.

  14. Navegue até as configurações do Compilador , altere a Configuração para Versão, definindo Informações de depuraçãocomo Somente Símbolos e clique em OK.

  15. Exclua Class1.cs/Introdução do projeto (se um deles tiver sido incluído como parte do modelo).

  16. CONTROL + CLIQUE na pasta Dependências/Referências do projeto e escolha Editar Referências.

  17. Selecione MathFuncs.Shared na guia Projetos e clique em OK.

  18. Repita as etapas 7 a 17 (ignorando a etapa 9) usando as seguintes configurações:

    NOME DO PROJETO NOME DO MODELO MENU NOVO PROJETO
    MathFuncs.Android Biblioteca de Classes Biblioteca do Android >
    MathFuncs.iOS Biblioteca de associação Biblioteca do iOS >
  19. Em Gerenciador de Soluções, clique duas vezes no projeto MathFuncs.Android e navegue até as configurações do Compilador.

  20. Com a Configuração definida como Depurar, edite Definir Símbolos para incluir o Android;.

  21. Altere a Configuração para Versão e, em seguida, edite Definir Símbolos para incluir também o Android;.

  22. Repita as etapas de 19 a 20 para MathFuncs.iOS, editando Definir Símbolos para incluir o iOS; em vez do Android; em ambos os casos.

  23. Crie a solução em Configuração de versão (CONTROL + COMMAND + B) e valide se todos os três assemblies de saída (Android, iOS, .NET Standard) (nas respectivas pastas de compartimento de projeto) compartilham o mesmo nome MathFuncs.dll.

Nesta fase, a solução deve ter três destinos, um para Android, iOS e .NET Standard e um projeto compartilhado referenciado por cada um dos três destinos. Eles devem ser configurados para usar o mesmo namespace padrão e assemblies de saída com o mesmo nome. Isso é necessário para a abordagem "isca e comutador" mencionada anteriormente.

Adicionando as bibliotecas nativas

O processo de adição das bibliotecas nativas à solução wrapper varia ligeiramente entre Android e iOS.

Referências nativas para MathFuncs.Android

  1. CONTROL + CLIQUE no projeto MathFuncs.Android e escolha Nova Pasta no menu Adicionar nomeando-a como lib.

  2. Para cada ABI (Interface Binária do Aplicativo), CONTROL + CLIQUE na pasta lib e escolha Nova Pasta no menu Adicionar , nomeando-a após a respectiva ABI. Nesse caso:

    • arm64-v8a
    • armeabi-v7a
    • x86
    • x86_64

    Observação

    Para obter uma visão geral mais detalhada, consulte o tópico Arquiteturas e CPUs no guia do desenvolvedor do NDK, especificamente a seção sobre como abordar o código nativo em pacotes de aplicativos.

  3. Verifique a estrutura da pasta:

    - lib
        - arm64-v8a
        - armeabi-v7a
        - x86
        - x86_64
    
  4. Adicione as bibliotecas .so correspondentes a cada uma das pastas ABI com base no seguinte mapeamento:

    arm64-v8a: lib/Android/arm64

    armeabi-v7a: lib/Android/arm

    x86: lib/Android/x86

    x86_64: lib/Android/x86_64

    Observação

    Para adicionar arquivos, CONTROL + CLICK na pasta que representa a respectiva ABI e escolha Adicionar Arquivos... no menu Adicionar . Escolha a biblioteca apropriada (no diretório PrecompiledLibs ) e clique em Abrir e, em seguida, clique em OK deixando a opção padrão para Copiar o arquivo para o diretório.

  5. Para cada um dos arquivos .so , CONTROL + CLICK , em seguida, escolha a opção EmbeddedNativeLibrary no menu Criar Ação .

Agora, a pasta lib deve aparecer da seguinte maneira:

- lib
    - arm64-v8a
        - libMathFuncs.so
    - armeabi-v7a
        - libMathFuncs.so
    - x86
        - libMathFuncs.so
    - x86_64
        - libMathFuncs.so

Referências nativas para MathFuncs.iOS

  1. CONTROL + CLIQUE no projeto MathFuncs.iOS e escolha Adicionar Referência Nativa no menu Adicionar .

  2. Escolha a biblioteca libMathFuncs.a (de libs/ios no diretório PrecompiledLibs ) e clique em Abrir

  3. CONTROL + CLICK no arquivo libMathFuncs (dentro da pasta Referências Nativas e escolha a opção Propriedades no menu

  4. Configure as propriedades de Referência Nativa para que elas sejam verificadas (mostrando um ícone de escala) no Painel de Propriedades :

    • Forçar carregamento
    • É C++
    • Link Inteligente

    Observação

    Usar um tipo de projeto de biblioteca de associação junto com uma referência nativa inscreve a biblioteca estática e permite que ela seja vinculada automaticamente ao aplicativo Xamarin.iOS que faz referência a ela (mesmo quando ela é incluída por meio de um pacote NuGet).

  5. Abra ApiDefinition.cs, excluindo o código comentado com modelo (deixando apenas o MathFuncs namespace) e execute a mesma etapa para Structs.cs

    Observação

    Um projeto de biblioteca de associação requer esses arquivos (com as ações de build ObjCBindingApiDefinition e ObjCBindingCoreSource ) para compilar. No entanto, escreveremos o código para chamar nossa biblioteca nativa, fora desses arquivos de uma forma que possa ser compartilhada entre os destinos da biblioteca android e iOS usando p/invoke padrão.

Escrevendo o código da biblioteca gerenciada

Agora, escreva o código C# para chamar a biblioteca nativa. O objetivo é ocultar qualquer complexidade subjacente. O consumidor não deve precisar de nenhum conhecimento funcional dos internos da biblioteca nativa ou dos conceitos de P/Invoke.

Criando um SafeHandle

  1. CONTROL + CLIQUE no projeto MathFuncs.Shared e escolha Adicionar Arquivo... no menu Adicionar .

  2. Escolha Classe Vazia na janela Novo Arquivo , nomeie-a como MyMathFuncsSafeHandle e clique em Novo

  3. Implemente a classe MyMathFuncsSafeHandle :

    using System;
    using Microsoft.Win32.SafeHandles;
    
    namespace MathFuncs
    {
        internal class MyMathFuncsSafeHandle : SafeHandleZeroOrMinusOneIsInvalid
        {
            public MyMathFuncsSafeHandle() : base(true) { }
    
            public IntPtr Ptr => handle;
    
            protected override bool ReleaseHandle()
            {
                // TODO: Release the handle here
                return true;
            }
        }
    }
    

    Observação

    Um SafeHandle é a maneira preferida de trabalhar com recursos não gerenciados no código gerenciado. Isso abstrai muitos códigos clichês relacionados à finalização crítica e ao ciclo de vida do objeto. O proprietário desse identificador pode tratá-lo posteriormente como qualquer outro recurso gerenciado e não precisará implementar o padrão descartável completo.

Criando a classe wrapper interna

  1. Abra MyMathFuncsWrapper.cs, alterando-o para uma classe estática interna

    namespace MathFuncs
    {
        internal static class MyMathFuncsWrapper
        {
        }
    }
    
  2. No mesmo arquivo, adicione a seguinte instrução condicional à classe :

    #if Android
        const string DllName = "libMathFuncs.so";
    #else
        const string DllName = "__Internal";
    #endif
    

    Observação

    Isso define o valor da constante DllName com base em se a biblioteca está sendo criada para Android ou iOS. Isso é para abordar as diferentes convenções de nomenclatura usadas por cada plataforma respectiva, mas também o tipo de biblioteca que está sendo usada nesse caso. O Android está usando uma biblioteca dinâmica e, portanto, espera um nome de arquivo, incluindo a extensão. Para iOS, '__Internal' é necessário, pois estamos usando uma biblioteca estática.

  3. Adicionar uma referência a System.Runtime.InteropServices na parte superior do arquivo MyMathFuncsWrapper.cs

    using System.Runtime.InteropServices;
    
  4. Adicione os métodos wrapper para lidar com a criação e o descarte da classe MyMathFuncs :

    [DllImport(DllName, EntryPoint = "CreateMyMathFuncsClass")]
    internal static extern MyMathFuncsSafeHandle CreateMyMathFuncs();
    
    [DllImport(DllName, EntryPoint = "DisposeMyMathFuncsClass")]
    internal static extern void DisposeMyMathFuncs(MyMathFuncsSafeHandle ptr);
    

    Observação

    Estamos passando nosso DllName constante para o atributo DllImport junto com o EntryPoint , que informa explicitamente ao runtime do .NET o nome da função a ser chamada dentro dessa biblioteca. Tecnicamente, não precisamos fornecer o valor do EntryPoint se nossos nomes de método gerenciado forem idênticos ao não gerenciado. Se um não for fornecido, o nome do método gerenciado será usado como o EntryPoint . No entanto, é melhor ser explícito.

  5. Adicione os métodos wrapper para nos permitir trabalhar com a classe MyMathFuncs usando o seguinte código:

    [DllImport(DllName, EntryPoint = "MyMathFuncsAdd")]
    internal static extern double Add(MyMathFuncsSafeHandle ptr, double a, double b);
    
    [DllImport(DllName, EntryPoint = "MyMathFuncsSubtract")]
    internal static extern double Subtract(MyMathFuncsSafeHandle ptr, double a, double b);
    
    [DllImport(DllName, EntryPoint = "MyMathFuncsMultiply")]
    internal static extern double Multiply(MyMathFuncsSafeHandle ptr, double a, double b);
    
    [DllImport(DllName, EntryPoint = "MyMathFuncsDivide")]
    internal static extern double Divide(MyMathFuncsSafeHandle ptr, double a, double b);
    

    Observação

    Estamos usando tipos simples para os parâmetros neste exemplo. Como o marshalling é uma cópia bit a bit nesse caso, não requer nenhum trabalho adicional da nossa parte. Observe também o uso da classe MyMathFuncsSafeHandle em vez do IntPtr padrão. O IntPtr é mapeado automaticamente para o SafeHandle como parte do processo de marshalling.

  6. Verifique se a classe MyMathFuncsWrapper concluída aparece como abaixo:

    using System.Runtime.InteropServices;
    
    namespace MathFuncs
    {
        internal static class MyMathFuncsWrapper
        {
            #if Android
                const string DllName = "libMathFuncs.so";
            #else
                const string DllName = "__Internal";
            #endif
    
            [DllImport(DllName, EntryPoint = "CreateMyMathFuncsClass")]
            internal static extern MyMathFuncsSafeHandle CreateMyMathFuncs();
    
            [DllImport(DllName, EntryPoint = "DisposeMyMathFuncsClass")]
            internal static extern void DisposeMyMathFuncs(MyMathFuncsSafeHandle ptr);
    
            [DllImport(DllName, EntryPoint = "MyMathFuncsAdd")]
            internal static extern double Add(MyMathFuncsSafeHandle ptr, double a, double b);
    
            [DllImport(DllName, EntryPoint = "MyMathFuncsSubtract")]
            internal static extern double Subtract(MyMathFuncsSafeHandle ptr, double a, double b);
    
            [DllImport(DllName, EntryPoint = "MyMathFuncsMultiply")]
            internal static extern double Multiply(MyMathFuncsSafeHandle ptr, double a, double b);
    
            [DllImport(DllName, EntryPoint = "MyMathFuncsDivide")]
            internal static extern double Divide(MyMathFuncsSafeHandle ptr, double a, double b);
        }
    }
    

Concluindo a classe MyMathFuncsSafeHandle

  1. Abra a classe MyMathFuncsSafeHandle , navegue até o comentário TODO do espaço reservado dentro do método ReleaseHandle :

    // TODO: Release the handle here
    
  2. Substitua a linha TODO :

    MyMathFuncsWrapper.DisposeMyMathFuncs(this);
    

Escrevendo a classe MyMathFuncs

Agora que o wrapper está concluído, crie uma classe MyMathFuncs que gerenciará a referência ao objeto MyMathFuncs do C++ não gerenciado.

  1. CONTROL + CLIQUE no projeto MathFuncs.Shared e escolha Adicionar Arquivo... no menu Adicionar .

  2. Escolha Classe Vazia na janela Novo Arquivo , nomeie-a como MyMathFuncs e clique em Novo

  3. Adicione os seguintes membros à classe MyMathFuncs :

    readonly MyMathFuncsSafeHandle handle;
    
  4. Implemente o construtor para a classe para que ele crie e armazene um identificador para o objeto MyMathFuncs nativo quando a classe for instanciada:

    public MyMathFuncs()
    {
        handle = MyMathFuncsWrapper.CreateMyMathFuncs();
    }
    
  5. Implemente a interface IDisposable usando o seguinte código:

    public class MyMathFuncs : IDisposable
    {
        ...
    
        protected virtual void Dispose(bool disposing)
        {
            if (handle != null && !handle.IsInvalid)
                handle.Dispose();
        }
    
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }
    
        // ...
    }
    
  6. Implemente os métodos MyMathFuncs usando a classe MyMathFuncsWrapper para executar o trabalho real sob o capô passando o ponteiro que armazenamos para o objeto não gerenciado subjacente. O código deve ser o seguinte:

    public double Add(double a, double b)
    {
        return MyMathFuncsWrapper.Add(handle, a, b);
    }
    
    public double Subtract(double a, double b)
    {
        return MyMathFuncsWrapper.Subtract(handle, a, b);
    }
    
    public double Multiply(double a, double b)
    {
        return MyMathFuncsWrapper.Multiply(handle, a, b);
    }
    
    public double Divide(double a, double b)
    {
        return MyMathFuncsWrapper.Divide(handle, a, b);
    }
    

Criando o nuspec

Para ter a biblioteca empacotada e distribuída por meio do NuGet, a solução precisa de um arquivo nuspec . Isso identificará qual dos assemblies resultantes será incluído para cada plataforma com suporte.

  1. CONTROL + CLIQUE na solução MathFuncs e, em seguida, escolha Adicionar Pasta de Solução no menu Adicionar nomeando-a SolutionItems.

  2. CONTROL + CLIQUE na pasta SolutionItems e escolha Novo Arquivo... no menu Adicionar .

  3. Escolha Arquivo XML Vazio na janela Novo Arquivo , nomeie-o MathFuncs.nuspec e clique em Novo.

  4. Atualize MathFuncs.nuspec com os metadados básicos do pacote a serem exibidos para o consumidor do NuGet . Por exemplo:

    <?xml version="1.0"?>
    <package>
        <metadata>
            <id>MathFuncs</id>
            <version>$version$</version>
            <authors>Microsoft Mobile Customer Advisory Team</authors>
            <description>Sample C++ Wrapper Library</description>
            <requireLicenseAcceptance>false</requireLicenseAcceptance>
            <copyright>Copyright 2018</copyright>
        </metadata>
    </package>
    

    Observação

    Consulte o documento de referência nuspec para obter mais detalhes sobre o esquema usado para este manifesto.

  5. Adicione um <files> elemento como um filho do <package> elemento (logo abaixo <metadata>), identificando cada arquivo com um elemento separado <file> :

    <files>
    
        <!-- Android -->
    
        <!-- iOS -->
    
        <!-- netstandard2.0 -->
    
    </files>
    

    Observação

    Quando um pacote é instalado em um projeto e há vários assemblies especificados pelo mesmo nome, o NuGet escolherá efetivamente o assembly mais específico para a plataforma fornecida.

  6. Adicione os <file> elementos para os assemblies do Android :

    <file src="MathFuncs.Android/bin/Release/MathFuncs.dll" target="lib/MonoAndroid81/MathFuncs.dll" />
    <file src="MathFuncs.Android/bin/Release/MathFuncs.pdb" target="lib/MonoAndroid81/MathFuncs.pdb" />
    
  7. Adicione os <file> elementos para os assemblies do iOS :

    <file src="MathFuncs.iOS/bin/Release/MathFuncs.dll" target="lib/Xamarin.iOS10/MathFuncs.dll" />
    <file src="MathFuncs.iOS/bin/Release/MathFuncs.pdb" target="lib/Xamarin.iOS10/MathFuncs.pdb" />
    
  8. Adicione os <file> elementos para os assemblies netstandard2.0 :

    <file src="MathFuncs.Standard/bin/Release/netstandard2.0/MathFuncs.dll" target="lib/netstandard2.0/MathFuncs.dll" />
    <file src="MathFuncs.Standard/bin/Release/netstandard2.0/MathFuncs.pdb" target="lib/netstandard2.0/MathFuncs.pdb" />
    
  9. Verifique o manifesto nuspec :

    <?xml version="1.0"?>
    <package>
    <metadata>
        <id>MathFuncs</id>
        <version>$version$</version>
        <authors>Microsoft Mobile Customer Advisory Team</authors>
        <description>Sample C++ Wrapper Library</description>
        <requireLicenseAcceptance>false</requireLicenseAcceptance>
        <copyright>Copyright 2018</copyright>
    </metadata>
    <files>
    
        <!-- Android -->
        <file src="MathFuncs.Android/bin/Release/MathFuncs.dll" target="lib/MonoAndroid81/MathFuncs.dll" />
        <file src="MathFuncs.Android/bin/Release/MathFuncs.pdb" target="lib/MonoAndroid81/MathFuncs.pdb" />
    
        <!-- iOS -->
        <file src="MathFuncs.iOS/bin/Release/MathFuncs.dll" target="lib/Xamarin.iOS10/MathFuncs.dll" />
        <file src="MathFuncs.iOS/bin/Release/MathFuncs.pdb" target="lib/Xamarin.iOS10/MathFuncs.pdb" />
    
        <!-- netstandard2.0 -->
        <file src="MathFuncs.Standard/bin/Release/netstandard2.0/MathFuncs.dll" target="lib/netstandard2.0/MathFuncs.dll" />
        <file src="MathFuncs.Standard/bin/Release/netstandard2.0/MathFuncs.pdb" target="lib/netstandard2.0/MathFuncs.pdb" />
    
    </files>
    </package>
    

    Observação

    Esse arquivo especifica os caminhos de saída do assembly de um build de versão , portanto, certifique-se de criar a solução usando essa configuração.

Neste ponto, a solução contém três assemblies .NET e um manifesto nuspec de suporte.

Distribuindo o wrapper do .NET com o NuGet

A próxima etapa é empacotar e distribuir o pacote NuGet para que ele possa ser facilmente consumido pelo aplicativo e gerenciado como uma dependência. O encapsulamento e o consumo podem ser feitos em uma única solução, mas distribuir a biblioteca por meio do NuGet ajuda na desacoplamento e nos permite gerenciar essas bases de código de forma independente.

Preparando um diretório de pacotes locais

A forma mais simples de feed do NuGet é um diretório local:

  1. No Finder, navegue até um diretório conveniente. Por exemplo, /Users.
  2. Escolha Nova Pasta no menu Arquivo , fornecendo um nome significativo, como local-nuget-feed.

Como criar o pacote

  1. Defina a Configuração de Build como Versão e execute um build usando COMMAND + B.

  2. Abra Terminal e altere o diretório para a pasta que contém o arquivo nuspec .

  3. No Terminal, execute o comando nuget pack especificando o arquivo nuspec , a Versão (por exemplo, 1.0.0) e o OutputDirectory usando a pasta criada na etapa anterior, ou seja, local-nuget-feed. Por exemplo:

    nuget pack MathFuncs.nuspec -Version 1.0.0 -OutputDirectory ~/local-nuget-feed
    
  4. Confirme se MathFuncs.1.0.0.nupkg foi criado no diretório local-nuget-feed .

[OPCIONAL] Usando um feed nuget privado com o Azure DevOps

Uma técnica mais robusta é descrita em Introdução aos pacotes NuGet no Azure DevOps, que mostra como criar um feed privado e enviar por push o pacote (gerado na etapa anterior) para esse feed.

É ideal ter esse fluxo de trabalho totalmente automatizado, por exemplo, usando o Azure Pipelines. Para obter mais informações, consulte Introdução ao Azure Pipelines.

Consumindo o wrapper do .NET de um aplicativo Xamarin.Forms

Para concluir o passo a passo, crie um aplicativo Xamarin.Forms para consumir o pacote publicado no feed nuget local.

Criando o projeto Xamarin.Forms

  1. Abra uma nova instância de Visual Studio para Mac. Isso pode ser feito no Terminal:

    open -n -a "Visual Studio"
    
  2. Em Visual Studio para Mac, clique em Novo Projeto (na Página de Boas-vindas) ou Em Nova Solução (no menu Arquivo).

  3. Na janela Novo Projeto, escolha Aplicativo formulários em branco (de dentro do Aplicativo Multiplataforma>) e clique em Avançar.

  4. Atualize os seguintes campos e clique em Avançar:

    • Nome do aplicativo: MathFuncsApp.
    • Identificador da organização: Use um namespace reverso, por exemplo, com.{your_org}.
    • Plataformas de destino: Use o padrão (destinos android e iOS).
    • Código compartilhado: Defina isso como .NET Standard (uma solução de "Biblioteca Compartilhada" é possível, mas além do escopo deste passo a passo).
  5. Atualize os seguintes campos e clique em Criar:

    • Nome do Projeto: MathFuncsApp.
    • Nome da solução: MathFuncsApp.
    • Localização: Use o local de salvamento padrão (ou escolha uma alternativa).
  6. Em Gerenciador de Soluções, CONTROL + CLICK no destino (MathFuncsApp.Android ou MathFuncs.iOS) para teste inicial e escolha Definir como Projeto de Inicialização.

  7. Escolha o dispositivo preferencial ouo Emulador do Simulador/.

  8. Execute a solução (COMMAND + RETURN) para validar se o projeto Xamarin.Forms com modelo é compilado e executado corretamente.

    Observação

    O iOS (especificamente o simulador) tende a ter o tempo de build/implantação mais rápido.

Adicionando o feed nuget local à configuração do NuGet

  1. No Visual Studio, escolha Preferências (no menu do Visual Studio ).

  2. Escolha Fontes na seção NuGet e clique em Adicionar.

  3. Atualize os seguintes campos e clique em Adicionar Fonte:

    • Nome: Forneça um nome significativo, por exemplo, Pacotes Locais.
    • Localização: Especifique a pasta local-nuget-feed criada na etapa anterior.

    Observação

    Nesse caso, não é necessário especificar um Nome de usuário e senha.

  4. Clique em OK.

Referenciando o pacote

Repita as etapas a seguir para cada projeto (MathFuncsApp, MathFuncsApp.Android e MathFuncsApp.iOS).

  1. CONTROL + CLIQUE no projeto e, em seguida, escolha Adicionar Pacotes NuGet... no menu Adicionar .
  2. Pesquise MathFuncs.
  3. Verifique se a versão do pacote é 1.0.0 e se os outros detalhes aparecem conforme o esperado, como Título e Descrição, ou seja, MathFuncs e Biblioteca de Wrapper C++ de exemplo.
  4. Selecione o pacote MathFuncs e clique em Adicionar Pacote.

Usando as funções de biblioteca

Agora, com uma referência ao pacote MathFuncs em cada um dos projetos, as funções estão disponíveis para o código C#.

  1. Abra MainPage.xaml.cs de dentro do projeto Xamarin.Formscomum do MathFuncsApp (referenciado por MathFuncsApp.Android e MathFuncsApp.iOS).

  2. Adicione instruções using para System.Diagnostics e MathFuncs na parte superior do arquivo:

    using System.Diagnostics;
    using MathFuncs;
    
  3. Declare uma instância da MyMathFuncs classe na parte superior da MainPage classe:

    MyMathFuncs myMathFuncs;
    
  4. Substitua os OnAppearing métodos e OnDisappearing da ContentPage classe base:

    protected override void OnAppearing()
    {
        base.OnAppearing();
    }
    
    protected override void OnDisappearing()
    {
        base.OnDisappearing();
    }
    
  5. Atualize o OnAppearing método para inicializar a myMathFuncs variável declarada anteriormente:

    protected override void OnAppearing()
    {
        base.OnAppearing();
        myMathFuncs = new MyMathFuncs();
    }
    
  6. Atualize o OnDisappearing método para chamar o Dispose método em myMathFuncs:

    protected override void OnDisappearing()
    {
        base.OnAppearing();
        myMathFuncs.Dispose();
    }
    
  7. Implemente um método privado chamado TestMathFuncs da seguinte maneira:

    private void TestMathFuncs()
    {
        var numberA = 1;
        var numberB = 2;
    
        // Test Add function
        var addResult = myMathFuncs.Add(numberA, numberB);
    
        // Test Subtract function
        var subtractResult = myMathFuncs.Subtract(numberA, numberB);
    
        // Test Multiply function
        var multiplyResult = myMathFuncs.Multiply(numberA, numberB);
    
        // Test Divide function
        var divideResult = myMathFuncs.Divide(numberA, numberB);
    
        // Output results
        Debug.WriteLine($"{numberA} + {numberB} = {addResult}");
        Debug.WriteLine($"{numberA} - {numberB} = {subtractResult}");
        Debug.WriteLine($"{numberA} * {numberB} = {multiplyResult}");
        Debug.WriteLine($"{numberA} / {numberB} = {divideResult}");
    }
    
  8. Por fim, chame TestMathFuncs no final do OnAppearing método:

    TestMathFuncs();
    
  9. Execute o aplicativo em cada plataforma de destino e valide a saída no Painel de Saída do Aplicativo que aparece da seguinte maneira:

    1 + 2 = 3
    1 - 2 = -1
    1 * 2 = 2
    1 / 2 = 0.5
    

    Observação

    Se você encontrar uma 'DLLNotFoundException' ao testar no Android ou um erro de build no iOS, certifique-se de marcar que a arquitetura de CPU do dispositivo/emulador/simulador que você está usando seja compatível com o subconjunto que escolhemos dar suporte.

Resumo

Este artigo explicou como criar um aplicativo Xamarin.Forms que usa bibliotecas nativas por meio de um wrapper .NET comum distribuído por meio de um pacote NuGet. O exemplo fornecido neste passo a passo é intencionalmente muito simplista para demonstrar mais facilmente a abordagem. Um aplicativo real terá que lidar com complexidades como tratamento de exceção, retornos de chamada, marshalling de tipos mais complexos e vinculação com outras bibliotecas de dependência. Uma consideração importante é o processo pelo qual a evolução do código C++ é coordenada e sincronizada com o wrapper e os aplicativos cliente. Esse processo pode variar dependendo se uma ou ambas as preocupações são de responsabilidade de uma única equipe. De qualquer forma, a automação é um benefício real. Abaixo estão alguns recursos que fornecem mais leitura sobre alguns dos principais conceitos, juntamente com os downloads relevantes.

Downloads

Exemplos

Leitura Adicional

Artigos relacionados ao conteúdo desta postagem