I'm testing out the new Android Studio C/C++ building via CMake through stable gradle (http://tools.android.com/tech-docs/external-c-builds).
In my app, an already rooted device needs to use an ABI-dependent binary that I compile inside Android Studio.
When I try to compile a standard library with
add_library(mylib SHARED mylib.c)
it gets automatically compiled and copied inside the lib/[ABI] folder of the APK (e.g. /lib/armeabi/mylib.so) but if I compile an executable binary with:
add_executable(mybinary mybinary.cpp)
binaries are corectly generated inside the build folder:
app/build/intermediates/cmake/debug/lib/armeabi/mybinary
app/build/intermediates/cmake/debug/lib/x86_64/mybinary
...
but they do not seem to be copied anywhere inside the apk.
Which is the correct way to handle this need? Is a gradle-task the way to go?
build.gradle:
apply plugin: 'com.android.application'
android {
compileSdkVersion 24
buildToolsVersion "24.0.1"
defaultConfig {
applicationId "com.my.app"
minSdkVersion 10
targetSdkVersion 24
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
externalNativeBuild {
cmake {
cppFlags ""
}
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
externalNativeBuild{
cmake{
path "CMakeLists.txt"
}
}
defaultConfig {
externalNativeBuild {
cmake {
targets "
arguments "-DANDROID_TOOLCHAIN=clang", "-DANDROID_PLATFORM=android-21"
cFlags "-DTEST_C_FLAG1", "-DTEST_C_FLAG2"
cppFlags "-DTEST_CPP_FLAG2", "-DTEST_CPP_FLAG2"
abiFilters 'x86', 'x86_64', 'armeabi', 'armeabi-v7a'
}
}
}
}
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
exclude group: 'com.android.support', module: 'support-annotations'
})
testCompile 'junit:junit:4.12'
compile 'com.android.support:appcompat-v7:24.1.1'
compile 'com.android.support:design:24.1.1'
compile 'com.android.support:recyclerview-v7:24.1.1'
compile 'eu.chainfire:libsuperuser:1.0.0.201607041850'
}
CMakeLists.txt
cmake_minimum_required(VERSION 3.4.1)
set(CMAKE_VERBOSE_MAKEFILE on)
add_executable(mybinary ${CMAKE_CURRENT_SOURCE_DIR}/mybinary.cpp)
target_link_libraries( mybinary libcustom)
target_include_directories (mybinary PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
mybinary.cpp
#include <stdlib.h>
#include <string>
#include <iostream>
using namespace std;
int main(int argc, char *argv[]) {
string hello = "Hello from C++";
cout << "Message from native code: " << hello << "\n";
return EXIT_SUCCESS;
}
How the app should interact with mybinary:
import eu.chainfire.libsuperuser.Shell;
...
Shell.SU.run("/path/to/mybinary");
Different Android devices use different CPUs, which in turn support different instruction sets. Each combination of CPU and instruction set has its own Application Binary Interface (ABI). An ABI includes the following information:
Starting in 2.2, Android Studio on 64 bit OS supports building C/C++ via CMake and ndk-build through stable gradle. In both cases, Gradle is configured to point at the external build system.
1. Compile your library for Android. First, grab the Android Native Development Kit (NDK). This includes a toolchain for cross-compiling C/C++ to Android. Extract the NDK somewhere sane, and add the tools to your path.
ABI Management. Different Android handsets use different CPUs, which in turn support different instruction sets. Each combination of CPU and instruction sets has its own Application Binary Interface, or ABI. The ABI defines, with great precision, how an application's machine code is supposed to interact with the system at runtime.
Ok, I've found a solution that seems to by quite comfortable but probably there are more proper ways out there;
CMakeLists.txt is by default placed inside myAppProject/app so I've added this line to CMakeLists.txt:
set(EXECUTABLE_OUTPUT_PATH "${CMAKE_CURRENT_SOURCE_DIR}/src/main/assets/${ANDROID_ABI}")
complete app/CMakeLists.txt:
cmake_minimum_required(VERSION 3.4.1)
set(CMAKE_VERBOSE_MAKEFILE on)
# set binary output folder to Android assets folder
set(EXECUTABLE_OUTPUT_PATH "${CMAKE_CURRENT_SOURCE_DIR}/src/main/assets/${ANDROID_ABI}")
add_subdirectory (src/main/cpp/mylib)
add_subdirectory (src/main/cpp/mybinary)
complete app/src/main/cpp/mybinary/CMakeLists.txt:
add_executable(mybinary ${CMAKE_CURRENT_SOURCE_DIR}/mybinary.cpp)
# mybinary, in this example, has mylib as dependency
target_link_libraries( mybinary mylib)
target_include_directories (mybinary PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
complete app/src/main/cpp/mylib/CMakeLists.txt:
add_library( # Sets the name of the library.
mylib
# Sets the library as a shared library.
SHARED
# Provides a relative path to your source file(s).
# Associated headers in the same location as their source
# file are automatically included.
${CMAKE_CURRENT_SOURCE_DIR}/mylib.cpp )
target_include_directories (mylib PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
Doing so, any executable binary is compiled directly into assets folder, inside a subfolder whose name is the target ABI, eg:
assets/armeabi/mybinary
assets/x86_64/mybinary
...
In order to use the proper binary inside the App, the correct binary should be selected:
String abi;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
abi = Build.SUPPORTED_ABIS[0];
} else {
//noinspection deprecation
abi = Build.CPU_ABI;
}
String folder;
if (abi.contains("armeabi-v7a")) {
folder = "armeabi-v7a";
} else if (abi.contains("x86_64")) {
folder = "x86_64";
} else if (abi.contains("x86")) {
folder = "x86";
} else if (abi.contains("armeabi")) {
folder = "armeabi";
}
...
AssetManager assetManager = getAssets();
InputStream in = assetManager.open(folder+"/" + "mybinary");
Then, the binary should be copied away from assets folder with the correct execute permissions:
OutputStream out = context.openFileOutput("mybinary", MODE_PRIVATE);
long size = 0;
int nRead;
while ((nRead = in.read(buff)) != -1) {
out.write(buff, 0, nRead);
size += nRead;
}
out.flush();
Log.d(TAG, "Copy success: " + " + size + " bytes");
File execFile = new File(context.getFilesDir()+"/mybinary");
execFile.setExecutable(true);
That's all!
UPDATE: gradle.build file:
apply plugin: 'com.android.application'
android {
compileSdkVersion 25
buildToolsVersion "25"
defaultConfig {
applicationId "com.myapp.example"
minSdkVersion 10
targetSdkVersion 25
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
externalNativeBuild {
cmake {
cppFlags ""
}
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
externalNativeBuild {
cmake {
path "CMakeLists.txt"
}
}
defaultConfig {
externalNativeBuild {
cmake {
targets "mylib", "mybinary"
arguments "-DANDROID_TOOLCHAIN=clang"
cFlags "-DTEST_C_FLAG1", "-DTEST_C_FLAG2"
cppFlags "-DTEST_CPP_FLAG2", "-DTEST_CPP_FLAG2"
abiFilters 'armeabi', 'armeabi-v7a', 'x86', 'x86_64'
}
}
}
}
dependencies {
compile fileTree(include: ['*.jar'], dir: 'libs')
androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
exclude group: 'com.android.support', module: 'support-annotations'
})
testCompile 'junit:junit:4.12'
compile 'com.android.support:appcompat-v7:25.0.0'
compile 'com.android.support:design:25.0.0'
compile 'com.android.support:recyclerview-v7:25.0.0'
compile 'com.android.support:cardview-v7:25.0.0'
compile 'eu.chainfire:libsuperuser:1.0.0.201607041850'
}
Make the executable files output to where the Android Gradle plugin expects libraries:
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}")
Fool the plugin into thinking your executable is a shared object:
add_executable(i_am_an_executable.so main.c)
Check the APK:
$ 7z l build/outputs/apk/app-debug.apk lib/ [2:08:56] Date Time Attr Size Compressed Name ------------------- ----- ------------ ------------ ------------------------ ..... 9684 4889 lib/armeabi/i_am_an_executable.so ..... 6048 1710 lib/arm64-v8a/i_am_an_executable.so ..... 9688 4692 lib/armeabi-v7a/i_am_an_executable.so ..... 5484 1715 lib/x86/i_am_an_executable.so ..... 6160 1694 lib/x86_64/i_am_an_executable.so
Access and run your executable; it is located in context.getApplicationInfo().nativeLibraryDir
.
The downside to this is that you cannot set android:extractNativeLibs
to false
— I don't know of any way to access lib/
in an APK from within the app.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With