¿Cómo usar el mismo código C ++ para Android e iOS?

119

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?

ademar111190
fuente
1
prueba cocos2d-x framework
glo
@glo parece bueno, pero estoy buscando algo más genérico, usar c ++ sin frameworks, "excluido JNI, obviamente".
ademar111190

Respuestas:

273

Actualizar.

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.

La respuesta

Antes de mostrar el código, consulte el siguiente diagrama.

Arco

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.

El código por pasos

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 >> ".

Código CPP compartido

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;
}

Unix

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

iOS

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 .mmy 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:

Xcode iPhone

Androide

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 CPPcarpeta 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 nativemé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 .soArchivo incrustado en nuestro APK, y loadLibrarylo 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 JNIEXPORTy 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 JNIEnvla forma en que tenemos acceso a las cosas de JNI, es crucial que hagamos nuestras conversiones como verá pronto. El segundo es una jobjectinstancia 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 jstringen 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 GetStringUTFCharsy NewStringUTF. Después de que nuestro código esté listo para ejecutarse en dispositivos Android, echemos un vistazo.

AndroidStudio Androide

ademar111190
fuente
7
Gran explicación
RED.Skull
9
No lo entiendo, pero +1 para una de las respuestas de más alta calidad en SO
Michael Rodrigues
16
@ ademar111190 De lejos, la publicación más útil. Esto no debería haberse cerrado.
Jared Burrows
6
@JaredBurrows, estoy de acuerdo. Votó para reabrir.
Entidad Omnipotente
3
@KVISH primero debe implementar el contenedor en Objective-C, luego accederá al contenedor de Objective-C rápidamente agregando el encabezado de contenedor a su archivo de encabezado de puente. No hay forma de acceder directamente a C ++ en Swift a partir de ahora. Para obtener más información, consulte stackoverflow.com/a/24042893/1853977
Chris
3

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});
    }
}
Boris Rasin
fuente