Skip to content

[jnigen] Suspend functions fail in minified release builds: keep the Kotlin classes jni looks up by name #3732

Closed

Description

@tarrinneal

Summary

In a minified Android release build, every call that goes through a Kotlin suspend function via jnigen bindings fails. Flutter turns on R8 for release builds by default.

java.lang.NoSuchMethodError: no non-static method "Lcom/example/test_plugin/NativeInteropHostIntegrationCoreApiRegistrar;.noopAsync(Lkotlin/coroutines/Continuation;)Ljava/lang/Object;"

The bound class is annotated with @Keep, so it and its methods survive R8. But R8 renames kotlin.coroutines.Continuation and rewrites the descriptors of kept methods to match. The descriptor baked into the generated bindings, (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;, no longer exists.

package:jni and jnigen-generated bindings look up six Kotlin stdlib classes by name. jni's consumer rules only keep com.github.dart_lang.jni.**, and kotlin-stdlib ships no R8 rules of its own. As a result, every app that uses suspend functions through jni has to find and add these rules itself.

Kotlin classes looked up by name

Class Used for Source
kotlin.coroutines.Continuation Descriptor of every suspend method, in generated calls and in implement dispatch; resumeWith Generated code, kotlin.dart#L53-L58
kotlin.coroutines.intrinsics.CoroutineSingletons Detecting COROUTINE_SUSPENDED kotlin.dart#L14-L16
kotlin.coroutines.intrinsics.IntrinsicsKt getCOROUTINE_SUSPENDED() kotlin.dart#L36-L42
kotlin.Result value field kotlin.dart#L28-L34
kotlin.Result$Failure exception field and constructor kotlin.dart#L19-L25, #L63-L65
kotlin.Unit INSTANCE, returned from Dart implementations of suspend fun …: Unit kotlin.dart#L44-L47

For example, jnigen 1.0.1 generates:

static final _id_noopAsync = NativeInteropHostIntegrationCoreApi._class.instanceMethodId(
  r'noopAsync',
  r'(Lkotlin/coroutines/Continuation;)Ljava/lang/Object;',
);

// ...and, in `implement`:
if ($d == r'noopAsync(Lkotlin/coroutines/Continuation;)Ljava/lang/Object;') {

Proposed fix

Add the rules to pkgs/jni/android/consumer-rules.pro so that every app depending on jni gets them:

 -keep class com.github.dart_lang.jni.** { *; }
+
+# Kotlin classes that package:jni and jnigen-generated bindings look up by name
+# to support suspend functions.
+-keep class kotlin.Unit { *; }
+-keep class kotlin.Result { *; }
+-keep class kotlin.Result$Failure { *; }
+-keep class kotlin.coroutines.Continuation { *; }
+-keep class kotlin.coroutines.intrinsics.CoroutineSingletons { *; }
+-keep class kotlin.coroutines.intrinsics.IntrinsicsKt { *; }

These rules only stop R8 from renaming or removing six small stdlib classes.

Optional follow-ups:

  • Remove -keep class kotlin.coroutines.** { *; } from the kotlin_plugin example's app rules. The example then checks that the consumer rules are enough.
  • Run a suspend function test in a release build on CI.

Verification

We hit this in Pigeon's JNI support (flutter/flutter#193880). We ran Pigeon's native interop integration suite in a minified release build. The suite has 339 tests, uses jnigen 1.0.1 bindings, and every Pigeon-generated class was annotated @Keep.

Configuration Passed Failed
No extra rules 210 129
With the six rules above 339 0

All 129 failures go through a suspend function, and the errors are NoSuchMethodErrors like the one above. Everything else passes without the rules, including sync calls in both directions, data classes, enums and errors.

Steps to reproduce

  1. Write a Kotlin class with a suspend fun and annotate it with @Keep.
  2. Generate jnigen bindings for it and call the function from Dart.
  3. Run flutter run --release. The call fails with the NoSuchMethodError above.

Environment

  • jni 1.1.0 and jnigen 1.0.1. kotlin.dart and consumer-rules.pro are unchanged on main.
  • Flutter 3.49.0-1.0.pre-194, JDK 17, arm64 Android emulator.
  • kotlin-stdlib ships no R8/ProGuard rules (checked 2.4.20). kotlinx-coroutines' bundled R8 rules don't cover these classes.

Related

Activity

  1. tarrinneal commented on Oct 6, 2026

    @tarrinneal
    CollaboratorAuthor
  2. removed
    needs-triageNeeds triage from the team. Remove this label once triaged.
    on Oct 7, 2026
  3. liamappelbe commented on Oct 7, 2026

    @liamappelbe
    Contributor

    Proposed fix SGTM. @tarrinneal I'm OOO for a few weeks, so can you make a PR with your fix and send it to Daco or Michael?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions