Как использовать один и тот же код C ++ для Android и iOS?

119

Android с NDK поддерживает код C / C ++, а iOS с Objective-C ++ также поддерживает, так как я могу писать приложения с собственным кодом C / C ++, совместно используемым Android и iOS?

ademar111190
источник
1
попробуйте фреймворк cocos2d-x
glo
@glo кажется хорошим, но я ищу более универсальную вещь, использующую c ++ без фреймворков, «очевидно, исключая JNI».
ademar111190

Ответы:

274

Обновить.

Этот ответ довольно популярен даже через четыре года после того, как я его написал, за эти четыре года многое изменилось, поэтому я решил обновить свой ответ, чтобы он лучше соответствовал нашей нынешней реальности. Идея ответа не меняется; реализация немного изменилась. Мой английский тоже изменился, он сильно улучшился, поэтому теперь ответ стал более понятным для всех.

Взгляните на репозиторий, чтобы вы могли загрузить и запустить код, который я покажу ниже.

Ответ

Прежде чем я покажу код, обратите внимание на следующую диаграмму.

арочный

Каждая ОС имеет свой пользовательский интерфейс и особенности, поэтому мы намерены написать специальный код для каждой платформы в этом отношении. С другой стороны, весь логический код, бизнес-правила и вещи, которыми можно поделиться, мы намерены написать с использованием C ++, чтобы мы могли скомпилировать один и тот же код для каждой платформы.

На схеме вы можете увидеть слой C ++ на самом нижнем уровне. Весь общий код находится в этом сегменте. Самый высокий уровень - это обычный код Obj-C / Java / Kotlin, здесь нет новостей, сложная часть - это средний уровень.

Средний уровень со стороны iOS прост; вам нужно только настроить свой проект для сборки с использованием варианта Obj-c, известного как Objective-C ++, и все, у вас есть доступ к коду C ++.

Со стороны Android дело стало сложнее: оба языка, Java и Kotlin, на Android работали под виртуальной машиной Java. Таким образом, единственный способ получить доступ к коду C ++ - использовать JNI , пожалуйста, найдите время, чтобы прочитать основы JNI. К счастью, сегодняшняя Android Studio IDE имеет значительные улучшения на стороне JNI, и при редактировании кода вам будет показано множество проблем.

Код по шагам

Наш образец - это простое приложение, в котором вы отправляете текст в CPP, оно преобразует этот текст во что-то еще и возвращает его. Идея состоит в том, что iOS отправит «Obj-C», а Android отправит «Java» со своих соответствующих языков, а код CPP создаст текст в следующем виде: «cpp передает привет << полученному тексту >> ».

Общий код CPP

Прежде всего, мы собираемся создать общий код CPP, сделав это, у нас есть простой файл заголовка с объявлением метода, который получает желаемый текст:

#include <iostream>

const char *concatenateMyStringWithCppString(const char *myString);

И реализация 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;
}

Юникс

Интересным бонусом является то, что мы можем использовать один и тот же код для Linux и Mac, а также для других систем Unix. Эта возможность особенно полезна, потому что мы можем быстрее протестировать наш общий код, поэтому мы собираемся создать Main.cpp, как показано ниже, чтобы выполнить его с нашей машины и посмотреть, работает ли общий код.

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

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

Для сборки кода необходимо выполнить:

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

IOS

Пора реализовать на мобильной стороне. Поскольку iOS имеет простую интеграцию, мы начинаем с нее. Наше приложение для iOS представляет собой типичное приложение Obj-c с одним отличием; файлы есть .mmи нет .m. т.е. это приложение Obj-C ++, а не приложение Obj-C.

Для лучшей организации мы создаем CoreWrapper.mm следующим образом:

#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

Этот класс отвечает за преобразование типов и вызовов CPP в типы и вызовы Obj-C. Это не обязательно, если вы можете вызвать код CPP для любого файла, который хотите на Obj-C, но он помогает сохранить организацию, и за пределами ваших файлов-оберток вы поддерживаете полный код в стиле Obj-C, только файл оберток становится в стиле CPP ,

Как только ваша оболочка подключена к коду CPP, вы можете использовать ее как стандартный код Obj-C, например 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

Посмотрите, как выглядит приложение:

Xcode iPhone

Android

Пришло время интеграции с Android. Android использует Gradle в качестве системы сборки, а для кода C / C ++ использует CMake. Итак, первое, что нам нужно сделать, это настроить CMake в файле gradle:

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

И второй шаг - добавить файл 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}
)

Файл CMake - это то место, где вам нужно добавить файлы CPP и папки заголовков, которые вы будете использовать в проекте, в нашем примере мы добавляем CPPпапку и файлы Core.h / .cpp. Чтобы узнать больше о конфигурации C / C ++, прочтите ее.

Теперь основной код является частью нашего приложения, пришло время создать мост, чтобы упростить и упорядочить вещи, мы создаем определенный класс с именем CoreWrapper, который будет нашей оболочкой между JVM и CPP:

public class CoreWrapper {

    public native String concatenateMyStringWithCppString(String myString);

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

}

Обратите внимание, что этот класс имеет nativeметод и загружает собственную библиотеку с именем native-lib. Эта библиотека - та, которую мы создаем, в конце концов, код CPP станет .soфайлом общего объекта, встроенным в наш APK, и loadLibraryон загрузит его. Наконец, когда вы вызываете собственный метод, JVM делегирует вызов загруженной библиотеке.

Теперь самая странная часть интеграции Android - это JNI; Нам нужен файл cpp, как показано ниже, в нашем случае "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;
}

}

Первое, что вы заметите, это то, что extern "C"эта часть необходима для правильной работы JNI с нашим кодом CPP и связями методов. Вы также увидите некоторые символы, которые JNI использует для работы с JVM, например, JNIEXPORTи JNICALL. Чтобы вы поняли значение этих вещей, необходимо уделить время и прочитать их , для целей данного руководства просто рассматривайте эти вещи как шаблон.

Одна важная вещь, которая обычно является корнем многих проблем, - это название метода; он должен соответствовать шаблону «Java_package_class_method». В настоящее время студия Android имеет отличную поддержку для этого, поэтому она может автоматически сгенерировать этот шаблон и показать вам, правильно он или не назван. В нашем примере наш метод называется «Java_ademar_androidioscppexample_CoreWrapper_concatenateMyStringWithCppString», потому что «ademar.androidioscppexample» - это наш пакет, поэтому мы заменяем «.» по «_» CoreWrapper - это класс, в котором мы связываем собственный метод, а «concatenateMyStringWithCppString» - это само имя метода.

Поскольку у нас правильно объявлен метод, пришло время проанализировать аргументы, первый параметр - это указатель на JNIEnvто, как мы имеем доступ к материалам JNI, очень важно, чтобы мы сделали наши преобразования, как вы скоро увидите. Второй - jobjectэто экземпляр объекта, который вы использовали для вызова этого метода. Вы можете думать, что это java " this ", в нашем примере нам не нужно его использовать, но мы все равно должны его объявить. После этого задания мы получим аргументы метода. Поскольку наш метод имеет только один аргумент - String «myString», у нас есть только «jstring» с тем же именем. Также обратите внимание, что наш возвращаемый тип также является jstring. Это связано с тем, что наш метод Java возвращает String, для получения дополнительной информации о типах Java / JNI прочтите его.

Последний шаг - преобразовать типы JNI в типы, которые мы используем на стороне CPP. В нашем примере мы преобразуем jstringего в const char *отправку, конвертируем в CPP, получаем результат и конвертируем обратно в jstring. Как и все другие шаги по JNI, это не сложно; это всего лишь шаблон, вся работа выполняется JNIEnv*аргументом, который мы получаем, когда вызываем GetStringUTFCharsи NewStringUTF. После этого наш код готов к запуску на устройствах Android, давайте посмотрим.

AndroidStudio Android

ademar111190
источник
7
Отличное объяснение
RED.Skull 08
9
Я не понимаю - но +1 за один из самых качественных ответов на SO
Майкл Родригес
16
@ ademar111190 Безусловно, самый полезный пост. Это не должно было быть закрыто.
Джаред Берроуз
6
@JaredBurrows, я согласен. Проголосовали за повторное открытие.
OmnipotentEntity
3
@KVISH вы должны сначала реализовать оболочку в Objective-C, затем вы быстро получите доступ к оболочке Objective-C, добавив заголовок оболочки в свой файл заголовка моста. На данный момент нет возможности напрямую получить доступ к C ++ в Swift. Для получения дополнительной информации см. Stackoverflow.com/a/24042893/1853977
Крис,
3

Подход, описанный в превосходном ответе выше, может быть полностью автоматизирован с помощью Scapix Language Bridge, который генерирует код оболочки на лету непосредственно из заголовков C ++. Вот пример :

Определите свой класс на 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);
};

И назовите его из Swift:

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

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

И с Java:

class View {
    private contact = new Contact;

    public void send(Contact friend) {
        contact.sendMessage("Hello", friend);
        contact.addTags({"a","b","c"});
        contact.addFriends({friend});
    }
}
Борис Расин
источник