Skip to content

fix(tsf): ThreadingModel 改回 Apartment(对齐 TSF 标准),类别 GUID 数值防御性修正 - #116

Merged
huanfeng merged 3 commits into
huanfeng:mainfrom
sage-z-cn:fix/tsf-registration-core
Sep 5, 2026
Merged

fix(tsf): ThreadingModel 改回 Apartment(对齐 TSF 标准),类别 GUID 数值防御性修正#116
huanfeng merged 3 commits into
huanfeng:mainfrom
sage-z-cn:fix/tsf-registration-core

Conversation

@sage-z-cn

@sage-z-cn sage-z-cn commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

内容(按 review 修订)

ThreadingModel:Both → Apartment——对齐 TSF 标准做法(weasel / SampleIME 同为 Apartment)。历史上的 Both 系 Go 时代为「Win11 现代应用兼容」引入的 workaround 带入,原始症状已失传。本机实测当前注册的 Both(x64 与 WOW6432Node 两份)改为 Apartment 后实打实生效。

类别 GUID 数值修正(防御性):UIELEMENTENABLED / SECUREMODE 的手抄数值对照 msctf.h 改为 SDK 真值。DEFINE_GUID 未定义 INITGUID 时只是声明、实际取值由 uuid.lib 提供,手写数值从未进过链接产物——本修正属防御性质:避免将来某个 TU 引入 initguid.h 后错值突然生效。

验证

关联:#113#114#115

游戏聊天框等 GIP 上下文(Win11 输入栈)激活 in-proc TSF IME 前,
msctf 按 ThreadingModel 与能力类别筛选,任一不满足即被静默筛除。

- Register.cpp:ThreadingModel 由 Both 改回 Apartment(TSF 标准;
  Both 系历史上为「Win11 现代应用兼容」引入,实测恰是 GIP 上下文
  激活失败的元凶之一)
- Register.cpp:修正两个类别 GUID 手抄错误(迁移期引入)——
  UIELEMENTENABLED/SECUREMODE 曾注册到系统不识别的假 GUID 上;
  改为 SDK 真值({49D2F9CF-...}/{49D2F9CE-...}),并在注册时对
  假值 UnregisterCategory 自愈清理历史脏项

(从 huanfeng#113 按作者建议拆分,仅含 Register.cpp 部分)
sage-z-cn added a commit to sage-z-cn/WindInput that referenced this pull request Sep 5, 2026
Win11 输入栈(GIP 路径,游戏聊天框等上下文)对 in-proc TSF IME 存在
未文档化的位置约束:InprocServer32 指向系统目录副本时可激活,指向
安装目录时被静默筛除(真机 A/B 实验证据见 huanfeng#115)。对齐 weasel 安装
模式与 wind-installer 修复版(huanfeng/wind-installer#1)。

(从 huanfeng#113 按作者建议拆分;ThreadingModel/GUID 修正已拆至 huanfeng#116
@huanfeng

huanfeng commented Sep 5, 2026

Copy link
Copy Markdown
Owner

感谢拆分。逐条核过之后有几个问题需要先澄清,主要集中在因果归因上——改动方向大体没问题,但描述里的因果链有一环不成立。


一、GUID 部分是空操作:手抄错值从未进过链接产物,也从未写进注册表

DEFINE_GUID 在未定义 INITGUID 时只展开成 EXTERN_C const GUID name;(声明),实际字节由链接期解析。本仓两条构建路径都定义 INITGUID

  • wind_tsf/src/Globals.cpp:11#pragma comment(lib, "uuid.lib")(CMake/MSVC 路径)
  • wind_tsf/Makefile:7 — clang + cargo-xwin,注释明写「用真 Windows SDK 头,故不再需要 MinGW 兼容垫片」
  • 全仓唯一提到 INITGUID 的地方是 wind_tsf/src/TextService.cpp:13-15,而那里正是在解释为什么引入 <initguid.h>

未打本 PR 的 main(ce1c1abc)所注册的机器上实测注册表:

HKLM\SOFTWARE\Microsoft\CTF\TIP\{99C2DEB0-...}\Category\Category
  {49D2F9CE-1F5E-11D7-A6D3-00065B84435C}   ← SECUREMODE 真值,已在
  {49D2F9CF-1F5E-11D7-A6D3-00065B84435C}   ← UIELEMENTENABLED 真值,已在
  (无 {6D60FCCF-...},无 {3527B835-...})

MinGW 时代同样不成立:已删除的垫片 git show 4b436075:wind_tsf/src/mingw_tsf_compat.cpp 第 45/47 行本就用 #define INITGUID 定义了真值 49d2f9ce / 49d2f9cf。另外 docs/redesign/tsf-migration.md:112 早有记载:「MSVC 构建:经 uuid.lib,完整保留两个类别(与 weasel 字节一致,行为不变)」。

所以:

  1. 描述里「曾把手抄错误值注册到系统不识别的假 GUID 上」不成立;
  2. 「真值 UIELEMENTENABLED 类别通过 UI-less 线程筛选是本修复生效的直接证据」也不成立——真值本来就在注册表里,LOL 聊天框修好不可能由这一改带来;
  3. 新增的自愈 UnregisterCategory 循环是死代码:它清理的表项从未存在过。每次 DllRegisterServer 会白跑两次 HKLM 往返,并很可能刷两条 Failed to unregister legacy typo category WARN,干扰后续排障。建议整段删除。
  4. 新增注释「手写数值仅在 MinGW 声明路径下生效」是错的(MinGW 下 DEFINE_GUID 同样只是声明,且垫片给的是真值)。

把数值改成 SDK 真值本身值得保留——万一将来某个 TU 引入 initguid.h,错值会突然生效。但请把叙述从「这是修复」降级为「消除未来隐患」。

另外 UnregisterCategories() 没有镜像新增的清理列表,与它自己那句「必须与 RegisterCategories 中的列表完全一致,防止注册表残留」的注释相矛盾。按上面的结论,正确处置是整段删掉而不是补进卸载列表。


二、真机验证存在混淆变量,目前无法归因到本 PR

本 PR 从 #113 拆出,而 #113 做的是把 DLL 复制进 System32/SysWOW64 再对系统副本 regsvr32,它给出的理由正是同一句「GIP 路径只激活位于系统目录的 in-proc TSF IME DLL,安装目录副本会被 msctf 在 COM 激活前静默筛除」。

两个 PR 各自声称是同一症状的原因,而 LOL 真机验证很可能是在两者都生效的环境下做的。要把功劳归给 #116,需要一次 A/B:

一个可以缩小搜索空间的事实:#113 里的 icacls ALL APPLICATION PACKAGES:(RX) 不是新增的,main 的 scripts/dev.ps1:886-891 已经在做,实测机器上 DLL 也确实带这条 ACE。所以 #113 相对 main 的净增量只有「系统目录副本」一条。


三、ThreadingModel 的因果陈述与现有实测冲突

实测确认当前注册的确实是 ThreadingModel=Both(x64 与 WOW6432Node 两份都是),所以这一改是实打实生效的,方向也对:TSF TIP 的标准就是 Apartment,weasel / SampleIME 同。这部分我倾向保留。

但新注释里这句需要降级:

msctf 会在 COM 激活前静默筛除非 Apartment 的 in-proc IME

一是与 COM 语义相悖(BothApartment 的超集,STA 宿主激活二者走同一路径);二是被本仓 2026-09-05 的 Dota 2 实测证伪:当时 ThreadingModel 就是 Both,输入法在游戏聊天框里已成功激活并走到显示候选窗(症状是弹窗把游戏卡死,不是没激活)。记录在 game-compat-tsf-uielement 分支的设计文档里,尚未合入 main。

建议改成「对齐 weasel / SampleIME 的 TSF 标准做法」,不要写成实测因果。

还有一点需要补验证:被删掉的旧注释写着 Both 是「为增强 Win11 现代应用兼容性」引入的。git log -S "Both" -- wind_tsf/src/Register.cpp 只有迁移提交 4b436075,说明这个 workaround 是从 Go 时代仓库带过来的、原始症状已失传。本 PR 只测了 LOL,没有覆盖 Both 当初想解决的那类宿主。合并前请补测:Win11 开始菜单搜索框、UWP / 微软商店应用、设置应用


小结

部分 处置
ThreadingModelApartment 可合并(注释措辞降级 + 补 UWP / 开始菜单真机)
GUID 数值修正 保留,但重写为「防御性修正」而非「修复」
typo 自愈循环 删除(死代码,清理从未存在的表项)
PR 描述的因果叙述 需重写,否则会在仓库里沉淀一条错误的「已定论」
真机归因 需 A/B 复测,当前证据无法区分 #116#113

顺带一提,Register.cpp:202-206256-260#ifndef __MINGW32__ 已是死分支(mingw_tsf_compat.h 全仓不存在),可以另开一个 PR 清理,不必混进这个。

- GUID 注释区重写:明确 DEFINE_GUID 声明语义(值由 uuid.lib 提供),
  数值改真值属防御性修正(防未来引入 initguid.h 时错值生效),
  删除「曾注册假 GUID」的不实叙述
- 删除 typo 自愈 UnregisterCategory 循环(清理从未存在的表项,死代码)
- ThreadingModel 注释降级:对齐 weasel / SampleIME 的 TSF 标准做法,
  去除「msctf 静默筛除非 Apartment」的因果表述
@sage-z-cn sage-z-cn changed the title fix(tsf): ThreadingModel 改回 Apartment,修正类别 GUID 手抄错误 fix(tsf): ThreadingModel 改回 Apartment(对齐 TSF 标准),类别 GUID 数值防御性修正 Sep 5, 2026
@sage-z-cn

Copy link
Copy Markdown
Contributor Author

感谢细致的 review,全部接受并已按处置表修订(969c1c69):

PR 描述已同步重写。#115 已发勘误评论修正「假 GUID」错误因果。

@huanfeng

huanfeng commented Sep 5, 2026

Copy link
Copy Markdown
Owner

复核 969c1c69,处置表五项全部落实,代码侧我这边没有新意见:

  • typo GUID 常量与自愈循环已整段删除,UnregisterCategories 的一致性问题随之消失 ✅
  • GUID 定义区注释已重写为准确叙述(「DEFINE_GUID 未定义 INITGUID 时只展开为声明,实际取值由链接期解析,手写数值并不进链接产物」)——这句留在这里很有价值,它正是这个坑将来会再犯的地方 ✅
  • ThreadingModel 注释已降级为「对齐 TSF 标准做法」,并保留了「Go 时代 workaround、原始症状已失传」的出处 ✅
  • 现代应用补测(开始菜单搜索框 / 设置 / 微软商店)已补 ✅

净 diff 收敛到 1 文件 +14/-6,很干净。

关于 A/B 归因

E1 / E3 / E2 的三格设计是成立的(缺的第四格「安装目录 + Both」即修复前原状,已知不可用),接受这个结论:本 PR 与系统目录部署是 AND 关系。

一个流程上的建议:这三组实验目前只存在于 #115 的勘误评论里的一句转述,issue 正文那张表仍是「修复模式」的整体口径,没有单变量分解。麻烦把 E1/E2/E3 补成一张表落到 #115 正文——它现在是支撑本 PR 必要性的唯一证据,埋在评论里过几个月就找不回来了。

另外补一条不依赖游戏场景归因也成立的支撑:#115 里你做的冰凌注册面取证显示它的 ThreadingModel 也是 Apartment,加上 weasel / SampleIME,本 PR 靠「与所有同类实现对齐」这一条即可独立成立,不必把论证重量全压在 E3 上。

CI 说明

当前 head 的 CI 处于 action_required(fork PR 需仓库侧批准才会跑),我会去批。

另外提醒一下:上一轮真正跑过的那次 CI 失败(charset_def::tests::dangerous_keys_are_rejected_not_sanitized与你的改动无关——本 PR 只动 wind_tsf/src/Register.cpp,而那个 job 根本不编译 C++。那是 main 上 wind-config 的一个跨平台缺陷(守卫用了 std::path::is_separator,Linux 下不认 \),我会在 main 上单独修掉,你不用管。等 main 转绿后重跑即可。

后续

代码侧我这边通过,等 CI 跑绿即可合。需要说明的是:按 E1,本 PR 单独并不能修好游戏场景——实际生效仍取决于系统目录部署那条线(#113 剩余部分 / wind-installer#1)的决定。那部分我们在 #115 继续讨论,不阻塞这个 PR。

感谢你在这轮里把因果叙述改对、还主动去 #115 发了勘误——这比修一个 bug 更有价值。

@huanfeng

huanfeng commented Sep 5, 2026

Copy link
Copy Markdown
Owner

补一条不依赖游戏场景归因的直接证据:把我这台机器上所有已注册的 TSF 输入法的 ThreadingModel 全扫了一遍(枚举 HKLM\SOFTWARE\Microsoft\CTF\TIP\*,再逐个查 HKLM\SOFTWARE\Classes\CLSID\{clsid}\InprocServer32):

ThreadingModel 组件
Apartment weasel.dll(System32)、Iime64_*.ime(冰凌,Program Files)、imjptip.dll(日文)、imkrtip.dll / imkrotip.dll(韩文)、IMTCTIP.DLL(繁体)、QQWubiTSF.dllTableTextService.dlltiptsf.dll(墨迹)、msctfui.dllmsimtf.dllPalmInputTSF.dllsoftkbd.dllSpTip.dll
Both wind_tsf.dll(清风正式版)、wind_tsf_dev.dll(清风 Dev 版)

(另:微软拼音 ChsIME.exe / 繁体 ChtIME.exe 注册的是 LocalServer32,进程外,不属于 in-proc 这一类。)

本机 17 个 in-proc TSF 组件里,清风的两份是唯二用 Both——包括微软自家全部 IME、weasel、冰凌、QQ五笔在内,其余一个不落全是 Apartment

这条证据的好处是它与游戏、与 GIP、与 E1/E3 都无关:改回 Apartment 不是「换一种赌法」,而是从「全场唯一的异类」回到所有同类实现的共同做法,风险面本身就被这十几个样本兜住了。#116 靠这一条即可独立成立。

顺带印证了 #115 里的两个取证:weasel 确实在 System32,冰凌确实在 Program Files(本机版本 13.8.6.260726,与你测的 13.9.1 略有出入,结论一致)。

@sage-z-cn

Copy link
Copy Markdown
Contributor Author

已按请求把 E1/E3/E2 单变量分解实验落成表补进 #115 正文(「诊断数据」章节末尾,含 2×2 矩阵与 AND 结论),并同步修正了正文里三处假 GUID 叙事残留。

您补充的全机 ThreadingModel 扫描(17 个 in-proc 组件中清风的两个 DLL 是唯二 Both)确实比 E3 单点更有说服力——「从全场唯一异类回到所有同类实现的共同做法」这个论证角度我完全认同,#116 的必要性有这条即可独立成立。

@huanfeng
huanfeng merged commit 726df7f into huanfeng:main Sep 5, 2026
4 checks passed
@github-actions github-actions Bot locked and limited conversation to collaborators Sep 5, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants