Являясь не 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, до того как я принялся за этот мини проект, я знал что:
- это система сборки проектов
- описание как собрать проект находится в CMakeLists.txt файле
- сборка идет в два этапа:
- подготавливаем проект к сборке
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 проекта нам потребуется:
- Сама библиотека (*.a файл)
- Интерфейс библиотеки (интерфейс 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)
13_beta2
21.11.2021 17:02Как-то новичково. Ни оптимизаций, ни мороки с pic/pie, ни граблей с разными требованиями динамического компоновщика в разных версия андроид.
house2008 Автор
21.11.2021 20:24Здравствуйте. Всё верно, статья — довольно простой туториал, не было задачи что-то усложнять. Про pic/pie, если вы имеете ввиду кастомные флаги компиляции
-DCMAKE_C_FLAGS="-fPIE -fPIC"
то да, приходилось использовать, но там всё довольно понятно было. Если я правильно помню, то при линковке библиотеки в Android проект, линковщик (могу ошибаться кто именно) выдавал ошибку чтобы пересобрать бинарник с этими ключами, что нагуглилось довольно быстро.
Про костыли, возможно мне просто повезло. Интегрировал несколько библиотек, сложностей не было, правда и минимальная Android версия в проектах была довольно высокой, по-моему от Android 9+ из-за специфики проекта.
Если у вас был интересный опыт в этом, поделитесь, думаю многим будет интересно почитать :) Спасибо!)
vitaly_KF
22.11.2021 02:33+1Безусловно, содержимое статьи полезно в качестве иллюстрации как все слелать с нуля, но в реальных проектах я бы рекомендовал использовать vcpkg, это если не отраслевой стандарт, то уж точно очень популярная штука для сборки c/cpp библиотек под практически все актуальные платформы и их варианты.
Т.е. Ваша сборка свелась бы к чему-то типа: ./vcpkg install opus:arm-android opus:x64-android и тд. Попробуйте обязательно.
Ryppka
22.11.2021 11:51+1Кстати, toolchain файл можно смело инклюдить в CMakeList.txt, если до project. А другие переменные, чтобы не заморачиваться со скриптами и -D -- можно записать во вспомогательный скрипт .cmake в папку cmake -- и тоже заинклюдить в головной CMakeList.
house2008 Автор
22.11.2021 16:08Видел такой подход, очень удобно, но сам не практиковал. Спасибо, взял на заметку.
quaer
А можно было просто взять java версию opus-а :)
house2008 Автор
Здравствуйте) opus была взята для примера. В реальном проекте были другие либы )