Android avec NDK supporte le code C/C++ et iOS avec Objective-C++ aussi, alors comment écrire des applications avec du code C/C++ natif partagé entre Android et iOS?
Cette réponse est très populaire même quatre ans après l'avoir écrite. Au cours des quatre dernières années, beaucoup de choses ont changé. J'ai donc décidé de mettre à jour ma réponse afin de 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é, alors la réponse est plus compréhensible pour tout le monde maintenant.
S'il vous plaît jeter un oeil à le repo afin que vous puissiez télécharger et exécuter le code que je vais montrer ci-dessous.
Avant que je montre le code, prenez beaucoup de choses sur le diagramme suivant.
Chaque système d'exploitation a son interface utilisateur et ses particularités. Nous avons donc l'intention d'écrire un code spécifique sur chaque plate-forme à cet égard. En d'autres termes, nous avons l'intention d'écrire en C++ pour tout le code logique, les règles commerciales et tout ce qui peut être partagé, 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 Obj-C/Java/Kotlin), pas de nouvelles ici, la partie la plus difficile est la couche intermédiaire.
La couche intermédiaire du côté iOS est simple; il vous suffit de configurer votre projet pour qu'il utilise une variante d'Obj-c connue sous le nom Objective-C++ et c'est tout, vous avez accès au code C++.
La chose est devenue plus difficile du côté Android, les deux langues, Java et Kotlin, sous Android, s'exécutent sous un Java Machine virtuelle. Le seul moyen d’accéder au code C++ est donc d’utiliser JNI , veuillez prendre le temps de lire les bases de JNI. Heureusement, le Android Studio IDE présente de grandes améliorations du côté de JNI et de nombreux problèmes s’affichent lorsque vous modifiez votre code.
Notre exemple est une application simple pour laquelle vous envoyez un texte au CPP, et convertit ce texte en quelque chose d'autre et le renvoie. L'idée est qu'iOS enverra "Obj-C" et Android enverra "Java" à partir de leurs langues respectives, et le code CPP créera un texte à la suite "cpp dit bonjour à << texte reçu >> ".
Tout d'abord, nous allons créer le code CPP partagé. Pour ce faire, 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;
}
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 fichier Main.cpp comme suit pour l'exécuter à partir de notre ordinateur 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 construire le code, vous devez exécuter:
$ g++ Main.cpp Core.cpp -o main
$ ./main
cpp says hello to Unix
Il est temps de mettre en œuvre sur le côté mobile. Dans la mesure où iOS a une intégration simple, nous commençons avec elle. Notre application iOS est une application typique d'Obj-c avec une seule différence. les fichiers sont .mm
et pas .m
. c'est-à-dire qu'il s'agit d'une application Obj-C++ et non d'une application Obj-C.
Pour une meilleure organisation, nous créons le fichier 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 du code CPP sur n'importe quel fichier souhaité sur Obj-C, mais il est utile de garder l'organisation et, en dehors de vos fichiers wrapper, vous conservez un code complet style Obj-C, seul le fichier wrappers devient stylé CPP. .
Une fois que votre wrapper est 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 oeil à quoi ressemble l'application:
Le moment est venu pour Android. Android utilise Gradle en tant que système de construction, et au code C/C++, il utilise CMake. C'est donc la première chose dont nous avons besoin à faire est de configurer le fichier CMake on 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 dans le projet. Dans notre exemple, nous ajoutons le dossier CPP
et les fichiers Core.h/.cpp. Pour en savoir plus sur la configuration C/C++, veuillez lisez-le.
Maintenant que le code principal fait partie de notre application, il est temps de créer le pont. Pour simplifier et organiser les choses, nous créons une classe spécifique nommée CoreWrapper qui sera notre enveloppe entre la JVM et le CPP:
public class CoreWrapper {
public native String concatenateMyStringWithCppString(String myString);
static {
System.loadLibrary("native-lib");
}
}
Notez que cette classe a une méthode native
et charge une bibliothèque native nommée native-lib
. Cette bibliothèque est celle que nous avons créée. Au final, le code CPP deviendra un objet partagé .so
Fichier incorporé dans notre APK, et le loadLibrary
le chargera. Enfin, lorsque vous appelez la méthode native, la machine virtuelle Java déléguera l'appel à la bibliothèque chargée.
Maintenant la partie la plus étrange de 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 le extern "C"
cette partie est nécessaire au bon fonctionnement de JNI avec nos liens de code et de méthode CPP. Vous verrez également certains symboles que JNI utilise pour travailler avec la machine virtuelle Java en tant que JNIEXPORT
et JNICALL
. Pour que vous compreniez la signification de ces choses, il est nécessaire de prendre un certain temps et lisez-le , pour les besoins de ce tutoriel, considérez simplement ces choses comme un passe-partout.
Une chose importante et qui est 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, Android studio dispose d’un excellent support, il peut donc générer ce standard automatiquement et vous indiquer quand il est correct ou non nommé. Dans notre exemple, notre méthode est nommée "Java_ademar_androidioscppexample_CoreWrapper_concatenateMyStringCithString" "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 elle-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 JNIEnv
c'est la manière dont nous avons accès aux fichiers JNI. à bientot. Le second est un jobject
c'est l'instance de l'objet que vous avez utilisé pour appeler cette méthode. Vous pouvez le penser comme Java " this ", sur notre exemple, nous n'avons pas besoin de l'utiliser, mais nous devons toujours le déclarer. Après ce travail, nous allons recevoir les arguments de la méthode. Comme notre méthode n’a qu’un seul argument - une chaîne "myString", nous n’avons qu’un "jstring" portant le même nom. que notre type de retour est aussi un jstring, car notre méthode Java renvoie une chaîne, pour plus d’informations sur les types Java/JNI, veuillez lisez-le.
La dernière étape consiste à convertir les types JNI en types que nous utilisons côté CPP. Sur notre exemple, nous transformons le jstring
en un const char *
l’envoyer converti au CPP, obtenir le résultat et reconvertir en jstring
. Comme toutes les autres étapes sur JNI, ce n’est pas difficile; il n’est que de la matière, tout le travail est effectué par le JNIEnv*
argument que nous recevons lorsque nous appelons les GetStringUTFChars
et NewStringUTF
. Après cela, notre code est prêt à fonctionner sur les appareils Android, jetons un coup d’œil.).
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 de Java:
class View {
private contact = new Contact;
public void send(Contact friend) {
contact.sendMessage("Hello", friend);
contact.addTags({"a","b","c"});
contact.addFriends({friend});
}
}