Come utilizzare lo stesso codice C ++ per Android e iOS?

Android con NDK supporta il codice C / C ++ e iOS con Objective-C ++ ha anche il supporto, quindi come posso scrivere applicazioni con codice C / C ++ nativo condiviso tra Android e iOS?

Aggiornare.

Questa risposta è abbastanza popolare anche quattro anni dopo averlo scritto, in questi quattro anni sono cambiate molte cose, quindi ho deciso di aggiornare la mia risposta per adattarla meglio alla nostra attuale realtà. L’idea di risposta non cambia; l’implementazione è cambiata un po ‘. Anche il mio inglese è cambiato, è migliorato molto, quindi la risposta è più comprensibile a tutti ora.

Si prega di dare un’occhiata al repository in modo da poter scaricare ed eseguire il codice che mostrerò di seguito.

La risposta

Prima di mostrare il codice, si prega di prendere molto sul diagramma seguente.

Arco

Ogni sistema operativo ha la sua interfaccia utente e le sue peculiarità, quindi intendiamo scrivere codice specifico per ciascuna piattaforma a tale riguardo. In altre mani, tutto il codice logico, le regole aziendali e le cose che possono essere condivise intendiamo scrivere usando C ++, così possiamo compilare lo stesso codice su ciascuna piattaforma.

Nel diagramma, puoi vedere il livello C ++ al livello più basso. Tutto il codice condiviso è in questo segmento. Il livello più alto è il normale codice Obj-C / Java / Kotlin, nessuna novità qui, la parte difficile è il livello intermedio.

Lo strato intermedio sul lato iOS è semplice; hai solo bisogno di configurare il tuo progetto per build usando una variante di Obj-c conosciuta come Objective-C ++ ed è tutto, hai accesso al codice C ++.

La cosa è diventata più difficile sul lato Android, entrambe le lingue, Java e Kotlin, su Android, funzionano con una Java Virtual Machine. Quindi l’unico modo per accedere al codice C ++ è l’utilizzo di JNI , si prega di prendere tempo per leggere le basi di JNI. Fortunatamente, l’IDE Android Studio di oggi ha notevoli miglioramenti sul lato JNI e molti problemi ti vengono mostrati mentre modifichi il tuo codice.

Il codice a passi

Il nostro esempio è una semplice app che invii un testo a CPP e converte quel testo in qualcos’altro e lo restituisce. L’idea è che iOS invierà “Obj-C” e Android invierà “Java” dalle loro rispettive lingue e il codice CPP creerà un testo come segue “cpp dice ciao a << testo ricevuto >> “.

Codice CPP condiviso

Prima di tutto, creeremo il codice CPP condiviso, facendolo abbiamo un semplice file di intestazione con la dichiarazione del metodo che riceve il testo desiderato:

#include  const char *concatenateMyStringWithCppString(const char *myString); 

E l’implementazione del CPP:

 #include  #include "Core.h" const char *CPP_BASE_STRING = "cpp says hello to %s"; const char *concatenateMyStringWithCppString(const char *myString) { char *concatenatedString = new char[strlen(CPP_BASE_STRING) + strlen(myString)]; sprintf(concatenatedString, CPP_BASE_STRING, myString); return concatenatedString; } 

Unix

Un bonus interessante è che possiamo anche usare lo stesso codice per Linux e Mac così come per altri sistemi Unix. Questa possibilità è particolarmente utile perché possiamo testare il nostro codice condiviso più velocemente, quindi creeremo un Main.cpp come segue per eseguirlo dalla nostra macchina e vedere se il codice condiviso funziona.

 #include  #include  #include "../CPP/Core.h" int main() { std::string textFromCppCore = concatenateMyStringWithCppString("Unix"); std::cout << textFromCppCore << '\n'; return 0; } 

Per creare il codice, devi eseguire:

 $ g++ Main.cpp Core.cpp -o main $ ./main cpp says hello to Unix 

iOS

È ora di implementare sul lato mobile. Per quanto riguarda iOS, abbiamo un'integrazione semplice che stiamo iniziando. La nostra app per iOS è una tipica app Obj-c con una sola differenza; i file sono .mm e non .m . cioè È un'app Obj-C ++, non un'app Obj-C.

Per un'organizzazione migliore, creiamo CoreWrapper.mm come segue:

 #import "CoreWrapper.h" @implementation CoreWrapper + (NSString*) concatenateMyStringWithCppString:(NSString*)myString { const char *utfString = [myString UTF8String]; const char *textFromCppCore = concatenateMyStringWithCppString(utfString); NSString *objcString = [NSString stringWithUTF8String:textFromCppCore]; return objcString; } @end 

Questa class ha la responsabilità di convertire i tipi di CPP e le chiamate a tipi e chiamate Obj-C. Non è obbligatorio una volta che è ansible chiamare il codice CPP su qualsiasi file che si desidera su Obj-C, ma aiuta a mantenere l'organizzazione, e al di fuori dei file wrapper si mantiene un codice in stile Obj-C completo, solo il file wrapper diventa in stile CPP .

Una volta che il tuo wrapper è collegato al codice CPP, puoi usarlo come un codice Obj-C standard, ad esempio ViewController "

 #import "ViewController.h" #import "CoreWrapper.h" @interface ViewController () @property (weak, nonatomic) IBOutlet UILabel *label; @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; NSString* textFromCppCore = [CoreWrapper concatenateMyStringWithCppString:@"Obj-C++"]; [_label setText:textFromCppCore]; } @end 

Guarda come appare l'app:

Xcode i phone

android

Ora è il momento per l'integrazione con Android. Android utilizza Gradle come sistema di compilazione e al codice C / C ++ utilizza CMake. Quindi la prima cosa che dobbiamo fare è configurare il CMake sul file gradle:

 android { ... externalNativeBuild { cmake { path "CMakeLists.txt" } } ... defaultConfig { externalNativeBuild { cmake { cppFlags "-std=c++14" } } ... } 

E il secondo passo è aggiungere il file CMakeLists.txt:

 cmake_minimum_required(VERSION 3.4.1) include_directories ( ../../CPP/ ) add_library( native-lib SHARED src/main/cpp/native-lib.cpp ../../CPP/Core.h ../../CPP/Core.cpp ) find_library( log-lib log ) target_link_libraries( native-lib ${log-lib} ) 

Il file CMake è dove devi aggiungere i file CPP e le cartelle di intestazione che userai sul progetto, sul nostro esempio, stiamo aggiungendo la cartella CPP e i file Core.h / .cpp. Per saperne di più sulla configurazione C / C ++ si prega di leggerlo.

Ora il codice core è parte della nostra app è il momento di creare il bridge, per rendere le cose più semplici e organizzate, creiamo una class specifica chiamata CoreWrapper per essere il nostro wrapper tra JVM e CPP:

 public class CoreWrapper { public native String concatenateMyStringWithCppString(String myString); static { System.loadLibrary("native-lib"); } } 

Nota che questa class ha un metodo native e carica una libreria native-lib chiamata native-lib . Questa libreria è quella che creiamo, alla fine, il codice CPP diventerà un object condiviso .so File incorporato nel nostro APK e loadLibrary lo caricherà. Infine, quando si chiama il metodo nativo, la JVM delegherà la chiamata alla libreria caricata.

Ora la parte più strana dell'integrazione Android è la JNI; Abbiamo bisogno di un file cpp come segue, nel nostro caso "native-lib.cpp":

 extern "C" { JNIEXPORT jstring JNICALL Java_ademar_androidioscppexample_CoreWrapper_concatenateMyStringWithCppString(JNIEnv *env, jobject /* this */, jstring myString) { const char *utfString = env->GetStringUTFChars(myString, 0); const char *textFromCppCore = concatenateMyStringWithCppString(utfString); jstring javaString = env->NewStringUTF(textFromCppCore); return javaString; } } 

La prima cosa che noterete è la extern "C" questa parte è necessaria per JNI funzionare correttamente con il nostro codice CPP e i collegamenti del metodo. Vedrai anche alcuni simboli usati da JNI per lavorare con JVM come JNIEXPORT e JNICALL . Per capire il significato di queste cose, è necessario prendersi un po 'di tempo e leggerlo , per questo scopo del tutorial è sufficiente considerare queste cose come standard.

Una cosa significativa e di solito la radice di molti problemi è il nome del metodo; deve seguire lo schema "Java_package_class_method". Attualmente, Android Studio ha un eccellente supporto per poter generare automaticamente questa piastra e mostrarti quando è corretta o non nominata. Nel nostro esempio, il nostro metodo è denominato "Java_ademar_androidioscppexample_CoreWrapper_concatenateMyStringWithCppString" perché "ademar.androidioscppexample" è il nostro pacchetto, quindi sostituiamo "." per "_", CoreWrapper è la class in cui stiamo collegando il metodo nativo e "concatenateMyStringWithCppString" è il nome del metodo stesso.

Dato che abbiamo il metodo dichiarato correttamente, è il momento di analizzare gli argomenti, il primo parametro è un puntatore di JNIEnv è il modo in cui abbiamo accesso a elementi JNI, è cruciale fare le nostre conversioni come vedremo presto. Il secondo è un jobject è l'istanza dell'object che hai usato per chiamare questo metodo. Puoi pensare come java " questo ", sul nostro esempio non abbiamo bisogno di usarlo, ma dobbiamo ancora dichiararlo. Dopo questo jobject riceveremo gli argomenti del metodo perché il nostro metodo ha solo un argomento, una stringa "myString" abbiamo solo un "jstring" con lo stesso nome. Si noti anche che il nostro tipo di ritorno è anche una stringa jstring, è perché il nostro metodo Java restituisce una stringa, per ulteriori informazioni sui tipi Java / JNI leggete per favore .

Il passaggio finale consiste nel convertire i tipi JNI nei tipi che utilizziamo sul lato CPP. Nel nostro esempio, stiamo trasformando la jstring in un const char * inviandolo convertito al CPP, ottenendo il risultato e convertendo nuovamente in jstring . Come tutti gli altri passaggi su JNI, non è difficile; è solo standardizzato, tutto il lavoro è svolto JNIEnv* che riceviamo quando chiamiamo GetStringUTFChars e NewStringUTF . Dopo che il nostro codice è pronto per essere eseguito su dispositivi Android, diamo un'occhiata.

Android Studio androide