Xamarin Binding Libraries

Quando parliamo di Xamarin, incontriamo spesso le parole wrapping o wrapper, quasi sempre in accostamento alle librerie di Mono Droid e Mono Touch, le quali a loro volta sono, come sappiamo, dei wrapper di quelle native di Android e IOS. Ma come funziona tutto ciò? E se avessimo bisogno di funzionalità o librerie non ancora “wrappate” in Xamarin, come ci dovremmo comportare? Questo articolo affronta in modo semplice l’approccio da tenere ed i passi da eseguire se ci troviamo davanti alla necessità di creare una Binding Library (da qui in avanti anche BL).

Una BL non è altro che una DLL che, attraverso l’uso di chiamate managed, incapsula i comportamenti di librerie native Java o Objective-C/Swift, tramite meccanismi di binding propri di Xamarin Platform. Il meccanismo di binding è più o meno simile per entrambe le piattaforme, ma, vista la sostanziale differenza fra i due runtime nativi, daremo un’occhiata ad ambo le parti.

Un’importante premessa da fare è che il processo di creazione di una Binding Library è tutt’altro che banale, in quanto dipende strettamente dai compilati nativi dei progetti originari, non sempre facilmente ottenibili oppure non sufficienti a coprire tutte le dipendenze richieste. È dunque un’attività da intraprendere solo in quelle occasioni cui non sia sufficiente l’ormai ben fornito parco di librerie cross-platform di Xamarin Platform e Xamarin.Forms.

Binding Library in Xamarin Android

Ovviamente, cercando di rendere managed delle chiamate native Android, dovremo trovare il modo di far interagire C# con Java. Per cominciare, Xamarin.Android implementa il binding tramite un meccanismo detto Managed Called Wrapper (MCW), il quale di base è un bridge alla JNI (Java Native Interface). In questo modo una chiamata managed farà uso del MCW che, attraverso JNI, eseguirà una chiamata nativa a Java. Per completezza accenniamo anche al fatto che esiste la possibilità di eseguire il processo appena descritto, ma inverso: Java, ovvero a monte il runtime di Android (ART) può eseguire chiamate a codice nativo tramite un altro bridge alla JNI, Android Callable Wrapper (ACW); la documentazione Microsoft delle BI di Android esplica intuitivamente i due processi tramite la seguente figura, in cui possiamo vedere da un lato l’interazione fra il .NET Framework e Java, tramite le Binding Library , mentre dall’altro le chiamate managed di ART.

Figura 1 – Interazione managed/nativo

Come anticipato, Xamarin.Android ha il suo meccanismo di binding MCW ed offre un template di BL da implementare, che andrà infine a creare la nostra libreria gestita. Per raggiungere il nostro scopo abbiamo bisogno dei compilati della libreria per cui fare il binding; è inoltre necessario porci qualche domanda:

  1. La libreria Android ha delle dipendenze da altre librerie?
  2. Quale Android API ha come target la libreria Android?
  3. Con quale JDK è stata compilata?
  4. Se l’abbiamo compilata noi, l’abbiamo compilata bene?

Domande 1 e 4

La prima e la quarta domanda sono in realtà piuttosto simili: non è raro che repository android offrano già la loro libreria compilata; questo ci facilita molto la vita, in quanto ci permette di sfruttare direttamente il compilato senza barcamenarci in goffi tentativi di building tramite Android Studio. Se così non fosse, rendendoci ancora più goffi di quel che già siamo, possiamo clonare il repository di interesse, aprirlo in Android Studio e cominciare l’epopea di building in Java fino ad ottenere un file .jar/.aar, dipendentemente dal tipo di libreria che stiamo compilando.

“The main difference between a Jar and an AAR is that AARs include resources such as layouts, drawables etc. This makes it a lot easier to create self-contained visual components. For example, if you have multiple apps that use the same login screen, with Jars you could share classes but not the layout, styles, etc., you still had to duplicate them. With AARs everything is bundled in one neat package.”

Android Archive Library (aar) vs standard jar – https://stackoverflow.com/questions/23915619/android-archive-library-aar-vs-standard-jar

Di seguito possiamo vedere un tentativo, apparentemente andato a buon fine su Android Studio, della build della libreria ElasticDownload. Come si può vedere l’IDE genera un file .aar nella cartella “outputs”. Ritengo che Android Studio sia sufficientemente parlante in fatto di errori di compilazione, dato che aiuta lo sviluppatore nelle azioni da intraprendere proponendo anche possibili soluzioni di pacchetti mancanti, pacchetti non aggiornati, sintassi deprecata in configurazione, etc.

Figura 2 – Build libreria Android in Android Studio

In Figura 2 affrontiamo anche la prima domanda da porci, infatti possiamo vedere che nel file build.gradle (una sorta di file di configurazione del progetto) esistono delle dipendenze da altre librerie e di conseguenza devono essere ottenuti i compilati – sebbene non serva gestire le librerie di com.android.* (in quanto targettizzate automaticamente da Xamarin in fase di binding). Inoltre possiamo considerare questo check come ricorsivo, dato che se la libreria di dipendenza ha a sua volta altre dipendenze diverse da Android, anche esse andranno gestite; questo procedimento è chiaramente oneroso e non ci assicura di ottenere tutti i compilati necessari.

Supponendo di riuscire a trovare tutti i compilati, possiamo passare alle due domande successive.

Domanda 2: Quale Android API ha come target la libreria Android?

Il target delle Android API è molto importante, non solo per la possibile variazione delle firme dei metodi e delle interfacce, ma anche per un discorso di retro-compatibilità tramite le librerie di Android.Support, che vanno di pari passo con l’evoluzione delle API. Dunque si consiglia di mettere sempre, come Android Target nel progetto Binding Library, un target maggiore od uguale a quello utilizzato nel progetto nativo. Una “piccola” complicazione ci è data dalla recente politica di Google, che ha deciso di accettare sul PlayStore solo apk compilati con target framework maggiore od uguale a 26. Questo potrebbe limitarci nell’uso di certe librerie datate, a meno che non riusciamo a portarle ad un target framework coerente.

Domanda 3: Con quale JDK è stata compilata?

L’ultima informazione fondamentale richiesta è la versione del JDK con quale il compilato che possiamo trovare offerto dai vari repository è stato compilato. Se questo non dovesse andare bene ai nostri scopi, possiamo sempre tentare con la risposta alla domanda 4, data in precedenza. Una volta ottenute tutte le informazioni, possiamo creare la nostra Binding Library tramite template apposito di Xamarin in Visual Studio o simili. In questo esempio vedremo delle immagini di Rider.

Creiamo dunque una nuova solution con template BindingsLibrary con Platform Android e Target Api desiderato.

Figura 3 – Template progetto Binding Library

Il template creerà delle cartelle predefinite. Nella cartella “JARS” andiamo a mettere tutti i nostri compilati Java ottenuta dall’analisi fatta precedentemente. Da notare che, nel caso in cui la libreria madre per cui fare il binding sia sotto forma di .aar, questo sarà l’unico file di quel formato a poter essere inserito nella cartella. Se avessimo la necessità di inserire più file .aar, occorrerebbe creare una BL per ogni .aar e referenziarle.

Figura 4 – Struttura progetto Binding Library Android

A questo punto dovremo scegliere il tipo di build output per i nostri compilati; vi elenco alcuni dei più interessanti e riporto la fonte dell’elenco completo (Build outputs – https://docs.microsoft.com/it-it/xamarin/android/platform/binding-java-library/#build-actions)

  • EmbeddedJar – Incorpora il .jar nella BL. Questa è build action più semplice e più comunemente usata. Utilizziamo questa opzione quando desideriamo che il .jar sia compilato automaticamente in byte code e confezionato nella Bindings Library;
  • InputJar – Non incorpora il .jar nella risultante BL, la quale avrà una dipendenza da questo .jar durante l’esecuzione. Utilizziamo questa opzione quando non desideriamo includere il .jar nella Bindings Library (ad esempio, per motivi di licenza). Se utilizziamo questa opzione, è necessario assicurarsi che il .jar sia disponibile sul dispositivo che esegue l’applicazione;
  • LibraryProjectZip – Incorpora un file .aar nella BL. È simile a EmbeddedJar, ad eccezione del fatto che è possibile accedere alle risorse (così come al codice) nel file .aar collegato. Utilizziamo questa opzione quando desideriamo incorporare un file .aar nella libreria di rilegature.
Figura 5 – Build action

Ora compiliamo il nostro progetto ed accertiamoci che non ci siano errori. Questo genererà una DLL che potremo far referenziare dal nostro progetto Xamarin.Android.

Figura 6 – Messaggi di warning dopo compilazione

Per fare ciò aggiungiamo tale progetto alla nostra solution e referenziamo la Binding Library, in modo da poterla usare nel codice.

Figura 7 – dipendenza BL

Come potete veder dall’immagine seguente, la libreria è stata correttamente importata ed è pronta per essere utilizzata nella nostra applicazione. Tuttavia è necessario dire che una Binding Library è stata correttamente generata se, superata la fase di compilazione del progetto Xamarin.Android, si renda stabile anche a runtime. Se così non fosse, occorrerebbe seguire un processo di troubleshooting alquanto oneroso, analizzando il tipo di errore ed andando ad utilizzare meccanismi di decorazione del processo di mapping delle chiamate native su C#.

Nell’esempio di ElasticDownload, mi sono trovato a dover risolvere un problema che generava il seguente messaggio di errore già in fase di building: “Class does not implement interface method”, nonostante il processo di binding avesse correttamente generato il codice delle classi che implementano l’interfaccia. Questo è un problema noto di scostamento tra i linguaggi C# e Java, in particolare riguardante la covarianza dei tipi di ritorno. Per risolvere il problema esistono due modi:

1. Scrivere una classe partial che integri la classe che implementa l’interfaccia e che da problemi, andando ad esplicitarne il metodo;

namespace Dummy.Namespace {
     partial class DummyClass {
         Java.Lang.Object DummyMethod() {
             return DummyMethod();
         }
     }
 } 

2. Far uso del file Metadata.xml contenuto nella cartella Transform, generata dallo scaffolding del template BL, rimuovendo la covarianza dal codice c# generato

<attr   path="/api/package[@name=dummy.namespace]/class[@name='DummyCLass']/method[@name=dummym ethod]"     name="managedReturn">Java.Lang.Object </attr> 

Altre risoluzioni di errori noti sono descritte su documentazione Microsoft (Troubleshoot Bindings – https://docs.microsoft.com/it-it/xamarin/android/platform/binding-java-library/troubleshooting-bindings).

Risolti dunque gli errori sia in fase di building che in fase di runtime, possiamo far uso della BI implementata nel nostro progetto Xamarin.Android e vederne il risultato.

Di seguito un’immagine che mostra l’utilizzo della Binding Library nel file axml e nella relativa activity, con allegata la schermata dell’applicazione di esempio risultante.

Figura 8 – Codice managed Xamarin.Android

BL in Xamarin.iOS

Il processo di creazione di una Binding Library per iOS è similare a quello per Android: dovremo generare una classe managed che andrà a mappare, tramite l’utilizzo di specifici attributi, le api native in Objective-C/Swift di interesse, compilarle per produrre la dll tramite template di BL di Xamarin ed associarle al nostro progetto Xamarin.iOS, il tutto condito con un pizzico di amaro Xcode.

La prima fase costa nel generare il file APIDefinition, che, tramite l’utilizzo di alcune risorse, va a generare la nostra dll. In particolare ha bisogno di:

  • File “StructsAndEnums” fornito dal template di BL, che può includere ogni enum o tipo usato dal codice nativo;
  • File Extra.cs, che può includere altre risorse di supporto ed estensione della BL generata;
  • Libreria Nativa iOS.

Il tutto è illustrato nello schema seguente (Figura 9).

Figura 9 – Informazioni per il Binding iOS

Per generare manualmente il file APIDefinition dovremmo utilizzare, come accennato in precedenza, degli attributi particolari, per esempio [Register] ed [Export]. Il primo viene utilizzato per cambiare il nome del tipo che Xamarin.iOS collega al relativo tipo di Objective-C, dato che quest’ultimo non supporta i namespace, concetto di cui abbiamo bisogno in C#. Il secondo viene utilizzato per registrare il costruttore della classe C# e richiamarlo su Objective-C tramite il nome di un parametro specifico, che dovrà essere definito nella documentazione di Objective-C tramite creazione di un’istanza di ObjCRuntime.Selector per ogni parametro che si vuole utilizzare. Infine occorrerà scrivere anche un metodo per ogni selector definito in documentazione, affiancandoli al costruttore.

Come è intuibile, il suddetto processo è abbastanza oneroso. Per fortuna esiste un tool che ci può semplificare notevolmente la vita, andando a scrivere automaticamente il file ApiDefinition. Ovviamente i meccanismi dietro questo tool, chiamato “Objective-Sharpie” sono abbastanza complicati, tanto che la documentazione Microsoft afferma:
Objective Sharpie is a tool for experienced Xamarin developers with advanced knowledge of Objective-C (and by extension, C). Before attempting to bind an Objective-C library you should have solid knowledge of how to build the native library on the command line (and a good understanding of how the native library works).” (Objective Sharpie Microsoft doc: https://docs.microsoft.com/it-it/xamarin/cross-platform/macios/binding/objectivesharpie/index ), dunque è da usare a nostro rischio e pericolo.

A questo punto, dopo aver installato Objective-Sharpie, ci occorre, come di consueto, il compilato del progetto iOS. Nel nostro caso scarichiamo la solution del progetto di esempio “Linear Progress Bar” (Linear Progress Bar – https://github.com/PhilippeBoisney/LinearProgressBar ), la apriamo tramite l’IDE XCode e cerchiamo di compilarla.

Figura 10 – Libreria importata in XCode

Una volta compilata è possibile utilizzare Objective-Sharpie come command line tool per generare i file necessari al binding, ovvero ApiDefinition.cs and StructsAndEnums.cs.
NB: per poter completare il processo di building di Objective-Sharpie è necessario disporre di un account developer iOS e di un provisioning profile associato ad un device. Il tool può elaborare file di progetto XCode oppure file di tipo .framework. Nel nostro caso, avendo fatto una build del progetto in XCode, ci basta identificare quale sdk abbiamo disponibili sulla macchina e proseguire. Per fare ciò usiamo il comando sharpie xcode -sdks.

Figura 11 – sdks disponibili

Vediamo che è disponibile l’sdk iphoneos12.2, dunque lo andremo ad utilizzare con target framework chiamando il comando “bind”. Se tutto andrà a buon fine, il comando genererà i file necessari al binding, a cui dovremo allegare il file di dipendenza nativa con estensione “.a”.

Figura 12 – Comando bind

Se la procedura descritta sopra non dovesse essere fattibile, Objective-Sharpie offre un comando per interfacciarsi con Cocoapods, un gestore di dipendenze che permette di integrare velocemente in XCode librerie Objective-C e Swift. Il suddetto comando legge la libreria direttamente dal repository di Cocoapods e genera le classi necessarie al binding, sempre che la libreria di interesse sia disponibile nel repository. Prendiamo in considerazione la libreria CircleProgressBar.

Figura 13 – Objective-Sharpie Cocoapods integration

Vediamo come Objective-Sharpie ci avverta riguardo agli warning che ha identificato, generando i file necessari al binding. In particolare, nei metodi definiti dalle conversioni che generano warning, Objective-Sharpie andrà a decorarli con l’attributo [Verify], che non permetterà la compilazione del progetto fino a quando non avremo risolto il problema.

Vediamo nell’immagine seguente tutti i file e le cartelle generate dal comando Bind dell’integrazione di ObjectiveSharpie con CocoaPods.

Figura 14 – Files generati bind command

Una volta generate le classi che ci servono, possiamo aggiungere al progetto di Binding Library di Xamarin.iOS, buildarlo e come fatto per Xamarin.Android, referenziarlo in un progetto ed usarlo.

Figura 15 – Struttura progetto Binding Library iOS

Conclusione

Abbiamo potuto toccare con mano la complessità dei processi di creazione di librerie managed a partire dalle native; un’operazione da fare se abbiamo la necessità di customizzare le nostre App con viste o comportamenti particolari e siamo abbastanza fortunati da trovare delle soluzioni native da gestire (ed altrettanto sfortunati per cui siamo i primi a doverlo fare). Molte delle librerie managed create con il meccanismo di BL si trovano su github, quindi è sempre bene dare un’occhiata prima lì.

Detto questo, nel caso in cui volessimo implementare un comportamento od una vista simile sia per Android che per iOS, potremmo compiere il passo successivo, portando tutto a comune tramite un Custom Renderer su Xamarin.Forms, in modo da aumentare la riusabilità del codice, ma questa è un’altra storia…

Un commento

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *