How Karukan Implements the fcitx5 Linux Frontend Using C FFI and C++ Addons
Karukan bridges the fcitx5 Linux input framework to a Rust core engine through a thin C FFI layer, allowing a C++ addon to forward key events and synchronize UI state without duplicating IME logic.
The open-source Japanese IME Karukan (togatoga/karukan) delivers a native Linux input experience by integrating with the fcitx5 framework. This integration relies on a carefully designed boundary between Rust and C++ code, using a C FFI bridge to maintain memory safety while achieving low-latency key processing. Understanding this architecture reveals how modern Rust-based input methods can plug into existing Linux input frameworks.
Architectural Overview
Karukan’s fcitx5 integration consists of three tightly-coupled layers that keep the heavy conversion logic in Rust while letting the C++ side handle fcitx5-specific UI contracts:
| Layer | Language | Role |
|---|---|---|
| Rust engine | Rust | Core IME logic lives in karukan-im. An opaque handle (*mut KarukanEngine) is exposed through a C-compatible FFI. |
| C FFI bridge | Rust → C | The karukan-fcitx5/src/ffi module implements a thin C API that the C++ addon can call. It converts Rust data (UTF-8 strings, candidate structures, timing information) into C-friendly types (null-terminated char*, uint32_t, CString). |
| C++ addon | C++ | The fcitx5 addon (karukan-fcitx5/fcitx5-addon/src/karukan.cpp) links against the generated static library, receives key events from fcitx5, forwards them to the Rust engine via the C API, and updates the fcitx5 UI (preedit, candidates, auxiliary text). |
The C FFI Bridge in karukan-fcitx5/src/ffi/mod.rs
The Rust side of the FFI defines an opaque KarukanEngine struct that encapsulates the engine state and several cache structures. These caches avoid allocating memory on every C call, which is critical for performance when the C++ addon polls for UI updates.
Opaque Engine Handle and Cache Strategy
The bridge holds an InputMethodEngine instance alongside user settings and cache structs for preedit text, candidates, commits, and auxiliary text:
/// Opaque handle to an IME engine instance
pub struct KarukanEngine {
engine: InputMethodEngine,
settings: Settings,
preedit: PreeditCache,
candidates: CandidateCache,
commit: CommitCache,
aux: AuxCache,
last_conversion_ms: u64,
last_process_key_ms: u64,
}
Helper macros ffi_ref! and ffi_mut! guard against null pointers coming from C code, ensuring that the Rust side never dereferences invalid handles passed by the addon.
Applying Engine Actions Safely
When the Rust engine returns an EngineAction (such as updating the preedit or committing text), the FFI bridge applies the action and fills the corresponding cache. For example, processing an UpdatePreedit action converts the caret position from character offsets to byte offsets and stores a null-terminated CString:
EngineAction::UpdatePreedit(preedit) => {
let caret_bytes = /* convert caret chars → byte offset */;
self.preedit.caret_bytes = caret_bytes as u32;
self.preedit.text = CString::new(preedit.text()).unwrap_or_default();
self.preedit.dirty = true;
}
Exported C API Surface
The C header at karukan-fcitx5/include/karukan.h declares the stable interface used by the C++ addon. The following functions form the contract between the Rust core and the fcitx5 frontend:
karukan_engine_new()– Allocates and initializes a newKarukanEngineinstance.karukan_engine_free(*mut KarukanEngine)– De-allocates the engine and persists learning data.karukan_engine_process_key(*mut KarukanEngine, uint32_t keysym, uint32_t state, int is_release)– Forwards a key event; returns non-zero if the key was consumed.karukan_engine_has_preedit(*mut KarukanEngine)– Checks whether a preedit string is available.karukan_engine_get_preedit/get_preedit_len/get_preedit_caret– Reads fromPreeditCacheto provide the preedit text, length, and caret byte offset.karukan_engine_has_candidates/get_candidate_count/get_candidate/get_candidate_description– Queries the candidate list and per-candidate descriptions fromCandidateCache.karukan_engine_has_commit/get_commit/get_commit_len– Returns text that the engine wants to commit.karukan_engine_has_aux/get_aux/get_aux_len– Returns auxiliary (reading-hint) text.karukan_engine_set_surrounding_text– Provides left-hand context for accurate conversion when the editor supports surrounding text.
The C++ Addon in karukan-fcitx5/fcitx5-addon/src/karukan.cpp
The fcitx5 addon follows the standard fcitx5 input method pattern, implementing InputMethodEngine and managing InputContext state through a custom KarukanState class.
State Management and Lifecycle
Each input context owns a KarukanState that holds a raw pointer to the Rust engine (rustEngine_). When the state is created, it allocates the Rust side via the FFI:
KarukanState::KarukanState(KarukanEngine* engine, InputContext* ic) : engine_(engine), ic_(ic) {
rustEngine_ = karukan_engine_new();
}
On deactivation, the addon calls karukan_engine_free to clean up resources and trigger learning persistence.
Key Event Translation
When fcitx5 delivers a key event, the addon translates the fcitx5 KeyEvent into the three-argument C API (keysym, state, is_release). Modifier bits are mapped to the constants defined in the addon header:
uint32_t state = 0;
if (keyEvent.key().states().test(KeyState::Shift)) state |= kShiftMask;
if (keyEvent.key().states().test(KeyState::Ctrl)) state |= kControlMask;
if (keyEvent.key().states().test(KeyState::Alt)) state |= kAltMask;
if (keyEvent.key().states().test(KeyState::Super)) state |= kSuperMask;
int isRelease = keyEvent.isRelease() ? 1 : 0;
int consumed = karukan_engine_process_key(rustEngine_, keysym, state, isRelease);
if (consumed) keyEvent.filterAndAccept();
If the Rust engine consumes the key, the addon marks the event as filtered and accepted, preventing further processing by other fcitx5 components.
UI Synchronization and Candidate Rendering
After processing a key, the addon calls updateUI(), which queries the Rust engine through the getter functions and mirrors the data into fcitx5 UI components:
- Preedit – Constructed with underline formatting and cursor positioning based on
karukan_engine_get_preedit_caret. - Auxiliary text – Retrieved via
karukan_engine_get_auxand shown above the candidate list. - Candidates – The
KarukanCandidateListclass populates itself by iterating over the candidate count and callingkarukan_engine_get_candidateandkarukan_engine_get_candidate_description:
void KarukanCandidateList::updateCandidates(::KarukanEngine* rustEngine) {
uint32_t count = karukan_engine_get_candidate_count(rustEngine);
uint32_t cursor = karukan_engine_get_candidate_cursor(rustEngine);
for (uint32_t i = 0; i < count; i++) {
const char* text = karukan_engine_get_candidate(rustEngine, i);
const char* desc = karukan_engine_get_candidate_description(rustEngine, i);
append<KarukanCandidateWord>(engine_, std::move(candidateText), i, description);
}
setGlobalCursorIndex(static_cast<int>(cursor));
}
For fcitx5 versions 5.1.9 and later, descriptions are rendered as right-side comments using setComment; older versions concatenate the description to the candidate text.
End-to-End Data Flow
- Initialization – When the user switches to Karukan, fcitx5 creates a
KarukanState, which callskarukan_engine_new()to allocate the Rust engine. - Key event – Each key press is translated to a keysym and modifier mask, then passed to
karukan_engine_process_key. The Rust engine returns a list ofEngineActions (e.g., update preedit, show candidates). - Caching – The C FFI bridge stores the results in the per-field caches (
PreeditCache,CandidateCache, etc.) without allocating on the C heap. - UI update – The C++ addon queries those caches through the exported getter functions and populates fcitx5 UI widgets (preedit, auxiliary text, candidate window).
- Commit and reset – When the engine signals a commit, the addon calls
ic_->commitString()with the text fromkarukan_engine_get_commit, then clears the panel; on deactivation,karukan_engine_save_learningpersists the neural model's state.
Summary
- Three-layer architecture separates Rust conversion logic from C++ fcitx5 plumbing via a C FFI bridge.
- Opaque handles (
*mut KarukanEngine) keep Rust memory safe while exposing a minimal C API inkarukan-fcitx5/include/karukan.h. - Zero-copy caching in the FFI layer (
PreeditCache,CandidateCache) prevents repeated allocations during UI polling. - Key event translation maps fcitx5 modifier states to bitmasks consumed by
karukan_engine_process_key. - Lifecycle hooks ensure the Rust engine receives surrounding text for context and persists learning data on deactivation.
Frequently Asked Questions
What is the role of the C FFI bridge in Karukan?
The C FFI bridge acts as a language boundary that exposes the Rust InputMethodEngine to C code. It manages an opaque handle (KarukanEngine) and caches results (preedit, candidates, commits) so that the C++ addon can query UI state without triggering Rust allocations on every call.
How does the C++ addon communicate with the Rust engine?
The addon links against the static library produced by the karukan-fcitx5 crate and calls the C functions declared in karukan.h. It forwards key events via karukan_engine_process_key and retrieves UI data through getter functions like karukan_engine_get_preedit and karukan_engine_get_candidate.
Why does Karukan use caching in the FFI layer?
Caching structures (PreeditCache, CandidateCache, etc.) store converted strings and offsets as C-compatible types (null-terminated char*, uint32_t). This design prevents the C++ addon from triggering Rust heap allocations during UI refresh cycles, ensuring low latency when fcitx5 polls for preedit or candidate updates.
Where is the fcitx5 addon source code located?
The C++ addon implementation resides in karukan-fcitx5/fcitx5-addon/src/karukan.cpp, with its build configuration in karukan-fcitx5/fcitx5-addon/CMakeLists.txt. The C FFI bridge is located in karukan-fcitx5/src/ffi/mod.rs, and the shared C header is at karukan-fcitx5/include/karukan.h.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →