-
@ 67078065:928c2893
2025-04-09 18:04:26If you need to use a C/C++ library in your Android app, you will need to use the Java Native Interface (JNI). This was my experience integrating a C blockchain library called "libqaeda" ,into an Android application we are working on with lash. Many thanks to lash for advising on using this platform , and who wrote the C library.
Libqaeda and What it does
libqaeda is a cryptographic library designed to enable bi-lateral countersigning of chains of promises and statements. At its core, the library provides a robust framework for creating verifiable certificates that require dual signatures - one from the requester and one from the responder. The library excels at establishing cryptographic proof chains that can be used for:
- Verifying authenticity and proof-of-ownership of certificates
- Tracking credit or commitments between individuals
- Creating chains of trust where each link requires mutual agreement
What makes libqaeda particularly flexible is its modular design. It offers customizable components for cryptography implementations, data storage backends, and trust management - allowing developers to adapt it to various environments from servers to mobile devices. While currently in alpha development status and not safe in any way, libqaeda shows promise as a foundation for applications requiring strong cryptographic verification of bilateral agreements.
Java Native Interface
JNI makes it possible to interact with native code( code written in C , C++ , Assembly).
It is useful in the following cases:
- Reusing existing native libraries in your java app
- Accessing low-level system APIs.
- Implementing JVM into a native application.
- Performance critical tasks such as gaming.
How JNI Works 1. Java calls the native methods which are declared with the native keyword. 2. The native code is compiled into a shared library (.so on Linux/Android, .dll on WIndows) 3. JVM loads the library at runtime and executes native functions.
The Challenge
We wanted to use the C library in the Android app, but we faced a few challenges:
- The library was originally built for desktop systems, not Android
- Android needs code compiled for specific CPU architectures like arm64-v8a
- There were dependencies on specific C libraries not available in Android
Step 1: Define the Java Interface
First, I created a Java class that declares the native methods I wanted to use:
``` package org.defalsified.android.badged.services;
public class LibQaeda { static { System.loadLibrary("qaeda"); }
public native long createDummyStore(); public native String dummyContentGet(int payloadType, long storePtr, byte[] key);
} ``` The static block loads our native library, and the native methods t
ell Java these functions are implemented in C.
Step 2: Create the JNI Function Implementations
Next, I created a C file that implements the Java native methods: ```
include
include
include "libqaeda/src/lq/store.h"
include "libqaeda/src/lq/err.h"
include
// Functions from the library extern int lq_dummy_content_get(enum payload_e typ, LQStore store, const char key, size_t key_len, char value, size_t value_len); extern struct lq_store_t LQDummyContent;
// JNI implementation JNIEXPORT jlong JNICALL Java_org_defalsified_android_badged_services_LibQaeda_createDummyStore (JNIEnv env, jobject thiz) { LQStore store = (LQStore)malloc(sizeof(LQStore)); store = LQDummyContent; return (jlong)store; }
JNIEXPORT jstring JNICALL Java_org_defalsified_android_badged_services_LibQaeda_dummyContentGet (JNIEnv env, jobject thiz, jint payloadType, jlong storePtr, jbyteArray key) { LQStore store = (LQStore)storePtr; jbyte keyBytes = (env)->GetByteArrayElements(env, key, NULL); jsize keyLength = (env)->GetArrayLength(env, key);
char value[4096] = {0}; size_t valueLen = sizeof(value); int result = lq_dummy_content_get( (enum payload_e)payloadType, store, (const char*)keyBytes, (size_t)keyLength, value, &valueLen ); (*env)->ReleaseByteArrayElements(env, key, keyBytes, JNI_ABORT); if (result != 0) { return NULL; } return (*env)->NewStringUTF(env, value);
} ``` The function names must match the Java class and method names, prefixed with Java_ and using underscores for package separators.
Step 3: Set Up CMake to Build the Native Library
The key step was creating a CMakeLists.txt file to build our native library. Initially, I tried to use the pre-built library, but it was incompatible with Android. So I decided to directly compile the necessary source files:
``` cmake_minimum_required(VERSION 3.10) project(badged_app)
set(LIBQAEDA_DIR ${CMAKE_CURRENT_SOURCE_DIR}/libqaeda) set(LIBQAEDA_SRC_DIR ${LIBQAEDA_DIR}/src)
include_directories( ${CMAKE_CURRENT_SOURCE_DIR} ${LIBQAEDA_SRC_DIR} ${LIBQAEDA_SRC_DIR}/aux/include ${LIBQAEDA_SRC_DIR}/aux/liblash/src ${LIBQAEDA_SRC_DIR}/aux/liblash/src/hex ${LIBQAEDA_SRC_DIR}/aux/liblash/src/rerr )
Create qaeda library with all required source files
add_library(qaeda SHARED ${CMAKE_CURRENT_SOURCE_DIR}/org_defalsified_android_badged_services_LibQaeda.c ${LIBQAEDA_SRC_DIR}/store/dummy.c ${LIBQAEDA_SRC_DIR}/aux/liblash/src/hex/hex.c ${LIBQAEDA_SRC_DIR}/aux/liblash/src/rerr/rerr.c )
Link against Android libraries
target_link_libraries(qaeda android log atomic m )
Create main app library
add_library(badged SHARED native-lib.cpp )
Link app against our JNI wrapper
target_link_libraries(badged qaeda android log ) ``` Instead of trying to use a pre-built library, I included just the specific source files we needed for our minimal implementation.
Step 4: Configure the Android Project
In the app's build.gradle file, I added the CMake configuration:
``` android { // Other settings...
externalNativeBuild { cmake { path = file("src/main/cpp/CMakeLists.txt") version = "3.22.1" } } // Specify the NDK version to use ndkVersion = "25.1.8937393" // Configure for multiple architectures defaultConfig { ndk { abiFilters.addAll(listOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64")) } }
} ```
Step 5: Use the Library in Your Android Activity
Finally, I used the library in my app:
``` public class MainActivity extends AppCompatActivity { private static final String TAG = "MainActivity";
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); TextView textView = new TextView(this); textView.setText("Welcome to Badges!"); try { LibQaeda libQaeda = new LibQaeda(); long storePtr = libQaeda.createDummyStore(); byte[] key = "test_key".getBytes(); String result = libQaeda.dummyContentGet(1, storePtr, key); textView.append("\n\nLibQaeda Test:\n" + result); } catch (Exception e) { textView.append("\n\nLibQaeda Test Error:\n" + e.getMessage()); } setContentView(textView); }
} ```
Key Lessons Learned
- Be careful with pre-built libraries for Android: Pre-built libraries need to be compiled specifically for Android's architectures (arm64-v8a, armeabi-v7a, x86, x86_64). If a library was built for desktop systems (like x86_64 Linux), it won't work on Android devices with different architectures.
- Consider direct compilation when needed: If you have access to the library's source code and encounter compatibility issues with pre-built versions, compiling the source files directly for Android can solve architecture incompatibility problems.
- Use Android NDK tools for cross-compilation: When you do need pre-built libraries, use the Android NDK's toolchain to properly cross-compile them for all target Android architectures.
- Be selective about which files to include: Only include the specific source files you need, which can reduce complexity and potential issues.
- Handle dependencies carefully: Make sure to include any header files and source code required by your library functions.
Conclusion
In this project, we successfully integrated a C library with an Android application using JNI. While this approach of directly compiling the necessary source files worked well for our immediate needs, there are other paths we could explore in the future. We may need to create an arm64 static library instead, but having the code running is an important first step. The JNI bridge provides a powerful way to leverage existing C/C++ code in Android applications. Whether you're working with algorithms, legacy systems, or performance-critical components, understanding how to connect Java and native code opens up many possibilities for Android development. By following the steps outlined in this blog post, you should now have the knowledge to integrate your own C libraries into Android applications, adapting the approach to your specific requirements. Adios!