Android con NDK tiene soporte para código C / C ++ e iOS con Objective-C ++ también tiene soporte, entonces, ¿cómo puedo escribir aplicaciones con código nativo C / C ++ compartido entre Android e iOS?
Android con NDK tiene soporte para código C / C ++ e iOS con Objective-C ++ también tiene soporte, entonces, ¿cómo puedo escribir aplicaciones con código nativo C / C ++ compartido entre Android e iOS?
Respuestas:
Esta respuesta es bastante popular incluso cuatro años después de que la escribí, en estos cuatro años muchas cosas han cambiado, así que decidí actualizar mi respuesta para que se ajuste mejor a nuestra realidad actual. La idea de respuesta no cambia; la implementación ha cambiado un poco. Mi inglés también ha cambiado, ha mejorado mucho, por lo que ahora la respuesta es más comprensible para todos.
Eche un vistazo al repositorio para que pueda descargar y ejecutar el código que mostraré a continuación.
Antes de mostrar el código, consulte el siguiente diagrama.
Cada SO tiene su UI y peculiaridades, por lo que pretendemos escribir código específico para cada plataforma en este sentido. En otras manos, todo el código lógico, las reglas comerciales y las cosas que se pueden compartir, pretendemos escribir usando C ++, para que podamos compilar el mismo código para cada plataforma.
En el diagrama, puede ver la capa C ++ en el nivel más bajo. Todo el código compartido está en este segmento. El nivel más alto es el código regular Obj-C / Java / Kotlin, no hay noticias aquí, la parte difícil es la capa intermedia.
La capa intermedia del lado de iOS es simple; solo necesita configurar su proyecto para construir usando una variante de Obj-c conocida como Objective-C ++ y es todo, tiene acceso al código C ++.
La cosa se volvió más difícil en el lado de Android, ambos lenguajes, Java y Kotlin, en Android, se ejecutan bajo una Máquina Virtual Java. Entonces, la única forma de acceder al código C ++ es usando JNI , tómese el tiempo para leer los conceptos básicos de JNI. Afortunadamente, el IDE de Android Studio de hoy tiene grandes mejoras en el lado de JNI, y se le muestran muchos problemas mientras edita su código.
Nuestra muestra es una aplicación simple que envía un texto a CPP, y convierte ese texto en otra cosa y lo devuelve. La idea es que iOS enviará "Obj-C" y Android enviará "Java" desde sus respectivos idiomas, y el código CPP creará un texto como "cpp saluda a << texto recibido >> ".
En primer lugar vamos a crear el código CPP compartido, para ello tenemos un archivo de encabezado simple con la declaración del método que recibe el texto deseado:
#include <iostream>
const char *concatenateMyStringWithCppString(const char *myString);
Y la implementación de 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;
}
Una ventaja interesante es que también podemos usar el mismo código para Linux y Mac, así como para otros sistemas Unix. Esta posibilidad es especialmente útil porque podemos probar nuestro código compartido más rápido, por lo que vamos a crear un Main.cpp como sigue para ejecutarlo desde nuestra máquina y ver si el código compartido está funcionando.
#include <iostream>
#include <string>
#include "../CPP/Core.h"
int main() {
std::string textFromCppCore = concatenateMyStringWithCppString("Unix");
std::cout << textFromCppCore << '\n';
return 0;
}
Para compilar el código, debe ejecutar:
$ g++ Main.cpp Core.cpp -o main
$ ./main
cpp says hello to Unix
Es hora de implementar en el lado móvil. En la medida en que iOS tiene una integración simple, estamos comenzando con ella. Nuestra aplicación para iOS es una aplicación típica de Obj-c con una sola diferencia; los archivos son .mm
y no .m
. es decir, es una aplicación Obj-C ++, no una aplicación Obj-C.
Para una mejor organización, creamos CoreWrapper.mm de la siguiente manera:
#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
Esta clase tiene la responsabilidad de convertir tipos y llamadas CPP a tipos y llamadas Obj-C. No es obligatorio una vez que puede llamar al código CPP en cualquier archivo que desee en Obj-C, pero ayuda a mantener la organización, y fuera de sus archivos de envoltura, mantiene un código completo con estilo de Obj-C, solo el archivo de envolturas se convierte en estilo CPP .
Una vez que su contenedor está conectado al código CPP, puede usarlo como un código Obj-C estándar, por ejemplo, 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
Eche un vistazo a cómo se ve la aplicación:
Ahora es el momento de la integración con Android. Android usa Gradle como sistema de compilación, y para el código C / C ++ usa CMake. Entonces, lo primero que debemos hacer es configurar el archivo CMake en gradle:
android {
...
externalNativeBuild {
cmake {
path "CMakeLists.txt"
}
}
...
defaultConfig {
externalNativeBuild {
cmake {
cppFlags "-std=c++14"
}
}
...
}
Y el segundo paso es agregar el archivo 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}
)
El archivo CMake es donde debe agregar los archivos CPP y las carpetas de encabezado que usará en el proyecto; en nuestro ejemplo, estamos agregando la CPP
carpeta y los archivos Core.h / .cpp. Para saber más sobre la configuración de C / C ++, léalo.
Ahora que el código central es parte de nuestra aplicación, es hora de crear el puente, para hacer las cosas más simples y organizadas, creamos una clase específica llamada CoreWrapper para que sea nuestra envoltura entre JVM y CPP:
public class CoreWrapper {
public native String concatenateMyStringWithCppString(String myString);
static {
System.loadLibrary("native-lib");
}
}
Tenga en cuenta que esta clase tiene un native
método y carga una biblioteca nativa llamada native-lib
. Esta biblioteca es la que creamos, al final, el código CPP se convertirá en un objeto compartido .so
Archivo incrustado en nuestro APK, y loadLibrary
lo cargará. Finalmente, cuando llame al método nativo, la JVM delegará la llamada a la biblioteca cargada.
Ahora, la parte más extraña de la integración de Android es el JNI; Necesitamos un archivo cpp como sigue, en nuestro 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;
}
}
Lo primero que notará es que extern "C"
esta parte es necesaria para que JNI funcione correctamente con nuestro código CPP y enlaces de métodos. También verá algunos símbolos que JNI usa para trabajar con JVM como JNIEXPORT
y JNICALL
. Para que comprenda el significado de esas cosas, es necesario tomarse un tiempo y leerlo , para los propósitos de este tutorial, simplemente considere estas cosas como un texto estándar.
Una cosa importante y generalmente la raíz de muchos problemas es el nombre del método; debe seguir el patrón "Java_package_class_method". Actualmente, Android Studio tiene un excelente soporte para él, por lo que puede generar este modelo automáticamente y mostrarle cuándo es correcto o no tiene nombre. En nuestro ejemplo, nuestro método se llama "Java_ademar_androidioscppexample_CoreWrapper_concatenateMyStringWithCppString" es porque "ademar.androidioscppexample" es nuestro paquete, así que reemplazamos el "." por "_", CoreWrapper es la clase donde estamos vinculando el método nativo y "concatenateMyStringWithCppString" es el nombre del método en sí.
Como tenemos el método correctamente declarado, es hora de analizar los argumentos, el primer parámetro es un puntero de JNIEnv
la forma en que tenemos acceso a las cosas de JNI, es crucial que hagamos nuestras conversiones como verá pronto. El segundo es una jobject
instancia del objeto que había utilizado para llamar a este método. Puede pensarlo como el java " this ", en nuestro ejemplo no necesitamos usarlo, pero aún debemos declararlo. Después de este trabajo, vamos a recibir los argumentos del método. Debido a que nuestro método tiene solo un argumento, una cadena "myString", solo tenemos una "jstring" con el mismo nombre. También observe que nuestro tipo de retorno es también jstring. Es porque nuestro método Java devuelve una cadena, para obtener más información sobre los tipos de Java / JNI, léalo.
El paso final es convertir los tipos JNI a los tipos que usamos en el lado de CPP. En nuestro ejemplo, estamos transformando el jstring
en un const char *
envío convertido a CPP, obteniendo el resultado y volviendo a convertir a jstring
. Como todos los demás pasos de JNI, no es difícil; solo está repetido, todo el trabajo lo realiza el JNIEnv*
argumento que recibimos cuando llamamos al GetStringUTFChars
y NewStringUTF
. Después de que nuestro código esté listo para ejecutarse en dispositivos Android, echemos un vistazo.
El enfoque descrito en la excelente respuesta anterior puede ser completamente automatizado por Scapix Language Bridge, que genera código de envoltura sobre la marcha directamente desde los encabezados de C ++. He aquí un ejemplo :
Defina su clase 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);
};
Y llámalo desde Swift:
class ViewController: UIViewController {
func send(friend: Contact) {
let c = Contact()
contact.sendMessage("Hello", friend)
contact.addTags(["a","b","c"])
contact.addFriends([friend])
}
}
Y desde Java:
class View {
private contact = new Contact;
public void send(Contact friend) {
contact.sendMessage("Hello", friend);
contact.addTags({"a","b","c"});
contact.addFriends({friend});
}
}