Como usar o mesmo código C ++ para Android e iOS?

119

Android com NDK tem suporte para código C / C ++ e iOS com Objective-C ++ também, então como posso escrever aplicativos com código C / C ++ nativo compartilhado entre Android e iOS?

ademar111190
fonte
1
experimente o framework cocos2d-x
glo
@glo parece bom, mas estou procurando uma coisa mais genérica, usando o c ++ sem frameworks, "excluído JNI, obviamente".
ademar111190

Respostas:

273

Atualizar.

Esta resposta é bastante popular até quatro anos depois de escrevê-la, nesses quatro anos muitas coisas mudaram, então decidi atualizar minha resposta para se adequar melhor à nossa realidade atual. A ideia da resposta não muda; a implementação mudou um pouco. Meu inglês também mudou, melhorou muito, então a resposta é mais compreensível para todos agora.

Por favor, dê uma olhada no repo para que você possa baixar e executar o código que mostrarei abaixo.

A resposta

Antes de mostrar o código, analise muito o diagrama a seguir.

Arco

Cada SO possui sua IU e peculiaridades, portanto pretendemos escrever um código específico para cada plataforma a este respeito. Em outras mãos, todos os códigos lógicos, regras de negócios e coisas que podem ser compartilhadas pretendemos escrever usando C ++, para que possamos compilar o mesmo código para cada plataforma.

No diagrama, você pode ver a camada C ++ no nível mais baixo. Todo o código compartilhado está neste segmento. O nível mais alto é o código Obj-C / Java / Kotlin regular, nenhuma notícia aqui, a parte difícil é a camada intermediária.

A camada intermediária para o lado do iOS é simples; você só precisa configurar seu projeto para construir usando uma variante do Obj-c conhecida como Objective-C ++ e é tudo, você tem acesso ao código C ++.

A coisa ficou mais difícil no lado do Android, ambas as linguagens, Java e Kotlin, no Android, rodam em uma máquina virtual Java. Portanto, a única maneira de acessar o código C ++ é usando JNI , reserve um tempo para ler os fundamentos de JNI. Felizmente, o Android Studio IDE de hoje tem grandes melhorias no lado JNI, e muitos problemas são mostrados a você enquanto edita seu código.

O código por etapas

Nosso exemplo é um aplicativo simples que envia um texto para o CPP, ele converte esse texto em outra coisa e o retorna. A ideia é que o iOS enviará "Obj-C" e o Android enviará "Java" de suas respectivas línguas, e o código CPP criará um texto como "cpp diz olá para << texto recebido >> ".

Código CPP compartilhado

Em primeiro lugar, vamos criar o código CPP compartilhado, fazendo isso temos um arquivo de cabeçalho simples com a declaração do método que recebe o texto desejado:

#include <iostream>

const char *concatenateMyStringWithCppString(const char *myString);

E a implementação do 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

Um bônus interessante é que também podemos usar o mesmo código para Linux e Mac, bem como outros sistemas Unix. Essa possibilidade é especialmente útil porque podemos testar nosso código compartilhado mais rápido, então vamos criar um Main.cpp como segue para executá-lo em nossa máquina e ver se o código compartilhado está funcionando.

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

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

Para construir o código, você precisa executar:

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

iOS

É hora de implementar no lado móvel. Já que o iOS tem uma integração simples, estamos começando com ele. Nosso aplicativo iOS é um aplicativo Obj-c típico com apenas uma diferença; os arquivos são .mme não .m. ou seja, é um aplicativo Obj-C ++, não um aplicativo Obj-C.

Para uma melhor organização, criamos o CoreWrapper.mm da seguinte forma:

#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 classe tem a responsabilidade de converter tipos e chamadas CPP em tipos e chamadas Obj-C. Não é obrigatório, uma vez que você pode chamar o código CPP em qualquer arquivo que desejar no Obj-C, mas ajuda a manter a organização, e fora de seus arquivos de invólucro, você mantém um código completo no estilo Obj-C, apenas o arquivo de invólucro torna-se no estilo CPP .

Depois que seu wrapper estiver conectado ao código CPP, você pode usá-lo como um código Obj-C padrão, por exemplo, 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

Dê uma olhada na aparência do aplicativo:

Xcode Iphone

Android

Agora é hora de integração com o Android. O Android usa o Gradle como sistema de compilação e, para o código C / C ++, usa o CMake. Portanto, a primeira coisa que precisamos fazer é configurar o CMake no arquivo gradle:

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

E a segunda etapa é adicionar o arquivo 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}
)

O arquivo CMake é onde você precisa adicionar os arquivos CPP e pastas de cabeçalho que você usará no projeto, em nosso exemplo, estamos adicionando a CPPpasta e os arquivos Core.h / .cpp. Para saber mais sobre a configuração C / C ++, leia.

Agora que o código principal é parte do nosso aplicativo, é hora de criar a ponte, para tornar as coisas mais simples e organizadas, criamos uma classe específica chamada CoreWrapper para ser nosso wrapper entre JVM e CPP:

public class CoreWrapper {

    public native String concatenateMyStringWithCppString(String myString);

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

}

Observe que esta classe possui um nativemétodo e carrega uma biblioteca nativa chamada native-lib. Esta biblioteca é a que criamos, no final, o código CPP se tornará um objeto compartilhado .soFile embed em nosso APK, e o loadLibrarycarregará. Finalmente, ao chamar o método nativo, a JVM delegará a chamada à biblioteca carregada.

Agora, a parte mais estranha da integração do Android é o JNI; Precisamos de um arquivo cpp da seguinte forma, em nosso 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;
}

}

A primeira coisa que você notará é que extern "C"esta parte é necessária para que o JNI funcione corretamente com nosso código CPP e ligações de método. Você também verá alguns símbolos que o JNI usa para trabalhar com JVM como JNIEXPORTe JNICALL. Para você entender o significado dessas coisas, é necessário reservar um tempo e lê-lo , para os fins deste tutorial apenas considere essas coisas como clichês.

Uma coisa significativa e geralmente a raiz de muitos problemas é o nome do método; ele precisa seguir o padrão "Java_package_class_method". Atualmente, o Android Studio tem um excelente suporte para ele, de modo que pode gerar esse boilerplate automaticamente e mostrar quando ele está correto ou não nomeado. Em nosso exemplo, nosso método é denominado "Java_ademar_androidioscppexample_CoreWrapper_concatenateMyStringWithCppString" porque "ademar.androidioscppexample" é nosso pacote, então substituímos "." por "_", CoreWrapper é a classe onde estamos vinculando o método nativo e "concatenateMyStringWithCppString" é o próprio nome do método.

Como temos o método declarado corretamente é hora de analisar os argumentos, o primeiro parâmetro é um ponteiro JNIEnvdele é a forma como temos acesso ao material JNI, é fundamental que façamos nossas conversões como você verá em breve. O segundo é jobjecta instância do objeto que você usou para chamar esse método. Você pode pensar nisso como o java " this ", em nosso exemplo não precisamos usá-lo, mas ainda precisamos declará-lo. Após este jobject, iremos receber os argumentos do método. Como nosso método tem apenas um argumento - uma String "myString", temos apenas uma "jstring" com o mesmo nome. Observe também que nosso tipo de retorno também é jstring. É porque nosso método Java retorna uma String, para obter mais informações sobre os tipos Java / JNI, leia.

A etapa final é converter os tipos JNI para os tipos que usamos no lado do CPP. Em nosso exemplo, estamos transformando o jstringem um const char *enviando-o convertido em CPP, obtendo o resultado e convertendo de volta para jstring. Como todas as outras etapas do JNI, não é difícil; é apenas boilerplated, todo o trabalho é feito pelo JNIEnv*argumento que recebemos quando chamamos o GetStringUTFCharse NewStringUTF. Depois que nosso código estiver pronto para ser executado em dispositivos Android, vamos dar uma olhada.

AndroidStudio Android

ademar111190
fonte
7
Ótima explicação
RED.Skull
9
Não entendi - mas +1 para uma das respostas de maior qualidade no SO
Michael Rodrigues
16
@ ademar111190 De longe a postagem mais útil. Isso não deveria ter sido fechado.
Jared Burrows
6
@JaredBurrows, eu concordo. Votado para reabrir.
OmnipotentEntity
3
@KVISH você tem que implementar o wrapper em Objective-C primeiro, então você acessará o wrapper Objective-C rapidamente adicionando o cabeçalho do wrapper ao seu arquivo de cabeçalho de ponte. Não há como acessar diretamente C ++ no Swift a partir de agora. Para obter mais informações, consulte stackoverflow.com/a/24042893/1853977
Chris
3

A abordagem descrita na excelente resposta acima pode ser completamente automatizada pelo Scapix Language Bridge, que gera o código do wrapper em tempo real diretamente dos cabeçalhos C ++. Aqui está um exemplo :

Defina sua classe em 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 chame de Swift:

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

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

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