diff --git a/openless-all/app/windows-ime/OpenLessIme.vcxproj b/openless-all/app/windows-ime/OpenLessIme.vcxproj
index 32c1ba4f..af417141 100644
--- a/openless-all/app/windows-ime/OpenLessIme.vcxproj
+++ b/openless-all/app/windows-ime/OpenLessIme.vcxproj
@@ -77,6 +77,7 @@
+
@@ -84,6 +85,7 @@
+
diff --git a/openless-all/app/windows-ime/src/ipc_client.cpp b/openless-all/app/windows-ime/src/ipc_client.cpp
new file mode 100644
index 00000000..d34b528e
--- /dev/null
+++ b/openless-all/app/windows-ime/src/ipc_client.cpp
@@ -0,0 +1,486 @@
+#include "ipc_client.h"
+
+#include
+#include
+
+#include "text_service.h"
+
+namespace {
+
+constexpr wchar_t kPipeName[] = L"\\\\.\\pipe\\OpenLessImeSubmit";
+constexpr DWORD kPipeBufferSize = 4096;
+constexpr size_t kMaxJsonLineBytes = 64 * 1024;
+
+struct SubmitMessage {
+ std::wstring type;
+ std::wstring session_id;
+ std::wstring text;
+ int protocol_version = 0;
+ bool has_type = false;
+ bool has_session_id = false;
+ bool has_text = false;
+ bool has_protocol_version = false;
+};
+
+bool AppendUtf8AsWide(const char* data,
+ int length,
+ std::wstring* output) {
+ if (length == 0) {
+ return true;
+ }
+
+ const int required = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, data,
+ length, nullptr, 0);
+ if (required <= 0) {
+ return false;
+ }
+
+ const size_t old_size = output->size();
+ output->resize(old_size + static_cast(required));
+ return MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, data, length,
+ output->data() + old_size, required) == required;
+}
+
+bool WideToUtf8(const std::wstring& value, std::string* output) {
+ output->clear();
+ if (value.empty()) {
+ return true;
+ }
+
+ const int required = WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS,
+ value.c_str(),
+ static_cast(value.size()),
+ nullptr, 0, nullptr, nullptr);
+ if (required <= 0) {
+ return false;
+ }
+
+ output->resize(static_cast(required));
+ return WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, value.c_str(),
+ static_cast(value.size()), output->data(),
+ required, nullptr, nullptr) == required;
+}
+
+void SkipWhitespace(const std::string& json, size_t* pos) {
+ while (*pos < json.size()) {
+ const char c = json[*pos];
+ if (c != ' ' && c != '\t' && c != '\r' && c != '\n') {
+ return;
+ }
+ ++(*pos);
+ }
+}
+
+int HexDigit(char c) {
+ if (c >= '0' && c <= '9') {
+ return c - '0';
+ }
+ if (c >= 'a' && c <= 'f') {
+ return c - 'a' + 10;
+ }
+ if (c >= 'A' && c <= 'F') {
+ return c - 'A' + 10;
+ }
+ return -1;
+}
+
+bool ParseJsonString(const std::string& json,
+ size_t* pos,
+ std::wstring* value) {
+ value->clear();
+ if (*pos >= json.size() || json[*pos] != '"') {
+ return false;
+ }
+ ++(*pos);
+
+ size_t segment_start = *pos;
+ while (*pos < json.size()) {
+ const char c = json[*pos];
+ if (static_cast(c) < 0x20) {
+ return false;
+ }
+
+ if (c == '"') {
+ if (!AppendUtf8AsWide(json.data() + segment_start,
+ static_cast(*pos - segment_start), value)) {
+ return false;
+ }
+ ++(*pos);
+ return true;
+ }
+
+ if (c != '\\') {
+ ++(*pos);
+ continue;
+ }
+
+ if (!AppendUtf8AsWide(json.data() + segment_start,
+ static_cast(*pos - segment_start), value)) {
+ return false;
+ }
+
+ ++(*pos);
+ if (*pos >= json.size()) {
+ return false;
+ }
+
+ const char escaped = json[*pos];
+ switch (escaped) {
+ case '"':
+ case '\\':
+ case '/':
+ value->push_back(static_cast(escaped));
+ ++(*pos);
+ break;
+ case 'b':
+ value->push_back(L'\b');
+ ++(*pos);
+ break;
+ case 'f':
+ value->push_back(L'\f');
+ ++(*pos);
+ break;
+ case 'n':
+ value->push_back(L'\n');
+ ++(*pos);
+ break;
+ case 'r':
+ value->push_back(L'\r');
+ ++(*pos);
+ break;
+ case 't':
+ value->push_back(L'\t');
+ ++(*pos);
+ break;
+ case 'u': {
+ if (*pos + 4 >= json.size()) {
+ return false;
+ }
+ uint32_t code_unit = 0;
+ for (int i = 1; i <= 4; ++i) {
+ const int digit = HexDigit(json[*pos + static_cast(i)]);
+ if (digit < 0) {
+ return false;
+ }
+ code_unit = (code_unit << 4) | static_cast(digit);
+ }
+ value->push_back(static_cast(code_unit));
+ *pos += 5;
+ break;
+ }
+ default:
+ return false;
+ }
+
+ segment_start = *pos;
+ }
+
+ return false;
+}
+
+bool ParseJsonInteger(const std::string& json, size_t* pos, int* value) {
+ if (*pos >= json.size() || json[*pos] < '0' || json[*pos] > '9') {
+ return false;
+ }
+
+ int parsed = 0;
+ while (*pos < json.size() && json[*pos] >= '0' && json[*pos] <= '9') {
+ parsed = parsed * 10 + (json[*pos] - '0');
+ ++(*pos);
+ }
+
+ *value = parsed;
+ return true;
+}
+
+bool SkipJsonValue(const std::string& json, size_t* pos) {
+ std::wstring ignored;
+ if (*pos >= json.size()) {
+ return false;
+ }
+ if (json[*pos] == '"') {
+ return ParseJsonString(json, pos, &ignored);
+ }
+ while (*pos < json.size() && json[*pos] != ',' && json[*pos] != '}') {
+ ++(*pos);
+ }
+ return true;
+}
+
+bool ParseSubmitMessage(const std::string& json, SubmitMessage* message) {
+ size_t pos = 0;
+ SkipWhitespace(json, &pos);
+ if (pos >= json.size() || json[pos] != '{') {
+ return false;
+ }
+ ++pos;
+
+ while (true) {
+ SkipWhitespace(json, &pos);
+ if (pos < json.size() && json[pos] == '}') {
+ ++pos;
+ break;
+ }
+
+ std::wstring key;
+ if (!ParseJsonString(json, &pos, &key)) {
+ return false;
+ }
+
+ SkipWhitespace(json, &pos);
+ if (pos >= json.size() || json[pos] != ':') {
+ return false;
+ }
+ ++pos;
+ SkipWhitespace(json, &pos);
+
+ if (key == L"type") {
+ message->has_type = ParseJsonString(json, &pos, &message->type);
+ if (!message->has_type) {
+ return false;
+ }
+ } else if (key == L"sessionId") {
+ message->has_session_id =
+ ParseJsonString(json, &pos, &message->session_id);
+ if (!message->has_session_id) {
+ return false;
+ }
+ } else if (key == L"text") {
+ message->has_text = ParseJsonString(json, &pos, &message->text);
+ if (!message->has_text) {
+ return false;
+ }
+ } else if (key == L"protocolVersion") {
+ message->has_protocol_version =
+ ParseJsonInteger(json, &pos, &message->protocol_version);
+ if (!message->has_protocol_version) {
+ return false;
+ }
+ } else if (!SkipJsonValue(json, &pos)) {
+ return false;
+ }
+
+ SkipWhitespace(json, &pos);
+ if (pos < json.size() && json[pos] == ',') {
+ ++pos;
+ continue;
+ }
+ if (pos < json.size() && json[pos] == '}') {
+ ++pos;
+ break;
+ }
+ return false;
+ }
+
+ SkipWhitespace(json, &pos);
+ return pos == json.size();
+}
+
+std::wstring EscapeJsonString(const std::wstring& value) {
+ std::wstring escaped;
+ for (wchar_t ch : value) {
+ switch (ch) {
+ case L'"':
+ escaped += L"\\\"";
+ break;
+ case L'\\':
+ escaped += L"\\\\";
+ break;
+ case L'\b':
+ escaped += L"\\b";
+ break;
+ case L'\f':
+ escaped += L"\\f";
+ break;
+ case L'\n':
+ escaped += L"\\n";
+ break;
+ case L'\r':
+ escaped += L"\\r";
+ break;
+ case L'\t':
+ escaped += L"\\t";
+ break;
+ default:
+ if (ch < 0x20) {
+ wchar_t buffer[7] = {};
+ swprintf_s(buffer, L"\\u%04X", static_cast(ch));
+ escaped += buffer;
+ } else {
+ escaped.push_back(ch);
+ }
+ break;
+ }
+ }
+ return escaped;
+}
+
+} // namespace
+
+OpenLessPipeServer::OpenLessPipeServer() = default;
+
+OpenLessPipeServer::~OpenLessPipeServer() {
+ Stop();
+}
+
+void OpenLessPipeServer::Start(OpenLessTextService* service) {
+ if (service == nullptr || thread_.joinable()) {
+ return;
+ }
+
+ stop_requested_.store(false);
+ service_ = service;
+ service_->AddRef();
+ thread_ = std::thread(&OpenLessPipeServer::Run, this);
+}
+
+void OpenLessPipeServer::Stop() {
+ stop_requested_.store(true);
+ if (thread_.joinable()) {
+ CancelSynchronousIo(thread_.native_handle());
+ WakePipe();
+ thread_.join();
+ }
+
+ if (service_ != nullptr) {
+ service_->Release();
+ service_ = nullptr;
+ }
+}
+
+void OpenLessPipeServer::Run() {
+ while (!stop_requested_.load()) {
+ HANDLE pipe = CreateNamedPipeW(
+ kPipeName, PIPE_ACCESS_DUPLEX,
+ PIPE_TYPE_MESSAGE | PIPE_READMODE_BYTE | PIPE_WAIT, 1,
+ kPipeBufferSize, kPipeBufferSize, 0, nullptr);
+ if (pipe == INVALID_HANDLE_VALUE) {
+ return;
+ }
+
+ {
+ std::lock_guard lock(pipe_mutex_);
+ pipe_handle_ = pipe;
+ }
+
+ const BOOL connected =
+ ConnectNamedPipe(pipe, nullptr)
+ ? TRUE
+ : (GetLastError() == ERROR_PIPE_CONNECTED ? TRUE : FALSE);
+
+ if (connected && !stop_requested_.load()) {
+ std::string line;
+ if (ReadJsonLine(pipe, &line)) {
+ HandleSubmitLine(pipe, line);
+ }
+ }
+
+ FlushFileBuffers(pipe);
+ DisconnectNamedPipe(pipe);
+ CloseHandle(pipe);
+
+ {
+ std::lock_guard lock(pipe_mutex_);
+ if (pipe_handle_ == pipe) {
+ pipe_handle_ = INVALID_HANDLE_VALUE;
+ }
+ }
+ }
+}
+
+bool OpenLessPipeServer::ReadJsonLine(HANDLE pipe, std::string* line) {
+ line->clear();
+ char buffer[1024] = {};
+
+ while (!stop_requested_.load() && line->size() < kMaxJsonLineBytes) {
+ DWORD bytes_read = 0;
+ if (!ReadFile(pipe, buffer, sizeof(buffer), &bytes_read, nullptr)) {
+ const DWORD error = GetLastError();
+ if (error == ERROR_MORE_DATA && bytes_read > 0) {
+ line->append(buffer, buffer + bytes_read);
+ continue;
+ }
+ return !line->empty();
+ }
+
+ if (bytes_read == 0) {
+ return !line->empty();
+ }
+
+ for (DWORD i = 0; i < bytes_read; ++i) {
+ if (buffer[i] == '\n') {
+ return true;
+ }
+ line->push_back(buffer[i]);
+ if (line->size() >= kMaxJsonLineBytes) {
+ return true;
+ }
+ }
+ }
+
+ return !line->empty();
+}
+
+void OpenLessPipeServer::HandleSubmitLine(HANDLE pipe, const std::string& line) {
+ SubmitMessage message;
+ if (!ParseSubmitMessage(line, &message) || !message.has_type ||
+ !message.has_protocol_version || !message.has_session_id ||
+ !message.has_text || message.protocol_version != 1 ||
+ message.type != L"submitText") {
+ WriteResult(pipe, message.session_id, L"failed", L"protocolError");
+ return;
+ }
+
+ if (service_ == nullptr) {
+ WriteResult(pipe, message.session_id, L"failed", L"serviceUnavailable");
+ return;
+ }
+
+ const HRESULT hr =
+ service_->SubmitTextFromPipe(message.session_id, message.text);
+ if (SUCCEEDED(hr)) {
+ WriteResult(pipe, message.session_id, L"committed", nullptr);
+ } else {
+ WriteResult(pipe, message.session_id, L"rejected", L"commitRejected");
+ }
+}
+
+bool OpenLessPipeServer::WriteResult(HANDLE pipe,
+ const std::wstring& session_id,
+ const wchar_t* status,
+ const wchar_t* error_code) {
+ std::wstring response = L"{\"type\":\"submitResult\",\"protocolVersion\":1,";
+ response += L"\"sessionId\":\"";
+ response += EscapeJsonString(session_id);
+ response += L"\",\"status\":\"";
+ response += status;
+ response += L"\",\"errorCode\":";
+ if (error_code == nullptr) {
+ response += L"null";
+ } else {
+ response += L"\"";
+ response += error_code;
+ response += L"\"";
+ }
+ response += L"}\n";
+
+ std::string utf8_response;
+ if (!WideToUtf8(response, &utf8_response)) {
+ return false;
+ }
+
+ DWORD bytes_written = 0;
+ return WriteFile(pipe, utf8_response.data(),
+ static_cast(utf8_response.size()), &bytes_written,
+ nullptr) &&
+ bytes_written == utf8_response.size();
+}
+
+void OpenLessPipeServer::WakePipe() {
+ HANDLE pipe =
+ CreateFileW(kPipeName, GENERIC_READ | GENERIC_WRITE, 0, nullptr,
+ OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
+ if (pipe != INVALID_HANDLE_VALUE) {
+ CloseHandle(pipe);
+ }
+}
diff --git a/openless-all/app/windows-ime/src/ipc_client.h b/openless-all/app/windows-ime/src/ipc_client.h
new file mode 100644
index 00000000..e0c0e799
--- /dev/null
+++ b/openless-all/app/windows-ime/src/ipc_client.h
@@ -0,0 +1,36 @@
+#pragma once
+
+#include
+#include
+#include
+#include
+#include
+
+class OpenLessTextService;
+
+class OpenLessPipeServer {
+ public:
+ OpenLessPipeServer();
+ OpenLessPipeServer(const OpenLessPipeServer&) = delete;
+ OpenLessPipeServer& operator=(const OpenLessPipeServer&) = delete;
+ ~OpenLessPipeServer();
+
+ void Start(OpenLessTextService* service);
+ void Stop();
+
+ private:
+ void Run();
+ bool ReadJsonLine(HANDLE pipe, std::string* line);
+ void HandleSubmitLine(HANDLE pipe, const std::string& line);
+ bool WriteResult(HANDLE pipe,
+ const std::wstring& session_id,
+ const wchar_t* status,
+ const wchar_t* error_code);
+ void WakePipe();
+
+ std::atomic stop_requested_{false};
+ std::thread thread_;
+ std::mutex pipe_mutex_;
+ HANDLE pipe_handle_ = INVALID_HANDLE_VALUE;
+ OpenLessTextService* service_ = nullptr;
+};
diff --git a/openless-all/app/windows-ime/src/text_service.cpp b/openless-all/app/windows-ime/src/text_service.cpp
index ed911832..7c4369e3 100644
--- a/openless-all/app/windows-ime/src/text_service.cpp
+++ b/openless-all/app/windows-ime/src/text_service.cpp
@@ -5,6 +5,21 @@
#include "edit_session.h"
extern LONG g_object_count;
+extern HINSTANCE g_module;
+
+namespace {
+
+constexpr wchar_t kMessageWindowClassName[] = L"OpenLessImeMessageWindow";
+constexpr UINT kSubmitTextMessage = WM_APP + 1;
+constexpr UINT kSubmitTextTimeoutMs = 3000;
+
+struct SubmitTextRequest {
+ const std::wstring* session_id = nullptr;
+ const std::wstring* text = nullptr;
+ HRESULT result = E_UNEXPECTED;
+};
+
+} // namespace
OpenLessTextService::OpenLessTextService() {
InterlockedIncrement(&g_object_count);
@@ -59,11 +74,19 @@ STDMETHODIMP OpenLessTextService::ActivateEx(ITfThreadMgr* thread_mgr,
Deactivate();
+ owner_thread_id_ = GetCurrentThreadId();
+
thread_mgr_ = thread_mgr;
thread_mgr_->AddRef();
client_id_ = client_id;
- const HRESULT hr = StartIpcServer();
+ HRESULT hr = EnsureMessageWindow();
+ if (FAILED(hr)) {
+ Deactivate();
+ return hr;
+ }
+
+ hr = StartIpcServer();
if (FAILED(hr)) {
Deactivate();
return hr;
@@ -74,12 +97,14 @@ STDMETHODIMP OpenLessTextService::ActivateEx(ITfThreadMgr* thread_mgr,
STDMETHODIMP OpenLessTextService::Deactivate() {
StopIpcServer();
+ DestroyMessageWindow();
if (thread_mgr_ != nullptr) {
thread_mgr_->Release();
thread_mgr_ = nullptr;
}
client_id_ = TF_CLIENTID_NULL;
+ owner_thread_id_ = 0;
return S_OK;
}
@@ -87,6 +112,74 @@ STDMETHODIMP OpenLessTextService::Deactivate() {
HRESULT OpenLessTextService::SubmitTextFromPipe(
const std::wstring& session_id,
const std::wstring& text) {
+ if (GetCurrentThreadId() == owner_thread_id_) {
+ return CommitTextOnOwnerThread(session_id, text);
+ }
+
+ if (message_window_ == nullptr) {
+ return E_UNEXPECTED;
+ }
+
+ SubmitTextRequest request{&session_id, &text, E_UNEXPECTED};
+ DWORD_PTR message_result = 0;
+ const LRESULT sent = SendMessageTimeoutW(
+ message_window_, kSubmitTextMessage, 0,
+ reinterpret_cast(&request), SMTO_ABORTIFHUNG,
+ kSubmitTextTimeoutMs, &message_result);
+ if (sent == 0) {
+ const DWORD error = GetLastError();
+ return HRESULT_FROM_WIN32(error != ERROR_SUCCESS ? error : ERROR_TIMEOUT);
+ }
+
+ return request.result;
+}
+
+HRESULT OpenLessTextService::StartIpcServer() {
+ pipe_server_.Start(this);
+ return S_OK;
+}
+
+void OpenLessTextService::StopIpcServer() {
+ pipe_server_.Stop();
+}
+
+HRESULT OpenLessTextService::EnsureMessageWindow() {
+ if (message_window_ != nullptr) {
+ return S_OK;
+ }
+
+ WNDCLASSW window_class = {};
+ window_class.lpfnWndProc = OpenLessTextService::MessageWindowProc;
+ window_class.hInstance = g_module;
+ window_class.lpszClassName = kMessageWindowClassName;
+
+ if (!RegisterClassW(&window_class)) {
+ const DWORD error = GetLastError();
+ if (error != ERROR_CLASS_ALREADY_EXISTS) {
+ return HRESULT_FROM_WIN32(error);
+ }
+ }
+
+ message_window_ =
+ CreateWindowExW(0, kMessageWindowClassName, L"", 0, 0, 0, 0, 0,
+ HWND_MESSAGE, nullptr, g_module, this);
+ if (message_window_ == nullptr) {
+ return HRESULT_FROM_WIN32(GetLastError());
+ }
+
+ return S_OK;
+}
+
+void OpenLessTextService::DestroyMessageWindow() {
+ if (message_window_ != nullptr) {
+ DestroyWindow(message_window_);
+ message_window_ = nullptr;
+ }
+}
+
+HRESULT OpenLessTextService::CommitTextOnOwnerThread(
+ const std::wstring& session_id,
+ const std::wstring& text) {
UNREFERENCED_PARAMETER(session_id);
if (thread_mgr_ == nullptr || client_id_ == TF_CLIENTID_NULL) {
@@ -131,8 +224,32 @@ HRESULT OpenLessTextService::SubmitTextFromPipe(
return edit_result;
}
-HRESULT OpenLessTextService::StartIpcServer() {
- return S_OK;
-}
+LRESULT CALLBACK OpenLessTextService::MessageWindowProc(HWND window,
+ UINT message,
+ WPARAM wparam,
+ LPARAM lparam) {
+ UNREFERENCED_PARAMETER(wparam);
-void OpenLessTextService::StopIpcServer() {}
+ if (message == WM_NCCREATE) {
+ const auto* create = reinterpret_cast(lparam);
+ SetWindowLongPtrW(window, GWLP_USERDATA,
+ reinterpret_cast(create->lpCreateParams));
+ return TRUE;
+ }
+
+ auto* service = reinterpret_cast(
+ GetWindowLongPtrW(window, GWLP_USERDATA));
+ if (message == kSubmitTextMessage && service != nullptr) {
+ auto* request = reinterpret_cast(lparam);
+ if (request == nullptr || request->session_id == nullptr ||
+ request->text == nullptr) {
+ return 0;
+ }
+
+ request->result =
+ service->CommitTextOnOwnerThread(*request->session_id, *request->text);
+ return 1;
+ }
+
+ return DefWindowProcW(window, message, wparam, lparam);
+}
diff --git a/openless-all/app/windows-ime/src/text_service.h b/openless-all/app/windows-ime/src/text_service.h
index 7ef32466..d6402a6a 100644
--- a/openless-all/app/windows-ime/src/text_service.h
+++ b/openless-all/app/windows-ime/src/text_service.h
@@ -4,6 +4,8 @@
#include
#include
+#include "ipc_client.h"
+
class OpenLessTextService final : public ITfTextInputProcessorEx {
public:
OpenLessTextService();
@@ -27,8 +29,20 @@ class OpenLessTextService final : public ITfTextInputProcessorEx {
private:
HRESULT StartIpcServer();
void StopIpcServer();
+ HRESULT EnsureMessageWindow();
+ void DestroyMessageWindow();
+ HRESULT CommitTextOnOwnerThread(const std::wstring& session_id,
+ const std::wstring& text);
+
+ static LRESULT CALLBACK MessageWindowProc(HWND window,
+ UINT message,
+ WPARAM wparam,
+ LPARAM lparam);
LONG ref_count_ = 1;
ITfThreadMgr* thread_mgr_ = nullptr;
TfClientId client_id_ = TF_CLIENTID_NULL;
+ DWORD owner_thread_id_ = 0;
+ HWND message_window_ = nullptr;
+ OpenLessPipeServer pipe_server_;
};