Status Update
Comments
aa...@yandex-team.ru <aa...@yandex-team.ru> #2
Also crashes in R8 version 8.5.35, 8.6.17, 8.7.2-dev
sg...@google.com <sg...@google.com> #3
You cannot apply keep rules for classes synthesized by R8, but if you keep the implemented interfaces, the merging should not happen.
That said, checking the implementing class of a lambda might not be a good idea. The specification for java.lang.invoke.LambdaMetafactory
Capture may involve allocation of a new function object, or may return a suitable existing function object. The identity of a function object produced by capture is unpredictable, and therefore identity-sensitive operations (such as reference equality, object locking, and System.identityHashCode()) may produce different results in different implementations, or even upon different invocations in the same implementation.
If I understand this correctly the only guarantee is that the function object implements the interface(s) specified.
aa...@yandex-team.ru <aa...@yandex-team.ru> #4
The actual problem is that R8 deletes FunInterface1 and FunInterface2 classes, which are used for runtime di:
fun hello() {
val instance1 = FunInterface1 { println("Doing business") }
val instance2 = FunInterface2 { println("Starting the show") }
val map = mapOf(
FunInterface1::class.java to instance1,
FunInterface2::class.java to instance2,
)
check(map.size == 2) { "Map size is ${map.size}" }
}
So do I have to list all fun interfaces in my sdk using -keep? I might do that for ~100 of them that exist right now, but this won't save future developers from adding another fun interface and forgetting to list them in .pro file.
sg...@google.com <sg...@google.com> #5
Not sure what the use case is here. However, if you want the interfaces to stay as is you will have to have a -keep
rule for them. One option to handle this without adding a rule for each interface could be to annotate them and add a single rule for all interfaces annotated by that annotation.
aa...@yandex-team.ru <aa...@yandex-team.ru> #6
Our use case is to create a static in-memory Di object, to put and get instances from all over the multi-module application. It kinda looks like
class DiContainer(bindings: Map<Class<*>, SingletonBinding<*>>) {
private val bindings: Map<Class<*>, SingletonBinding<*>> = HashMap(bindings)
fun <T : Any> instance(typeSpec: Class<*>): T {
@Suppress("UNCHECKED_CAST")
val result: T = (bindings[typeSpec]?.lazy?.value) as? T ?: error("No binding for $typeSpec found.")
return result
}
}
class SingletonBinding<out T : Any>(private val singleton: () -> T) {
val lazy = lazy { singleton() }
}
class DiScope {
val bindings: MutableMap<Class<*>, SingletonBinding<*>> = HashMap()
fun addBinding(typeSpec: Class<*>, binding: SingletonBinding<*>) {
check(typeSpec !in bindings) { "$typeSpec already registered." }
bindings[typeSpec] = binding
}
}
open class DiHolder {
@Volatile
@PublishedApi
internal var instance: DiContainer? = null
protected fun build(init: DiScope.() -> Unit) {
check(instance == null) { "Di already initialized" }
instance = DiContainer(DiScope().apply(init).bindings)
}
inline fun <reified T : Any> instance(): T = instance(T::class.java)
fun <T : Any> instance(typeSpec: Class<*>): T = instance!!.instance(typeSpec)
}
object Di : DiHolder() {
operator fun invoke(init: DiScope.() -> Unit) = build(init)
}
Now, when we use this, for example:
fun hello() {
Di {
addBinding(FunInterface1::class.java, SingletonBinding { FunInterface1 { println("Doing business") } })
addBinding(FunInterface2::class.java, SingletonBinding { FunInterface2 { println("Doing business") } })
}
// In some other place in code, but in the same process
Di.instance<FunInterface1>().doBusiness()
Di.instance<FunInterface2>().startTheShow()
}
Now we can get an interface instance from anywhere in the app, because Di is singleton. However, when running release with R8 full mode, I get this exception:
java.lang.RuntimeException: Unable to start activity ComponentInfo{com.example.r8/com.example.r8.MainActivity}: java.lang.IllegalStateException: class com.example.r8.MainActivityKt$$ExternalSyntheticLambda2 already registered.
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3782)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3922)
at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:103)
at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:139)
at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:96)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2443)
at android.os.Handler.dispatchMessage(Handler.java:106)
at android.os.Looper.loopOnce(Looper.java:205)
at android.os.Looper.loop(Looper.java:294)
at android.app.ActivityThread.main(ActivityThread.java:8176)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:552)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:971)
Caused by: java.lang.IllegalStateException: class com.example.r8.MainActivityKt$$ExternalSyntheticLambda2 already registered.
at com.example.r8.DiScope.addBinding(Unknown Source:37)
at com.example.r8.MainActivity.onCreate(SourceFile:3)
at android.app.Activity.performCreate(Activity.java:8595)
at android.app.Activity.performCreate(Activity.java:8573)
at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1456)
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3764)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3922)
at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:103)
at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:139)
at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:96)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2443)
at android.os.Handler.dispatchMessage(Handler.java:106)
at android.os.Looper.loopOnce(Looper.java:205)
at android.os.Looper.loop(Looper.java:294)
at android.app.ActivityThread.main(ActivityThread.java:8176)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:552)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:971)
aa...@yandex-team.ru <aa...@yandex-team.ru> #7
I also tried koin, and somehow their code arrangement dodges the type check, and everything works.
Oh, I see now, by default their
// These are reduced InsertKoinIO/koin sources
class InstanceRegistry {
val instances = ConcurrentHashMap<String, SingleInstanceFactory<*>>()
fun saveMapping(mapping: String, factory: SingleInstanceFactory<*>) {
if (instances.containsKey(mapping)) {
throw Exception("Already existing definition for ${factory.beanDefinition} at $mapping")
}
instances[mapping] = factory
}
inline fun <reified T> instance(): T {
return instances[T::class.getFullName()]?.get() as T
}
}
class SingleInstanceFactory<T>(val beanDefinition: () -> T) {
private var value: T? = null
private fun getValue(): T = value ?: error("Single instance created couldn't return value")
fun get(): T {
synchronized(this) {
if (value == null) {
value = beanDefinition.invoke()
}
}
return getValue()
}
}
fun KClass<*>.getFullName(): String = this.java.name
/// --------------
fun helloKoin() {
val koin = InstanceRegistry()
val shouldR8CrashTheApp = true
if (shouldR8CrashTheApp) {
koin.saveMapping(FunInterface1::class.getFullName(), SingleInstanceFactory { FunInterface1 { println("Do business") } })
koin.saveMapping(FunInterface2::class.getFullName(), SingleInstanceFactory{ FunInterface2 { println("Start the show") } })
} else {
// because in runtime FunInterfaces are clamped into one class, only one saveMapping is called, and everything works
val mappings = hashMapOf<String, SingleInstanceFactory<*>>()
mappings[FunInterface1::class.getFullName()] = SingleInstanceFactory { FunInterface1 { println("Do business") } }
mappings[FunInterface2::class.getFullName()] = SingleInstanceFactory { FunInterface2 { println("Start the show") } }
mappings.forEach { (mapping, factory) ->
koin.saveMapping(mapping, factory)
}
}
koin.instance<FunInterface1>().doBusiness()
koin.instance<FunInterface2>().startTheShow()
}
aa...@yandex-team.ru <aa...@yandex-team.ru> #8
It would also be nice, if FunInterface1::class.java == FunInterface2::class.java
and setOf(FunInterface1::class.java, FunInterface2::class.java).size == 2
aa...@yandex-team.ru <aa...@yandex-team.ru> #9
Also, is there a way to disable this optimization only for my classes? Or can I only remove all optimizations via -keep, allowobfuscation, allowshrinking class_names
?
ap...@google.com <ap...@google.com> #10
Branch: main
commit ccfe5ff0d44034c39187c5c2656503549a614934
Author: Christoffer Adamsen <christofferqa@google.com>
Date: Wed Aug 07 08:41:16 2024
Test Map with class const keys
Bug:
Change-Id: Icb32bf21030c7800c409ba5e4279dc2bdee05cb4
A src/test/java/com/android/tools/r8/classmerging/horizontal/ConstClassAfterVerticalClassMergingTest.java
ap...@google.com <ap...@google.com> #11
Branch: main
commit f5c7414f6891c90f375215f6e3e84db42435f0f6
Author: Christoffer Adamsen <christofferqa@google.com>
Date: Wed Aug 07 10:47:18 2024
Disable synthetic sharing of synthetics with vertically merged sources
Bug:
Change-Id: I9cc17b10095c5a74f3f142217b5f4c6a4de37f09
M src/main/java/com/android/tools/r8/shaking/Enqueuer.java
M src/main/java/com/android/tools/r8/shaking/KeepClassInfo.java
M src/main/java/com/android/tools/r8/shaking/KeepInfoCollection.java
A src/main/java/com/android/tools/r8/shaking/SyntheticKeepClassInfo.java
M src/main/java/com/android/tools/r8/synthesis/SyntheticFinalization.java
M src/main/java/com/android/tools/r8/verticalclassmerging/VerticalClassMerger.java
M src/test/java/com/android/tools/r8/classmerging/horizontal/ConstClassAfterVerticalClassMergingTest.java
sg...@google.com <sg...@google.com> #12
Turned out the issue was an interplay between the vertical class merging and sharing of synthesized lambda classes. This should be fixed and you can try a build from HEAD with the fix by merging the following into your settings.gradle
or settings.gradle.kts
:
pluginManagement {
buildscript {
repositories {
mavenCentral()
maven {
url = uri("https://storage.googleapis.com/r8-releases/raw/main")
}
}
dependencies {
classpath("com.android.tools:r8:f5c7414f6891c90f375215f6e3e84db42435f0f6")
}
}
}
Description
AGP: 8.5.1
R8: 8.5.27
Kotlin: 2.0.0
I create an SDK for other apps to consume, and I started to see crashes around fun interfaces, when consumers compile with full mode enabled.
Source:
When running this code in debug, no crash occurs. When running this code in release with minify and fullmode, it crashes, because classes are merged.
These fun interfaces are compiled by kotlin to invokedynamic:
Then, as I understand, R8 rewrites calls to invokedynamic to instantiation of R8-generated synthetic classes, then R8 merges these two classes into one because they are synthetic.
If I rewrite these SAM declarations to anonymous objects, kotlin compiles them to lambda classes (MainActivityKt$hello$instance1$1), and release mode does not crash
I also attached sample.
Is this a bug?
If not, how can I disable horizontal class merging for my classes?