Skip to content

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Using Core C++ library to share code between iOS and Android #634

Closed
mrousavy opened this issue Feb 24, 2021 · 19 comments
Closed

Using Core C++ library to share code between iOS and Android #634

mrousavy opened this issue Feb 24, 2021 · 19 comments

Comments

@mrousavy
Copy link
Contributor

Hi!

I'm developing a wrapper library for MMKV which will be used for the cross-platform framework React Native. Ideally I want to have a single Wrapper <-> MMKV layer, written in C++ which works on Android and iOS. (That's because I'm using JSI and I am somewhat forced to use a shared C++ codebase, so no platform dependent code in Java or ObjC)

I tried to use the MMKVCore pod instead of MMKV:

#include "mmkv-wrapper.h"
#include <MMKVCore/MMKV.h>

But that somehow still imports Obj-C code (NSObject, ..), because I am getting those errors:

Screenshot 2021-02-24 at 09 14 37

Is there a way that I can use MMKV-Core directly to share code between Android and iOS with a single C++ code-base?

Thanks!

@mrousavy
Copy link
Contributor Author

To clarify those errors a bit, when importing the <MMKVCore/MMKV.h> header in my C++ file, it falls under the MMKV_APPLE precondition check and automatically imports <Foundation/Foundation.h>. That doesn't work, since I'm not in a Objective-C++ (.mm) environment, but a pure C++ (.cpp) environment.

Screenshot 2021-02-24 at 09 17 02

@lingol
Copy link
Collaborator

lingol commented Feb 24, 2021

I don't recommend using the C++ Core directly for mobile cross-platform frameworks.
For example, you will likely face crashes due to losing some protection logic, this & that.

You should wrap on top of MMKV for iOS & MMKV for Android. That's how we support the flutter platform.

@mrousavy
Copy link
Contributor Author

Okay I see. I'll try to use Objective-C++ and JNI then, since I'm writing the compatibility layer for React Native in JSI. Thanks @lingol !

@lingol
Copy link
Collaborator

lingol commented Feb 24, 2021

If you really really want to use the Core directly, you can set FORCE_POSIX on the top of the file MMKVPredef.h.
Use it at your own risk.

@mrousavy
Copy link
Contributor Author

mrousavy commented Feb 24, 2021

@lingol I see, thanks. It would work out in my case if I could use the MMKV library on Android from a C++ file (cpp-adapter, with JNIEXPORT) - is that possible?

So in this cpp-adapter I want to call MMKV stuff:

Screenshot 2021-02-24 at 11 10 41

Unfortunately I'm not that experienced with the Android JNI build system and CMake, so I couldn't figure out how to correctly install MMKV and have it imported in my C++ adapter. Do you maybe know how that could work?

I tried to add the MMKV dependency to my build.gradle, but that only made MMKV available in the Java "world", there were no headers available for the C++ adapter.

I also tried to add this to my CMakeLists.txt:

cmake_minimum_required(VERSION 3.4.1)

set (CMAKE_VERBOSE_MAKEFILE ON)
set (CMAKE_CXX_STANDARD 11)

add_library(cpp
            SHARED
            cpp-adapter.cpp
)

include_directories(
            ../cpp
)

find_library(
        MMKV
        mmkv-static
)

target_link_libraries(
        cpp
        ${MMKV}
)

but that gave me the following error:

image

@mrousavy mrousavy reopened this Feb 24, 2021
@lingol
Copy link
Collaborator

lingol commented Feb 24, 2021

Well, you got to learn something about CMake & NDK development.
I could write a CMakeLists.txt for you. But then something else will come up. And you will ask again.
So why not just look it up yourself.

Everything you ask, they are already inside the MMKV repo. You just need to learn to know where to look for them.

@lingol
Copy link
Collaborator

lingol commented Feb 24, 2021

A little hint, you should get started by opening the Android/MMKV folder with Android Studio.

@mrousavy
Copy link
Contributor Author

@lingol you're right, I need to learn more about the NDK build system, I'm coming from the iOS world. Let me rephrase my question then;

Does the com.tencent:mmkv-static gradle dependency provide prefabs so that I can use it as a native dependency? I have followed the "Using native dependencies" Android Guide to try importing MMKV, but I can't get my CMake build system to find it:

/Users/mrousavy/Projects/react-native-mmkv/android/CMakeLists.txt : C/C++ debug|arm64-v8a : CMake Error at /Users/mrousavy/Projects/react-native-mmkv/android/CMakeLists.txt:26 (find_package):
  Could not find a package configuration file provided by "mmkv" with any of
  the following names:

    mmkvConfig.cmake
    mmkv-config.cmake

  Add the installation prefix of "mmkv" to CMAKE_PREFIX_PATH or set
  "mmkv_DIR" to a directory containing one of the above files.  If "mmkv"
  provides a separate development package or SDK, be sure it has been
  installed.
My Files

build.gradle:

buildscript {
  // Buildscript is evaluated before everything else so we can't use getExtOrDefault
  def kotlin_version = rootProject.ext.has('kotlinVersion') ? rootProject.ext.get('kotlinVersion') : project.properties['Mmkv_kotlinVersion']

  repositories {
    google()
    jcenter()
  }

  dependencies {
    classpath 'com.android.tools.build:gradle:4.1.0'
    // noinspection DifferentKotlinGradleVersion
    classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
  }
}

apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'

def getExtOrDefault(name) {
  return rootProject.ext.has(name) ? rootProject.ext.get(name) : project.properties['Mmkv_' + name]
}

def getExtOrIntegerDefault(name) {
  return rootProject.ext.has(name) ? rootProject.ext.get(name) : (project.properties['Mmkv_' + name]).toInteger()
}

android {
  compileSdkVersion getExtOrIntegerDefault('compileSdkVersion')
  buildToolsVersion getExtOrDefault('buildToolsVersion')
  defaultConfig {
    minSdkVersion 16
    targetSdkVersion getExtOrIntegerDefault('targetSdkVersion')
    versionCode 1
    versionName "1.0"

    externalNativeBuild {
        cmake {
            cppFlags "-O2 -frtti -fexceptions -Wall -fstack-protector-all"
            abiFilters 'x86', 'x86_64', 'armeabi-v7a', 'arm64-v8a'
        }
    }

  }

  buildFeatures {
    prefab true // required to add native .aar NDK dependencies from gradle
  }

  externalNativeBuild {
      cmake {
          path "CMakeLists.txt"
      }
  }

  buildTypes {
    release {
      minifyEnabled false
    }
  }
  lintOptions {
    disable 'GradleCompatible'
  }
  compileOptions {
    sourceCompatibility JavaVersion.VERSION_1_8
    targetCompatibility JavaVersion.VERSION_1_8
  }
}

repositories {
  mavenCentral()
  jcenter()
  google()

  def found = false
  def defaultDir = null
  def androidSourcesName = 'React Native sources'

  if (rootProject.ext.has('reactNativeAndroidRoot')) {
    defaultDir = rootProject.ext.get('reactNativeAndroidRoot')
  } else {
    defaultDir = new File(
            projectDir,
            '/../../../node_modules/react-native/android'
    )
  }

  if (defaultDir.exists()) {
    maven {
      url defaultDir.toString()
      name androidSourcesName
    }

    logger.info(":${project.name}:reactNativeAndroidRoot ${defaultDir.canonicalPath}")
    found = true
  } else {
    def parentDir = rootProject.projectDir

    1.upto(5, {
      if (found) return true
      parentDir = parentDir.parentFile

      def androidSourcesDir = new File(
              parentDir,
              'node_modules/react-native'
      )

      def androidPrebuiltBinaryDir = new File(
              parentDir,
              'node_modules/react-native/android'
      )

      if (androidPrebuiltBinaryDir.exists()) {
        maven {
          url androidPrebuiltBinaryDir.toString()
          name androidSourcesName
        }

        logger.info(":${project.name}:reactNativeAndroidRoot ${androidPrebuiltBinaryDir.canonicalPath}")
        found = true
      } else if (androidSourcesDir.exists()) {
        maven {
          url androidSourcesDir.toString()
          name androidSourcesName
        }

        logger.info(":${project.name}:reactNativeAndroidRoot ${androidSourcesDir.canonicalPath}")
        found = true
      }
    })
  }

  if (!found) {
    throw new GradleException(
            "${project.name}: unable to locate React Native android sources. " +
                    "Ensure you have you installed React Native as a dependency in your project and try again."
    )
  }
}

def kotlin_version = getExtOrDefault('kotlinVersion')

dependencies {
  // noinspection GradleDynamicVersion
  api 'com.facebook.react:react-native:+'
  implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
  implementation 'com.tencent:mmkv-static:1.2.7'
}

CMakeLists.txt

cmake_minimum_required(VERSION 3.9.0)

# Needed to locate double-conversion src correctly for folly includes
#execute_process (COMMAND ln "-s" "src" "../../react-native/third-party/double-conversion-1.1.6/double-conversion")

include_directories(
        ../node_modules/react-native/React
        ../node_modules/react-native/React/Base
        ../node_modules/react-native/ReactCommon/jsi
)

add_library(cpp  # <-- Library name
        SHARED
        #MMKV/Core # <-- EXCLUDE_FROM_ALL
        # Provides a relative path to your source file(s).
        ../node_modules/react-native/ReactCommon/jsi/jsi/jsi.cpp
        cpp-adapter.cpp
)

set_target_properties(
        cpp PROPERTIES
        CXX_STANDARD 17
        CXX_EXTENSIONS OFF
        POSITION_INDEPENDENT_CODE ON)

find_package(mmkv REQUIRED CONFIG)

find_library(log-lib log)

target_link_libraries(cpp
        mmkv
        ${log-lib}
        android)

I appreciate any help. ❤️

@mrousavy
Copy link
Contributor Author

JFYI: I have published my project at react-native-mmkv if you want to mention that in this project's README so users know there is an unofficial library for React Native available.

It currently works by using git submodules, which of course requires me to update it everytime an important commit lands.

Using MMKV as a gradle dependency would solve this issue, but afaik this requires you to add Prefabs to the gradle artifact. (see the Android Guide I linked in the comment above)

@lingol
Copy link
Collaborator

lingol commented Feb 25, 2021

I'm glad that you figure it out with something working.

As for your concern about the git submodule. Here's the thing. You get two options.

  1. Use MMKV Core directly, which means compile by source. And it requires the git submodule.
  2. Use MMKV prebuild binary, aka the Gradle packages, which means you have to stick to what MMKV's dynamic lib (libMMKV.so) has exported, which also means you can only access the C methods that have been marked with MMKV_JNI or MMKV_EXPORT. There are two sets of MMKV C methods, one for JNI (MMKV_JNI, native-bridge.cpp), and one for Flutter (MMKV_EXPORT, flutter-bridge.cpp). You probably should use the Flutter one.

@lingol
Copy link
Collaborator

lingol commented Feb 25, 2021

If you like to try the MMKV C methods set for Flutter, there's one thing you should keep in mind. The C methods are intended for internal use only. There are no public headers declare about them. And they could be changed in the future without further notice.

@mrousavy
Copy link
Contributor Author

Hey @lingol - I'm revisiting this topic right now and I am wondering why on Apple, strings and buffers are not just supported as is?

The set(std::string, ...), getString(...) and getBytes(...) methods would compile without any problems, yet they are removed on Apple.

MMKV/Core/MMKV.h

Lines 278 to 304 in 33003ea

#ifdef MMKV_APPLE
bool set(NSObject<NSCoding> *__unsafe_unretained obj, MMKVKey_t key);
bool set(NSObject<NSCoding> *__unsafe_unretained obj, MMKVKey_t key, uint32_t expireDuration);
NSObject *getObject(MMKVKey_t key, Class cls);
#else // !defined(MMKV_APPLE)
bool set(const char *value, MMKVKey_t key);
bool set(const char *value, MMKVKey_t key, uint32_t expireDuration);
bool set(const std::string &value, MMKVKey_t key);
bool set(const std::string &value, MMKVKey_t key, uint32_t expireDuration);
bool set(const mmkv::MMBuffer &value, MMKVKey_t key);
bool set(const mmkv::MMBuffer &value, MMKVKey_t key, uint32_t expireDuration);
bool set(const std::vector<std::string> &vector, MMKVKey_t key);
bool set(const std::vector<std::string> &vector, MMKVKey_t key, uint32_t expireDuration);
// inplaceModification is recommended for faster speed
bool getString(MMKVKey_t key, std::string &result, bool inplaceModification = true);
mmkv::MMBuffer getBytes(MMKVKey_t key);
bool getBytes(MMKVKey_t key, mmkv::MMBuffer &result);
bool getVector(MMKVKey_t key, std::vector<std::string> &result);
#endif // MMKV_APPLE

I feel like the NSObject<NSCoding> methods could be used as additional helper methods, but they don't need to fully replace the std::string and buffer methods, unless I'm missing something here?

Thank you!

@lingol
Copy link
Collaborator

lingol commented Mar 26, 2024

It's mainly because ObjC already has NSString & NSData that does the job of std::string & MMBuffer. In practice, an iOS dev would most certainly use them instead of these C++ STL replacements.

Adding the C++ part back in iOS is OK. But it will increase the binary size a bit. And it might force iOS users to rename an ObjC file (.m) to an ObjC++ file (.mm) when they import <MMKV/MMKV.h>.

@mrousavy
Copy link
Contributor Author

I see, well in my case I'm not using the ObjC classes, since I bridge MMKV to JavaScript/React Native, and that's all C++ types (std::string, raw buffers, ...), so the additional NS.. conversion step could ideally be avoided.

Would you be interested in a PR where I expose those C++ specific methods? only under a #ifdef __cplusplus flag

@lingol
Copy link
Collaborator

lingol commented Mar 26, 2024

Don't bother. At this time, as long as you don't support iOS 12 (well maybe even lower), you can just use MMKVCore instead.

@mrousavy
Copy link
Contributor Author

Well yea, but MMKVCore does not expose the std::string, MMBuffer, and std::vector methods if the target OS is Apple.
I was talking about a PR where those methods are also exposed, if compiled with __cplusplus flag?

@mrousavy
Copy link
Contributor Author

Wait the whole Core/MMKV.h header is only available with __cplusplus:

#ifdef __cplusplus

So I guess the std::string, MMBuffer, etc. methods are all available, even if imported in Objective-C (since you import it in libMMKV.mm.

So I guess I'm talking about something like this: #1260

@lingol lingol reopened this Mar 27, 2024
@lingol
Copy link
Collaborator

lingol commented Mar 27, 2024

One more thing, if all you care about is the C++ interface, defining FORCE_POSIX is the way to go. It will make the type of a key as std::string as well.
https://github.com/Tencent/MMKV/blob/master/Core/MMKVPredef.h#L28

@mrousavy
Copy link
Contributor Author

Yea, I just played around with that more and I think you're right. My PR probably isn't going to be a full solution anyways, since keys are still NSString, etc.

So let's close that PR, and I'll just roll with FORCE_POSIX, thanks 👍

@Tencent Tencent locked and limited conversation to collaborators Mar 28, 2024
@lingol lingol converted this issue into discussion #1262 Mar 28, 2024

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants