如何在Android和iOS上使用相同的C++代码?

135

Android配合NDK支持C/C++代码,iOS也配合Objective-C ++提供支持,那么如何编写原生C/C++代码的应用程序,以在Android和iOS之间共享呢?


1
尝试使用cocos2d-x框架。 - glo
@glo 看起来不错,但我正在寻找更通用的东西,使用C++而不需要框架,“显然排除JNI”。 - ademar111190
2个回答

308

更新。

即使我写下这个答案已经有四年了,但它仍然很受欢迎。在这四年中,许多事情发生了变化,因此我决定更新我的答案,以更好地适应我们当前的现实。 答案的思路没有改变;实现略有变化。我的英语也发生了很大改善,所以现在每个人都能更容易地理解这个答案。

请查看repo,以便您下载并运行我将在下面展示的代码。

答案

在我展示代码之前,请先看以下图表。

Arch

每个操作系统都有自己的用户界面和特点,因此我们打算针对各平台编写特定代码。 另一方面,所有逻辑代码、业务规则和可共享的东西都打算使用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对<<接收到的文本>>说hello”。

共享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应用程序,唯一的区别是文件的扩展名为.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

看看应用程序的外观:

Xcode iPhone

Android

现在是时候进行Android集成了。Android使用Gradle作为构建系统,并且使用CMake来编译C/C++代码。所以我们需要在gradle文件中配置CMake:

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使用的一些符号,如JNIEXPORTJNICALL。为了理解这些东西的含义,有必要花点时间去阅读文档。对于本教程目的,只需将这些内容视为样板。

一个重要的事情,通常是问题的根源是方法的名称;它需要遵循模式“Java_package_class_method”。目前,Android Studio对此有很好的支持,因此它可以自动生成此样板,并在命名是否正确时向您显示。在我们的示例中,我们的方法被命名为“Java_ademar_androidioscppexample_CoreWrapper_concatenateMyStringWithCppString”,因为“ademar.androidioscppexample”是我们的包名,因此我们用“_”替换“。”,“CoreWrapper”是连接本地方法的类,“concatenateMyStringWithCppString”是方法名本身。

由于我们已经正确声明了方法,现在是分析参数的时候了。第一个参数是一个JNIEnv指针,这是我们访问JNI内容的方式,因此我们必须进行转换,很快您就会看到。第二个参数是一个jobject,它是调用此方法所使用的对象的实例。您可以将其视为Java中的“this”,在我们的示例中,我们不需要使用它,但仍需要声明它。在此jobject之后,我们将接收方法的参数。因为我们的方法只有一个参数 - 一个字符串“myString”,我们只有一个具有相同名称的“jstring”。还要注意我们的返回类型也是jstring。这是因为我们的Java方法返回一个字符串,有关Java / JNI类型的更多信息,请阅读文档。

最后一步是将JNI类型转换为我们在CPP侧使用的类型。在我们的示例中,我们将jstring转换为const char *,将其转换为CPP发送,获得结果并转换回jstring。像JNI上的所有其他步骤一样,它并不难;它只是样板文件,所有工作都是由我们调用GetStringUTFCharsNewStringUTF时接收到的JNIEnv*参数完成的。完成后,我们的代码即可在Android设备上运行,让我们来看一下。

AndroidStudio Android


12
谢谢您的夸奖!我不太明白您的意思,但感谢您在Stack Overflow上给出高质量的回答。 - Michael Rodrigues
17
到目前为止最有帮助的帖子。这个帖子不应该被关闭。 - Jared Burrows
6
@JaredBurrows,我同意。投票支持重新开放。 - OmnipotentEntity
4
顺便提一下,您不必在ViewController的实现中使用Objective-C++,因为您正在导入标准的#import "CoreWrapper.h",所以可以使用标准的Objective-C,您的ViewController实现中没有引用C++。 - dulaccc
4
JNI包装器很长是因为它使用了包名+类名+方法名,这是JNI的工作方式,我别无选择。在cpp中,我试图让它变得明确,因为这是一种方法而不是美容比赛。 - ademar111190
显示剩余15条评论

5

上述优秀答案中描述的方法可以完全由Scapix语言桥自动化,该桥会直接从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});
    }
}

这个支持 Kotlin(Android)吗?还是只能使用 Java? - user1300214
1
@Pixel Scapix 生成 Java 绑定,非常容易从 Kotlin 中使用:https://kotlinlang.org/docs/java-interop.html - Boris Rasin

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接