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?
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 }
[package]
name = "exper2"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["staticlib"]
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);
}
}
%module exper2
%{
#include "cbindgen/exper2.h"
%}
%include "cbindgen/exper2.h"
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
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.