Jak używać tego samego kodu C ++ dla Androida i iOS?

119

Android z NDK obsługuje również kod C / C ++, a iOS z Objective-C ++ również obsługuje, więc jak mogę pisać aplikacje z natywnym kodem C / C ++ współdzielonym między systemami Android i iOS?

ademar111190
źródło
1
wypróbuj framework cocos2d-x
glo
@glo wydaje się dobre, ale szukam czegoś bardziej ogólnego, używając C ++ bez frameworków, „oczywiście wykluczone JNI”.
ademar111190

Odpowiedzi:

273

Aktualizacja.

Ta odpowiedź jest dość popularna nawet cztery lata po jej napisaniu, w ciągu tych czterech lat wiele się zmieniło, więc postanowiłem zaktualizować swoją odpowiedź, aby lepiej pasowała do naszej obecnej rzeczywistości. Pomysł odpowiedzi się nie zmienia; realizacja trochę się zmieniła. Mój angielski też się zmienił, bardzo się poprawił, więc odpowiedź jest teraz bardziej zrozumiała dla wszystkich.

Spójrz na repozytorium , abyś mógł pobrać i uruchomić kod, który pokażę poniżej.

Odpowiedź

Zanim pokażę kod, przeanalizuj poniższy diagram.

Łuk

Każdy system operacyjny ma swój interfejs użytkownika i cechy szczególne, dlatego zamierzamy napisać określony kod do każdej platformy w tym zakresie. Z drugiej strony, cały kod logiczny, reguły biznesowe i rzeczy, które można udostępniać, zamierzamy napisać w C ++, abyśmy mogli skompilować ten sam kod na każdej platformie.

Na diagramie możesz zobaczyć warstwę C ++ na najniższym poziomie. Cały wspólny kod znajduje się w tym segmencie. Najwyższy poziom to zwykły kod Obj-C / Java / Kotlin, nie ma tu żadnych nowości, najtrudniejsza jest warstwa środkowa.

Środkowa warstwa po stronie iOS jest prosta; musisz tylko skonfigurować swój projekt do kompilacji przy użyciu wariantu Obj-c znanego jako Objective-C ++ i to wszystko, masz dostęp do kodu C ++.

Sytuacja stała się trudniejsza po stronie Androida, oba języki, Java i Kotlin, na Androida działają pod wirtualną maszyną Java. Tak więc jedynym sposobem uzyskania dostępu do kodu C ++ jest użycie JNI , poświęć trochę czasu na przeczytanie podstaw JNI. Na szczęście dzisiejsze IDE Android Studio ma ogromne ulepszenia po stronie JNI, a podczas edycji kodu wyświetlanych jest wiele problemów.

Kod według kroków

Nasza próbka to prosta aplikacja, w której wysyłasz tekst do CPP, a ona konwertuje ten tekst na coś innego i zwraca go. Pomysł jest taki, że iOS wyśle ​​„Obj-C”, a Android wyśle ​​„Java” z odpowiednich języków, a kod CPP utworzy następujący tekst: „cpp mówi cześć << otrzymano tekst >> ”.

Wspólny kod CPP

Przede wszystkim utworzymy udostępniony kod CPP, robiąc to mamy prosty plik nagłówkowy z deklaracją metody, która otrzymuje żądany tekst:

#include <iostream>

const char *concatenateMyStringWithCppString(const char *myString);

Oraz wdrożenie 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

Ciekawym bonusem jest to, że możemy użyć tego samego kodu dla Linuksa i Maca, a także innych systemów Unix. Ta możliwość jest szczególnie przydatna, ponieważ możemy szybciej przetestować udostępniony kod, więc utworzymy plik Main.cpp w następujący sposób, aby wykonać go z naszej maszyny i sprawdzić, czy współdzielony kod działa.

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

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

Aby zbudować kod, musisz wykonać:

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

iOS

Czas na wdrożenie po stronie mobilnej. O ile iOS ma prostą integrację, zaczynamy od niego. Nasza aplikacja na iOS jest typową aplikacją Obj-c z tylko jedną różnicą; pliki są .mmi nie .m. tj. jest to aplikacja Obj-C ++, a nie aplikacja Obj-C.

Dla lepszej organizacji tworzymy plik CoreWrapper.mm w następujący sposób:

#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

Ta klasa jest odpowiedzialna za konwersję typów CPP i wywołań na typy i wywołania Obj-C. Nie jest to obowiązkowe, gdy możesz wywołać kod CPP w dowolnym pliku w Obj-C, ale pomaga to zachować organizację, a poza plikami opakowującymi utrzymujesz kompletny kod w stylu Obj-C, tylko plik opakowania otrzymuje styl CPP .

Po podłączeniu opakowania do kodu CPP możesz użyć go jako standardowego kodu Obj-C, np. 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

Zobacz, jak wygląda aplikacja:

Xcode iPhone

Android

Teraz przyszedł czas na integrację z Androidem. Android używa Gradle jako systemu kompilacji, a do kodu C / C ++ używa CMake. Więc pierwszą rzeczą, którą musimy zrobić, to skonfigurować CMake w pliku gradle:

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

Drugim krokiem jest dodanie pliku 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}
)

Plik CMake to miejsce, w którym musisz dodać pliki CPP i foldery nagłówkowe, których będziesz używać w projekcie, w naszym przykładzie dodajemy CPPfolder i pliki Core.h / .cpp. Aby dowiedzieć się więcej o konfiguracji C / C ++, przeczytaj to.

Teraz główny kod jest częścią naszej aplikacji, czas stworzyć mostek, aby uprościć i uporządkować rzeczy, tworzymy specjalną klasę o nazwie CoreWrapper, która będzie naszym opakowaniem między JVM i CPP:

public class CoreWrapper {

    public native String concatenateMyStringWithCppString(String myString);

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

}

Zwróć uwagę, że ta klasa ma nativemetodę i ładuje bibliotekę natywną o nazwie native-lib. Ta biblioteka jest tą, którą tworzymy, w końcu kod CPP stanie się współdzielonym .soplikiem obiektu osadzonym w naszym pliku APK i loadLibraryzaładuje go. Na koniec, po wywołaniu metody natywnej, JVM przekaże wywołanie do załadowanej biblioteki.

Teraz najbardziej dziwną częścią integracji Androida jest JNI; Potrzebujemy następującego pliku cpp, w naszym przypadku „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;
}

}

Pierwszą rzeczą, którą zauważysz, jest to, że extern "C"ta część jest niezbędna do poprawnej pracy JNI z naszym kodem CPP i powiązaniami metod. Zobaczysz także symbole używane przez JNI do pracy z JVM jako JNIEXPORTi JNICALL. Abyś zrozumiał znaczenie tych rzeczy, musisz poświęcić trochę czasu i przeczytać to , dla celów tego samouczka potraktuj je jako szablon.

Jedną istotną rzeczą i zwykle źródłem wielu problemów jest nazwa metody; musi być zgodny ze wzorcem „Java_package_class_method”. Obecnie studio Android ma dla niego doskonałe wsparcie, więc może automatycznie wygenerować ten szablon i pokazać ci, kiedy jest poprawny lub nie został nazwany. W naszym przykładzie nasza metoda nosi nazwę „Java_ademar_androidioscppexample_CoreWrapper_concatenateMyStringWithCppString”, ponieważ „ademar.androidioscppexample” to nasz pakiet, więc zastępujemy „.” przez „_”, CoreWrapper jest klasą, w której łączymy metodę natywną, a „concatenateMyStringWithCppString” jest samą nazwą metody.

Ponieważ mamy poprawnie zadeklarowaną metodę, czas na analizę argumentów, pierwszy parametr jest wskaźnikiem JNIEnv, to sposób, w jaki mamy dostęp do rzeczy JNI, kluczowe jest, abyśmy dokonali naszych konwersji, jak wkrótce się przekonasz. Drugi to jobjectinstancja obiektu, którego użyłeś do wywołania tej metody. Możesz pomyśleć, że to java " this ", na naszym przykładzie nie musimy go używać, ale nadal musimy to zadeklarować. Po tym zadaniu otrzymamy argumenty metody. Ponieważ nasza metoda ma tylko jeden argument - String „myString”, mamy tylko „jstring” o tej samej nazwie. Zauważ też, że nasz typ zwracany jest również jstringiem. Dzieje się tak, ponieważ nasza metoda Java zwraca ciąg znaków. Aby uzyskać więcej informacji na temat typów Java / JNI, przeczytaj ją.

Ostatnim krokiem jest konwersja typów JNI do typów, których używamy po stronie CPP. W naszym przykładzie przekształcamy jstringgo w const char *wysyłanie przekonwertowanego na CPP, uzyskujemy wynik i konwertujemy z powrotem na jstring. Jak wszystkie inne kroki na JNI, nie jest to trudne; jest to tylko kotłowe, cała praca jest wykonywana przez JNIEnv*argument, który otrzymujemy, gdy wywołujemy GetStringUTFCharsi NewStringUTF. Po tym nasz kod jest gotowy do uruchomienia na urządzeniach z Androidem, spójrzmy.

AndroidStudio Android

ademar111190
źródło
7
Świetne wyjaśnienie
RED.Skull
9
Nie rozumiem - ale +1 za jedną z najwyższej jakości odpowiedzi na SO
Michael Rodrigues
16
@ ademar111190 Zdecydowanie najbardziej pomocny post. To nie powinno być zamknięte.
Jared Burrows
6
@JaredBurrows, zgadzam się. Zagłosowano na ponowne otwarcie.
OmnipotentEntity
3
@KVISH musisz najpierw zaimplementować opakowanie w Objective-C, a następnie szybko uzyskasz dostęp do opakowania Objective-C, dodając nagłówek opakowania do pliku nagłówka mostkowania. Obecnie nie ma możliwości bezpośredniego dostępu do C ++ w Swift. Więcej informacji można znaleźć na stronie stackoverflow.com/a/24042893/1853977
Chris
3

Podejście opisane w doskonałej odpowiedzi powyżej może być całkowicie zautomatyzowane przez Scapix Language Bridge, który generuje kod opakowujący w locie bezpośrednio z nagłówków C ++. Oto przykład :

Zdefiniuj swoją klasę w 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);
};

I zadzwoń od Swift:

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

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

A z 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
źródło