C++ 静态库:实现 wrappers/bridges 中针对本机移动方法的定义(Java、Objective-C)

C++ static library: implement definitions in wrappers/bridges toward native mobile methods (Java, Objective-C)

我有一个 C++ 静态库 (.a),我想在本机移动开发中使用它。

为此,我必须在 C++ 和本机之间建立桥梁 languages/SDKs :

现在,假设我的 C++ 静态库中实现的代码严重依赖于这个函数:

const void sendRawData(std::vector<unsigned char> data);

事情是这样的:这个方法没有在我的库中实现,只是定义了。对于前面提到的所有三个目标,都有类似的方法:例如 java.

中的 void sendData(String data);

是否可以编译 - 例如 - Android 库(.aar 文件),其中静态库方法中的 sendRawData 符号在我的部分实现android-studio 项目,在哪桥接到前面提到的原生类似方法?

关于iOS,我们可以使用xcode生成一个.framework库,方法类似。

感谢您的帮助。

此答案正在进行中。

因此,您想采用 C++ 静态库并将其中一个函数(在库的编译中未定义)桥接到本机移动函数 (iOS/Android/Windows Phone)。


首先最重要的问题:

你为什么要这样做?

在我的例子中, 它是代码维护的混合体(让三个团队在三个不同的 code-bases 上工作只是为了添加一个小功能很烦人),以及降低项目成本。假设您正在处理一些高度敏感的代码,您很可能必须对处理逻辑的整个代码进行认证。三个目标意味着三倍的认证。因此,在 C++ 库中开发程序的整个逻辑非常重要,该库将 完全 与其他平台(iOS、Android , Windows Phone), 这意味着完全相同的 CRC32.


我们要怎么做?

我们将在每个平台上完成 walk-through 如何做到这一点。在示例中,我们将声明一个 C++ static void NativePrint(std::string str) 方法,该方法将由名为 NativeAPI 的 C++ 模块调用。此 API 稍后将暴露给移动开发人员(我们将这样做,使用 Djinni)以实际允许他们调用您的通用 C++ 方法。

这可以分为 6 个步骤:

1。编译静态库

CallsNativePrint.h:

#pragma once
#include <cstdint>
class CallsNativePrint
{
public:
    static void __attribute__((weak)) NativePrint(int32_t i);
};

NativeAPI.h:

#pragma once
#include "CallsNativePrint.h"
class NativeAPI
{
public:
    static void IncrementAndCallNativePrint(int32_t i);
};

NativeAPI.cpp:

#include "NativeAPI.h"
void NativeAPI::IncrementAndCallNativePrint(int32_t i)
{
    // You can here do whatever you want with str.
    // The purpose of this exercise is to define a logic inside
    // of your C++ code, not just to call native methods.
    i++;
    CallsNativePrint::NativePrint(i);
}

编译:Makefile

.PHONY:all
all:
    gcc -std=c++11 -c NativeAPI.cpp -o NativeAPI.o
    ar rcs libNativePrint.a NativeAPI.o

完成这一步后,您就可以正确编译本机库了。其中最重要的部分在 CallsNativePrint.h 中:static void __attribute__((weak)) NativePrint(std::string str);

__attribute__((weak)) 部分告诉编译器该函数的符号将弱链接,这意味着我们稍后可以用一个强一(默认情况下所有符号都是强的)。

一旦这一步结束,我们可以检查编译后的库并检查我们的函数的符号确实是 weakly-typed:

$ nm libNativePrint.a

NativeAPI.o:
   SUCCESS -->   w _ZN16CallsNativePrint11NativePrintEi
0000000000000000 T _ZN9NativeAPI27IncrementAndCallNativePrintEi
0000000000000000 r _ZStL19piecewise_construct

对于接下来的步骤,我们将把它分成 Android 和 iOS,从 Android 开始。

Android

2。创建 Android Studio 项目

为了限制我们的工作,我们将让 Android Studio 生成项目的整个 back-bone。然后,我们将使用它的构建器工具将我们的 C++ 静态库合并到解决方案中。

继续创建一个新的 Android Studio 项目:

  • 姓名:MyNativeAndroidLibrary
  • 公司域名:overflow.stack.com
  • 包括 C++ 支持:选中。
  • Select 您的应用 运行 的外形规格:我检查了“Phone 和平板电脑”的情况,“API 15:Android 4.0.3" 虽然我怀疑它的重要性。
  • 不要添加任何 activity。
  • 我无一例外地选择了C++11工具链或者运行时间类型信息支持

首先,我们将在 android-studio 中使用 cmake 编译我们的 previously-created 源代码。这将使事情变得更容易,因为 android-studio 将自动生成每个需要的架构并将它们放在正确的文件夹中。

让你的项目检查员处于 Android 模式,前往 ExternalBuildFiles/CMakeLists.txt,删除其中的所有内容,然后添加:

cmake_minimum_required(VERSION 3.4.1)

# Path to our source code
set(LIB_SRC_DIR ${CMAKE_SOURCE_DIR}/../../native-library-cpp)

# Add our c++ static library (android-studio recompiles it)
add_library(native-library-cpp STATIC ${LIB_SRC_DIR}/NativeAPI.cpp)
include_directories(${LIB_SRC_DIR}/)

# Native shared library generated by android-studio: will contain JNI code
add_library(native-lib SHARED src/main/cpp/native-lib.cpp)
include_directories(src/main/cpp/)

# Find native Android logging library: that's what we'll call!
find_library(log-lib log)
# Link the JNI shared library with our static library
target_link_libraries(native-lib
                      native-library-cpp log-lib)

确保将 LIB_SRC_DIR 路径更改为与您的匹配的路径。我的 Android Studio 项目目录和包含本机 C++ 库的目录在同一目录中。

然后,我们要配置Android Studio 生成一个.aar 文件。转到 app:build.gradle,并将第一行 (apply plugin: 'com.android.application') 更改为 apply plugin: 'com.android.library'。您还必须删除行 applicationId,否则构建将失败。

一旦完成,我们就可以开始将 C++ 方法桥接到我们的本机 Java 方法!

3。将 C++ NativePrint 方法桥接到本机 Android 函数

为此,我们将在 Android Studio 自动生成的 native-lib.cpp 文件中编写代码。首先,创建一个包含以下内容的 native-lib.h 文件(通过 right-clicking on native-lib.cpp):

#pragma once

#include <cstdint>
#include <cstdbool>
#include <string>

#include <android/log.h>
#include <jni.h>

// JNI fields used to link C++ to Java
extern JavaVM   *jvm;
extern JNIEnv   *env;
extern jclass    cls;
extern jmethodID mid;

class CallsNativePrint
{
private:
    // JNI initializers
    static bool _hasBeenInitialized;
    static void InitializeJNIFields();
    static bool GetNativeMethod();

public:
    static void NativePrint(int32_t i); // We're going to re-define this symbol
    // Note that it is already existing in native-library-cpp, as a weak symbol.
};

这个header定义了一些东西:

  • 它包括其中使用的任何内容。
  • 它声明了一些 extern JNI-related 变量:这实际上被推荐为 JNI 最佳实践。获取对 java classes 或方法的引用需要很长时间。你不想每次打电话都这样做 JNI 方法。获取对 JVM/Java 环境的引用也是如此。
  • 它声明了 CallsNativePrint class。在那里,我们 re-declare static void NativePrint(int32_t i) 方法。一旦定义,它的符号将删除静态库中的那个,它是每次调用IncrementAndCallNativePrint时都会调用的方法!

现在,在 native-lib.cpp 中,我们必须转储所有这些文档化代码:

#include <jni.h>
#include <string>

#include "native-lib.h"

// JNI-related variables declaration
JavaVM   *jvm;
JNIEnv   *env;
jclass    cls;
jmethodID mid;

bool CallsNativePrint::_hasBeenInitialized = false;

void CallsNativePrint::InitializeJNIFields()
{
    env->GetJavaVM(&jvm);
    JavaVMAttachArgs args;
    args.version = JNI_VERSION_1_6;
    args.name = NULL;
    args.group = NULL;
    jvm->AttachCurrentThread(&env, &args);
    CallsNativePrint::_hasBeenInitialized = true;
}

// Find a Java method based on class name ("JavaBridge"),
// method name ("PrintInteger"), and JNI prototype string ("(I)V").
bool CallsNativePrint::GetNativeMethod()
{
    if (cls || mid)
    {
        __android_log_write(ANDROID_LOG_INFO, "native-lib",
                            "Native method has already been recovered!");
        return true;
    }

    cls = env->FindClass("JavaBridge");
    if (!cls) {
        __android_log_write(ANDROID_LOG_ERROR, "native-lib",
                            "Couldn't find Java JavaBridge class!");
        return false;
    }

    jmethodID mid = env->GetStaticMethodID(cls, "PrintInteger",
                                           "(I)V");
    if (mid == nullptr)
    {
        __android_log_write(ANDROID_LOG_ERROR, "native-lib",
                            "Couldn't find Java StaticRuaBridge::SendRawCommandBridge method!");
        return false;
    }

    return true;
}

// Converts int32_t to jint (no conversion needed in this case!)
// then calls the `mid` method recovered in GetNativeMethod().
void CallsNativePrint::NativePrint(int32_t i)
{
    if (!_hasBeenInitialized) { InitializeJNIFields(); }

    if (GetNativeMethod())
    {
        jint ji = i;
        env->CallStaticVoidMethod(cls, mid, ji); // Call the JavaBridge method!
    }
}

完成后,我们必须实际编写从 JNI 调用的 Java 代码。创建一个JavaBridge.java java class(右击java/文件夹),然后pu其中:

package com.stack.overflow.mynativeandroidlibrary;

import android.util.Log;

public class JavaBridge {
    public static void PrintInteger(int i) {
        Log.i("JavaBridge", "PrintInteger: " + i);
    }
}

如果正确完成,一旦一切都链接在一起,CallsNativePrint::NativePrint(int32_t i) 现在应该关联到 native-lib.cpp 末尾的代码,这意味着 JNI 代码直接调用 Java 方法。成功!

现在,如果我们希望我们的库有任何用处,我们需要公开一个 Java API,这将允许 Android 开发人员直接调用我们的 C++ 中定义的方法图书馆 ;在我们的例子中,它将是从 Java 到 IncrementAndCallNativePrint(int32_t i)!

的直接调用

要检查一切是否正常,您可以通过单击 Build -> Build APK 上的顶部栏生成一个 .aar 文件。

4。向 Android 开发人员

公开 C++ 方法

对于这部分,我们将广泛使用Djinni.首先,安装它。

然后,在项目的根目录下创建一个 djinni-generation/ 文件夹。在此项目中,创建一个包含以下内容的 api.djinni 文件:

NativeAPIExposer = interface +c {
    static IncrementAndCallNativePrint(i: i32);
}

使用djinni,这将允许我们创建描述此API的C++、Objective-C和Java文件(将在C, 注意 +c!).

创建另一个名为 run-djinni.sh 的文件,内容如下:

#! /usr/bin/env /bin/bash

base_dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
cpp_out="$base_dir/generated-src/cpp"
jni_out="$base_dir/generated-src/jni"
java_out="$base_dir/generated-src/java/com/stack/overflow"
objc_out="$base_dir/generated-src/objc"

java_package="com.stack.overflow"
namespace="native_api"
djinni="$base_dir/api.djinni"

mkdir ./generated-src/
djinni                                \
   --java-out $java_out               \
   --java-package $java_package       \
   --ident-java-field mFooBar         \
                                      \
   --cpp-out $cpp_out                 \
   --cpp-namespace $namespace         \
                                      \
   --jni-out $jni_out                 \
   --ident-jni-class NativeFooBar     \
   --ident-jni-file NativeFooBar      \
                                      \
   --objc-out $objc_out               \
   --objc-type-prefix DB              \
                                      \
   --idl $djinni

完成后,generated-src/ 文件夹将像这样填充:

$ tree -pf ./generated-src/
generated-src
├── [drwxrwxr-x]  generated-src/cpp
│   └── [-rw-rw-r--]  generated-src/cpp/NativeAPIExposer.hpp
├── [drwxrwxr-x]  generated-src/java
│   └── [drwxrwxr-x]  generated-src/java/com
│       └── [drwxrwxr-x]  generated-src/java/com/stack
│           └── [drwxrwxr-x]  generated-src/java/com/stack/overflow
│               └── [-rw-rw-r--]  generated-src/java/com/stack/overflow/NativeAPIExposer.java
├── [drwxrwxr-x]  generated-src/jni
│   ├── [-rw-rw-r--]  generated-src/jni/NativeNativeAPIExposer.cpp
│   └── [-rw-rw-r--]  generated-src/jni/NativeNativeAPIExposer.hpp
└── [drwxrwxr-x]  generated-src/objc
    └── [-rw-rw-r--]  generated-src/objc/DBNativeAPIExposer.h

对于Android项目,抓取generated-src/cpp/generated-src/java/generated-src/jni/中的文件,并将它们导入到Android中的相应文件夹中工作室。

注意:如果找不到jni/文件夹,请从Android项目视图切换到Project项目视图。

我们现在必须实现 NativeAPIExposer 方法。在 cpp/ 文件夹中创建一个 NativeAPIExposer.cpp 文件,内容如下:

#include "NativeAPIExposer.hpp"
#include "NativeAPI.h"

void native_api::NativeAPIExposer::IncrementAndCallNativePrint(int32_t i)
{
    NativeAPI::IncrementAndCallNativePrint(i); // Call the native method!
}

我们现在必须编译新的库并将它们添加到 CMakeLists.txt 文件中:

# Native API exposer
set(DJINNI_PATH RELATIVE/PATH/TO/DJINNI)
add_library(native-api-lib SHARED
            src/main/cpp/NativeAPIExposer.cpp
            src/main/jni/NativeNativeAPIExposer.cpp
            ${DJINNI_PATH}/support-lib/jni/djinni_main.cpp
            ${DJINNI_PATH}/support-lib/jni/djinni_support.cpp)
include_directories(src/main/cpp/ src/main/jni/ ${DJINNI_PATH}/support-lib/jni/)

编辑以下行:

target_link_libraries(native-lib native-library-cpp log-lib)

收件人:

target_link_libraries(native-lib native-library-cpp log-lib native-api-lib)

5。生成 .aar

如前所述,此时您只需点击Build -> Build APK上的顶部栏即可。

6.在另一个 Android 项目

中使用本机库

首先创建一个新的 Android Studio 项目(有一个空的 activity),然后添加生成的 .aar 作为依赖项。

这样做:

  • File -> New -> New Module, « Import .JAR/.AAR Package », 和 select 由 Android Studio 生成的,应该在 "MyNativeAndroidLibrary/app/build/outputs/aar/" .
  • 右键单击您的项目 -> 打开模块设置 -> 依赖项 -> 添加模块依赖项 -> 添加我们自己的模块。

在 activity 的代码中,您应该可以使用以下代码访问我们的函数:

iOS

2。创建 Android Studio 项目

占位符

3。将 C++ NativePrint 方法桥接到本机 iOS 函数

占位符

4。将 C++ 方法导出给 iOS 开发人员

对于这部分,我们将大量使用 Djinni.

5。生成 .framework

占位符

6.在另一个 iOS 项目

中使用本机库