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.
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.
./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:
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:
| Code | Meaning |
|---|---|
-1 | The GraalVM isolate could not be created. |
-2 | run was called twice. |
-3 | The library could not work out its own path, so it cannot find Skia and AWT. |
-4 | Not on the main thread. |
-5 | The 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.
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.