dioxus-compose guide

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.

shell
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.

shell
./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:

shell
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:

CodeMeaning
-1The GraalVM isolate could not be created.
-2run was called twice.
-3The library could not work out its own path, so the runtime layout is unknown.
-4Not on the main thread.
-5The 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.

Collect metadata while typing Korean

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.