graalvm-mobile

frankitox 2026-01-08T15:55:33.921589Z

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 .

frankitox 2026-01-30T17:16:23.630679Z

That generates a bb.o , which is a relocatable ELF. I guess I now need to do linking via Android NDK

👍 1
rjsheperd 2026-01-30T17:26:34.621809Z

@franquito nice. Which version of GraalVM / iOS SDK were you able to build against?

frankitox 2026-01-30T17:31:01.280149Z

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

frankitox 2026-01-30T18:09:13.332999Z

Should I see anything once the app launches? It gets stucked on the splash screen

rjsheperd 2026-01-30T18:09:52.829079Z

For iOS, I had to plug in a device and set up debugging to see any outputs

➕ 1
phronmophobic 2026-01-30T18:10:27.044549Z

The compile-shared example doesn't have any UI

phronmophobic 2026-01-30T18:11:08.909149Z

If you don't call the exposed functions, then none of the clojure code will run.

frankitox 2026-01-30T19:37:29.762879Z

@rjsheperd to turn the bb.o into a libbb.so did you run a custom command? Or is that taken care by xcode?

rjsheperd 2026-01-30T19:47:21.076309Z

I haven't gotten that far 😅

😢 1
frankitox 2026-01-30T19:59:11.692989Z

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.so

phronmophobic 2026-01-30T20:33:48.199049Z

what'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?

phronmophobic 2026-01-30T20:34:42.545679Z

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?

frankitox 2026-01-28T19:46:23.430069Z

Adrian, from what I read about grease, is built on top of membrane. Which doesn't seem to support Android, right?

frankitox 2026-01-28T19:46:53.332729Z

(I managed to run the HelloGluon project, after tons of hiccups)

phronmophobic 2026-01-28T19:50:33.345529Z

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.

frankitox 2026-01-28T20:00:52.748929Z

You mean the https://github.com/phronmophobic/grease/tree/main/examples/objc example?

phronmophobic 2026-01-28T20:20:09.045759Z

this example https://github.com/phronmophobic/grease/tree/main?tab=readme-ov-file#usage

👍 1
borkdude 2026-01-08T15:56:16.048829Z

cc @smith.adriane

👍 1
phronmophobic 2026-01-08T16:47:14.796869Z

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

phronmophobic 2026-01-08T16:48:00.179749Z

Unfortunately, grease has experienced a lot of bitrot.

phronmophobic 2026-01-08T16:51:17.863969Z

• 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

phronmophobic 2026-01-08T16:54:37.001379Z

I was kinda hoping all of the mobile support would improve for graalvm, but it's kind of in a weird place.

phronmophobic 2026-01-08T16:59:40.371069Z

Although it's been a few months since I've checked, so maybe there's some working setup someone has found.

phronmophobic 2026-01-08T17:02:04.536509Z

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/113https://github.com/oracle/graal/issues/8776

phronmophobic 2026-01-08T17:03:05.405139Z

Jank may be a promising alternative approach

frankitox 2026-01-08T17:53:53.286329Z

Thanks! I'm revisiting the repo, I'm trying to understand as much as I can to try an Android build

frankitox 2026-01-08T17:54:14.121379Z

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

phronmophobic 2026-01-08T18:01:00.544819Z

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.

phronmophobic 2026-01-08T18:02:07.363359Z

You should also check out clojure dart if you haven't.

frankitox 2026-01-08T19:44:23.981779Z

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

phronmophobic 2026-01-08T19:46:53.689379Z

They just announced a repl, but I am also not interested in flutter.

phronmophobic 2026-01-08T19:49:19.504029Z

> 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.

frankitox 2026-01-08T19:52:13.292359Z

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

👍 1
rjsheperd 2026-01-18T21:04:32.470899Z

@franquito any luck?

frankitox 2026-01-22T20:22:37.008199Z

Nope, I'll report if I get somewhere

frankitox 2026-02-07T16:25:05.805479Z

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?

phronmophobic 2026-02-07T19:29:17.067369Z

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.

🙏 1
frankitox 2026-02-02T13:35:28.652829Z

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

👍 1
rjsheperd 2026-02-02T16:40:50.499959Z

Nice find!

rjsheperd 2026-02-02T16:42:09.053219Z

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?

frankitox 2026-02-02T16:45:29.542959Z

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.so

frankitox 2026-02-02T17:02:52.332349Z

I'm sorry about all the noisy flags... I'll add all I have to a repo eventually

rjsheperd 2026-02-02T17:53:14.113899Z

Nice. I'm trying to take better notes on my process for building OpenJDK 25

1
frankitox 2026-02-02T18:39:09.878339Z

Did you manage to run the project in iPhone?

phronmophobic 2026-02-02T18:45:39.248859Z

@franquito did you run yours on an android phone?

frankitox 2026-02-02T18:48:21.768499Z

I got stuck again with a segmentation fault

frankitox 2026-02-02T18:52:54.915379Z

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)

frankitox 2026-02-02T18:54:02.120549Z

Then I got another suggestion that I need to initialize the GraalVM environment or something like that? Does it ring any bell for you?

phronmophobic 2026-02-02T18:54:11.085579Z

that looks right for the jni side.

phronmophobic 2026-02-02T18:54:34.158249Z

yea, I was just going to say that the native image binding calls will need the isolate stuff

👍 1
phronmophobic 2026-02-02T18:55:29.698839Z

I completely forgot I did something similar to the dummy thing, https://github.com/phronmophobic/grease/blob/3dc4343683c070541242c7a2b1df0a3f4cbc79cd/xcode/MobileTest/csource/FromGluonAppDelegate.c#L77

frankitox 2026-02-02T18:55:53.129089Z

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 <<<");
}

phronmophobic 2026-02-02T18:57:13.996109Z

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

👍 1
phronmophobic 2026-02-02T18:57:50.905759Z

when you built the bb.o, I think it also spits out a header file like bb.h

phronmophobic 2026-02-02T18:58:43.773409Z

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.

frankitox 2026-02-02T19:00:14.914309Z

Mmm thanks. My launcher.c code calls run_main . I'll try using graal_create_isolate instead

phronmophobic 2026-02-02T19:02:38.106279Z

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);
    }

frankitox 2026-02-02T19:04:36.655319Z

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?

phronmophobic 2026-02-02T19:05:31.873009Z

You don't need to build a shared library. You can just link`.o` files.

phronmophobic 2026-02-02T19:05:46.870839Z

On iOS, you can't link shared libraries. Everything has to be static libs.

phronmophobic 2026-02-02T19:07:11.702849Z

On iOS, it's a little different since everything is native code. I don't quite remember how things work on android

frankitox 2026-02-02T19:07:12.293979Z

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 stripped

phronmophobic 2026-02-02T19:09:14.243989Z

That 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

frankitox 2026-02-02T19:14:12.041259Z

I'll report back when I get a new error 😛

2
🚀 2
🎉 1
frankitox 2026-02-02T21:55:56.719069Z

Looking at this > graal_isolate_t *isolate = cljmain_view.isolate; It seeems you donn't need to create an isolate?

frankitox 2026-02-02T21:57:16.400329Z

Or can you get me more context from that quoted code?

phronmophobic 2026-02-02T21:59:12.957939Z

I create the isolate once at app startup

frankitox 2026-02-02T22:46:03.367339Z

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;
    }

}

phronmophobic 2026-02-02T22:52:24.536079Z

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

phronmophobic 2026-02-02T22:53:31.768309Z

Maybe try a 0 arity function or just hard code a string to pass to clj_eval to see what happens.

👍 1
phronmophobic 2026-02-02T22:53:49.614569Z

clj_eval(t, "42");

frankitox 2026-02-02T23:14:28.005399Z

No luck 🤔 I think this one will get me stuck for a while. Thankss!!!