uniffi-rs icon indicating copy to clipboard operation
uniffi-rs copied to clipboard

java.lang.VerifyError: Bad type on operand stack

Open Lonami opened this issue 3 years ago • 6 comments

My uniffi version is 0.21.0. I think there might be an issue when trying to construct uniffi types within a Jetpack Compose @Preview @Composable, but I'm not sure if it's a problem with uniffi, the Kotlin compiler, the Jetpack library, or my own code.

My .udl contains something like this:

enum Formatting {
    "Bold",
    "Italic",
    "Pre",
};

dictionary TextFormat {
    Formatting format;
    i32 offset;
    i32 length;
    string? extra;
};

dictionary Message {
    i32 id;
    string sender;
    string text;
    timestamp date;
    timestamp? edit_date;
    sequence<TextFormat> formatting;
};

I have the following Kotlin code constructing the Message instance:

@Preview
@Composable
fun MessagePreview() {
    val msg = remember {
        Message(
            id = 1,
            sender = "Alice",
            text = "Testing",
            date = Instant.now(),
            editDate = null,
            formatting = listOf(),
        )
    }

    Text(text = msg.text)
}
Trimmed build.gradle
// using gradle-7.4

// project
buildscript {
    ext {
        compose_ui_version = '1.2.1'
    }
}
plugins {
    id 'com.android.application' version '7.2.2' apply false
    id 'com.android.library' version '7.2.2' apply false
    id 'org.jetbrains.kotlin.android' version '1.6.10' apply false
    id "org.mozilla.rust-android-gradle.rust-android" version "0.9.3"
}

// module
plugins {
    id 'com.android.application'
    id 'org.jetbrains.kotlin.android'
    id "org.mozilla.rust-android-gradle.rust-android"
}

android {
    namespace 'com.example.myapp'
    compileSdk 33

    defaultConfig {
        applicationId "com.example.myapp"
        minSdk 26
        targetSdk 33
        versionCode 1
        versionName "0.1.0"

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
        vectorDrawables {
            useSupportLibrary true
        }
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
    kotlinOptions {
        jvmTarget = '1.8'
    }
    buildFeatures {
        compose true
    }
    composeOptions {
        kotlinCompilerExtensionVersion '1.1.1'
    }
    packagingOptions {
        resources {
            excludes += '/META-INF/{AL2.0,LGPL2.1}'
        }
    }
    ndkVersion '25.1.8937393'
}

dependencies {
    implementation 'androidx.core:core-ktx:1.9.0'
    implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.5.1'
    implementation "androidx.lifecycle:lifecycle-viewmodel-compose:2.5.1"
    implementation 'androidx.activity:activity-compose:1.6.0'
    implementation "androidx.compose.ui:ui:$compose_ui_version"
    implementation "androidx.compose.ui:ui-tooling-preview:$compose_ui_version"
    implementation 'androidx.compose.material:material:1.2.1'
    implementation "androidx.navigation:navigation-compose:2.5.2"
    implementation "net.java.dev.jna:jna:5.12.0@aar"
    testImplementation 'junit:junit:4.13.2'
    androidTestImplementation 'androidx.test.ext:junit:1.1.3'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
    androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_ui_version"
    debugImplementation "androidx.compose.ui:ui-tooling:$compose_ui_version"
    debugImplementation "androidx.compose.ui:ui-test-manifest:$compose_ui_version"
}

The render preview fails as follows:

Stack Trace
java.lang.VerifyError: Bad type on operand stack
Exception Details:
  Location:
    uniffi/myapp/LiveLiterals$MyappKt.Byte$arg-0$call-put$branch$if$fun-write$class-FfiConverterOptionalString()B @11: ireturn
  Reason:
    Type 'java/lang/Object' (current frame, stack[0]) is not assignable to integer
  Current Frame:
    bci: @11
    flags: { }
    locals: { 'uniffi/myapp/LiveLiterals$MyappKt' }
    stack: { 'java/lang/Object' }
  Bytecode:
    0000000: 2a13 030c 1303 7401 b803 1cac          

	at uniffi.myapp.Message.<clinit>(myapp.kt)
	at com.example.myapp.ui.screens.SomeScreenKt.MessagePreview(SomeScreen.kt:151)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.base/java.lang.reflect.Method.invoke(Method.java:566)
	at androidx.compose.ui.tooling.ComposableInvoker.invokeComposableMethod(ComposableInvoker.kt:155)
	at androidx.compose.ui.tooling.ComposableInvoker.invokeComposable(ComposableInvoker.kt:195)
	at androidx.compose.ui.tooling.ComposeViewAdapter$init$3$1$composable$1.invoke(ComposeViewAdapter.kt:590)
	at androidx.compose.ui.tooling.ComposeViewAdapter$init$3$1$composable$1.invoke(ComposeViewAdapter.kt:588)
	at androidx.compose.ui.tooling.ComposeViewAdapter$init$3$1.invoke(ComposeViewAdapter.kt:625)
	at androidx.compose.ui.tooling.ComposeViewAdapter$init$3$1.invoke(ComposeViewAdapter.kt:583)
	at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:107)
	at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:34)
	at androidx.compose.runtime.CompositionLocalKt.CompositionLocalProvider(CompositionLocal.kt:228)
	at androidx.compose.ui.tooling.InspectableKt.Inspectable(Inspectable.kt:61)
	at androidx.compose.ui.tooling.ComposeViewAdapter$WrapPreview$1.invoke(ComposeViewAdapter.kt:531)
	at androidx.compose.ui.tooling.ComposeViewAdapter$WrapPreview$1.invoke(ComposeViewAdapter.kt:530)
	at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:107)
	at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:34)
	at androidx.compose.runtime.CompositionLocalKt.CompositionLocalProvider(CompositionLocal.kt:228)
	at androidx.compose.ui.tooling.ComposeViewAdapter.WrapPreview(ComposeViewAdapter.kt:525)
	at androidx.compose.ui.tooling.ComposeViewAdapter.access$WrapPreview(ComposeViewAdapter.kt:124)
	at androidx.compose.ui.tooling.ComposeViewAdapter$init$3.invoke(ComposeViewAdapter.kt:583)
	at androidx.compose.ui.tooling.ComposeViewAdapter$init$3.invoke(ComposeViewAdapter.kt:580)
	at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:107)
	at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:34)
	at androidx.compose.ui.platform.ComposeView.Content(ComposeView.android.kt:402)
	at androidx.compose.ui.platform.AbstractComposeView$ensureCompositionCreated$1.invoke(ComposeView.android.kt:248)
	at androidx.compose.ui.platform.AbstractComposeView$ensureCompositionCreated$1.invoke(ComposeView.android.kt:247)
	at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:107)
	at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:34)
	at androidx.compose.runtime.CompositionLocalKt.CompositionLocalProvider(CompositionLocal.kt:228)
	at androidx.compose.ui.platform.CompositionLocalsKt.ProvideCommonCompositionLocals(CompositionLocals.kt:177)
	at androidx.compose.ui.platform.AndroidCompositionLocals_androidKt$ProvideAndroidCompositionLocals$3.invoke(AndroidCompositionLocals.android.kt:123)
	at androidx.compose.ui.platform.AndroidCompositionLocals_androidKt$ProvideAndroidCompositionLocals$3.invoke(AndroidCompositionLocals.android.kt:122)
	at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:107)
	at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:34)
	at androidx.compose.runtime.CompositionLocalKt.CompositionLocalProvider(CompositionLocal.kt:228)
	at androidx.compose.ui.platform.AndroidCompositionLocals_androidKt.ProvideAndroidCompositionLocals(AndroidCompositionLocals.android.kt:114)
	at androidx.compose.ui.platform.WrappedComposition$setContent$1$1$3.invoke(Wrapper.android.kt:157)
	at androidx.compose.ui.platform.WrappedComposition$setContent$1$1$3.invoke(Wrapper.android.kt:156)
	at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:107)
	at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:34)
	at androidx.compose.runtime.CompositionLocalKt.CompositionLocalProvider(CompositionLocal.kt:228)
	at androidx.compose.ui.platform.WrappedComposition$setContent$1$1.invoke(Wrapper.android.kt:156)
	at androidx.compose.ui.platform.WrappedComposition$setContent$1$1.invoke(Wrapper.android.kt:140)
	at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:107)
	at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:34)
	at androidx.compose.runtime.ActualJvm_jvmKt.invokeComposable(ActualJvm.jvm.kt:74)
	at androidx.compose.runtime.ComposerImpl$doCompose$2$5.invoke(Composer.kt:3193)
	at androidx.compose.runtime.ComposerImpl$doCompose$2$5.invoke(Composer.kt:3183)
	at androidx.compose.runtime.SnapshotStateKt__DerivedStateKt.observeDerivedStateRecalculations(DerivedState.kt:252)
	at androidx.compose.runtime.SnapshotStateKt.observeDerivedStateRecalculations(Unknown Source)
	at androidx.compose.runtime.ComposerImpl.doCompose(Composer.kt:3183)
	at androidx.compose.runtime.ComposerImpl.composeContent$runtime_release(Composer.kt:3119)
	at androidx.compose.runtime.CompositionImpl.composeContent(Composition.kt:584)
	at androidx.compose.runtime.Recomposer.composeInitial$runtime_release(Recomposer.kt:811)
	at androidx.compose.runtime.CompositionImpl.setContent(Composition.kt:519)
	at androidx.compose.ui.platform.WrappedComposition$setContent$1.invoke(Wrapper.android.kt:140)
	at androidx.compose.ui.platform.WrappedComposition$setContent$1.invoke(Wrapper.android.kt:131)
	at androidx.compose.ui.platform.AndroidComposeView.setOnViewTreeOwnersAvailable(AndroidComposeView.android.kt:1015)
	at androidx.compose.ui.platform.WrappedComposition.setContent(Wrapper.android.kt:131)
	at androidx.compose.ui.platform.WrappedComposition.onStateChanged(Wrapper.android.kt:182)
	at androidx.lifecycle.LifecycleRegistry$ObserverWithState.dispatchEvent(LifecycleRegistry.java:360)
	at androidx.lifecycle.LifecycleRegistry.addObserver(LifecycleRegistry.java:202)
	at androidx.compose.ui.platform.WrappedComposition$setContent$1.invoke(Wrapper.android.kt:138)
	at androidx.compose.ui.platform.WrappedComposition$setContent$1.invoke(Wrapper.android.kt:131)
	at androidx.compose.ui.platform.AndroidComposeView.onAttachedToWindow(AndroidComposeView.android.kt:1102)
	at android.view.View.dispatchAttachedToWindow(View.java:20753)
	at android.view.ViewGroup.dispatchAttachedToWindow(ViewGroup.java:3490)
	at android.view.ViewGroup.dispatchAttachedToWindow(ViewGroup.java:3497)
	at android.view.ViewGroup.dispatchAttachedToWindow(ViewGroup.java:3497)
	at android.view.ViewGroup.dispatchAttachedToWindow(ViewGroup.java:3497)
	at android.view.ViewGroup.dispatchAttachedToWindow(ViewGroup.java:3497)
	at android.view.ViewGroup.dispatchAttachedToWindow(ViewGroup.java:3497)
	at android.view.AttachInfo_Accessor.setAttachInfo(AttachInfo_Accessor.java:58)
	at com.android.layoutlib.bridge.impl.RenderSessionImpl.inflate(RenderSessionImpl.java:366)
	at com.android.layoutlib.bridge.Bridge.createSession(Bridge.java:436)
	at com.android.tools.idea.layoutlib.LayoutLibrary.createSession(LayoutLibrary.java:121)
	at com.android.tools.idea.rendering.RenderTask.createRenderSession(RenderTask.java:717)
	at com.android.tools.idea.rendering.RenderTask.lambda$inflate$9(RenderTask.java:873)
	at com.android.tools.idea.rendering.RenderExecutor$runAsyncActionWithTimeout$3.run(RenderExecutor.kt:192)
	at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128)
	at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628)
	at java.base/java.lang.Thread.run(Thread.java:829)

Because the error says:

java.lang.VerifyError: Bad type on operand stack
Exception Details:
  Location:
    uniffi/myapp/LiveLiterals$MyappKt.Byte$arg-0$call-put$branch$if$fun-write$class-FfiConverterOptionalString()B @11: ireturn

I assumed it might be an issue with uniffi, but I totally understand it might be some problem on my end. If that's the case, please don't hesitate to close this issue.

Generated FfiConverterOptionalString
public object FfiConverterOptionalString: FfiConverterRustBuffer<String?> {
    override fun read(buf: ByteBuffer): String? {
        if (buf.get().toInt() == 0) {
            return null
        }
        return FfiConverterString.read(buf)
    }

    override fun allocationSize(value: String?): Int {
        if (value == null) {
            return 1
        } else {
            return 1 + FfiConverterString.allocationSize(value)
        }
    }

    override fun write(value: String?, buf: ByteBuffer) {
        if (value == null) {
            buf.put(0)
        } else {
            buf.put(1)
            FfiConverterString.write(value, buf)
        }
    }
}

Getting rid of string? extra in TextFormat yields a similar error, but this time the location is different:

uniffi/myapp/LiveLiterals$MyappKt.Byte$arg-0$call-put$branch$if$fun-write$class-FfiConverterOptionalTimestamp()B @11: ireturn

Getting rid of that fails elsewhere in my project with a different unrelated optional. It might come down to the way Kotlin is initializing stuff, but I really have no idea.

┆Issue is synchronized with this Jira Task ┆friendlyId: UNIFFI-208

Lonami avatar Oct 29 '22 10:10 Lonami

If I comment out all offending FfiConverter until the preview stops complaining (and replace their uses in method definitions with a TODO()), the @Preview starts working again. I don't think the implementation itself (read, allocationSize and write) have any part on this. It might be the way the sub-classing is defined (as in FfiConverterTypeTextFormat : FfiConverterRustBuffer<TextFormat>). But I really have no idea why it could possibly fail with VerifyError… Maybe it's the way the interface is defined?

interface FfiConverterRustBuffer<KotlinType> : FfiConverter<KotlinType, RustBuffer.ByValue>

But this really seems like a bug in the compiler, perhaps being unable these generics.

What is most strange to me is that the application runs fine when not inside a @Preview.

Lonami avatar Oct 30 '22 17:10 Lonami

It's definitely not easily reproducable in just plain Kotlin. Could you put a full failing example into a repository so I can run and test it? Might be easier than me trying to piece together the parts to get it to build. I'm happy to take a look then.

badboy avatar Oct 31 '22 14:10 badboy

Thanks a lot for taking the time to look into this. I've created a new project from a Compose template, and after configuring the Rust plugins and uniffi and setting up the NDK version, setup the code necessary to trigger the issue.

I've published the failing code at https://github.com/Lonami/VerifyError-Repro.

Please let me know if you need any more details to import or run the project, or if I can help in any other way. After importing the project, assuming both the NDK and uniffi are configured and installed in the system correctly, it should be possible to open MainActivity.kt in Android Studio and select the Split or Design view (to the right) to trigger the VerifyError during the @Preview of DefaultPreview:

image

(The squiggly line under Instant.now() is Call requires API level 26 (current min is 21): java.time.Instant#now, which can be safely ignored for this demo.)

The code generated by uniffi-bindgen is placed at app/build/generated/source/uniffi/debug/java/uniffi/verifyerror:

image

The code generation is done by app/build.gradle (at the bottom of the file, android.applicationVariants.all { ...) and invokes the following command:

commandLine 'uniffi-bindgen', 'generate', '../native/src/verifyerror.udl', '--language', 'kotlin', '--no-format', '--out-dir', "${buildDir}/generated/source/uniffi/${variant.name}/java"

Lonami avatar Oct 31 '22 14:10 Lonami

I spent a bit of time on it this morning. I am able to reproduce it using the provided code. I'm fairly sure now this is some miscompilation in whatever does the preview rendering. I don't think the uniffi code is inherently wrong. But I also don't know where the buggy behavior is hiding.

Unfortunately I can't spent much more time on this right now.

badboy avatar Nov 01 '22 11:11 badboy

Thanks, yeah, it's probably safe to close this issue as the bug is unlikely to be uniffi's fault (technically, I'm sure the code could be generated in a different way to not trigger this problem, but that's a workaround and not the right solution).

Lonami avatar Nov 01 '22 15:11 Lonami

The repository I mentioned earlier was deleted but here's the main contents of the files:

MainActivity.kt

package com.example.verifyerror

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import com.example.verifyerror.ui.theme.VerifyErrorReproTheme
import uniffi.verifyerror.Message
import java.time.Instant

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            VerifyErrorReproTheme {
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colors.background
                ) {
                    Greeting("Android")
                }
            }
        }
    }
}

@Composable
fun Greeting(name: String) {
    Text(text = "Hello $name!")
}

@Preview(showBackground = true)
@Composable
fun DefaultPreview() {
    val msg = remember {
        Message(
            id = 1,
            sender = "Alice",
            text = "Testing",
            date = Instant.now(),
            editDate = null,
            formatting = listOf(),
        )
    }

    Text(text = msg.text)
}

AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <application
        android:allowBackup="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.VerifyErrorRepro"
        tools:targetApi="31">
        <activity
            android:name=".MainActivity"
            android:exported="true"
            android:label="@string/app_name"
            android:theme="@style/Theme.VerifyErrorRepro">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>

            <meta-data
                android:name="android.app.lib_name"
                android:value="" />
        </activity>
    </application>

</manifest>

build.gradle

plugins {
    id 'com.android.application'
    id 'org.jetbrains.kotlin.android'
    id "org.mozilla.rust-android-gradle.rust-android"
}

android {
    namespace 'com.example.verifyerror'
    compileSdk 32

    defaultConfig {
        applicationId "com.example.verifyerror"
        minSdk 21
        targetSdk 32
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
        vectorDrawables {
            useSupportLibrary true
        }
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
    kotlinOptions {
        jvmTarget = '1.8'
    }
    buildFeatures {
        compose true
    }
    composeOptions {
        kotlinCompilerExtensionVersion '1.1.1'
    }
    packagingOptions {
        resources {
            excludes += '/META-INF/{AL2.0,LGPL2.1}'
        }
    }
    ndkVersion '25.1.8937393'
}

dependencies {

    implementation 'androidx.core:core-ktx:1.7.0'
    implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1'
    implementation 'androidx.activity:activity-compose:1.3.1'
    implementation "androidx.compose.ui:ui:$compose_ui_version"
    implementation "androidx.compose.ui:ui-tooling-preview:$compose_ui_version"
    implementation 'androidx.compose.material:material:1.1.1'
    implementation "net.java.dev.jna:jna:5.12.0@aar"
    testImplementation 'junit:junit:4.13.2'
    androidTestImplementation 'androidx.test.ext:junit:1.1.3'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
    androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_ui_version"
    debugImplementation "androidx.compose.ui:ui-tooling:$compose_ui_version"
    debugImplementation "androidx.compose.ui:ui-test-manifest:$compose_ui_version"
}

cargo {
    module = "../native"
    libname = "uniffi_verifyerror"
    targets = ["arm64"]
    profile = 'release'
}

tasks.whenTaskAdded { task ->
    if ((task.name == 'javaPreCompileDebug' || task.name == 'javaPreCompileRelease')) {
        task.dependsOn 'cargoBuild'
    }
}

android.applicationVariants.all { variant ->
    def t = tasks.register("generate${variant.name.capitalize()}UniFFIBindings", Exec) {
        workingDir "${project.projectDir}"
        commandLine 'uniffi-bindgen', 'generate', '../native/src/verifyerror.udl', '--language', 'kotlin', '--no-format', '--out-dir', "${buildDir}/generated/source/uniffi/${variant.name}/java"
    }
    variant.javaCompileProvider.get().dependsOn(t)
    def sourceSet = variant.sourceSets.find { it.name == variant.name }
    sourceSet.java.srcDir new File(buildDir, "generated/source/uniffi/${variant.name}/java")
}

Maybe this can be closed as stale.

Lonami avatar Oct 14 '23 20:10 Lonami