Hello 👋 I'm looking at the grease project, specifically the https://github.com/phronmophobic/grease/blob/main/src/com/phronemophobic/grease/ios.clj#L28-L29 file requires com.phronemophobic.clj-libffi , but I can't seem to it find anywhere in deps.edn .
That generates a bb.o , which is a relocatable ELF. I guess I now need to do linking via Android NDK
@franquito nice. Which version of GraalVM / iOS SDK were you able to build against?
I've only run scripts/compile-shared but modified for Android. Using Gluon's suggested release graalvm-svm-java17-linux-gluon-22.1.0.1-Final
Should I see anything once the app launches? It gets stucked on the splash screen
For iOS, I had to plug in a device and set up debugging to see any outputs
The compile-shared example doesn't have any UI
If you don't call the exposed functions, then none of the clojure code will run.
You'll have to do something like https://github.com/phronmophobic/grease/blob/3dc4343683c070541242c7a2b1df0a3f4cbc79cd/xcode/MobileTest/MobileTest/AppDelegate.swift#L17
@rjsheperd to turn the bb.o into a libbb.so did you run a custom command? Or is that taken care by xcode?
I haven't gotten that far 😅
I hit this at runtime
> java.lang.UnsatisfiedLinkError: dlopen failed: cannot locate symbol "Java_java_net_AbstractPlainSocketImpl_isReusePortAvailable0" referenced by "/data/app/Kfuzq4jf9r80dQ8Lh9Abuw==/com.gluonhq.helloandroid-F-j74Cg_PtXD6GrzVZuQrQ==/base.apk!/lib/arm64-v8a/libbb.so"...
when trying to link using
/home/self/.gluon/substrate/Android/ndk/26.3.11579264/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android24-clang++ \
library/build/out/bb.o \
-fPIC \
-fdata-sections -ffunction-sections \
-Wl,--gc-sections \
-L/home/self/.gluon/substrate/javaStaticSdk/18-ea+prep18-9/android-aarch64/staticjdk/lib/static \
-L/home/self/graals/graalvm-svm-java17-linux-gluon-22.1.0.1-Final/lib/svm/clibraries/27/android-aarch64 \
-Wl,--whole-archive \
-ljava -lnio -lzip -lnet -lprefs -ljvm -lfdlibm -lz -ldl -lj2pkcs11 -ljaas -lextnet \
-Wl,--no-whole-archive \
-shared \
-landroid -llog -lffi -llibchelper -static-libstdc++ \
-o /home/self/files/android/app/src/main/jniLibs/arm64-v8a/libbb.sowhat's in /home/self/.gluon/substrate/javaStaticSdk/18-ea+prep18-9/android-aarch64/staticjdk/lib/static? does the missing symbol exist in one of the libs there?
Do all of -ljava -lnio -lzip -lnet -lprefs -ljvm -lfdlibm -lz -ldl -lj2pkcs11 -ljaas -lextnet exist in the static lib dir? Are there any libs in the static lib dir that aren't linked with -ltheextralib?
Adrian, from what I read about grease, is built on top of membrane. Which doesn't seem to support Android, right?
(I managed to run the HelloGluon project, after tons of hiccups)
there's an example in grease without membrane. There's also another example that does use membrane. Both examples are for iOS, but the same approach could be used for android.
You mean the https://github.com/phronmophobic/grease/tree/main/examples/objc example?
this example https://github.com/phronmophobic/grease/tree/main?tab=readme-ov-file#usage
It should be pulled in by https://github.com/phronmophobic/grease/blob/2ef455d395042a3720a53bfe5cd3a697fe86e263/deps.edn#L68. objcjure is available on clojars https://clojars.org/com.phronemophobic/objcjure
Unfortunately, grease has experienced a lot of bitrot.
• Some of the download links were broken. • Newer releases of gluon's graalvm didn't work for me (I haven't tried the most recent). • The github build actions no longer run due to deprecated subactions
I was kinda hoping all of the mobile support would improve for graalvm, but it's kind of in a weird place.
Although it's been a few months since I've checked, so maybe there's some working setup someone has found.
There are some indications that the following might work: • https://github.com/utopia-rise/ios-graal-jdk-21 from: • https://github.com/utopia-rise/godot-kotlin-jvm/issues/113 • https://github.com/oracle/graal/issues/8776
Jank may be a promising alternative approach
Thanks! I'm revisiting the repo, I'm trying to understand as much as I can to try an Android build
Oh, that's sad to hear. This project seemed like the best suited framework for Clojure development in mobile. Damn, yes, Jank never crossed my mind
The way I initially figured things out was trying the gluon samples, https://github.com/gluonhq/gluon-samples/tree/master/HelloGluon. Last time I tried, they were stuck on an old version of maven that was annoying to obtain, so I haven't tried it recently. They have some proprietary javafx stuff, but I believe the graalvm part is all GPL. If you can get the hello world sample working, you can check the logs to see how to find the non proprietary pieces and config.
You should also check out clojure dart if you haven't.
Right, I gotta revisit that Gluon sample then, I remember I managed to get the config but then got stuck trying to stitch the binary into Android. Yeah, dart lacks a REPL and also adds a whole suit of Flutter stuff that I don't care for
They just announced a repl, but I am also not interested in flutter.
> got stuck trying to stitch the binary into Android Let me know if you need help with this. The strategy for hooking into Android is essentially the same as for iOS.
For now, GPT haven't help much, so I'm just reading the code. Thanks 🙏 I'll ping you once I have a cleaner picture of how all glues together
@franquito any luck?
Nope, I'll report if I get somewhere
Hi Adrian! I see this https://github.com/phronmophobic/grease/blob/main/scripts/compile-shared#L65 during compilation. This is so that xcode can do the linking right? I don't need .h files bundled into the final APK?
the xcode build runs after that script so that line is just putting the .o file and *.h files somewhere that xcode can find them. I then run Xcode's build which needs the header files.
Thanks for the questions! Turns out substrate has a https://github.com/gluonhq/substrate/blob/cc3d9b5/src/main/resources/native/android/c/dummy.c just for this
Nice find!
Are you having to build a substrate lib.so file using native-image? Or do you just include substrate/src/main/resouces/native/android in your includes?
I just added the dummy.o file to the linking stage. First command compiles a relocatable dummy.o and second links bb.o
/home/self/.gluon/substrate/Android/ndk/26.3.11579264/toolchains/llvm/prebuilt/linux-x86_64/bin/clang -c \
-DGVM_VERBOSE \
-DSUBSTRATE \
-target aarch64-linux-android \
-I. \
-fPIC \
-DGVM_17 \
-I/home/self/files/gluon-samples/HelloGluon/target/gluonfx/aarch64-android/gvm/HelloGluon \
-o /home/self/files/android/dummy.o \
/home/self/files/android/dummy.c
/home/self/.gluon/substrate/Android/ndk/26.3.11579264/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android24-clang++ \
/home/self/files/gluon-samples/HelloGluon/target/gluonfx/aarch64-android/gvm/HelloGluon/dummy.o \
/home/self/files/android/launcher.o \
/home/self/files/android/grease_jni.o \
/home/self/files/grease-android/library/build/out/bb.o \
-fPIC \
-g -O0 \
-fno-omit-frame-pointer \
-funwind-tables \
-Wl,--build-id \
-fdata-sections -ffunction-sections \
-Wl,--gc-sections \
-L/home/self/.gluon/substrate/javaStaticSdk/18-ea+prep18-9/android-aarch64/staticjdk/lib/static \
-L/home/self/graals/graalvm-svm-java17-linux-gluon-22.1.0.1-Final/lib/svm/clibraries/27/android-aarch64 \
-Wl,--whole-archive \
-ljava -lnio -lzip -lnet -lprefs -lfdlibm -lj2pkcs11 -ljaas -lextnet \
-ljvm -lz -ldl \
-Wl,--no-whole-archive \
-shared \
-landroid -llog -static-libstdc++ \
-lffi -ljvm -llibchelper \
-o /home/self/files/android/app/src/main/jniLibs/arm64-v8a/libbb.soI'm sorry about all the noisy flags... I'll add all I have to a repo eventually
Nice. I'm trying to take better notes on my process for building OpenJDK 25
Did you manage to run the project in iPhone?
@franquito did you run yours on an android phone?
I got stuck again with a segmentation fault
Honestly, I've been going further thanks to ChatGPT. First I got a suggestion that to glue libbb.so in Android you need JNI bindings. Which suggested this
#include <jni.h>
#include <stdint.h>
extern int64_t clj_add(int64_t a, int64_t b);
extern int64_t clj_sub(int64_t a, int64_t b);
extern int64_t clj_eval(const char* s);
extern void clj_prn(int64_t id);
extern void clj_start_server(void);
JNIEXPORT jlong JNICALL
Java_com_gluonhq_helloandroid_MainActivity_clj_1add(JNIEnv*, jobject, jlong a, jlong b) {
return clj_add(a, b);
}
JNIEXPORT jlong JNICALL
Java_com_gluonhq_helloandroid_MainActivity_clj_1sub(JNIEnv*, jobject, jlong a, jlong b) {
return clj_sub(a, b);
}
JNIEXPORT jlong JNICALL
Java_com_gluonhq_helloandroid_MainActivity_clj_1eval(JNIEnv* env, jobject, jstring s) {
const char* cstr = (*env)->GetStringUTFChars(env, s, 0);
int64_t r = clj_eval(cstr);
(*env)->ReleaseStringUTFChars(env, s, cstr);
return r;
}
JNIEXPORT void JNICALL
Java_com_gluonhq_helloandroid_MainActivity_clj_1prn(JNIEnv*, jobject, jlong id) {
clj_prn(id);
}
JNIEXPORT void JNICALL
Java_com_gluonhq_helloandroid_MainActivity_clj_1start_1server(JNIEnv*, jobject) {
clj_start_server();
}
So I'm compiling that and add it to the linking command for bb.o (see grease_jni.o parameter)Then I got another suggestion that I need to initialize the GraalVM environment or something like that? Does it ring any bell for you?
that looks right for the jni side.
yea, I was just going to say that the native image binding calls will need the isolate stuff
I completely forgot I did something similar to the dummy thing, https://github.com/phronmophobic/grease/blob/3dc4343683c070541242c7a2b1df0a3f4cbc79cd/xcode/MobileTest/csource/FromGluonAppDelegate.c#L77
Right, I found this https://github.com/gluonhq/substrate/blob/cc3d9b5/src/main/resources/native/android/c/launcher.c file in substrate, which I trimmed and added something like this
#include <jni.h>
#include <stdlib.h>
#include <errno.h>
#include <android/log.h>
#define LOG_TAG "GRAAL_LAUNCHER"
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
// Provided by Graal native-image
extern int run_main(int argc, char *argv[]);
// Graal runtime marker
extern int __svm_vm_is_static_binary __attribute__((weak)) = 1;
// Global JVM state
static JavaVM* androidVM = NULL;
static JNIEnv* androidEnv = NULL;
// === JNI bootstrap ===
JNIEXPORT jint JNICALL
JNI_OnLoad(JavaVM *vm, void *reserved)
{
LOGE(">>> JNI_OnLoad ENTERED1 <<<");
androidVM = vm;
jint res = (*vm)->GetEnv(vm, (void **)&androidEnv, JNI_VERSION_1_6);
LOGE("JNI_OnLoad: vm=%p GetEnv=%d env=%p", vm, res, androidEnv);
if (res != JNI_OK) {
LOGE("JNI_OnLoad: GetEnv FAILED");
return JNI_ERR;
}
LOGE(">>> JNI_OnLoad EXIT <<<");
return JNI_VERSION_1_6;
}
// === Entry point from Java ===
// MUST be called explicitly from Java
JNIEXPORT void JNICALL
Java_com_gluonhq_helloandroid_MainActivity_startGraalApp(
JNIEnv *env,
jobject thiz)
{
LOGE(">>> startGraalApp ENTERED <<<");
LOGE("env=%p thiz=%p", env, thiz);
const char *argv[] = {
"graal-app"
};
LOGE("Calling run_main()");
int rc = run_main(1, (char **)argv);
LOGE("run_main() returned %d", rc);
LOGE(">>> startGraalApp EXIT <<<");
}that launcher looks like if you're embedding a JVM. Here's what I did with regards to the isolate stuff, https://github.com/phronmophobic/grease/blob/3dc4343683c070541242c7a2b1df0a3f4cbc79cd/xcode/MobileTest/csource/Bridge.m
when you built the bb.o, I think it also spits out a header file like bb.h
I believe https://www.graalvm.org/22.2/reference-manual/native-image/native-code-interoperability/C-API/index.html is the docs, but it's frustratingly terse.
Mmm thanks. My launcher.c code calls run_main . I'll try using graal_create_isolate instead
actually, I think the Bridge.m code is a little wrong with respect to the thread parameter. I started using something like:
graal_isolate_t *isolate = _clj_main_view.isolate;
graal_isolatethread_t* thread = graal_get_current_thread(isolate);
if (thread){
// already attached, do not detach
com_phronemophobic_clj_libffi_callback(thread , key, ret, args);
} else{
graal_attach_thread(isolate, &thread);
com_phronemophobic_clj_libffi_callback(thread , key, ret, args);
graal_detach_thread(thread);
}
That's ok. I'm happy with just having another type of error. Does the code in https://github.com/phronmophobic/grease/tree/3dc4343683c070541242c7a2b1df0a3f4cbc79cd/xcode/MobileTest/csource is built and then the relocatable object files are used with the bb.o to link the final shared library? Or they constitute another extra shared library?
You don't need to build a shared library. You can just link`.o` files.
On iOS, you can't link shared libraries. Everything has to be static libs.
On iOS, it's a little different since everything is native code. I don't quite remember how things work on android
This is what I get as final output
file app/src/main/jniLibs/arm64-v8a/libbb.so
app/src/main/jniLibs/arm64-v8a/libbb.so: ELF 64-bit LSB shared object, ARM aarch64, version 1 (SYSV), dynamically linked, BuildID[xxHash]=5a141d13b7c0694c, with debug_info, not strippedThat doesn't look wrong. The android jni projects I've worked on were setup so long ago that I don't remember the mechanics. I just know the script file where I add the .h and .cpp files and the build script does the rest
I'll report back when I get a new error 😛
Looking at this > graal_isolate_t *isolate = cljmain_view.isolate; It seeems you donn't need to create an isolate?
Or can you get me more context from that quoted code?
I create the isolate once at app startup
Now I'm getting a cryptic Abort message: 'destroying mutex with owner or contenders. Owner:6650' . I put some logs on the JNI glue code and is loggin until the call clj_eval (first branch of the if)
#include <jni.h>
#include <stdint.h>
#include <android/log.h>
#include "graal_isolate.h"
// for setenv
#include <stdlib.h>
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, "JNI-GRAAL", __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, "JNI-GRAAL", __VA_ARGS__)
graal_isolate_t *isolate = NULL;
graal_isolatethread_t *thread = NULL;
// Graal-exported Clojure function
extern int64_t clj_eval(graal_isolatethread_t*, const char*);
__attribute__((constructor))
static void graal_debug_env() {
setenv("GRAALVM_ABORT_ON_ERROR", "1", 1);
}
JNIEXPORT jlong JNICALL
Java_com_gluonhq_helloandroid_MainActivity_clj_1eval(
JNIEnv* env, jobject obj, jstring s) {
LOGI("JNI clj_eval: entry");
if ( !isolate ){
if (graal_create_isolate(NULL, &isolate, &thread) != 0) {
LOGE("JNI clj_eval: error creating isolate");
return 1;
}
}
graal_isolatethread_t* t = graal_get_current_thread(isolate);
if (t) {
LOGI("JNI clj_eval: already attaced %p", t);
const char* cstr = (*env)->GetStringUTFChars(env, s, NULL);
LOGI("JNI clj_eval: UTF");
int64_t r = clj_eval(t, cstr);
LOGI("JNI clj_eval: after eval");
(*env)->ReleaseStringUTFChars(env, s, cstr);
return r;
} else {
LOGI("JNI clj_eval: about to attach");
graal_attach_thread(isolate, &t);
LOGI("JNI clj_eval: attached!");
const char* cstr = (*env)->GetStringUTFChars(env, s, NULL);
int64_t r = clj_eval(t, cstr);
(*env)->ReleaseStringUTFChars(env, s, cstr);
LOGI("JNI clj_eval: about to detach");
graal_detach_thread(t);
LOGI("JNI clj_eval: detached");
return r;
}
}It doesn't seem like the string functions are guaranteed to be null terminated, https://docs.oracle.com/en/java/javase/11/docs/specs/jni/functions.html#string-operations
Maybe try a 0 arity function or just hard code a string to pass to clj_eval to see what happens.
clj_eval(t, "42");
No luck 🤔 I think this one will get me stuck for a while. Thankss!!!