📜 ⬆️ ⬇️

NDK c half-kick

Instead of prologue


I, perhaps, will break the tradition of all such articles and will not tell you why we need NDK. If you are reading this, then you need it for some reason. But of course, we will consider one of the real use cases.

This story began with the fact that at Google I / O 2015 support for native development was presented right from the comfort of the studio, Android-studio. Naturally, Google, as always, promised and kept waiting until they rolled it out for ordinary people. And on July 9, it happened: a piece of the pie that was promised to us appeared on the stable channel.

Naturally, it is not so easy to take and start using it the way we would like.

image

But we will still.
')

Cooking tool


To begin with, we will create a simple project with one activity, on which we need to throw 2 TextView, EditText and Button. Next, go to the project settings (⌘;) and in the SDK Location, specify the path to the NDK. If you haven’t had anything like this yet, the studio will kindly offer to download it.

The hardest thing is over;) Now let's set up the build system. To do this, we stamp in the gradle / wrapper / gradle-wrapper.properties and change the distributionUrl specified there to this:

distributionUrl=https\://services.gradle.org/distributions/gradle-2.5-all.zip 

An experimental plugin that does all the magic, so far only works with this version of Gradle.

Next, you need to pull this same plugin on the project. This can be done in the root build.gradle file. To do this, change the name and version of the plugin:

 classpath 'com.android.tools.build:gradle-experimental:0.2.0' 

And go to the build file of the module, where the interesting begins. Official documentation suggests us to bring the file to this form:

build.gradle
 apply plugin: 'com.android.model.application' model { android { compileSdkVersion = 22 buildToolsVersion = "22.0.1" defaultConfig.with { applicationId = "com.redmadrobot.ndkdemo" minSdkVersion.apiLevel = 15 targetSdkVersion.apiLevel = 22 versionCode = 1 versionName = "1.0" } } /* * native build settings */ android.ndk { moduleName = "security" cppFlags += "-std=c++11" stl = "stlport_static" } android.buildTypes { release { minifyEnabled = false proguardFiles += file('proguard-rules.txt') } } android.productFlavors { // for detailed abiFilter descriptions, refer to "Supported ABIs" @ // https://developer.android.com/ndk/guides/abis.html#sa create("arm") { ndk.abiFilters += "armeabi" } create("arm7") { ndk.abiFilters += "armeabi-v7a" } create("arm8") { ndk.abiFilters += "arm64-v8a" } create("x86") { ndk.abiFilters += "x86" } create("x86-64") { ndk.abiFilters += "x86_64" } create("mips") { ndk.abiFilters += "mips" } create("mips-64") { ndk.abiFilters += "mips64" } // To include all cpu architectures, leaves abiFilters empty create("all") } } dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) compile 'com.android.support:appcompat-v7:22.2.1' } 



And promises that everything will be fine. Lying Will not be. When trying to assemble a project, the studio will spit in a rather indistinct log and will offer to try on the role of an Altai shaman. Having rummaged (as usual) on the Internet, I found a solution. You must explicitly tell the build system that Java 1.7 will be used. Who would have thought ... The result is the following build file:

Correct build.gradle
 apply plugin: 'com.android.model.application' model { android { compileSdkVersion = 22 buildToolsVersion = "22.0.1" defaultConfig.with { applicationId = "com.redmadrobot.ndkdemo" minSdkVersion.apiLevel = 15 targetSdkVersion.apiLevel = 22 versionCode = 1 versionName = "1.0" } } /* * native build settings */ android.ndk { moduleName = "security" cppFlags += "-std=c++11" stl = "stlport_static" } android.buildTypes { release { minifyEnabled = false proguardFiles += file('proguard-rules.txt') } } android.productFlavors { // for detailed abiFilter descriptions, refer to "Supported ABIs" @ // https://developer.android.com/ndk/guides/abis.html#sa create("arm") { ndk.abiFilters += "armeabi" } create("arm7") { ndk.abiFilters += "armeabi-v7a" } create("arm8") { ndk.abiFilters += "arm64-v8a" } create("x86") { ndk.abiFilters += "x86" } create("x86-64") { ndk.abiFilters += "x86_64" } create("mips") { ndk.abiFilters += "mips" } create("mips-64") { ndk.abiFilters += "mips64" } // To include all cpu architectures, leaves abiFilters empty create("all") } // Our workaround compileOptions.with { sourceCompatibility = JavaVersion.VERSION_1_7 targetCompatibility = JavaVersion.VERSION_1_7 } } dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) compile 'com.android.support:appcompat-v7:22.2.1' } 


I will not consider the file format in detail. Here is a link for inquiring minds: tyk

All preparations are complete and can be programmed. Doing everything described above in order to write Hello world would be at least a crime, so we will do something more interesting.

Formulation of the problem


As promised, we will touch on one of the main cases of using native code in Android applications. I'm talking about security. It often happens that you need to hide from prying eyes any information by encrypting it. And then a reasonable question arises: how to ensure the security of keys and encryption algorithms? After all, any student can get an almost exact copy of the source code of the application and learn all the turnout and passwords!

Yes this is true. And here we come to the aid of native libraries. All algorithms, keys and the like can be hidden deep in these libraries, and the interface can be set out from a pair of encrypt / decrypt functions. Yes, it also breaks, but not by anyone. The person should be motivated enough to get into the native code of your application. But that's another story. And so it is decided! We will write a data encryption system. In the article I will consider VERY primitive algorithms. Please do not use them in production.

 public class MainActivity extends AppCompatActivity { static { System.loadLibrary("security"); } public native String encrypt(String plainText); public native String decrypt(String ciphertext); 

Here we load the library and declare 2 methods that will be used for encryption / decryption. Naturally, the studio will highlight the names of these methods in red and say the JNI function ... Cannot resolve , since we have not written them yet. And here the beauty of integration, which I spoke at the very beginning, comes into play. Place the cursor on the function name (for example, encrypt), press Alt + Enter and we will be prompted to create this function. Kindly agree and observe a truly magical act! We have created the jni directory, and in it the file security.c has the following content:

 #include <jni.h> JNIEXPORT jstring JNICALL Java_ru_freedomlogic_ndktester_MainActivity_encrypt(JNIEnv *env, jobject instance, jstring plainText_) { const char *plainText = (*env)->GetStringUTFChars(env, plainText_, 0); // TODO (*env)->ReleaseStringUTFChars(env, plainText_, plainText); return (*env)->NewStringUTF(env, returnValue); } 

Yes, it's just some kind of good magic !!! Everything is cool, but there is a nuance. Why C? I want C ++! How to make the default create a cpp file, I did not find, so it was decided to just rename the file. The experienced reader will now grin my naivety and be right. Of course, nothing worked right away. I had to get a tool to solve any problems.



The result of the rite that touched the upper and lower worlds was such a ritual of coming to C ++.

Now we will write the code of our “super-cryptosystem”.

security.cpp
 #include <jni.h> #include <algorithm> #include "base64.h" using namespace std; unsigned char key[] = {4, 2, 9, 4, 9, 6, 7, 2, 9, 5}; string applyXor(string sequence) { int maxIndex = sizeof(key) - 1; string result = sequence; size_t sequenceSize = sequence.size(); int keyIndex = 0; for (int i = 0; i < sequenceSize; i++) { if (keyIndex > maxIndex) keyIndex = 0; result[i] = sequence[i] ^ key[keyIndex++]; } return result; } extern "C" { JNIEXPORT jstring JNICALL Java_com_redmadrobot_ndkdemo_MainActivity_encrypt(JNIEnv *env, jobject instance, jstring plainText_) { const char *plainText = env->GetStringUTFChars(plainText_, 0); string sequence = applyXor(plainText); size_t sequenceSize = sequence.size(); reverse(sequence.begin(), sequence.end()); int code = 0; for (unsigned long i = 0; i < sequenceSize; i++) { code = sequence[i] + 5; sequence[i] = (char) code; } env->ReleaseStringUTFChars(plainText_, plainText); return env->NewStringUTF(base64::encode(sequence).c_str()); } JNIEXPORT jstring JNICALL Java_com_redmadrobot_ndkdemo_MainActivity_decrypt(JNIEnv *env, jobject instance, jstring ciphertext_) { const char *ciphertext = env->GetStringUTFChars(ciphertext_, 0); string sequence = base64::decode(ciphertext); size_t sequenceSize = sequence.size(); int code; for (int i = 0; i < sequenceSize; i++) { code = sequence[i] - 5; sequence[i] = (char) code; } reverse(sequence.begin(), sequence.end()); env->ReleaseStringUTFChars(ciphertext_, ciphertext); return env->NewStringUTF(applyXor(sequence).c_str()); } } 


Fully explain everything that happens, I do not see the point, but I will give a few explanations. First extern "C" {...} . If you do not wrap the interface, the compiler will simply change the names of the functions, and as a result, your application will crash in runtime, referring to the non-existent (by that time) function name. Read more here . The second ambush awaits us with the functions GetStringUTFChars and NewStringUTF. They very poorly digest the mess that is obtained as a result of encryption, which again leads to the application crashes. Details here .

Now it remains to apply these functions in your code.

  @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); final EditText inputText = (EditText) findViewById(R.id.input_text); final Button encryptButton = (Button) findViewById(R.id.encrypt_button); final TextView cipherText = (TextView) findViewById(R.id.cipher_text); final TextView plainText = (TextView) findViewById(R.id.plain_text); encryptButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(final View v) { String cipher = encrypt(inputText.getText().toString()); cipherText.setText(cipher); plainText.setText(decrypt(cipher)); } }); } 

Then just run, and everything just works. Or does not work. In this case, we need a handy debugger, which is still a bit slow, but integrated into the ecosystem, and this is important. To debug the native code, you need to create a new Android Native configuration, set breakpoints to enjoy convenient debugging.



Summarizing


The guys from Google did a great job in order to give us another handy tool. Of course, it is not yet perfect and sometimes it is necessary to very actively knock on a tambourine, but in general this is the thing that can already be used. The demo project code can be found on GitHub . More information can be found here . Quick compilation to all!

See also:
Chronos library: make writing long operations easier
We put controllers on a diet: Android
Architectural design of mobile applications: part 1

Source: https://habr.com/ru/post/265117/


All Articles