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


119

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?


1
prova il framework cocos2d-x
glo

@glo sembra buono, ma sto cercando una cosa più generica, usando il c ++ senza framework, "escluso JNI, ovviamente".
ademar111190

Risposte:


273

Aggiornare.

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

Dai un'occhiata al repository in modo da poter scaricare ed eseguire il codice che ti mostrerò di seguito.

La risposta

Prima di mostrare il codice, per favore prendi molto sul diagramma seguente.

Arco

Ogni OS ha la sua UI e le sue peculiarità, quindi intendiamo scrivere codice specifico per ogni piattaforma a questo proposito. In altre mani, tutto il codice logico, le regole aziendali e le cose che possono essere condivise intendiamo scrivere utilizzando C ++, in modo da poter 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 notizia qui, la parte difficile è il livello intermedio.

Il livello intermedio sul lato iOS è semplice; devi solo configurare il tuo progetto per costruire 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, entrambi i linguaggi, Java e Kotlin, su Android, funzionano con una Java Virtual Machine. Quindi l'unico modo per accedere al codice C ++ è usare JNI , per favore prenditi del tempo per leggere le basi di JNI. Fortunatamente, l'IDE di Android Studio di oggi ha enormi 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 rispettive lingue, e il codice CPP creerà un testo come seguito "cpp dice ciao a << testo ricevuto >> ".

Codice CPP condiviso

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

#include <iostream>

const char *concatenateMyStringWithCppString(const char *myString);

E l'implementazione del CPP:

#include <string.h>
#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 <iostream>
#include <string>
#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 iOS abbia una semplice integrazione, stiamo iniziando da essa. La nostra app iOS è una tipica app Obj-c con una sola differenza; i file sono .mme non .m. ovvero è un'app Obj-C ++, non un'app Obj-C.

Per una migliore organizzazione, 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 classe ha la responsabilità di convertire i tipi e le chiamate CPP in tipi e chiamate Obj-C. Non è obbligatorio una volta che puoi chiamare il codice CPP su qualsiasi file che desideri su Obj-C, ma aiuta a mantenere l'organizzazione e al di fuori dei tuoi file wrapper mantieni un codice completo in stile Obj-C, solo il file wrapper diventa in stile CPP .

Una volta che il tuo wrapper è connesso al codice CPP, puoi usarlo come 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

Dai un'occhiata a come appare l'app:

Xcode i phone

androide

Ora è il momento per l'integrazione con Android. Android utilizza Gradle come sistema di compilazione e per il 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 passaggio è 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 utilizzerai nel progetto, nel nostro esempio, stiamo aggiungendo la CPPcartella ei file Core.h / .cpp. Per saperne di più sulla configurazione C / C ++, leggilo.

Ora il codice di base fa parte della nostra app è ora di creare il bridge, per rendere le cose più semplici e organizzate creiamo una classe 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 classe ha un nativemetodo e carica una libreria nativa denominata native-lib. Questa libreria è quella che creiamo, alla fine, il codice CPP diventerà un .sofile oggetto condiviso incorporato nel nostro APK e lo loadLibrarycaricherà. Infine, quando si chiama il metodo nativo, la JVM delegherà la chiamata alla libreria caricata.

Ora la parte più strana dell'integrazione Android è il 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 noterai è che extern "C"questa parte è necessaria affinché JNI funzioni correttamente con il nostro codice CPP e i collegamenti ai metodi. Vedrai anche alcuni simboli che JNI usa per lavorare con JVM come JNIEXPORTe JNICALL. Per capire il significato di queste cose, è necessario prenderti un po 'di tempo e leggerlo , per gli scopi di questo tutorial considera queste cose come standard.

Una cosa significativa e di solito la radice di molti problemi è il nome del metodo; deve seguire il modello "Java_package_class_method". Attualmente, Android Studio ha un eccellente supporto per questo, quindi può generare automaticamente questo boilerplate e mostrarti quando è corretto o non è nominato. Nel nostro esempio il nostro metodo è denominato "Java_ademar_androidioscppexample_CoreWrapper_concatenateMyStringWithCppString" perché "ademar.androidioscppexample" è il nostro pacchetto, quindi sostituiamo "." da "_", CoreWrapper è la classe in cui stiamo collegando il metodo nativo e "concatenateMyStringWithCppString" è il nome del metodo stesso.

Poiché il metodo è stato dichiarato correttamente, è tempo di analizzare gli argomenti, il primo parametro è un puntatore di JNIEnvesso è il modo in cui abbiamo accesso alle cose JNI, è fondamentale per fare le nostre conversioni come vedrai presto. Il secondo è jobjectl'istanza dell'oggetto che avevate usato per chiamare questo metodo. Puoi pensarlo come il java " questo ", nel nostro esempio non abbiamo bisogno di usarlo, ma dobbiamo comunque dichiararlo. Dopo questo jobject, riceveremo gli argomenti del metodo. Poiché il nostro metodo ha un solo argomento - una stringa "myString", abbiamo solo una "jstring" con lo stesso nome. Notare inoltre che anche il nostro tipo di ritorno è un jstring. È perché il nostro metodo Java restituisce una stringa, per ulteriori informazioni sui tipi Java / JNI leggerlo.

Il passaggio finale consiste nel convertire i tipi JNI nei tipi che utilizziamo sul lato CPP. Nel nostro esempio, stiamo trasformando il jstringin un const char *invio convertito in CPP, ottenendo il risultato e convertendolo di nuovo in jstring. Come tutti gli altri passaggi su JNI, non è difficile; è solo boilerplated, tutto il lavoro è svolto JNIEnv*dall'argomento che riceviamo quando chiamiamo GetStringUTFCharse NewStringUTF. Dopo che il nostro codice è pronto per essere eseguito su dispositivi Android, diamo un'occhiata.

Android Studio androide


7
Ottima spiegazione
RED.Skull

9
Non capisco, ma +1 per una delle risposte di altissima qualità su SO
Michael Rodrigues

16
@ ademar111190 Di gran lunga il post più utile. Questo non avrebbe dovuto essere chiuso.
Jared Burrows

6
@ JaredBurrows, sono d'accordo. Votato per riaprire.
OnnipotentEntity

3
@KVISH devi prima implementare il wrapper in Objective-C, quindi accederai rapidamente al wrapper Objective-C aggiungendo l'intestazione del wrapper al tuo file di intestazione bridging. Non c'è modo di accedere direttamente a C ++ in Swift fin d'ora. Per ulteriori informazioni, vedere stackoverflow.com/a/24042893/1853977
Chris

3

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

Definisci la tua classe in C ++:

#include <scapix/bridge/object.h>

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

E chiamalo 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});
    }
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.