web-dev-qa-db-ja.com

AndroidとiOSに同じC ++コードを使用する方法は?

Androidで [〜#〜] ndk [〜#〜] がC/C++コードをサポートし、iOSで Objective-C++ がサポートされているため、アプリケーションを作成するにはどうすればよいですかAndroidとiOSの間で共有されるネイティブC/C++コード

114
ademar111190

更新。

この回答は、私が書いてから4年経っても非常に人気があり、この4年間で多くのことが変わりました。そのため、現在の現実に合わせて回答を更新することにしました。答えのアイデアは変わりません。実装が少し変更されました。私の英語も変わったので、かなり改善されたので、答えは誰にとっても理解しやすいものになりました。

repo をご覧ください。以下に示すコードをダウンロードして実行できます。

答え

コードを表示する前に、次の図をよく読んでください。

Arch

各OSにはUIと特性があるため、この点に関して各プラットフォームに特定のコードを書くつもりです。一方、すべてのロジックコード、ビジネスルール、および共有できるものはC++を使用して記述するため、同じコードを各プラットフォームにコンパイルできます。

図では、最下層にC++レイヤーがあります。すべての共有コードはこのセグメントにあります。最高レベルは通常のObj-C/Java/Kotlinコードです。ここではニュースはありません。難しい部分は中間層です。

IOS側の中間層は単純です。 Objective-C++ として知られるObj-cのバリアントを使用してビルドするようにプロジェクトを設定するだけで、C++コードにアクセスできます。

問題はAndroid側で難しくなりました。AndroidのJavaとKotlinの両方の言語は、Java仮想マシンで実行されます。したがって、C++コードにアクセスする唯一の方法は、 [〜#〜] jni [〜#〜] を使用することです。JNIの基本をお読みください。幸いなことに、今日のAndroid Studio IDEはJNI側で大幅に改善されており、コードの編集中に多くの問題が表示されます。

ステップバイコード

サンプルは、CPPにテキストを送信するシンプルなアプリであり、そのテキストを他のテキストに変換して返します。アイデアは、iOSが「Obj-C」を送信し、Androidがそれぞれの言語から「Java」を送信し、CPPコードが「cpp says hello to<<受信したテキスト>>"。

共有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;
}

Unix

おもしろいボーナスは、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から始めています。私たちのiOSアプリは典型的なObj-cアプリですが、違いは1つだけです。ファイルは.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タイプと呼び出しに変換する役割があります。 Obj-C上の任意のファイルでCPPコードを呼び出すことができれば必須ではありませんが、組織を維持するのに役立ち、ラッパーファイルの外側で完全な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

アプリの外観を見てみましょう。

XcodeiPhone

Android

これで、Android統合の時間です。 AndroidはビルドシステムとしてGradleを使用し、C/C++コードではCMakeを使用します。したがって、最初に行う必要があるのは、CMake on gradleファイルを構成することです。

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

2番目のステップは、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 AP​​Kに埋め込まれたファイルになり、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がJNIEXPORTおよびJNICALLとしてJVMで動作するために使用するいくつかのシンボルが表示されます。これらのことの意味を理解するには、時間をかけて 読む/ する必要があります。このチュートリアルでは、これらのことを定型的なものと考えてください。

重要なことの1つであり、通常、多くの問題の原因はメソッドの名前です。パターン「Java_package_class_method」に従う必要があります。現在、Androidスタジオは優れたサポートを提供しているため、このボイラープレートを自動的に生成し、正しいか名前が付けられていないかを表示できます。この例では、メソッドの名前は「Java_ademar_androidioscppexample_CoreWrapper_concatenateMyStringWithCppString」です。これは「ademar.androidioscppexample」がパッケージであるため、「。」を置き換えるためです。 「_」の場合、CoreWrapperはネイティブメソッドをリンクするクラスであり、「concatenateMyStringWithCppString」はメソッド名そのものです。

メソッドを正しく宣言したので、引数を分析する時が来ました。最初のパラメーターはJNIEnvのポインターです。これはJNIにアクセスする方法です。変換を行うことが重要ですすぐに参照してください。 2番目はjobjectです。これは、このメソッドの呼び出しに使用したオブジェクトのインスタンスです。これはJava "this"と考えることができます。この例では、使用する必要はありませんが、宣言する必要があります。このジョブジェクトの後、メソッドの引数を受け取ります。このメソッドには引数が1つ(ストリング「myString」)しかないため、同じ名前の「jstring」しかありません。また、戻り値の型もjstringであることに注意してください。これは、Javaメソッドが文字列を返すためです。Java/ JNIタイプの詳細については、 それをお読みください。

最後のステップは、JNIタイプをCPP側で使用するタイプに変換することです。この例では、jstringconst char *に変換し、CPPに変換して送信し、結果を取得してjstringに戻します。 JNIの他のすべての手順と同様に、難しくありません。それは定型的なものであり、すべての作業はGetStringUTFCharsおよびNewStringUTFを呼び出すときに受け取るJNIEnv*引数によって行われます。コードをAndroidデバイスで実行する準備ができたら、見てみましょう。

AndroidStudioAndroid

251
ademar111190

上記の優れた回答で説明されているアプローチは、C++ヘッダーから直接オンザフライでラッパーコードを生成する Scapix Language Bridge によって完全に自動化できます。 は次のとおりです。

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