Integrating Conan in Android Studio

At the Cross building to Android with the NDK we learned how to build a package for Android using the NDK. In this example we are going to learn how to do it with the Android Studio and how to use the libraries in a real Android application.

Creating a new project

First of all, download and install the Android Studio IDE.

Then create a new project selecting Native C++ from the templates.

In the next wizard window, select a name for your application, for example MyConanApplication, you can leave the “Minimum SDK” with the suggested value (21 in our case), but remember the value as we are using it later in the Conan profile at os.api_level`

In the “Build configuration language” you can choose between Groovy DSL (build.gradle) or Kotlin DSL (build.gradle.kts) in order to use conanInstall task bellow.

Select a “C++ Standard” in the next window, again, remember the choice as later we should use the same in the profile at compiler.cppstd.

In the project generated with the wizard we have a folder cpp with a native-lib.cpp. We are going to modify that file to use zlib and print a message with the used zlib version. Copy only the highlighted lines, it is important to keep the function name.

native-lib.cpp

  1. #include <jni.h>
  2. #include <string>
  3. #include "zlib.h"
  4. extern "C" JNIEXPORT jstring JNICALL
  5. Java_com_example_myconanapp_MainActivity_stringFromJNI(
  6. JNIEnv* env,
  7. jobject /* this */) {
  8. std::string hello = "Hello from C++, zlib version: ";
  9. hello.append(zlibVersion());
  10. return env->NewStringUTF(hello.c_str());
  11. }

Now we are going to learn how to introduce a requirement to the zlib library and how to prepare our project.

Introducing dependencies with Conan

conanfile.txt

We need to provide the zlib package with Conan. Create a file conanfile.txt in the cpp folder:

conanfile.txt

  1. [requires]
  2. zlib/1.2.12
  3. [generators]
  4. CMakeToolchain
  5. CMakeDeps
  6. [layout]
  7. cmake_layout

build.gradle

We are going to automate calling conan install before building the Android project, so the requires are prepared, open the build.gradle file in the My_Conan_App.app (Find it in the Gradle Scripts section of the Android project view). Paste the task conanInstall contents after the plugins and before the android elements:

GroovyKotlin

build.gradle

  1. plugins {
  2. ...
  3. }
  4. task conanInstall {
  5. def conanExecutable = "conan" // define the path to your conan installation
  6. def buildDir = new File("app/build")
  7. buildDir.mkdirs()
  8. ["Debug", "Release"].each { String build_type ->
  9. ["armv7", "armv8", "x86", "x86_64"].each { String arch ->
  10. def cmd = conanExecutable + " install " +
  11. "../src/main/cpp --profile android -s build_type="+ build_type +" -s arch=" + arch +
  12. " --build missing -c tools.cmake.cmake_layout:build_folder_vars=['settings.arch']"
  13. print(">> ${cmd} \n")
  14. def sout = new StringBuilder(), serr = new StringBuilder()
  15. def proc = cmd.execute(null, buildDir)
  16. proc.consumeProcessOutput(sout, serr)
  17. proc.waitFor()
  18. println "$sout $serr"
  19. if (proc.exitValue() != 0) {
  20. throw new Exception("out> $sout err> $serr" + "\nCommand: ${cmd}")
  21. }
  22. }
  23. }
  24. }
  25. android {
  26. compileSdk 32
  27. defaultConfig {
  28. ...

build.gradle.kts

  1. plugins {
  2. ...
  3. }
  4. tasks.register("conanInstall") {
  5. val conanExecutable = "conan" // define the path to your conan installation
  6. val buildDir = file("app/build")
  7. buildDir.mkdirs()
  8. val buildTypes = listOf("Debug", "Release")
  9. val architectures = listOf("armv7", "armv8", "x86", "x86_64")
  10. doLast {
  11. buildTypes.forEach { buildType ->
  12. architectures.forEach { arch ->
  13. val cmd = "$conanExecutable install ../../src/main/cpp --profile android-studio " +
  14. "-s build_type=$buildType -s arch=$arch --build missing " +
  15. "-c tools.cmake.cmake_layout:build_folder_vars=['settings.arch']"
  16. println(">> $cmd")
  17. val proc = ProcessBuilder(cmd.split(" "))
  18. .directory(buildDir)
  19. .start()
  20. val result = proc.inputStream.bufferedReader().readText()
  21. val errors = proc.errorStream.bufferedReader().readText()
  22. proc.waitFor()
  23. if (proc.exitValue() != 0) {
  24. throw Exception("Execution failed! Output: $result Error: $errors")
  25. }
  26. println(result)
  27. if (errors.isNotBlank()) {
  28. println("Errors: $errors")
  29. }
  30. }
  31. }
  32. }
  33. }
  34. tasks.named("preBuild").configure {
  35. dependsOn("conanInstall")
  36. }
  37. android {
  38. compileSdk 32
  39. defaultConfig {
  40. ...

The conanInstall task is calling conan install for Debug/Release and for each architecture we want to build, you can adjust these values to match your requirements.

If we focus on the conan install task we can see:

  1. We are passing a --profile android, so we need to create the profile. Go to the profiles folder in the conan config home directory (check it running conan config home) and create a file named android with the following contents:

    System NDKConan NDK package

    1. include(default)
    2. [settings]
    3. os=Android
    4. os.api_level=21
    5. compiler=clang
    6. compiler.version=12
    7. compiler.libcxx=c++_static
    8. compiler.cppstd=14
    9. [conf]
    10. tools.android:ndk_path=/opt/homebrew/share/android-ndk
    1. include(default)
    2. [settings]
    3. os=Android
    4. os.api_level=21
    5. compiler=clang
    6. compiler.version=12
    7. compiler.libcxx=c++_static
    8. compiler.cppstd=14
    9. [tool_requires]
    10. *: android-ndk/r26d

    You might need to modify:

    • tools.android:ndk_path conf: The location of the NDK provided by Android Studio. You should be able to see the path to the NDK if you open the cpp/includes folder in your IDE.

    • compiler.version: Check the NDK documentation or find a bin folder containing the compiler executables like x86_64-linux-android31-clang. In a Macos installation it is found in the NDK path + toolchains/llvm/prebuilt/darwin-x86_64/bin. Run ./x86_64-linux-android31-clang --version to check the running clang version and adjust the profile.

    • compiler.libcxx: The supported values are c++_static and c++_shared.

    • compiler.cppstd: The C++ standard version, this should be the value you selected in the Wizard.

    • os.api_level: Use the same value you selected in the Wizard.

  2. We are passing -c tools.cmake.cmake_layout:build_folder_vars=['settings.arch'], thanks to that, Conan will create a different folder for the specified settings.arch so we can have all the configurations available at the same time.

To make Conan work we need to pass CMake a custom toolchain. We can do it introducing a single line in the same file, in the android/defaultConfig/externalNativeBuild/cmake element:

build.gradle

  1. android {
  2. compileSdk 32
  3. defaultConfig {
  4. applicationId "com.example.myconanapp"
  5. minSdk 21
  6. targetSdk 21
  7. versionCode 1
  8. versionName "1.0"
  9. testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
  10. externalNativeBuild {
  11. cmake {
  12. cppFlags '-v'
  13. arguments("-DCMAKE_TOOLCHAIN_FILE=conan_android_toolchain.cmake")
  14. }
  15. }

conan_android_toolchain.cmake

Create a file called conan_android_toolchain.cmake in the cpp folder, that file will be responsible of including the right toolchain depending on the ANDROID_ABI variable that indicates the build configuration that the IDE is currently running:

conan_android_toolchain.cmake

  1. # During multiple stages of CMake configuration, the toolchain file is processed and command-line
  2. # variables may not be always available. The script exits prematurely if essential variables are absent.
  3. if ( NOT ANDROID_ABI OR NOT CMAKE_BUILD_TYPE )
  4. return()
  5. endif()
  6. if(${ANDROID_ABI} STREQUAL "x86_64")
  7. include("${CMAKE_CURRENT_LIST_DIR}/build/x86_64/${CMAKE_BUILD_TYPE}/generators/conan_toolchain.cmake")
  8. elseif(${ANDROID_ABI} STREQUAL "x86")
  9. include("${CMAKE_CURRENT_LIST_DIR}/build/x86/${CMAKE_BUILD_TYPE}/generators/conan_toolchain.cmake")
  10. elseif(${ANDROID_ABI} STREQUAL "arm64-v8a")
  11. include("${CMAKE_CURRENT_LIST_DIR}/build/armv8/${CMAKE_BUILD_TYPE}/generators/conan_toolchain.cmake")
  12. elseif(${ANDROID_ABI} STREQUAL "armeabi-v7a")
  13. include("${CMAKE_CURRENT_LIST_DIR}/build/armv7/${CMAKE_BUILD_TYPE}/generators/conan_toolchain.cmake")
  14. else()
  15. message(FATAL "Not supported configuration")
  16. endif()

CMakeLists.txt

Finally, we need to modify the CMakeLists.txt to link with the zlib library:

CMakeLists.txt

  1. cmake_minimum_required(VERSION 3.18.1)
  2. project("myconanapp")
  3. add_library(myconanapp SHARED native-lib.cpp)
  4. find_library(log-lib log)
  5. find_package(ZLIB CONFIG)
  6. target_link_libraries(myconanapp ${log-lib} ZLIB::ZLIB)

Building the application

If we build our project we can see that conan install is called multiple times building the different configurations of zlib.

Then if we run the application in a Virtual Device or in a real device pairing it with the QR code we can see:

Android application showing the zlib 1.2.11

Once we have our project configured, it is very easy to change our dependencies and keep developing the application, for example, we can edit the conanfile.txt file and change the zlib to the version 1.12.2:

  1. [requires]
  2. zlib/1.2.12
  3. [generators]
  4. CMakeToolchain
  5. CMakeDeps
  6. [layout]
  7. cmake_layout

If we click build and then run the application, we will see that the zlib dependency has been updated:

Android application showing the zlib 1.2.12