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

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

Ответ 1

Обновить.

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

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

Ответ

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

Arch

Каждая ОС имеет свой пользовательский интерфейс и особенности, поэтому мы намерены написать конкретный код для каждой платформы в этом отношении. В других руках весь логический код, бизнес-правила и вещи, которыми можно поделиться, мы намереваемся написать, используя 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 Файл будет loadLibrary в наш 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", в нашем примере нам не нужно его использовать, но мы все равно должны объявить его. После этого задания мы собираемся получить аргументы метода. Поскольку наш метод имеет только один аргумент - строку "myString", у нас есть только "jstring" с тем же именем. Также обратите внимание, что наш тип возврата также является jstring. Это потому, что наш метод Java возвращает строку, для получения дополнительной информации о типах Java/JNI, пожалуйста, прочитайте его.

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

AndroidStudio Android

Ответ 2

Подход, описанный в превосходном ответе выше, может быть полностью автоматизирован 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);
};

И позвони из Свифта:

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