Являясь не Android разработчиком, но имея хорошие базовые знания в Java, мне выпала небольшая research задача под Android платформу, для решения которой нужно было интегрировать стороннюю с/c++ библиотеку в Android Studio проект. В данном статье будет:


  • пошаговое описание как собрать c/c++ проект, который корректно настроен под CMake систему сборки
  • интеграция полученной библиотеки в Android проект через Android Studio

Введение


Поиск по ключевым словам в русскоязычном сегменте интернета на удивление дал мало результатов на данную тему. Но нашлась единственная довольно подробная статья на хабре https://habr.com/ru/company/e-Legion/blog/487046/, с которой вы заметите как сходства, так и различия. Для сравнения подходов решил так же для примера использовать проект с открытым исходным кодом https://opus-codec.org, что и в указанной статье. Система на которой будут выполняться все эксперименты MacOS Big Sur имея на борту cmake версии 3.19.3.


Сразу хочу отметить, что не являюсь экспертом по CMake и Android. Все действия, описанные в статье, вы делаете на свой страх и риск и автор не несет никакой ответственности за ваше потраченное время.


Начало


Являясь мобильным разработчиком довольно редко имею дело с c/c++ библиотеками, а уж с CMake системой сборки и подавно. Но задача интересная, хоть немного отвлечься от рутинного натягивания json на UI. Сроки вменяемые, можно во всем разобраться самому, а не копипастить решения со стэковерфлоу. Итак, давайте скачаем проект с гитхаба и начнем наш путь в мир приключений:


git clone git@github.com:xiph/opus.git
cd opus

Касательно CMake, до того как я принялся за этот мини проект, я знал что:


  1. это система сборки проектов
  2. описание как собрать проект находится в CMakeLists.txt файле
  3. сборка идет в два этапа:

  • подготавливаем проект к сборке

cmake -S . -B ./build

где через -S мы указываем где лежит CMakeLists.txt, в данном случае мы находимся там же где и проект, поэтому передаем текущую директорию, а через -B передаем директорию куда сложить результаты подготовки проекта к сборке.


Давайте запустим эту команду и посмотрим, что будет. На выходе получаем много текста. Текст описывает как прошла настройка проекта к сборке под текущий default toolchain. Из всего этого нам только интересны следующие строчки


-- The C compiler identification is AppleClang 13.0.0.13000029
-- Check for working C compiler: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/cc - skipped

То есть умолчанию для компиляции проекта CMake выставил локальный clang компилятор. Более того, для сборки статической библиотеки нужен не только компилятор, но и линковщик, и другие вспомогательные программы, хотя в логах мы их и не видим, но если посмотреть созданный файл ./build/CMakeCache.txt то там вы найдете более полную информацию какие именно вспомогательные программы выставились. Мы заострим наше внимание только на некоторых из них, а именно:


CMAKE_C_COMPILER:FILEPATH=/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/cc
CMAKE_AR:FILEPATH=/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/ar
CMAKE_LINKER:FILEPATH=/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/ld
CMAKE_RANLIB:FILEPATH=/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/ranlib
CMAKE_STRIP:FILEPATH=/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/strip

Как можно видеть все эти программы относятся к общей директории XcodeDefault.xctoolchain.


  • Компиляция проекта и создание статической библиотеки

cmake --build ./build

Снова вызываем cmake, но уже с другими параметрами, а именно с параметром --build и затем директорию с предыдущего шага (если библиотека долго собирается, то через параметр -j можно указать количество потоков cmake может использовать для сборки проекта). И видим успешную сборку библиотеки:


[100%] Linking C static library libopus.a
[100%] Built target opus

Мы собрали статическую библиотеку, осталось ее только интегрировать в проект. Но сначала давайте посмотрим для какой архитектуры наша библиотека собрана:


lipo -archs ./build/libopus.a

где результат будет x86_64, что говорит нам о том, что библиотека собрана под локальную машину, потому что CMake так настроил проект на первом этапе (на борту используемой для сборки машины стоит Intel Core i5, если бы мы собирали проект на новых Apple M чипах, то архитектура была бы arm64). Причем библиотека скомпилирована локальным компилятором используя локальный toochain. То есть, даже если у нас будет другой компьютер, например, с Ubuntu на том же Intel Core i5 и мы поместим собранную нами библиотеку на него, то наша библиотека не будет совместима с локальными Ubuntu бинарниками, потому что наша библиотека собрана с другим toolchain (в данном случае MacOS x86_64).


Из всего этого следует вывод, чтобы собрать библиотеку под Android, нам нужно чтобы CMake использовал вместо локального toolchain, toochain который предоставляет Android платформа. Как этого делать я не знал, поэтому поиск по ключевым словам android cmake toolchain приводит на https://developer.android.com/ndk/guides/cmake где написано, что NDK содержит специальный cmake toochain файл, который упрощает сборку проектов под Android. Узнаем что такое NDK https://developer.android.com/ndk, а это есть Native Development Kit — набор инструментов для работы с c/c++ кодом на Android платформе.


То есть вкратце, нам нужен NDK, чтобы получить доступ до Android toolchain. Установка NDK подробно описана тут https://developer.android.com/studio/projects/install-ndk. После того как NDK установилась мы можем вынести в переменную директорию где находится NDK


NDK=$HOME/Library/Android/sdk/ndk/21.4.7075529/

затем следуя инструкции https://developer.android.com/ndk/guides/cmake#usage у нас получилась команда для подготовки проекта под сборку под Android платформу.


cmake -S . -B ./build \
    -DCMAKE_TOOLCHAIN_FILE=$NDK/build/cmake/android.toolchain.cmake \ 
    -DANDROID_ABI=$ABI \
    -DANDROID_NATIVE_API_LEVEL=$MINSDKVERSION

где мы видим ранее неопределенные переменные $MINSDKVERSION и $ABI. $MINSDKVERSION описывает минимальную версию Android с которой библиотека будет работать, то есть выставляя, например 29, библиотека может быть интегрирована в проекты Android 10+. Соотношение версий SDK и OS описано тут https://developer.android.com/studio/releases/platforms.


Осталось разобраться с $ABI. Опять же, хорошая документация есть на сайте посвященному Android разработке https://developer.android.com/ndk/guides/abis. Вкратце, ABI на Android может быть 4 видов armeabi-v7a, arm64-v8a, x86 и x86_64 как минимум (самые популярные). Библиотека, собранная под ABI одного Android устройства не будет совместима с Android устройством с другим ABI. Поэтому, чтобы ваша библиотека работала на всех или большинстве устройств нужно собрать библиотеку по каждый ABI, но об этом позже.


Теперь для пробной сборки выберем arm64-v8a ABI и повторим те же шаги, что делали ранее когда, собирали под локальную машину:


  • Удаляем старую сборку

rm -fr ./build

  • Подготавливаем проект к сборке относительно Android toolchain

cmake -S . -B ./build \
    -DCMAKE_TOOLCHAIN_FILE=$NDK/build/cmake/android.toolchain.cmake \ 
    -DANDROID_ABI=arm64-v8a \
    -DANDROID_NATIVE_API_LEVEL=29

и давайте посмотрим на созданный ./build/CMakeCache.txt


CMAKE_AR:FILEPATH=/Users/mikhail/Library/Android/sdk/ndk/21.4.7075529/toolchains/llvm/prebuilt/darwin-x86_64/bin/aarch64-linux-android-ar
CMAKE_LINKER:FILEPATH=/Users/mikhail/Library/Android/sdk/ndk/21.4.7075529/toolchains/llvm/prebuilt/darwin-x86_64/bin/aarch64-linux-android-ld
CMAKE_RANLIB:FILEPATH=/Users/mikhail/Library/Android/sdk/ndk/21.4.7075529/toolchains/llvm/prebuilt/darwin-x86_64/bin/aarch64-linux-android-ranlib
CMAKE_STRIP:FILEPATH=/Users/mikhail/Library/Android/sdk/ndk/21.4.7075529/toolchains/llvm/prebuilt/darwin-x86_64/bin/aarch64-linux-android-strip
CMAKE_C_COMPILER:FILEPATH=/Users/mikhail/Library/Android/sdk/ndk/21.4.7075529/toolchains/llvm/prebuilt/darwin-x86_64/bin/clang

где мы видим, что компилятор, линкер и другие вспомогательные программы проставлены из Android NDK. Вся эта магия происходит благодаря CMAKE_TOOLCHAIN_FILE параметру, что мы передали.


  • Теперь просто запускаем компиляцию и конечную сборку библиотеки

cmake --build ./build -j 4

После успешного завершение мы можем проверить под какую архитектуру собрана библиотека:


objdump -x ./build/libopus.a | grep architecture

которая нам выдаст


architecture: aarch64

что и есть верно для ABI arm64-v8a


Собираем библиотеку под все ABI


У нас получится подобный скрипт:


for ABI in "arm64-v8a" "x86_64" "x86" "armeabi-v7a"; do

    DESTINATION_DIR=./build/opus/$ABI
    mkdir -p $DESTINATION_DIR

    cmake -S . -B $DESTINATION_DIR \
        -DCMAKE_TOOLCHAIN_FILE=$NDK/build/cmake/android.toolchain.cmake \
        -DANDROID_ABI=$ABI \
        -DANDROID_NATIVE_API_LEVEL=29

    cmake --build $DESTINATION_DIR -j 4

done

В итоге у нас собралось 4 библиотеки:


./build/opus/arm64-v8a/libopus.a
./build/opus/armeabi-v7a/libopus.a
./build/opus/x86_64/libopus.a
./build/opus/x86/libopus.a

Теперь посмотрим какие архитектуры у нас собрались:


objdump -x ./build/opus/arm64-v8a/libopus.a | grep architecture
architecture: aarch64

objdump -x ./build/opus/armeabi-v7a/libopus.a | grep architecture 
architecture: arm

objdump -x ./build/opus/x86_64/libopus.a | grep architecture 
architecture: x86_64

objdump -x ./build/opus/x86/libopus.a | grep architecture 
architecture: i386

Интеграция собранной библиотеки в Android Studio проект


Собрали библиотеку, теперь займемся добавлением ее в Android проект через Android Studio. Для работы с библиотекой из Android проекта нам потребуется:


  1. Сама библиотека (*.a файл)
  2. Интерфейс библиотеки (интерфейс c/с++ библиотек описывается в заголовочных файлах, имеют в большинстве случаев расширение *.h, на программистском сленге просто хедер файлы)

Создаем Android проект в Android Studio как File -> New -> New Project -> Native C++. После чего Android Studio создаст для нас шаблонный c/c++ source файл и соответствующий ему CMakeLists.txt файл в котором опишет как его собрать. Для добавления нашей, ранее собранной библиотеки, нам просто необходимо немного дополнить CMakeLists.txt файл, где нужно указать путь до библиотеки, что мы собрали и путь до директории с заголовочными файлами (если мы будем использовать API в нативном коде).


Но для начала скопируем директорию ./build/opus/ в Android проект, а именно в директорию где лежит CMakeLists.txt файл (у меня это директория называется cpp внутри проекта). И поместим все нужные заголовочные файлы в директорию /opus/include.


Измемения для CMakeLists.txt


  • Описываем название нашей библиотеки, какого она вида (статическая) и помечаем ее импортируемой.

add_library(opus STATIC IMPORTED)

  • Описываем путь, где лежит библиотека

set_target_properties(opus PROPERTIES IMPORTED_LOCATION ${CMAKE_SOURCE_DIR}/opus/${ANDROID_ABI}/libopus.a)

  • И наконец указываем директорию где лежат заголовочные файлы

include_directories(${CMAKE_SOURCE_DIR}/opus/include)

и теперь просто добавляем в уже существующий перечень линковщика нашу библиотеку


target_link_libraries(/* другие либы ... */ opus)

То есть до наших изменений CMakeLists.tx файл был таким


cmake_minimum_required(VERSION 3.10.2)
project("nativeopuslibdemo")

add_library(nativeopuslibdemo SHARED native-lib.cpp)
find_library(log-lib log)
target_link_libraries(nativeopuslibdemo ${log-lib})

а после стал таким


cmake_minimum_required(VERSION 3.10.2)
project("nativeopuslibdemo")

add_library(opus STATIC IMPORTED)
set_target_properties(opus PROPERTIES IMPORTED_LOCATION ${CMAKE_SOURCE_DIR}/opus/${ANDROID_ABI}/libopus.a)
include_directories(${CMAKE_SOURCE_DIR}/opus/include)

add_library(nativeopuslibdemo SHARED native-lib.cpp)
find_library(log-lib log)
target_link_libraries(nativeopuslibdemo opus ${log-lib})

После всех изменений проект должен успешно собраться. Интеграция завершена, теперь к API библиотеки можно обращаться из нативного кода, нам нужно только включить (include) в source файле необходимые заголовочные файлы.


Финальная проверка


Проект собрался успешно, теперь для уверенности попробуем сделать dummy API check, то есть вызовем какое-нибудь простенькое API библиотеки и проверим, что всё работает. Я посмотрел на сайте проекта их examples и взял оттуда небольшой кусок кода:


// native-lib.cpp

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

static void dummy_check() {
    OpusEncoder *encoder;
    int err;

    encoder = opus_encoder_create(48000, 2, OPUS_APPLICATION_AUDIO, &err);
    if (encoder != nullptr && err == 0) {
        __android_log_print(ANDROID_LOG_VERBOSE, "opus_native", "Opus encoder success!");
        opus_encoder_destroy(encoder);
    } else {
        __android_log_print(ANDROID_LOG_VERBOSE, "opus_native", "Opus encoder failed %i!", err);
    }
}

После запуска я увидел в консоли приложения Opus encoder success!


Итог


Потратив совсем немного времени, у нас получилось создать общий скрипт для сборки c/c++ библиотек, использующие cmake систему сборки, под Android платформу. Этот скрипт я уже много раз использовал на других нативных проектах.


Планирую, возможно, очередную статью как эту же библиотеку собрать под iOS в виде *.xcframework.

Комментарии (10)


  1. quaer
    21.11.2021 16:42

    А можно было просто взять java версию opus-а :)


    1. house2008 Автор
      21.11.2021 20:03

      Здравствуйте) opus была взята для примера. В реальном проекте были другие либы )


  1. 13_beta2
    21.11.2021 17:02

    Как-то новичково. Ни оптимизаций, ни мороки с pic/pie, ни граблей с разными требованиями динамического компоновщика в разных версия андроид.


    1. house2008 Автор
      21.11.2021 20:24

      Здравствуйте. Всё верно, статья — довольно простой туториал, не было задачи что-то усложнять. Про pic/pie, если вы имеете ввиду кастомные флаги компиляции

      -DCMAKE_C_FLAGS="-fPIE -fPIC"

      то да, приходилось использовать, но там всё довольно понятно было. Если я правильно помню, то при линковке библиотеки в Android проект, линковщик (могу ошибаться кто именно) выдавал ошибку чтобы пересобрать бинарник с этими ключами, что нагуглилось довольно быстро.

      Про костыли, возможно мне просто повезло. Интегрировал несколько библиотек, сложностей не было, правда и минимальная Android версия в проектах была довольно высокой, по-моему от Android 9+ из-за специфики проекта.

      Если у вас был интересный опыт в этом, поделитесь, думаю многим будет интересно почитать :) Спасибо!)


  1. vitaly_KF
    22.11.2021 02:33
    +1

    Безусловно, содержимое статьи полезно в качестве иллюстрации как все слелать с нуля, но в реальных проектах я бы рекомендовал использовать vcpkg, это если не отраслевой стандарт, то уж точно очень популярная штука для сборки c/cpp библиотек под практически все актуальные платформы и их варианты.

    Т.е. Ваша сборка свелась бы к чему-то типа: ./vcpkg install opus:arm-android opus:x64-android и тд. Попробуйте обязательно.


    1. house2008 Автор
      22.11.2021 16:07

      Спасибо! Про vcpkg звучит интересно, обязательно посмотрю)


  1. Ryppka
    22.11.2021 11:51
    +1

    Кстати, toolchain файл можно смело инклюдить в CMakeList.txt, если до project. А другие переменные, чтобы не заморачиваться со скриптами и -D -- можно записать во вспомогательный скрипт .cmake в папку cmake -- и тоже заинклюдить в головной CMakeList.


    1. house2008 Автор
      22.11.2021 16:08

      Видел такой подход, очень удобно, но сам не практиковал. Спасибо, взял на заметку.


  1. a__v__k
    26.11.2021 14:44

    до Prefab не добрались?


    1. house2008 Автор
      26.11.2021 15:10

      Здравствуйте. Если честно первый раз слышу) Бегло попробовал, что-то не получилось ничего собрать, будет свободное время — попробую разобраться, что к чему. Спасибо за наводку!)