From 650f00812d2b886c05b7b03b53eb9af36d843bb8 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Mon, 17 Aug 2026 23:25:22 +0900 Subject: [PATCH] compose: a sequence belongs to one context MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit One xkb_compose_state served the whole session, so a dead key left half typed in one window was completed by the first key in the next. Two IBus contexts through the real processkey(), before this: want length 0, got length 2; byte 0: want , got 0xc3 (text) want 0, got 1 (req.op) Context A pressed dead_acute; context B pressed e, got é, and went down the composed-text branch instead of sending a key. Only COMPOSING is sticky -- xkbcommon starts over by itself after COMPOSED and CANCELLED, which the table in compose_test already pins -- and nothing here ever called xkb_compose_state_reset. That static was also fed from three procs, two of them live at once, with no lock, which xkbcommon forbids. Reaching it needs two focused windows, so it cannot be made to fail on demand and has no test: it goes because the sharing goes, not because anything guards it. So a state per frontend, each starting over when the key comes from another context. All are made in composeinit(), on threadmain, before proccreate, because xkb_compose_state_new refs the table and that ref is a plain increment -- b160: mov (%rdi),%edx b16a: add $0x1,%edx b170: mov %edx,(%rdi) -- so making a state from a shared table on two procs would race in place of the feed. Made before the procs exist they can still share the one table. An owner address that gets reused says so with composedrop: ibus hands out a contexts[] slot again, xim can malloc an Ic at a freed one, and wl's single context, which serves every client in turn, drops at every activate and deactivate. Co-Authored-By: Claude Opus 5 (1M context) --- compose.c | 44 ++++++++++++++++++++++++++++++++++++-------- dat.h | 9 +++++++++ fn.h | 3 ++- ibus.c | 3 ++- tests/compose_test.c | 43 +++++++++++++++++++++++++++++++------------ wl.c | 3 ++- xim.c | 3 ++- 7 files changed, 84 insertions(+), 24 deletions(-) diff --git a/compose.c b/compose.c index 89d7611..9486f59 100644 --- a/compose.c +++ b/compose.c @@ -5,7 +5,13 @@ #include #include -static struct xkb_compose_state *cstate; +/* + * One state per frontend, all made before the procs are: xkbcommon locks + * nothing, and even its table refcount is a plain increment. cowner is + * the context whose sequence a state holds. + */ +static struct xkb_compose_state *cstate[Ncompose]; +static void *cowner[Ncompose]; /* Dead keys and Compose: no frontend's client does them for us. */ void @@ -14,6 +20,7 @@ composeinit(void) struct xkb_context *ctx; struct xkb_compose_table *table; char *locale; + int i; locale = setlocale(LC_CTYPE, ""); ctx = xkb_context_new(XKB_CONTEXT_NO_FLAGS); @@ -22,30 +29,51 @@ composeinit(void) table = xkb_compose_table_new_from_locale(ctx, locale != nil ? locale : "C", XKB_COMPOSE_COMPILE_NO_FLAGS); if(table != nil) - cstate = xkb_compose_state_new(table, XKB_COMPOSE_STATE_NO_FLAGS); + for(i = 0; i < Ncompose; i++) + cstate[i] = xkb_compose_state_new(table, + XKB_COMPOSE_STATE_NO_FLAGS); xkb_compose_table_unref(table); xkb_context_unref(ctx); } +/* The context is going, and the sequence it left half typed with it. */ +void +composedrop(int who, void *owner) +{ + if(cstate[who] == nil || owner != cowner[who]) + return; + xkb_compose_state_reset(cstate[who]); + cowner[who] = nil; +} + /* * Feeds sym to the Compose table. Returns 1 while a sequence is * unfinished: the key belongs to the sequence, not to the engine. A * finished sequence leaves its text in buf, which the caller commits - * after whatever the engine had pending. + * after whatever the engine had pending. A sequence belongs to the + * context that started it: a key from another one starts over. */ int -composekey(u32int sym, char *buf, int n) +composekey(int who, void *owner, u32int sym, char *buf, int n) { + struct xkb_compose_state *cs; + buf[0] = '\0'; - if(cstate == nil || - xkb_compose_state_feed(cstate, sym) != XKB_COMPOSE_FEED_ACCEPTED) + cs = cstate[who]; + if(cs == nil) return 0; - switch(xkb_compose_state_get_status(cstate)){ + if(owner != cowner[who]){ + xkb_compose_state_reset(cs); + cowner[who] = owner; + } + if(xkb_compose_state_feed(cs, sym) != XKB_COMPOSE_FEED_ACCEPTED) + return 0; + switch(xkb_compose_state_get_status(cs)){ case XKB_COMPOSE_COMPOSING: case XKB_COMPOSE_CANCELLED: return 1; case XKB_COMPOSE_COMPOSED: - xkb_compose_state_get_utf8(cstate, buf, n); + xkb_compose_state_get_utf8(cs, buf, n); break; default: break; diff --git a/dat.h b/dat.h index 581302a..71066a2 100644 --- a/dat.h +++ b/dat.h @@ -41,6 +41,15 @@ enum Colselfg = 0xffffff, }; +/* Each frontend composes on its own: they run in different procs. */ +enum +{ + Composeibus, + Composexim, + Composewl, + Ncompose, +}; + /* Popup metrics in pixels; popupscale is GDK_SCALE, for HiDPI. */ extern int popupscale; #define Fontsz (32*popupscale) diff --git a/fn.h b/fn.h index 327761f..738edd3 100644 --- a/fn.h +++ b/fn.h @@ -48,7 +48,8 @@ void wlthread(void*); int keymeaningful(u32int); void composeinit(void); -int composekey(u32int, char*, int); +void composedrop(int, void*); +int composekey(int, void*, u32int, char*, int); void* emalloc(ulong); void* erealloc(void*, ulong); diff --git a/ibus.c b/ibus.c index 63a1106..6a8e1b0 100644 --- a/ibus.c +++ b/ibus.c @@ -509,6 +509,7 @@ static void dropcontext(Ictx *ctx) { releasecontext(ctx); + composedrop(Composeibus, ctx); if(preowner == ctx) preowner = nil; memset(ctx, 0, sizeof *ctx); @@ -536,7 +537,7 @@ processkey(Ictx *ctx, u32int sym, u32int state, char *text, int n, Keyres *res) return 0; ctx->focused = 1; /* An IBus client drops a dead key we do not take: compose it here. */ - if(composekey(sym, text, n)) + if(composekey(Composeibus, ctx, sym, text, n)) return -1; if(text[0] != '\0') /* Composed text follows whatever was pending. */ diff --git a/tests/compose_test.c b/tests/compose_test.c index 76805cc..a82cb97 100644 --- a/tests/compose_test.c +++ b/tests/compose_test.c @@ -3,24 +3,37 @@ #include "test.h" #include -/* A sequence swallows its keys and hands its text back at the end. */ +/* + * A sequence swallows its keys and hands its text back at the end, and + * belongs to the one context, in the one frontend, that started it. + */ void compose_sequences(struct ct *t) { + static int a, b; /* two input contexts, told apart by address */ static const struct { + int who; + void *owner; u32int sym; int composing; char *text; } keys[] = { - { XKB_KEY_a, 0, "" }, - { XKB_KEY_dead_acute, 1, "" }, - { XKB_KEY_e, 0, "é" }, - { XKB_KEY_Multi_key, 1, "" }, - { XKB_KEY_o, 1, "" }, - { XKB_KEY_c, 0, "©" }, - { XKB_KEY_dead_acute, 1, "" }, - { XKB_KEY_Escape, 1, "" }, - { XKB_KEY_a, 0, "" }, + { Composexim, &a, XKB_KEY_a, 0, "" }, + { Composexim, &a, XKB_KEY_dead_acute, 1, "" }, + { Composexim, &a, XKB_KEY_e, 0, "é" }, + { Composexim, &a, XKB_KEY_Multi_key, 1, "" }, + { Composexim, &a, XKB_KEY_o, 1, "" }, + { Composexim, &a, XKB_KEY_c, 0, "©" }, + { Composexim, &a, XKB_KEY_dead_acute, 1, "" }, + { Composexim, &a, XKB_KEY_Escape, 1, "" }, + { Composexim, &a, XKB_KEY_a, 0, "" }, + /* another context of the same frontend starts over */ + { Composexim, &a, XKB_KEY_dead_acute, 1, "" }, + { Composexim, &b, XKB_KEY_e, 0, "" }, + /* another frontend does not touch this one's sequence */ + { Composexim, &a, XKB_KEY_dead_acute, 1, "" }, + { Composewl, &a, XKB_KEY_o, 0, "" }, + { Composexim, &a, XKB_KEY_e, 0, "é" }, }; char buf[Maxutf]; int i; @@ -29,9 +42,15 @@ compose_sequences(struct ct *t) return; composeinit(); for(i = 0; i < nelem(keys); i++){ - CT_EQ_INT(t, keys[i].composing, - composekey(keys[i].sym, buf, sizeof buf)); + CT_EQ_INT(t, keys[i].composing, composekey(keys[i].who, + keys[i].owner, keys[i].sym, buf, sizeof buf)); CT_EQ_STR(t, keys[i].text, buf); } + /* a context that goes takes its half-typed sequence with it */ + CT_EQ_INT(t, 1, + composekey(Composewl, &a, XKB_KEY_dead_acute, buf, sizeof buf)); + composedrop(Composewl, &a); + CT_EQ_INT(t, 0, composekey(Composewl, &a, XKB_KEY_e, buf, sizeof buf)); + CT_EQ_STR(t, "", buf); unsetenv("XCOMPOSEFILE"); } diff --git a/wl.c b/wl.c index ef9cac6..28ea693 100644 --- a/wl.c +++ b/wl.c @@ -356,6 +356,7 @@ leave(void) Keyres res; releasekeys(); + composedrop(Composewl, &context); if(engaged){ sendrequest(Keyrelease, 0, 0, &res); engaged = 0; @@ -506,7 +507,7 @@ grabkey(void*, struct zwp_input_method_keyboard_grab_v2*, u32int, return; } sym = xkb_state_key_get_one_sym(kstate, code + 8); - if(composekey(sym, text, sizeof text)) + if(composekey(Composewl, &context, sym, text, sizeof text)) return; /* a sequence in the making */ key = ipckeysym(sym, xkb_keysym_to_utf32(sym)); if(keymeaningful(key)) diff --git a/xim.c b/xim.c index 85e334a..80d223b 100644 --- a/xim.c +++ b/xim.c @@ -384,7 +384,7 @@ kpress(Ic *state, xcb_key_press_event_t *ev) int n; sym = keymaplookup(kstate, ev->detail, ev->state); - if(composekey(sym, text, sizeof text)) + if(composekey(Composexim, state, sym, text, sizeof text)) return; key = ipckeysym(sym, xkb_keysym_to_utf32(sym)); if(keymeaningful(key)) @@ -424,6 +424,7 @@ icfree(void *p) state = p; /* The owner remains valid until imthread acknowledges its release. */ release(state); + composedrop(Composexim, state); icunlink(state); free(state); }