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?
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?
Respostas:
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.
Antes de mostrar o código, analise muito o diagrama a seguir.
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.
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 >> ".
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;
}
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
É 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 .mm
e 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:
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 CPP
pasta 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 native
mé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 .so
File embed em nosso APK, e o loadLibrary
carregará. 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 JNIEXPORT
e 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 JNIEnv
dele é a forma como temos acesso ao material JNI, é fundamental que façamos nossas conversões como você verá em breve. O segundo é jobject
a 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 jstring
em 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 GetStringUTFChars
e NewStringUTF
. Depois que nosso código estiver pronto para ser executado em dispositivos Android, vamos dar uma olhada.
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});
}
}