Mahi Mahi
Mahi Mahi

Reputation: 13

Linking my dynamic Rust library to my Android project via NDK results in Unsatisfied link error

I am planning to implement a zero-knowledge proof based geolocation tracking, but unfortunately the zkp are only available as Rust libraries (if you don't know what zero-knowledge proof is, assume it as a library which helps you to prove something, just telling this for context).

The desired behaviour is the .so file to be linked to my Kotlin file via JNI.

Now my Android project structure is:

majorapplication/
|
|-app/
|--src/
|---main/
|----cpp/
|-----CMakeLists.TXT
|-----majorapplication.cpp
|----java/
|----jniLibs/
|-----x86/
|-----x86_64/
|------lib_zkp.so
|------libmajorapplication.so
|-----arm64-v8a/
|-----armeabi-v7a/
|-rustbulletproof(rust project)
|--cargo.toml
|--cbindgen.toml
|--build.rs
|--src/
|---lib.rs(contains the FFI code)

First, I wrote the Rust library with the following configuration:

cargo.toml:

[package]
name = "rustbulletproof"
version = "0.1.0"
edition = "2021"

[dependencies]
bulletproofs = "4.0.0"
curve25519-dalek-ng = "4.1.0"
merlin = "3.0.0"
rand = "0.8.5"
libc = "0.2.169"
geo-types = "0.7.9"
anyhow = "1.0.71"
serde = { version = "1.0", features = ["derive"] }
bincode = "1.3"

[lib]
name = "zkp_proximity"
crate-type = ["cdylib"]  # For FFI with Android

[build-dependencies]
cbindgen = "0.26"

cbindgen.toml:

language = "C++"
include_guard = "BULLETPROOFS_GEO_H"
namespace = "bulletproofs_geo"

[defines]
"target_os = android" = "ANDROID"

[export]
prefix = "BPG_"
include = ["ProofData"]

[parse]
parse_deps = true
include = ["bulletproofs", "curve25519_dalek_ng","merlin","rand","libc","geo-types","anyhow","serde","bincode"]

build.rs:

use std::env;

fn main() {
    let crate_dir = env::var("CARGO_MANIFEST_DIR").unwrap();
    let config = cbindgen::Config::from_file("cbindgen.toml")
        .expect("Unable to find cbindgen.toml");

    cbindgen::Builder::new()
        .with_crate(crate_dir)
        .with_config(config)
        .generate()
        .expect("Unable to generate bindings")
        .write_to_file("include/bulletproofs_geo.h");
}

lib.rs:

use std::mem;
mod distance;
mod prover;

#[repr(C)]
pub struct ProofData {
    proof_bytes: *const u8,
    proof_len: usize,
    ristretto_bytes: *const u8,
    ristretto_len: usize,
    bp_gens_capacity: usize,
    bp_gens_party_capacity: usize,
    // We'll serialize these into bytes for FFI
    pedersen_bytes: [u8; 64], // 2 compressed points, 32 bytes each
}

#[no_mangle]
pub extern "C" fn start_proof(x1: f64, y1: f64, x2: f64, y2: f64) -> ProofData {
    let c1 = geo_types::Coord { x: x1, y: y1 };
    let c2 = geo_types::Coord { x: x2, y: y2 };
    let mut distance: u64 = 0;

    if let Ok(d) = distance::distance_from_coords(&c1, &c2) {
        distance = (d * 1000.0) as u64;
    }

    let range_proof_res_obj = prover::fun_prover(distance);
    let (pc_gens, bp_gens, proof_result) = range_proof_res_obj;

    match proof_result {
        Ok((proof, ristretto)) => {
            let proof_bytes = proof.to_bytes();
            let ristretto_bytes = bincode::serialize(&ristretto).unwrap();

            // Convert Pedersen generators to bytes
            let mut pedersen_bytes = [0u8; 64];
            pedersen_bytes[..32].copy_from_slice(&pc_gens.B.compress().to_bytes());
            pedersen_bytes[32..].copy_from_slice(&pc_gens.B_blinding.compress().to_bytes());

            let proof_ptr = proof_bytes.as_ptr();
            let proof_len = proof_bytes.len();
            let ristretto_ptr = ristretto_bytes.as_ptr();
            let ristretto_len = ristretto_bytes.len();

            // Prevent deallocation
            mem::forget(proof_bytes);
            mem::forget(ristretto_bytes);

            ProofData {
                proof_bytes: proof_ptr,
                proof_len,
                ristretto_bytes: ristretto_ptr,
                ristretto_len,
                bp_gens_capacity: bp_gens.gens_capacity,
                bp_gens_party_capacity: bp_gens.party_capacity,
                pedersen_bytes,
            }
        }
        Err(_) => ProofData {
            proof_bytes: std::ptr::null(),
            proof_len: 0,
            ristretto_bytes: std::ptr::null(),
            ristretto_len: 0,
            bp_gens_capacity: 0,
            bp_gens_party_capacity: 0,
            pedersen_bytes: [0u8; 64],
        }
    }
}

#[no_mangle]
pub extern "C" fn free_proof_data(data: ProofData) {
    if !data.proof_bytes.is_null() {
        unsafe {
            Vec::from_raw_parts(
                data.proof_bytes as *mut u8,
                data.proof_len,
                data.proof_len
            );
        }
    }

    if !data.ristretto_bytes.is_null() {
        unsafe {
            Vec::from_raw_parts(
                data.ristretto_bytes as *mut u8,
                data.ristretto_len,
                data.ristretto_len
            );
        }
    }
}

Now I built the above code to .so files using the following command:

cargo ndk -t x86 -t x86_64 -t arm64-v8a -t armeabi-v7a -o ./../app/src/main/jniLibs build --release

This concludes the Rust part.

Now, using the NDK from Android, I built this with the following configuration:

app/build.gradle.kts:

import com.android.sdklib.devices.Abi

plugins {
    alias(libs.plugins.android.application)
    alias(libs.plugins.kotlin.android)
    alias(libs.plugins.kotlin.compose)
}

android {
    namespace = "com.example.majorapplication"
    compileSdk = 35

    defaultConfig {
        applicationId = "com.example.majorapplication"
        minSdk = 29
        targetSdk = 35
        versionCode = 1
        versionName = "1.0"

        testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"

        sourceSets {
            getByName("main") {
                jniLibs {

                    srcDirs("src/main/jniLibs")
                }
            }
        }

        ndk {
            abiFilters += listOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64")
        }

    }

    buildTypes {
        release {
            isMinifyEnabled = false
            proguardFiles(
                getDefaultProguardFile("proguard-android-optimize.txt"),
                "proguard-rules.pro"
            )
        }
        debug {
            // Enable debugging for native code
            isJniDebuggable = true
        }
    }
    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_11
        targetCompatibility = JavaVersion.VERSION_11
    }
    kotlinOptions {
        jvmTarget = "11"
    }
    buildFeatures {
        compose = true
    }
    externalNativeBuild {
        cmake {
            path = file("src/main/cpp/CMakeLists.txt")
            version = "3.22.1"
        }
    }
    ndkVersion = rootProject.extra["ndkVersion"] as String

}

dependencies {
    implementation(libs.androidx.core.ktx)
    implementation(libs.androidx.lifecycle.runtime.ktx)
    implementation(libs.androidx.activity.compose)
    implementation(platform(libs.androidx.compose.bom))
    implementation(libs.androidx.ui)
    implementation(libs.androidx.ui.graphics)
    implementation(libs.androidx.ui.tooling.preview)
    implementation(libs.androidx.material3)
    implementation(libs.play.services.location)
    testImplementation(libs.junit)
    androidTestImplementation(libs.androidx.junit)
    androidTestImplementation(libs.androidx.espresso.core)
    androidTestImplementation(platform(libs.androidx.compose.bom))
    androidTestImplementation(libs.androidx.ui.test.junit4)
    debugImplementation(libs.androidx.ui.tooling)
    debugImplementation(libs.androidx.ui.test.manifest)
}

CMakeLists.TXT:

cmake_minimum_required(VERSION 3.22.1)
project("majorapplication")

set(RUST_LIB_NAME zkp_proximity)
set(JNI_LIB_NAME majorapplication)

# Set the path relative to CMakeLists.txt location
set(RUST_LIB_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../jniLibs/${ANDROID_ABI})

message(STATUS "ANDROID_ABI: ${ANDROID_ABI}")
message(STATUS "Looking for library at: ${RUST_LIB_DIR}/${ANDROID_ABI}/lib${RUST_LIB_NAME}.so")

add_library(${CMAKE_PROJECT_NAME} SHARED
        majorapplication.cpp)
# Add Rust library - use consistent naming without 'lib' prefix in target name
add_library(${RUST_LIB_NAME} SHARED IMPORTED)

if(NOT EXISTS "${RUST_LIB_DIR}/lib${RUST_LIB_NAME}.so")
    message(FATAL_ERROR "Rust library not found at: ${RUST_LIB_DIR}/lib${RUST_LIB_NAME}.so")
endif()

set_target_properties(${RUST_LIB_NAME} PROPERTIES
        IMPORTED_LOCATION "${RUST_LIB_DIR}/lib${RUST_LIB_NAME}.so"
)

#set_target_properties(${RUST_LIB_NAME} PROPERTIES
#        IMPORTED_LOCATION ${RUST_LIB_DIR}/lib${RUST_LIB_NAME}.so)

# Add JNI library
include_directories(${CMAKE_SOURCE_DIR}/../../../../rustbulletproof/include)

# Link against ${RUST_LIB_NAME} instead of libzkp_proximity
target_link_libraries(
        ${CMAKE_PROJECT_NAME}
        ${RUST_LIB_NAME}
        android
        log)

majorapplication.cpp:

#include <jni.h>
#include <string>
#include <dlfcn.h>
#include <android/log.h>
#include "bulletproofs_geo.h" // Your generated header

extern "C" {
JNIEXPORT jobject JNICALL
Java_com_example_majorapplication_BulletproofsBinding_generateProof(
        JNIEnv *env,
        jobject /* this */,
        jdouble x1,
        jdouble y1,
        jdouble x2,
        jdouble y2) {

    // Call the Rust function
    bulletproofs_geo::BPG_ProofData proof_data = bulletproofs_geo::start_proof(x1, y1, x2, y2);

    // Create byte arrays for proof and commitment
    jbyteArray proofBytes = env->NewByteArray(proof_data.proof_len);
    jbyteArray ristrettoBytes = env->NewByteArray(proof_data.ristretto_len);
    jbyteArray pedersenBytes = env->NewByteArray(64); // Fixed size for Pedersen gens

    // Copy data to Java byte arrays
    env->SetByteArrayRegion(proofBytes, 0, proof_data.proof_len,
                            reinterpret_cast<const jbyte*>(proof_data.proof_bytes));
    env->SetByteArrayRegion(ristrettoBytes, 0, proof_data.ristretto_len,
                            reinterpret_cast<const jbyte*>(proof_data.ristretto_bytes));
    env->SetByteArrayRegion(pedersenBytes, 0, 64,
                            reinterpret_cast<const jbyte*>(proof_data.pedersen_bytes));

    // Get the ProofResult class
    jclass proofResultClass = env->FindClass("com/example/majorapplication/ProofResult");
    if (proofResultClass == nullptr) {
        return nullptr;
    }

    // Get the constructor
    jmethodID constructor = env->GetMethodID(proofResultClass, "<init>",
                                             "([B[B[BJJ)V");
    if (constructor == nullptr) {
        return nullptr;
    }

    // Create and return the ProofResult object
    jobject result = env->NewObject(proofResultClass, constructor,
                                    proofBytes,
                                    ristrettoBytes,
                                    pedersenBytes,
                                    (jlong)proof_data.bp_gens_capacity,
                                    (jlong)proof_data.bp_gens_party_capacity);

    // Free the native memory
    free_proof_data(proof_data);

    return result;
}
}

This concludes the JNI part.

Now lets tackle the issue of this post: inside the src directory of the Android app module, I have a class which uses the shared library:

BulletproofBindings.kt:

package com.example.majorapplication

import android.util.Log

class BulletproofsBinding {
    companion object {
        init {
            try {
                System.loadLibrary("majorapplication")
            } catch (e: UnsatisfiedLinkError) {
                Log.e("BulletproofsBinding", "Error loading library: ${e.message}")
                throw e
            }
        }
    }

    private external fun generateProof(x1: Double, y1: Double, x2: Double, y2: Double): ProofResult?

    fun generateProofForCoordinates(
        lat1: Double,
        lon1: Double,
        lat2: Double,
        lon2: Double
    ): ProofResult? {
        return generateProof(lat1, lon1, lat2, lon2)
    }
}

proofresult.kt:

package com.example.majorapplication
data class ProofResult(
    val proofBytes: ByteArray,
    val ristrettoBytes: ByteArray,
    val pedersenBytes: ByteArray,
    val bpGensCapacity: Long,
    val bpGensPartyCapacity: Long
) {
    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (javaClass != other?.javaClass) return false

        other as ProofResult

        if (!proofBytes.contentEquals(other.proofBytes)) return false
        if (!ristrettoBytes.contentEquals(other.ristrettoBytes)) return false
        if (!pedersenBytes.contentEquals(other.pedersenBytes)) return false
        if (bpGensCapacity != other.bpGensCapacity) return false
        if (bpGensPartyCapacity != other.bpGensPartyCapacity) return false

        return true
    }

    override fun hashCode(): Int {
        var result = proofBytes.contentHashCode()
        result = 31 * result + ristrettoBytes.contentHashCode()
        result = 31 * result + pedersenBytes.contentHashCode()
        result = 31 * result + bpGensCapacity.hashCode()
        result = 31 * result + bpGensPartyCapacity.hashCode()
        return result
    }
}

Now the issue is, whenever I build the app, it is executing properly, the .so files are copied into the jni libs folders properly, as I analysed through the apk inspector, but when I run it, it is throwing an UnsatisfiedLink error.

Output in logcat when app is built:

BulletproofsBinding     com.example.majorapplication         E  Error loading library: dlopen failed: 
library "C:/Users/mahes/AndroidStudioProjects/MajorApplication/app/src/main/cpp/../jniLibs/x86_64/libzkp_proximity.so" not found: 
needed by /data/app/~~-72oLmY0_i-Hq9cSQjWwCg==/com.example.majorapplication-qlCmojO1J_C3_1icpRzu4g==/base.apk!/lib/x86_64/libmajorapplication.so
 in namespace classloader-namespace
2025-02-23 17:22:44.529 13474-13474 AndroidRuntime          com.example.majorapplication         D  Shutting down VM
2025-02-23 17:22:44.531 13474-13474 AndroidRuntime          com.example.majorapplication         E  FATAL EXCEPTION: main
                                                                                                    Process: com.example.majorapplication, PID: 13474
                                                                                                    java.lang.UnsatisfiedLinkError: dlopen failed: 
library "C:/Users/mahes/AndroidStudioProjects/MajorApplication/app/src/main/cpp/../jniLibs/x86_64/libzkp_proximity.so" not found: 
needed by /data/app/~~-72oLmY0_i-Hq9cSQjWwCg==/com.example.majorapplication-qlCmojO1J_C3_1icpRzu4g==/base.apk!/lib/x86_64/libmajorapplication.so 
in namespace classloader-namespace
                                                                                                        at java.lang.Runtime.loadLibrary0(Runtime.java:1077)
                                                                                                        at java.lang.Runtime.loadLibrary0(Runtime.java:998)
                                                                                                        at java.lang.System.loadLibrary(System.java:1661)
                                                                                                        at com.example.majorapplication.BulletproofsBinding.<clinit>(BulletproofsBinding.kt:9)
                                                                                                        at com.example.majorapplication.ComposableSingletons$MainActivityKt$lambda-1$1.invoke(MainActivity.kt:29)
                                                                                                        at com.example.majorapplication.ComposableSingletons$MainActivityKt$lambda-1$1.invoke(MainActivity.kt:24)
                                                                                                        at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke

Upvotes: -4

Views: 37

Answers (0)

Related Questions