swiftlang / swift-java

Apache License 2.0
708 stars 25 forks source link

Class loader hints for Android #154

Open lhoward opened 2 hours ago

lhoward commented 2 hours ago

Android JVM class resolution is tricky, as which classes are resolvable depends not only on the JNI environment but also the stack frame.

From https://developer.android.com/training/articles/perf-jni:

You can get into trouble if you create a thread yourself (perhaps by calling pthread_create and then attaching it with AttachCurrentThread). Now there are no stack frames from your application. If you call FindClass from this thread, the JavaVM will start in the "system" class loader instead of the one associated with your application, so attempts to find app-specific classes will fail.

There are a few ways to work around this:

  • Do your FindClass lookups once, in JNI_OnLoad, and cache the class references for later use. Any FindClass calls made as part of executing JNI_OnLoad will use the class loader associated with the function that called System.loadLibrary (this is a special rule, provided to make library initialization more convenient). If your app code is loading the library, FindClass will use the correct class loader.
  • Pass an instance of the class into the functions that need it, by declaring your native method to take a Class argument and then passing Foo.class in.
  • Cache a reference to the ClassLoader object somewhere handy, and issue loadClass calls directly. This requires some effort.

Doing FindClass() lookups in JNI_OnLoad isn't really viable unless JavaKit caches classes and we can ’poison’ the cache by resolving any classes we will need there. It's is not ideal though as it requires a priori knowledge of all the application classes (I suppose, this could be done with a macro). Conversely, passing an instance of the class into functions that need it doesn't make for a great API. Neither of these is particularly ergonomic because getJNIClass() can't be overridden (it's in an extension), so one ends up needing to reimplement dynamicJavaNewObjectInstance().

Caching a reference to the ClassLoader object might be the best approach, this can be done in JNI_OnLoad (as long as you know the name of at least one application class), and then perhaps it could be an additional optional argument to initialisers. Or JNIEnvironment becomes a class to which class loaders can be attached.

Anyway, just flagging this as food for thought.

lhoward commented 2 hours ago

As a test, I cached the class loader in JNI_OnLoad, and implemented my own FindClass() that used it. (I thought I might be able to swizzle the JNI env, which whilst ugly would be quite convenient, but no dice – not writable memory.)

This gets me a few steps further but plenty of things like as() are broken. So this is a bit of a tricky one. I think this is going to come up in any non-trivial Android application because you're likely going to need to allocate Java objects from the Swift side (edit: and from multiple threads).

lhoward commented 2 hours ago

In theory this shouldn't be a problem if we resolve the classes directly within the native method entry point (as in, not JNI_OnLoad but the first API call). Needs more investigation at this end, ISTR perhaps classes were visible in OnLoad but not from application methods. Gah.

lhoward commented 44 minutes ago

Maybe we could have an optional protocol a Swift class could adopt that returns the preferred class loader as a static variable. That should be fairly easy to prototype at least.