Kiến trúc bộ gõ tiếng Việt nguyên bản không dùng Event Tap và bài toán phân giải va chạm song ngữ đối xứng
Báo cáo này trình bày các nhận định kỹ thuật và kết quả đo kiểm thực nghiệm của dự án Sen Việt (VieLotusIM). Chúng tôi phân tích nguyên nhân gốc rễ khiến phương pháp chặn phím tầng thấp (CGEventTap / tiêm phím xóa giả lập) gây nuốt chữ trên trình duyệt hiện đại, đề xuất kiến trúc đồng bộ trạng thái nguyên bản qua InputMethodKit / TSF / IBus, và thiết lập chuẩn đánh giá đối xứng hai chiều cho bài toán gõ trộn tiếng Việt – tiếng Anh trên tập dữ liệu 9.091 ca kiểm thử.
§1. Nhận định cốt lõi: Vì sao kiến trúc CGEventTap truyền thống gặp giới hạn trên hệ điều hành hiện đại?
Trong suốt hai thập kỷ qua, phần lớn các bộ gõ tiếng Việt bên thứ ba trên macOS đều lựa chọn kiến trúc chặn sự kiện bàn phím tầng thấp (CGEventTap). Khi người dùng gõ chữ t-i-e-e, chữ tiê chưa được tạo ra trong vùng đệm hệ thống; thay vào đó, chữ tie đã lọt vào ứng dụng, sau đó bộ gõ phát sinh một chuỗi sự kiện Backspace giả lập qua CGEventSource để xóa chữ cũ rồi bắn chuỗi Unicode tiê mới vào luồng sự kiện cửa sổ.
Cách tiếp cận này từng hoạt động tốt trên các khung soạn thảo văn bản đồng bộ thế hệ cũ, nhưng bộc lộ 4 xung đột cấu trúc không thể vá triệt để trên macOS hiện đại và các ứng dụng kiến trúc bất đồng bộ (Chromium, Electron, React Virtual DOM, IDE hiện đại):
| Khía cạnh kỹ thuật | Bộ gõ dựa trên CGEventTap (Tiêm Backspace) | Sen Việt (Pure InputMethodKit / TSF / IBus) |
|---|---|---|
| Đồng bộ hóa giao diện bất đồng bộ (React / Electron / URL Bar) | Phím Backspace giả lập chạy đua (race condition) với vòng lặp đối chiếu Virtual DOM hoặc gợi ý tự động trên thanh địa chỉ, gây hiện tượng nhân đôi âm tiết (t-ti-tiế-tiếng) hoặc nhảy con trỏ. |
Thay thế nguyên tử qua insertText:replacementRange: (UTF-16) trên AppKit hoặc phát đúng vòng đời W3C compositionstart / compositionupdate trên trình duyệt. |
| Quyền hệ thống & lỗi câm TCC (csreq mismatch) | Bắt buộc đòi quyền Accessibility (Trợ năng) toàn hệ thống. Khi cập nhật phiên bản mới, chữ ký mã (Designated Requirement) thay đổi khiến macOS âm thầm chặn sự kiện dù nút bật trong Cài đặt vẫn sáng. | 0 quyền Accessibility, 0 quyền Input Monitoring. Vận hành đúng giao thức bàn phím chính thống của hệ điều hành. |
| Lịch sử hoàn tác (Undo Stack Cmd+Z) | Mỗi lần tiêm phím xóa và chèn lại chữ sẽ làm vỡ ngăn xếp hoàn tác của trình soạn thảo thành từng mảnh ký tự rời rạc. | Bảo toàn nguyên vẹn ngăn xếp hoàn tác theo từng từ của ứng dụng đích. |
| Ô nhập mật khẩu (Secure Event Input) | Bị vô hiệu hóa đột ngột hoặc gây kẹt trạng thái khi trình duyệt/Terminal bật EnableSecureEventInput(). |
Kiểm tra chủ động IsSecureEventInputEnabled() và tự động chuyển sang passthrough an toàn, tuyệt đối không lưu vết phím. |
§2. Mô phỏng thực nghiệm: So sánh phản ứng máy trạng thái trên các ca gõ thực tế
Chọn một trong 6 kịch bản kiểm thử thực tế dưới đây để quan sát sự khác biệt giữa cơ chế tiêm phím xóa truyền thống, bộ gõ mặc định Simple Telex, và máy trạng thái InputSessionManager + SmartBilingualDetector của Sen Việt:
§3. Nghịch lý va chạm song ngữ Anh – Việt và nguyên lý công bố đối xứng
Khi người Việt làm việc hàng ngày, hàng nghìn từ tiếng Anh phổ biến chứa các chữ cái trùng khớp với phím điều khiển Telex: s (dấu sắc), f (dấu huyền), r (dấu hỏi), x (dấu ngã), j (dấu nặng), w (ư/ơ/ă), và cặp nguyên âm oo, ee, aa. Ví dụ: gõ test bị biến thành tết, toast thành toát, floor thành flô, restrict bị biến dạng dấu hỏi/sắc.
Tuy nhiên, một nhận định khoa học quan trọng của nghiên cứu này là: Mọi tuyên bố "đạt 100% khôi phục từ tiếng Anh" nếu chỉ đo một chiều đều là phóng đại năng lực của bộ gõ. Nếu một bộ gõ chỉ đơn giản nhét toàn bộ từ điển tiếng Anh vào danh sách trắng (whitelist) vô điều kiện, nó sẽ đạt 100% ở phía tiếng Anh nhưng lại phá hủy khả năng gõ các âm tiết tiếng Việt đồng hình (chẳng hạn người dùng không bao giờ gõ được chữ tết bằng test, chữ đất bằng ddats, hay chữ lốt bằng loots khi đứng đầu câu).
ambiguous_both_sides và 100.00% (182 / 182 — 0 mất mát nghiêm trọng) ở nhóm âm tiết tiếng Việt chỉ có duy nhất một đường gõ (whitelist_safe = never).
Bảng 3.1 — Ma trận kiểm thử chính (9.091 ca, có cô lập ngữ cảnh từng dòng)
| Nhóm kiểm thử (Bucket) | Mô tả điều kiện thực thi | Đạt / Tổng | Tỷ lệ | Ngưỡng chốt (Ratchet) |
|---|---|---|---|---|
transform |
Biến đổi âm tiết tiếng Việt thuần túy (Telex & SEN Flex) | 400 / 400 | 100.00% | 100.00% |
keep_as_typed |
Từ vựng tiếng Anh không chứa chuỗi biến âm hợp lệ của tiếng Việt | 362 / 362 | 100.00% | 100.00% |
cancel_keep_composed |
Hủy dấu / thoát mũ ở cuối từ (zz, ss, lặp ba nguyên âm aaa → aa) |
67 / 67 | 100.00% | 100.00% |
restore_raw |
Khôi phục từ tiếng Anh bị biến dạng bởi phím dấu Telex (kiểm tra luật âm vị tắt -c, -ch, -p, -t, nguyên âm đôi bất khả và băm từ điển 64-bit) |
7.213 / 7.213 | 100.00% | 100.00% |
ambiguous_needs_context |
Từ lưỡng nghĩa (vừa là từ tiếng Anh hợp lệ, vừa tạo thành âm tiết tiếng Việt hợp lệ) khi được mồi ngữ cảnh tiếng Anh đi trước | 1.049 / 1.049 | 100.00% | 100.00% |
| Tổng hợp bộ kiểm thử chính | 0 xung đột thỏa mãn (Satisfiability) · 0 xung đột chính sách (Policy Coherence) | 9.091 / 9.091 | 100.00% | 100.00% |
Bảng 3.2 — Ma trận kiểm thử đối xứng không ngữ cảnh (Chạy lại các ca lưỡng nghĩa với context rỗng)
| Chỉ số đối xứng | Ý nghĩa ngôn ngữ học & kiểm định | Đạt / Tổng | Tỷ lệ |
|---|---|---|---|
vn_survives_collision |
375 từ lưỡng nghĩa viết thường khi gõ độc lập không có ngữ cảnh tiếng Anh: âm tiết tiếng Việt phải được giữ lại | 365 / 375 | 97.33% |
↳ whitelist_safe = never |
Nhóm âm tiết tiếng Việt mà chuỗi phím đó là con đường duy nhất để gõ ra chữ Việt (không được phép mất dù chỉ 1 từ) | 182 / 182 | 100.00% (0 lỗi) |
↳ whitelist_safe = degrades_vn |
Nhóm âm tiết có nhiều cách gõ thay thế (ví dụ gõ kiểu Telex mở rộng) | 183 / 193 | 94.82% |
ambiguous_both_sides |
Chỉ số chuẩn mực kép: Ra đúng tiếng Anh khi có ngữ cảnh tiếng Anh VÀ ra đúng tiếng Việt khi gõ độc lập | 365 / 375 | 97.33% |
§4. Cơ chế trình diễn thích ứng theo đặc tính khung soạn thảo (ClientAdapter)
Một nhận định thực tế khác của nhóm phát triển là: Không có một chế độ hiển thị đơn lẻ nào tối ưu cho mọi ứng dụng trên macOS nếu tuân thủ thuần túy InputMethodKit. Các ứng dụng Cocoa/AppKit truyền thống xử lý hoàn hảo phép thay thế phạm vi trực tiếp (insertText:replacementRange:) không cần gạch chân, trong khi các trình soạn thảo web dùng React, ProseMirror, Monaco hay thanh địa chỉ Chromium lại dựa vào sự kiện compositionstart / compositionend (tức chế độ setMarkedText có gạch chân tạm thời) để không nhân đôi ký tự khi đối chiếu DOM bất đồng bộ.
Thay vì dùng CGEventTap để cưỡng bức màn hình, Sen Việt kết hợp bộ phân loại ClientAdapter, lớp bảo vệ biên độ DirectReplacementGuard (đảm bảo không bao giờ xóa lố ra ngoài phạm vi [compositionStartLocation, virtualCursorLocation]) và tập luật dữ liệu mở typing-modes.yml:
| Nhóm ứng dụng | Ứng dụng đại diện | Chế độ trình diễn | Cơ chế đồng bộ kỹ thuật |
|---|---|---|---|
| Standard AppKit / Cocoa | Notes, Pages, TextEdit, Mail, Safari, Xcode, Finder, MS Office | Direct Replacement |
Thay thế trực tiếp theo đơn vị mã UTF-16 qua insertText:replacementRange:, không gạch chân, giữ nguyên Undo stack. |
| Chromium, Electron & CEF | Chrome, Arc, Edge, Brave, VS Code, Cursor, Slack, Notion, Obsidian, Zalo | Marked Text (W3C) |
Đồng bộ với vòng đời W3C IME composition để triệt tiêu hoàn toàn lỗi lặp chữ (t-ti-tiế-tiếng) và nhảy con trỏ trong React/Virtual DOM. |
| GPU & POSIX Terminals | Ghostty, Alacritty, Kitty, WezTerm, iTerm2, Terminal.app | Terminal Direct / Marked |
Đồng bộ byte xóa PTY (\u{7F}) và độ dài UTF-16 code units, tương thích với các công cụ dòng lệnh như Claude Code, tmux, zsh. |
| Dynamic Fallback | Ô nhập liệu tùy biến trả về NSNotFound |
Auto Marked Fallback |
Khi DirectReplacementGuard phát hiện ứng dụng đích bỏ qua replacementRange, hệ thống tự động chuyển sang setMarkedText ngay trong phiên gõ. |
§5. Kiến trúc đa nền tảng (Pure Swift 6 Core) và bài toán nạp In-Process COM trên Windows 11 ARM64
Để cùng một lõi ngôn ngữ học phục vụ đồng nhất cả 5 hệ điều hành (macOS, iOS/iPadOS, Windows, Linux, Android), toàn bộ thư viện VieLotusCore được viết bằng Swift 6 thuần túy không phụ thuộc giao diện đồ họa hệ thống, và xuất giao diện cầu nối C-ABI không sao chép bộ nhớ (VieLotusCAPI, 26 hàm trong vielotus.h) cho Windows TSF và Linux IBus / Fcitx5.
Nghiên cứu tình huống kỹ thuật trên Windows 11 (x64 & ARM64): Trên Windows, một bộ gõ chuẩn Text Services Framework (TSF) là một thư viện COM In-Process (VieLotusTIP.dll) được nạp trực tiếp vào không gian địa chỉ của từng tiến trình ứng dụng (notepad.exe, explorer.exe, trình duyệt) và được đăng ký qua regsvr32.exe. Nếu VieLotusTIP.dll liên kết tĩnh (Static PE Import) vào VieLotusCAPI.dll:
1) Khi trình cài đặt gọi regsvr32.exe hoặc khi một ứng dụng có thư mục làm việc tại C:\Windows\System32 khởi tạo COM đối tượng, trình nạp PE của Windows sẽ tìm VieLotusCAPI.dll cùng chuỗi thư viện Swift Runtime (swiftCore.dll, swiftDispatch.dll, FoundationEssentials.dll) trong System32 thay vì thư mục cài đặt, gây ra lỗi RegSvr32 exit code 0x3 (ERROR_MOD_NOT_FOUND).
2) Để giải quyết triệt để ở cấp kiến trúc, VieLotusTIP.dll được biên dịch với Static MSVC Runtime (/MT) và chỉ phụ thuộc vào 4 DLL nhân Windows (KERNEL32, USER32, OLE32, ADVAPI32). Khi phiên gõ kích hoạt (ActivateEx), VieLotusTIP.dll tự xác định thư mục chứa chính nó qua GetModuleFileNameW(g_hInst, ...) và nạp động VieLotusCAPI.dll bằng cờ LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR | LOAD_LIBRARY_SEARCH_DEFAULT_DIRS, đảm bảo cô lập hoàn toàn đường dẫn DLL cho cả bản x64 lẫn bản ARM64 nguyên bản.
§6. Hiệu năng độ trễ đuôi (p99 Tail Latency) & quy trình kiểm chứng độc lập (Open-Core)
Trong trải nghiệm gõ phím thời gian thực, độ trễ trung bình (mean latency) thường che giấu các nhịp khựng vi mô khi bộ phân tích ngữ cảnh hoặc bộ thu gom rác bộ nhớ hoạt động. Do đó, Sen Việt kiểm soát chất lượng bằng độ trễ đuôi phân vị thứ 99 (p99 latency) trên bài kiểm thử chịu tải 250.000 phím liên tục (VieLotusCLI --stress) chạy qua 3 luồng tải trộn lẫn:
| Luồng tải kiểm thử (250.000 phím) | Ngưỡng trần cho phép (Gate) | Kết quả thực đo (Apple Silicon) | Trạng thái bộ nhớ |
|---|---|---|---|
| Telex tiếng Việt thuần (Độ trễ p99) | ≤ 500.00 µs | 150 – 220 µs | Ổn định < 30 MB RSS (0 rò rỉ) |
| VNI tiếng Việt thuần (Độ trễ p99) | ≤ 300.00 µs | 60 – 110 µs | Ổn định < 30 MB RSS (0 rò rỉ) |
| Song ngữ Anh – Việt & mã nguồn (Độ trễ p99) | ≤ 700.00 µs | 250 – 380 µs | Ổn định < 30 MB RSS (0 rò rỉ) |
| Thông lượng xử lý duy trì (Sustained Throughput) | ≥ 25.000 phím/giây | > 40.000 phím/giây | Chạy 3 tiến trình độc lập, lấy kết quả thấp nhất |
Tự tay kiểm chứng hộp đen trong 30 giây (Open-Core Verification)
Sen Việt được phát hành theo mô hình Open-Core: toàn bộ mã nguồn phần vỏ hệ thống (Sources/VieLotusIM, CrossPlatform/) được mở công khai theo giấy phép MIT để bất kỳ kỹ sư bảo mật nào cũng xác minh được tính chất Zero CGEventTap / Zero Keylogging, đi kèm công cụ dòng lệnh đã biên dịch VieLotusCLI để kiểm chứng độc lập toàn bộ 9.091 ca kiểm thử và bài đo 250.000 phím:
# 1. Kiểm tra chữ ký băm SHA-256 của các binary phát hành
shasum -a 256 -c Binaries/checksums.sha256
# 2. Chạy toàn bộ 9.091 ca kiểm thử chính + 1.049 ca đối xứng không ngữ cảnh
./Binaries/VieLotusCLI --benchmark
# 3. Chạy cổng kiểm định 3 tiến trình độc lập + đo độ trễ đuôi p99 (250.000 phím)
./scripts/benchmark_ratchet.sh --with-latency
# 4. Xác minh mã nguồn vỏ không chứa bất kỳ lời gọi CGEventTap nào
grep -rn "CGEvent" Sources/VieLotusIM/