Troubleshooting
Failures we actually hit, and how to recognise them
Almost every problem on this page is one of three things wearing different clothes: the renderer library is not where the linker expects it, the JDK is the wrong one, or something touched the text path that should not have.
The linker cannot find the renderer
Symptoms: cargo run … --features native-renderer fails with
library not found for -ldioxus_compose_renderer, or the binary links but dies at
startup with a dynamic-loader error naming
libdioxus_compose_renderer.dylib.
The native-renderer feature makes the Rust build look for the library in a fixed
location relative to the workspace:
dioxus-compose-renderer/build/native-image/dist/lib, which is also baked in as
an rpath. If that directory does not exist or is empty, the renderer has not been built.
ls dioxus-compose-renderer/build/native-image/dist/lib
# expect: libdioxus_compose_renderer.dylib, libskiko-macos-*.dylib,
# libjawt.dylib, libawt_lwawt.dylib
./dioxus-compose-renderer/desktop/scripts/build-native.sh
Do not move the four files apart. The renderer finds itself with dladdr and
derives java.home, skiko.library.path and
skiko.data.path from that one directory. A library copied somewhere on its own
will load and then fail to find Skia or AWT.
It builds and runs, but no window appears
If you built without --features native-renderer, this is expected: the Host
falls back to a no-op renderer, launch returns immediately and the process exits.
Nothing is broken, that build exists for tests and benchmarks.
If the feature is on, isolate the halves. Run the smoke test: it links a minimal C
host against the library and calls run directly.
./dioxus-compose-renderer/desktop/scripts/smoke-test.sh
A window there and none from Rust points at the linking of your binary. No window there either means the renderer library itself is bad, usually the next problem.
Wrong JDK: the AWT-on-Darwin trap
Symptom: the build script stops with
error: set GRAALVM_HOME to a Liberica NIK 25 Full installation. That is the good
case, it caught the problem for you.
The bad case is a native-image on PATH from upstream GraalVM that
builds something which 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); Liberica NIK
Full links it statically. Check what you are actually pointing at:
echo "$GRAALVM_HOME"
"$GRAALVM_HOME"/bin/native-image --version # expect a Liberica NIK 25 build
A related variant: the JVM metadata run and the native build must use the same JDK. The
scripts enforce this by setting JAVA_HOME from GRAALVM_HOME for the
metadata run. If you run the renderer on some other JVM and then build the image, the
reachability metadata describes a JDK that is not the one being compiled.
The app starts and immediately returns, or hangs on exit
dioxus_compose_renderer_run must be called on the process main thread. Called
anywhere else on macOS it returns RUN_NOT_MAIN_THREAD (-4) without
doing anything. Other negative codes from the same entry point 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 the runtime layout is unknown. |
-4 | Not on the main thread. |
-5 | The renderer thread could not be started. |
If instead the window closes but the process never exits, that is the AppKit re-entry
problem: AWT was allowed to own the event loop and re-entered [NSApp run], so
control never returns to the Host. The design keeps the main thread running
NSApplication itself and the renderer on a secondary thread precisely to avoid
this.
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. This means key events reached the Host while composition was in progress. The rule is that the renderer does not forward key events during composition at all; an 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 re-order while a syllable is being built. That is the signature of
text round-tripping through the Host: something set the field's value from Rust during
composition. TextField is uncontrolled for this reason, and the
SetText command is defined to wait 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. By design
this is treated as a configuration problem, not an implementation one: AOT
compilation does not change which code paths run. Check, in order, the
InputMethodDescriptor ServiceLoader resources, the JNI and reflection metadata,
the locale and charset resources, and CJK font fallback.
The tracing agent only records code paths that actually execute. Running the app and
closing it records nothing about the IME. Run
collect-metadata.sh, then type Korean into the window, click things and let the
window close on its own. Metadata collected without doing that will produce a native image
whose text input is broken in exactly this way.
The full manual checklist, composing 안녕하세요, jamo-level backspace, cursor movement committing the composition, insertion mid-sentence, Enter handling in a multiline field, pasting mixed text, candidate windows for Japanese and Chinese, no tofu glyphs, lives in SPEC §6 and is run against the native image, not the JVM shell.
ProtocolError events
A widget type, property or modifier the renderer does not recognise does not crash anything.
The renderer sends a ProtocolError event carrying a code and a message. Seeing
one means the two sides disagree about the schema, normally a renderer library built from
different sources than the Rust crate you are running. Rebuild the renderer.
An outright mismatch is caught earlier: the handshake compares a schema hash and a protocol version and refuses to initialise if they differ.
The test suite fails on generated Kotlin
A failing test that compares generated Kotlin against the checked-in file means you changed the Rust schema without regenerating. This is the intended alarm: the schema is the single source of truth, and a Kotlin interpreter that was not updated should fail the build rather than produce a mismatch at runtime.