crustswigentry-pointandroid-native-library

Android app crashes on load library, cannot find entry point getThreadLocalsEv, how to fix? [example added]


My code is crashing on System.loadLibrary("hypoboleus"); with an error that it cannot find an entry point. I have created a so file and included it in the app/src/main/jniLibs/arm64-v8a/ folder as libhypoboleus.so. The Java runtime is calling java.lang.Runtime.loadLibrary0, which is trying to run getThreadLocalsEv from my so file. Edit: added example code to the end of this question.

This used to work in an earlier version of the NDK, but I upgraded and then it made me change my build script. My current version of the NDK is 25.2.9519653.

The specific message text is java.lang.UnsatisfiedLinkError: dlopen failed: TLS symbol "_ZZN8gwp_asan15getThreadLocalsEvE6Locals" in dlopened "/data/app/~~nqWwoXRjQhq9nhyacG54hA==/hk.jennyemily.hypoboleus-EFD9ul4kulUwExi3Ee0LJQ==/base.apk!/lib/arm64-v8a/libhypoboleus.so" referenced from "/data/app/~~nqWwoXRjQhq9nhyacG54hA==/hk.jennyemily.hypoboleus-EFD9ul4kulUwExi3Ee0LJQ==/base.apk!/lib/arm64-v8a/libhypoboleus.so" using IE access model.

The entry point is present in the so file: nm -D gives 0000000000000000 W _ZZN8gwp_asan15getThreadLocalsEvE6Locals.

My build line is path-to-ndk/25.2.9519653/toolchains/llvm/prebuilt/linux-x86_64/bin/clang -target aarch64-linux-android-clang -nostartfiles -shared path/hypoboleus_wrap.c path/libhypoboleus_c.a -lm -lz -o path/libhypoboleus.so -I path -fPIC [I have simplified the paths]

I've tried what I think is the obvious but nothing seems to work. All I can think of is that I am building C but the error refers tyo what looks to me like a C++ entry. The code is mainly written in Rust (pretending to be C) with a standard shim generated by SWIG.

Can anyone please advise me, or at least give me some idea how to investigate further?

Rust code

If I delete the two lines marked "delete me" then everything works. Note that new_engine() is not called anywhere.

use std::sync::{Arc, Mutex};
pub struct UserHandle {
    ptr: Arc<Mutex<UserData>>, // delete me
}
pub struct UserData {}
#[no_mangle]
pub unsafe extern "C" fn new_engine() -> *mut UserHandle {
    Box::into_raw(Box::new(UserHandle {
       ptr: (Arc::new(Mutex::new(UserData {}))), // delete me
    }))
}
#[no_mangle]
pub unsafe extern "C" fn get_answer() -> std::ffi::c_int { 57 }

Cargo file

[package]
name = "exper2"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["staticlib"] 

Android main activity

The crash occurs on System.loadLibrary("exper2"); so it never gets to call get_answer().

package com.example.experiment1;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.util.Log;
import android.widget.TextView;

import exper2.*;

public class MainActivity extends AppCompatActivity {
    private final static String TAG = "exper2";

    static {
        try {
            Log.d(TAG, "loading library [Java]...");
            System.loadLibrary("exper2");
            Log.d(TAG, "loaded library [Java].");
        } catch (Exception e) {
            Log.e(TAG, "exception in load library [Java]: " + e);
        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Log.d(TAG, "looking for answer [Java]...");
        int answer = exper2.get_answer();
        Log.d(TAG, "answer is " + answer + " [Java]...");
        TextView textView = (TextView) findViewById(R.id.answerText);
        textView.setText("The answer is " + answer);
    }
}

SWIG interface file

%module exper2
%{
#include "cbindgen/exper2.h"
%}
%include "cbindgen/exper2.h"

Build script

Once you have built the dynamic library, you can build the Java using Android studio and then run it.

LIBID=$1

GITDIR="$HOME/git"
export ARCH=aarch64-linux-android
case $LIBID in
    exper2)
        RUSTID=exper2
        REPO=hypoboleus
        ANDAPP=Experiment1
        ;;
    *)
        echo "parameter missing or unknown \"'$LIBID'\", valid hypoboleus or exper2"
        exit 1
        ;;
esac
RUSTID2=${RUSTID//-/_}
TARGETBASE=$GITDIR/$REPO/$RUSTID/target
TARGETDIR=$TARGETBASE/$ARCH/release

export BASE2=$GITDIR/$REPO/$RUSTID
export APPBASE=$GITDIR/$REPO/$ANDAPP
export APPMAIN=$APPBASE/app/src/main
export ANDROID_HOME=/work/android/sdk
export NDK_VERSION=25.2.9519653
export API=33
export NDK_HOME=$ANDROID_HOME/ndk/$NDK_VERSION
export TOOLCHAIN=$NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64
export CC=$TOOLCHAIN/bin/clang
export AR=$TOOLCHAIN/bin/llvm-ar
export RANLIB=$TOOLCHAIN/bin/llvm-ranlib

cd $BASE2
echo 'base for rust' $BASE2 ', target dir for rust build' $TARGETDIR
# rm -rf $TARGETBASE
mkdir -p $TARGETDIR
RUSTEX_ANDR=$TARGETDIR/lib${RUSTID2}.a
# rm -f $RUSTEX_ANDR
cargo build --target $ARCH --target-dir $TARGETBASE --release --lib
echo 'archive to' $RUSTEX_ANDR
if [[ ! -f $RUSTEX_ANDR ]]; then
    echo "no library file" $RUSTEX_ANDR
    ls $TARGETDIR/lib*
    exit 1
fi

CBINDGEN_DIR=$BASE2/cbindgen/
rm -rf $CBINDGEN_DIR
mkdir -p $CBINDGEN_DIR
cbindgen $BASE2/src/lib.rs --output $CBINDGEN_DIR/${LIBID}.h --lang c

LIBDIR=$APPMAIN/java/$LIBID
echo 'library is called' $LIBID ', stored in' $LIBDIR
rm -rf $LIBDIR
mkdir -p $LIBDIR
swig -outdir $LIBDIR -java -package $LIBID $BASE2/$LIBID.i

SODIR=$APPMAIN/jniLibs/arm64-v8a
rm -rf $SODIR
mkdir -p $SODIR
SONAME=lib${LIBID}.so
echo 'dynamic library to' $SODIR/$SONAME

$CC -target $ARCH-clang -nostartfiles -shared $BASE2/${LIBID}_wrap.c $RUSTEX_ANDR -lm -lz -o $SODIR/$SONAME -I $BASE2 -fPIC

Solution

  • In the end, I reverted to version 22 of the NDK.

    My previous build had used version 22, and had worked. So I downloaded NSK version 22 from the Android website and rebuilt my app, and it worked.

    This is a workaround rather than a proper solution, but it will do me for now.