Skip to content

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

Dopo un'ampia raccolta di informazioni siamo stati in grado di risolvere questo pantano che hanno molti dei nostri utenti. Ti diamo la soluzione e vogliamo servirti come un grande supporto.

Soluzione:

Aggiornamento.

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

Date un'occhiata al repo in modo da poter scaricare ed eseguire il codice che mostrerò di seguito.

La risposta

Prima di mostrare il codice, si prega di osservare il seguente diagramma.

Arco

Ogni sistema operativo ha la sua interfaccia utente e le sue peculiarità, quindi intendiamo scrivere codice specifico per ogni piattaforma. In altre parole, tutto il codice logico, le regole di business e le cose che possono essere condivise intendiamo scriverle usando il C++, in modo da poter compilare lo stesso codice per ogni piattaforma.

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

Il livello intermedio del lato iOS è semplicee; basta configurare il progetto in modo che venga compilato utilizzando una variante di Obj-C nota come Objective-C++ ed è tutto, avete accesso al codice C++.

La cosa si fa più difficile sul lato Android: entrambi i linguaggi, Java e Kotlin, su Android girano sotto una Java Virtual Machine. Quindi l'unico modo per accedere al codice C++ è utilizzare JNI; prendetevi del tempo per leggere le basi di JNI. Fortunatamente, l'IDE Android Studio di oggi ha apportato notevoli miglioramenti sul lato JNI e molti problemi vengono mostrati mentre si modifica il codice.

Il codice per passi

Il nostro esempio è una semplice applicazione che invia un testo al CPP, il quale converte il testo in qualcos'altro e lo restituisce. L'idea è che iOS invierà "Obj-C" e Android invierà "Java" dai rispettivi linguaggi, e il codice CPP creerà un testo come "cpp says hello to". < text received >".

Codice CPP condiviso

Prima di tutto, creeremo il codice CPP condiviso, per farlo 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 vantaggio interessante è che possiamo usare lo stesso codice anche per Linux e Mac, oltre che 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 costruire il codice, è necessario eseguire:

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

iOS

È il momento di implementare il lato mobile. Poiché iOS ha un'integrazione semplice, cominciamo con esso. La nostra applicazione iOS è una tipica applicazione Obj-c con un solo difference; i file sono .mm e non .m. cioè è un'applicazione Obj-C++, non un'applicazione Obj-C.

Per una migliore organizzazione, creiamo il 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 classe ha la responsabilità di convertire i tipi e le chiamate CPP in tipi e chiamate Obj-C. Non è obbligatorio, perché si può chiamare il codice CPP su qualsiasi file Obj-C, ma aiuta a mantenere l'organizzazione e, al di fuori dei file wrapper, a mantenere un codice completo in stile Obj-C; solo i file wrapper diventano in stile CPP.

Una volta che il wrapper è collegato al codice CPP, è possibile utilizzarlo 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

Date un'occhiata all'aspetto dell'applicazione:

Xcodei phone

Android

Ora è il momento dell'integrazione con Android. Android usa Gradle come sistema di compilazione e per il codice C/C++ usa CMake. Quindi la prima cosa da fare è configurare il file CMake su gradle:

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

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 è il punto in cui è necessario aggiungere i file CPP e le cartelle header che si utilizzeranno nel progetto; nel nostro esempio, stiamo aggiungendo il file CPP e i file Core.h/.cpp. Per saperne di più sulla configurazione di C/C++, leggete qui.

Ora che il codice centrale fa parte della nostra applicazione, è il momento di creare il bridge; per rendere le cose più semplici e organizzate, creiamo una classe specifica, denominata CoreWrapper, che funga da wrapper tra JVM e CPP:

public class CoreWrapper {

    public native String concatenateMyStringWithCppString(String myString);

    static {
        System.loadLibrary("native-lib");
    }

}

Si noti che questa classe ha una classe native e carica una libreria nativa chiamata native-lib. Questa libreria è quella che creiamo, alla fine, il codice CPP diventerà un oggetto condiviso .so incorporato nel nostro APK e il file 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 di Android è 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 si noterà è la dicitura extern "C" questa parte è necessaria per far funzionare correttamente JNI con il nostro codice CPP e i collegamenti dei metodi. Si vedranno anche alcuni simboli che JNI utilizza per lavorare con la JVM, come ad esempio JNIEXPORT e JNICALL. Per capire il significato di questi simboli, è necessario prendersi un po' di tempo e leggerli; per gli scopi di questo tutorial, considerateli come boilerplate.

Una cosa significativa e di solito all'origine di molti problemi è il nome del metod; deve seguire lo schema "Java_package_class_method". Attualmente, Android studio dispone di un ottimo supporto, per cui è in grado di generare automaticamente questo boilerplate e di mostrare quando il nome è corretto o meno. Nel nostro esempio il metodo si chiama "Java_ademar_androidioscppexample_CoreWrapper_concatenateMyStringWithCppString" perché "ademar.androidioscppexample" è il nostro pacchetto, quindi sostituiamo il "." con "_", CoreWrapper è la classe in cui stiamo collegando il metodo nativo e "concatenateMyStringWithCppString" è il nome del metodo stesso.

Avendo dichiarato correttamente il metodo, è il momento di analizzare gli argomenti, il primo parametro è un puntatore di JNIEnv è il modo in cui abbiamo accesso alle cose di JNI, è cruciale per effettuare le nostre conversioni, come si vedrà tra poco. Il secondo è un parametro jobject è l'istanza dell'oggetto utilizzato per chiamare questo metodo. Si può pensare che sia come il metodo java "questo"Nel nostro esempio non abbiamo bisogno di usarlo, ma dobbiamo comunque dichiararlo. Dopo questo progetto, riceveremo gli argomenti del metodo. Poiché il nostro metodo ha un solo argomento, una stringa "myString", abbiamo solo una "jstring" con lo stesso nome. Si noti anche che il nostro tipo di ritorno è anch'esso una jstring. Questo perché il nostro metodo Java restituisce una stringa; per maggiori informazioni sui tipi Java/JNI, leggete qui.

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

Android StudioAndroid

L'approccio descritto nell'eccellente risposta precedente può essere completamente automatizzato da Scapix Language Bridge, che genera codice wrapper al volo direttamente dalle intestazioni di C++. Ecco un esempio:

Definire la classe in C++:

#include 

class contact : public scapix::bridge::object
{
public:
    std::string name();
    void send_message(const std::string& msg, std::shared_ptr from);
    void add_tags(const std::vector& tags);
    void add_friends(std::vector> friends);
};

E chiamarla da Swift:

class ViewController: UIViewController {
    func send(friend: Contact) {
        let c = Contact()

        contact.sendMessage("Hello", friend)
        contact.addTags(["a","b","c"])
        contact.addFriends([friend])
    }
}

E da Java:

class View {
    private contact = new Contact;

    public void send(Contact friend) {
        contact.sendMessage("Hello", friend);
        contact.addTags({"a","b","c"});
        contact.addFriends({friend});
    }
}



Utilizzate il nostro motore di ricerca

Ricerca
Generic filters

Lascia un commento

Il tuo indirizzo email non sarà pubblicato.