compose-rust guide

Troubleshooting

Failures you can meet, and how to recognise them

Most problems on this page are one of three things: the renderer is not where the linker expects it, the JDK is the wrong one, or something touched the text path that should not have.

The build cannot find the renderer

Outside this repository the build downloads the renderer, so a failure here comes with a message that says what it tried and what to do. Read it. It tells no network apart from a release that does not carry your target, and the offline case names the files to place and the directory to place them in. COMPOSE_RUST_RENDERER_DIR points the build at a renderer you already have.

Inside a checkout the build prefers the native image at renderer/build/native-image/dist/lib over anything it could download. If that directory exists but is empty, the renderer has not been built.

shell, macOS
ls renderer/build/native-image/dist/lib
# expect libcompose_rust_renderer.dylib with Skia and AWT libraries beside it

./renderer/desktop/scripts/build-native.sh

Do not move those files apart. The native image finds itself at runtime and derives where Skia and AWT are from its own directory. A library copied somewhere on its own loads and then fails to find them.

The Kotlin/Native static library is linked only when DXC_MACOS_NATIVE_LIB or DXC_LINUX_NATIVE_LIB names the directory that holds libcompose_rust_renderer.a. If you built it and the binary still loads the native image, check that variable.

It builds and runs, but no window appears

A binary built with default-features = false does not do this quietly: it prints what is missing and exits non-zero. A build with the mock-renderer feature is silent on purpose, because it asks for a Host with no window.

Otherwise, isolate the halves. The smoke test links a small C host against the library and calls run directly.

shell
./renderer/desktop/scripts/smoke-test.sh

A window there and none from Rust points at how your binary is linked. No window there either means the renderer library itself is bad, usually the next problem.

Wrong JDK: the AWT-on-Darwin trap

This applies to the native image on macOS. The good case is the build script stopping with no Liberica NIK 25 Full found, and GRAALVM_HOME is not set: it caught the problem for you.

The bad case is a native-image on PATH from upstream GraalVM. It builds something that then fails at runtime with missing AWT classes or symbols, or opens no window at all. Upstream GraalVM skips AWT on Darwin (oracle/graal#13272), and Liberica NIK Full links it statically. Check what you are pointing at:

shell
echo "$GRAALVM_HOME"
"$GRAALVM_HOME"/bin/native-image --version   # expect a Liberica NIK 25 build

./scripts/install-nik.sh installs the right one. The metadata run and the native build must also use the same JDK, which the scripts enforce by setting JAVA_HOME from GRAALVM_HOME. Otherwise the reachability metadata describes a JDK that is not the one being compiled.

The app starts and returns at once

compose_rust_renderer_run must be called on the process main thread. Called anywhere else on macOS it returns RUN_NOT_MAIN_THREAD (-4) and does nothing. The native image's other negative codes are worth knowing:

CodeMeaning
-1The GraalVM isolate could not be created.
-2run was called twice.
-3The library could not work out its own path, so it cannot find Skia and AWT.
-4Not on the main thread.
-5The renderer thread could not be started.

The Linux Kotlin/Native renderer uses -2 and -4 with the same meanings.

IME problems

Three distinct failures, easy to confuse:

Enter submits and eats the syllable

You type Korean, press Enter to commit the syllable, and the message is sent without it. Key events reached the Host while composition was in progress. The renderer must not forward key events during composition, because Enter mid-composition is a commit, not a submit. If you see this, the bug is on the renderer side of the key path, not in your handler.

Composition resets as you type

Characters disappear or reorder while a syllable is being built. That is the signature of text making a round trip through the Host: something set the field's text from Rust during composition. TextField is uncontrolled for this reason, and SetText waits until composition ends.

No composition at all in the native image

Korean input works when the renderer runs on the JVM but not in the native image. Treat it as a configuration problem, because ahead-of-time compilation does not change which code runs. Check, in order, the InputMethodDescriptor ServiceLoader resources, the JNI and reflection metadata, the locale and charset resources, and CJK font fallback.

Collect metadata while typing Korean

The tracing agent records only code paths that actually run. Running the app and closing it records nothing about the IME. Run collect-metadata.sh, type Korean into the window, click things, and let the window close on its own. Metadata collected without that produces a native image whose text input is broken in exactly this way.

ProtocolError events

A widget, property or modifier the renderer does not recognise crashes nothing. The renderer sends a ProtocolError event with a code and a message. Seeing one means the two sides disagree about the schema, normally a renderer built from different sources than the Rust crate you are running. Rebuild the renderer.

An outright mismatch is caught earlier. The build compares the renderer's schema hash with the crate's, and the handshake compares them again at startup and refuses to initialise if they differ.

The test suite fails on generated Kotlin

A failing test that compares generated Kotlin with the checked-in file means the Rust schema changed without regenerating. That is the intended alarm: the schema is the single source, and a Kotlin side that was not updated should fail the build rather than misbehave at runtime. Run cargo run -p compose-rust --bin codegen and commit the result.