Comment utiliser le même code C ++ pour Android et iOS?

119

Android avec NDK prend en charge le code C / C ++ et iOS avec Objective-C ++ prend également en charge, alors comment puis-je écrire des applications avec du code C / C ++ natif partagé entre Android et iOS?

ademar111190
la source
1
essayez cocos2d-x framework
glo
@glo ça a l'air bien, mais je cherche un truc plus générique, en utilisant le c ++ sans framework, "exclu JNI, évidemment".
ademar111190

Réponses:

273

Mettre à jour.

Cette réponse est assez populaire même quatre ans après que je l'ai écrite, au cours de ces quatre années, beaucoup de choses ont changé, j'ai donc décidé de mettre à jour ma réponse pour mieux correspondre à notre réalité actuelle. L'idée de réponse ne change pas; la mise en œuvre a un peu changé. Mon anglais a également changé, il s'est beaucoup amélioré, donc la réponse est plus compréhensible pour tout le monde maintenant.

Veuillez jeter un œil au dépôt afin de pouvoir télécharger et exécuter le code que je montrerai ci-dessous.

La réponse

Avant d'afficher le code, veuillez en prendre beaucoup sur le diagramme suivant.

Cambre

Chaque OS a son interface utilisateur et ses particularités, nous avons donc l'intention d'écrire un code spécifique à chaque plate-forme à cet égard. Dans d'autres mains, nous avons l'intention d'écrire tout le code logique, les règles métier et les choses qui peuvent être partagées en C ++, afin de pouvoir compiler le même code sur chaque plate-forme.

Dans le diagramme, vous pouvez voir la couche C ++ au niveau le plus bas. Tout le code partagé est dans ce segment. Le niveau le plus élevé est le code Obj-C / Java / Kotlin régulier, pas de nouvelles ici, la partie difficile est la couche intermédiaire.

La couche intermédiaire côté iOS est simple; il vous suffit de configurer votre projet pour construire en utilisant une variante d'Obj-c appelée Objective-C ++ et c'est tout, vous avez accès au code C ++.

La chose est devenue plus difficile du côté d'Android, les deux langages, Java et Kotlin, sous Android, fonctionnent sous une machine virtuelle Java. Donc, le seul moyen d'accéder au code C ++ est d'utiliser JNI , prenez le temps de lire les bases de JNI. Heureusement, l'EDI Android Studio actuel présente de vastes améliorations du côté JNI, et de nombreux problèmes vous sont signalés lorsque vous modifiez votre code.

Le code par étapes

Notre exemple est une application simple qui vous permet d'envoyer un texte au CPP, qui convertit ce texte en autre chose et le renvoie. L'idée est, iOS enverra "Obj-C" et Android enverra "Java" à partir de leurs langues respectives, et le code CPP créera un texte comme suit "cpp dit bonjour à << texte reçu >> ".

Code CPP partagé

Tout d'abord, nous allons créer le code CPP partagé, ce faisant, nous avons un simple fichier d'en-tête avec la déclaration de méthode qui reçoit le texte souhaité:

#include <iostream>

const char *concatenateMyStringWithCppString(const char *myString);

Et la mise en œuvre du RPC:

#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 intéressant est que nous pouvons également utiliser le même code pour Linux et Mac ainsi que pour d'autres systèmes Unix. Cette possibilité est particulièrement utile car nous pouvons tester notre code partagé plus rapidement, nous allons donc créer un Main.cpp comme suit pour l'exécuter depuis notre machine et voir si le code partagé fonctionne.

#include <iostream>
#include <string>
#include "../CPP/Core.h"

int main() {
  std::string textFromCppCore = concatenateMyStringWithCppString("Unix");
  std::cout << textFromCppCore << '\n';
  return 0;
}

Pour créer le code, vous devez exécuter:

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

iOS

Il est temps de mettre en œuvre du côté mobile. Dans la mesure où iOS a une intégration simple, nous commençons par elle. Notre application iOS est une application Obj-c typique avec une seule différence; les fichiers sont .mmet non .m. c'est-à-dire qu'il s'agit d'une application Obj-C ++, pas d'une application Obj-C.

Pour une meilleure organisation, nous créons le CoreWrapper.mm comme suit:

#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

Cette classe a la responsabilité de convertir les types et les appels CPP en types et appels Obj-C. Ce n'est pas obligatoire une fois que vous pouvez appeler le code CPP sur n'importe quel fichier que vous voulez sur Obj-C, mais cela aide à conserver l'organisation, et en dehors de vos fichiers wrapper, vous maintenez un code de style Obj-C complet, seul le fichier des wrappers devient CPP style .

Une fois votre wrapper connecté au code CPP, vous pouvez l'utiliser comme code Obj-C standard, par exemple 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

Jetez un œil à l'apparence de l'application:

Xcode iPhone

Android

Il est maintenant temps pour l'intégration Android. Android utilise Gradle comme système de construction et pour le code C / C ++, il utilise CMake. Donc, la première chose à faire est de configurer le CMake sur le fichier gradle:

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

Et la deuxième étape consiste à ajouter le fichier 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}
)

Le fichier CMake est l'endroit où vous devez ajouter les fichiers CPP et les dossiers d'en-tête que vous utiliserez sur le projet, dans notre exemple, nous ajoutons le CPPdossier et les fichiers Core.h / .cpp. Pour en savoir plus sur la configuration C / C ++, veuillez le lire.

Maintenant, le code principal fait partie de notre application, il est temps de créer le pont, pour rendre les choses plus simples et organisées, nous créons une classe spécifique nommée CoreWrapper pour être notre wrapper entre JVM et CPP:

public class CoreWrapper {

    public native String concatenateMyStringWithCppString(String myString);

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

}

Notez que cette classe a une nativeméthode et charge une bibliothèque native nommée native-lib. Cette bibliothèque est celle que nous créons, à la fin, le code CPP deviendra un objet partagé .soFichier intégré dans notre APK, et le loadLibrarychargera. Enfin, lorsque vous appelez la méthode native, la JVM déléguera l'appel à la bibliothèque chargée.

Maintenant, la partie la plus étrange de l'intégration Android est le JNI; Nous avons besoin d'un fichier cpp comme suit, dans notre cas "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 première chose que vous remarquerez est que extern "C"cette partie est nécessaire pour que JNI fonctionne correctement avec notre code CPP et nos liens de méthode. Vous verrez également quelques symboles que JNI utilise pour fonctionner avec JVM comme JNIEXPORTet JNICALL. Pour que vous compreniez le sens de ces choses, il est nécessaire de prendre un temps et de le lire , pour les besoins de ce tutoriel, considérez simplement ces choses comme passe-partout.

Une chose importante et généralement à l'origine de nombreux problèmes est le nom de la méthode; il doit suivre le modèle "Java_package_class_method". Actuellement, le studio Android a un excellent support pour cela afin qu'il puisse générer automatiquement ce passe-partout et vous montrer quand il est correct ou non nommé. Dans notre exemple, notre méthode s'appelle "Java_ademar_androidioscppexample_CoreWrapper_concatenateMyStringWithCppString" c'est parce que "ademar.androidioscppexample" est notre package, nous remplaçons donc le "." par "_", CoreWrapper est la classe où nous lions la méthode native et "concatenateMyStringWithCppString" est le nom de la méthode lui-même.

Comme nous avons correctement déclaré la méthode, il est temps d'analyser les arguments, le premier paramètre est un pointeur de JNIEnvcelui-ci est la façon dont nous avons accès aux trucs JNI, il est crucial de faire nos conversions comme vous le verrez bientôt. Le second est un jobjectc'est l'instance de l'objet que vous aviez utilisé pour appeler cette méthode. Vous pouvez le penser comme le java " this ", dans notre exemple, nous n'avons pas besoin de l'utiliser, mais nous devons toujours le déclarer. Après ce jobject, nous allons recevoir les arguments de la méthode. Parce que notre méthode n'a qu'un seul argument - une chaîne "myString", nous n'avons qu'une "jstring" avec le même nom. Notez également que notre type de retour est également un jstring. C'est parce que notre méthode Java renvoie une chaîne, pour plus d'informations sur les types Java / JNI, veuillez la lire.

La dernière étape consiste à convertir les types JNI en types que nous utilisons côté CPP. Dans notre exemple, nous transformons le jstringen un const char *envoi converti en CPP, obtenons le résultat et reconvertissons en jstring. Comme toutes les autres étapes sur JNI, ce n'est pas difficile; il n'est que passe-partout, tout le travail est fait par l' JNIEnv*argument que nous recevons lorsque nous appelons le GetStringUTFCharset NewStringUTF. Après cela, notre code est prêt à fonctionner sur les appareils Android, jetons un coup d'œil.

AndroidStudio Android

ademar111190
la source
7
Grande explication
RED.Skull
9
Je ne comprends pas - mais +1 pour l'une des réponses de la plus haute qualité sur SO
Michael Rodrigues
16
@ ademar111190 De loin le message le plus utile. Cela n'aurait pas dû être fermé.
Jared Burrows
6
@JaredBurrows, je suis d'accord. A voté pour la réouverture.
OmnipotentEntity
3
@KVISH, vous devez d'abord implémenter le wrapper dans Objective-C, puis vous accéderez au wrapper Objective-C en swift en ajoutant l'en-tête de wrapper à votre fichier d'en-tête de pontage. Aucun moyen d'accéder directement au C ++ dans Swift pour le moment. Pour plus d'informations, consultez stackoverflow.com/a/24042893/1853977
Chris
3

L'approche décrite dans l'excellente réponse ci-dessus peut être complètement automatisée par Scapix Language Bridge qui génère du code wrapper à la volée directement à partir des en-têtes C ++. Voici un exemple :

Définissez votre classe en 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);
};

Et appelez-le de Swift:

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

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

Et depuis Java:

class View {
    private contact = new Contact;

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