From e950d1ddc78822bdf8fdaaf169692d0477e95669 Mon Sep 17 00:00:00 2001 From: millionart Date: Fri, 1 May 2026 20:20:25 +0800 Subject: [PATCH] feat: scaffold OpenLess TSF IME DLL --- openless-all/app/windows-ime/OpenLessIme.sln | 21 +++ .../app/windows-ime/OpenLessIme.vcxproj | 91 +++++++++ .../app/windows-ime/src/class_factory.cpp | 75 ++++++++ .../app/windows-ime/src/class_factory.h | 20 ++ openless-all/app/windows-ime/src/dllmain.cpp | 54 ++++++ openless-all/app/windows-ime/src/guids.h | 21 +++ openless-all/app/windows-ime/src/registry.cpp | 176 ++++++++++++++++++ openless-all/app/windows-ime/src/registry.h | 6 + openless-all/app/windows-ime/src/resource.rc | 33 ++++ .../app/windows-ime/src/text_service.cpp | 95 ++++++++++ .../app/windows-ime/src/text_service.h | 34 ++++ 11 files changed, 626 insertions(+) create mode 100644 openless-all/app/windows-ime/OpenLessIme.sln create mode 100644 openless-all/app/windows-ime/OpenLessIme.vcxproj create mode 100644 openless-all/app/windows-ime/src/class_factory.cpp create mode 100644 openless-all/app/windows-ime/src/class_factory.h create mode 100644 openless-all/app/windows-ime/src/dllmain.cpp create mode 100644 openless-all/app/windows-ime/src/guids.h create mode 100644 openless-all/app/windows-ime/src/registry.cpp create mode 100644 openless-all/app/windows-ime/src/registry.h create mode 100644 openless-all/app/windows-ime/src/resource.rc create mode 100644 openless-all/app/windows-ime/src/text_service.cpp create mode 100644 openless-all/app/windows-ime/src/text_service.h diff --git a/openless-all/app/windows-ime/OpenLessIme.sln b/openless-all/app/windows-ime/OpenLessIme.sln new file mode 100644 index 00000000..a2fc9f08 --- /dev/null +++ b/openless-all/app/windows-ime/OpenLessIme.sln @@ -0,0 +1,21 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{BC8A1FFA-BEE3-4634-8014-F334798102B3}") = "OpenLessIme", "OpenLessIme.vcxproj", "{F45287CB-1F31-4D7C-9B62-5748C2F8C9B0}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|x64 = Debug|x64 + Release|x64 = Release|x64 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {F45287CB-1F31-4D7C-9B62-5748C2F8C9B0}.Debug|x64.ActiveCfg = Debug|x64 + {F45287CB-1F31-4D7C-9B62-5748C2F8C9B0}.Debug|x64.Build.0 = Debug|x64 + {F45287CB-1F31-4D7C-9B62-5748C2F8C9B0}.Release|x64.ActiveCfg = Release|x64 + {F45287CB-1F31-4D7C-9B62-5748C2F8C9B0}.Release|x64.Build.0 = Release|x64 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/openless-all/app/windows-ime/OpenLessIme.vcxproj b/openless-all/app/windows-ime/OpenLessIme.vcxproj new file mode 100644 index 00000000..0275bfae --- /dev/null +++ b/openless-all/app/windows-ime/OpenLessIme.vcxproj @@ -0,0 +1,91 @@ + + + + + Debug + x64 + + + Release + x64 + + + + 17.0 + Win32Proj + {F45287CB-1F31-4D7C-9B62-5748C2F8C9B0} + OpenLessIme + 10.0 + + + + DynamicLibrary + true + v143 + Unicode + + + DynamicLibrary + false + v143 + true + Unicode + + + + + + + + + + + + + + Level4 + true + WIN32;_WINDOWS;_USRDLL;OPENLESSIME_EXPORTS;%(PreprocessorDefinitions) + true + stdcpp17 + + + Windows + msctf.lib;ole32.lib;uuid.lib;advapi32.lib;%(AdditionalDependencies) + + + + + Level4 + true + true + true + WIN32;NDEBUG;_WINDOWS;_USRDLL;OPENLESSIME_EXPORTS;%(PreprocessorDefinitions) + true + stdcpp17 + + + Windows + true + true + msctf.lib;ole32.lib;uuid.lib;advapi32.lib;%(AdditionalDependencies) + + + + + + + + + + + + + + + + + + + + diff --git a/openless-all/app/windows-ime/src/class_factory.cpp b/openless-all/app/windows-ime/src/class_factory.cpp new file mode 100644 index 00000000..7cfb095e --- /dev/null +++ b/openless-all/app/windows-ime/src/class_factory.cpp @@ -0,0 +1,75 @@ +#include "class_factory.h" + +#include +#include + +#include "text_service.h" + +extern LONG g_lock_count; +extern LONG g_object_count; + +OpenLessClassFactory::OpenLessClassFactory() { + InterlockedIncrement(&g_object_count); +} + +OpenLessClassFactory::~OpenLessClassFactory() { + InterlockedDecrement(&g_object_count); +} + +STDMETHODIMP OpenLessClassFactory::QueryInterface(REFIID iid, void** object) { + if (object == nullptr) { + return E_POINTER; + } + *object = nullptr; + + if (iid == IID_IUnknown || iid == IID_IClassFactory) { + *object = static_cast(this); + AddRef(); + return S_OK; + } + + return E_NOINTERFACE; +} + +STDMETHODIMP_(ULONG) OpenLessClassFactory::AddRef() { + return static_cast(InterlockedIncrement(&ref_count_)); +} + +STDMETHODIMP_(ULONG) OpenLessClassFactory::Release() { + const ULONG count = static_cast(InterlockedDecrement(&ref_count_)); + if (count == 0) { + delete this; + } + return count; +} + +STDMETHODIMP OpenLessClassFactory::CreateInstance(IUnknown* outer, + REFIID iid, + void** object) { + if (object == nullptr) { + return E_POINTER; + } + *object = nullptr; + + if (outer != nullptr) { + return CLASS_E_NOAGGREGATION; + } + + auto* service = new (std::nothrow) OpenLessTextService(); + if (service == nullptr) { + return E_OUTOFMEMORY; + } + + const HRESULT hr = service->QueryInterface(iid, object); + service->Release(); + return hr; +} + +STDMETHODIMP OpenLessClassFactory::LockServer(BOOL lock) { + if (lock) { + InterlockedIncrement(&g_lock_count); + } else { + InterlockedDecrement(&g_lock_count); + } + return S_OK; +} diff --git a/openless-all/app/windows-ime/src/class_factory.h b/openless-all/app/windows-ime/src/class_factory.h new file mode 100644 index 00000000..bb2de3e9 --- /dev/null +++ b/openless-all/app/windows-ime/src/class_factory.h @@ -0,0 +1,20 @@ +#pragma once + +#include + +class OpenLessClassFactory final : public IClassFactory { + public: + OpenLessClassFactory(); + OpenLessClassFactory(const OpenLessClassFactory&) = delete; + OpenLessClassFactory& operator=(const OpenLessClassFactory&) = delete; + ~OpenLessClassFactory() override; + + STDMETHODIMP QueryInterface(REFIID iid, void** object) override; + STDMETHODIMP_(ULONG) AddRef() override; + STDMETHODIMP_(ULONG) Release() override; + STDMETHODIMP CreateInstance(IUnknown* outer, REFIID iid, void** object) override; + STDMETHODIMP LockServer(BOOL lock) override; + + private: + LONG ref_count_ = 1; +}; diff --git a/openless-all/app/windows-ime/src/dllmain.cpp b/openless-all/app/windows-ime/src/dllmain.cpp new file mode 100644 index 00000000..84476ba2 --- /dev/null +++ b/openless-all/app/windows-ime/src/dllmain.cpp @@ -0,0 +1,54 @@ +#include + +#include + +#include "class_factory.h" +#include "guids.h" +#include "registry.h" + +HINSTANCE g_module = nullptr; +LONG g_lock_count = 0; +LONG g_object_count = 0; + +BOOL APIENTRY DllMain(HINSTANCE instance, DWORD reason, LPVOID reserved) { + UNREFERENCED_PARAMETER(reserved); + + if (reason == DLL_PROCESS_ATTACH) { + g_module = instance; + DisableThreadLibraryCalls(instance); + } + + return TRUE; +} + +STDAPI DllCanUnloadNow() { + return (g_lock_count == 0 && g_object_count == 0) ? S_OK : S_FALSE; +} + +STDAPI DllGetClassObject(REFCLSID clsid, REFIID iid, void** object) { + if (object == nullptr) { + return E_POINTER; + } + *object = nullptr; + + if (clsid != CLSID_OpenLessTextService) { + return CLASS_E_CLASSNOTAVAILABLE; + } + + auto* factory = new (std::nothrow) OpenLessClassFactory(); + if (factory == nullptr) { + return E_OUTOFMEMORY; + } + + const HRESULT hr = factory->QueryInterface(iid, object); + factory->Release(); + return hr; +} + +STDAPI DllRegisterServer() { + return RegisterOpenLessTextService(g_module); +} + +STDAPI DllUnregisterServer() { + return UnregisterOpenLessTextService(); +} diff --git a/openless-all/app/windows-ime/src/guids.h b/openless-all/app/windows-ime/src/guids.h new file mode 100644 index 00000000..a22782bf --- /dev/null +++ b/openless-all/app/windows-ime/src/guids.h @@ -0,0 +1,21 @@ +#pragma once + +#include +#include + +inline constexpr CLSID CLSID_OpenLessTextService = { + 0x6b9f3f4f, + 0x5ee7, + 0x42d6, + {0x9c, 0x61, 0x9f, 0x80, 0xb0, 0x3a, 0x5d, 0x7d}, +}; + +inline constexpr GUID GUID_OpenLessProfile = { + 0x9b5f5e04, + 0x23f6, + 0x47da, + {0x9a, 0x26, 0xd2, 0x21, 0xf6, 0xc3, 0xf0, 0x2e}, +}; + +inline constexpr wchar_t kOpenLessImeName[] = L"OpenLess Voice Input"; +inline constexpr LANGID kOpenLessLangId = 0x0804; diff --git a/openless-all/app/windows-ime/src/registry.cpp b/openless-all/app/windows-ime/src/registry.cpp new file mode 100644 index 00000000..6a5e5656 --- /dev/null +++ b/openless-all/app/windows-ime/src/registry.cpp @@ -0,0 +1,176 @@ +#include "registry.h" + +#include +#include + +#include "guids.h" + +namespace { + +constexpr wchar_t kClsidKey[] = + L"Software\\Classes\\CLSID\\{6B9F3F4F-5EE7-42D6-9C61-9F80B03A5D7D}"; +constexpr wchar_t kInprocServer32Key[] = + L"Software\\Classes\\CLSID\\{6B9F3F4F-5EE7-42D6-9C61-9F80B03A5D7D}\\InprocServer32"; + +HRESULT HResultFromWin32Error(LSTATUS status) { + return status == ERROR_SUCCESS ? S_OK : HRESULT_FROM_WIN32(status); +} + +HRESULT SetStringValue(HKEY key, const wchar_t* name, const wchar_t* value) { + const auto byte_count = + static_cast((wcslen(value) + 1) * sizeof(wchar_t)); + return HResultFromWin32Error( + RegSetValueExW(key, name, 0, REG_SZ, + reinterpret_cast(value), byte_count)); +} + +HRESULT RegisterComServer(HINSTANCE module) { + wchar_t module_path[MAX_PATH] = {}; + const DWORD path_len = + GetModuleFileNameW(module, module_path, ARRAYSIZE(module_path)); + if (path_len == 0) { + return HRESULT_FROM_WIN32(GetLastError()); + } + if (path_len == ARRAYSIZE(module_path)) { + return HRESULT_FROM_WIN32(ERROR_INSUFFICIENT_BUFFER); + } + + HKEY clsid_key = nullptr; + LSTATUS status = + RegCreateKeyExW(HKEY_CURRENT_USER, kClsidKey, 0, nullptr, + REG_OPTION_NON_VOLATILE, KEY_WRITE, nullptr, &clsid_key, + nullptr); + HRESULT hr = HResultFromWin32Error(status); + if (FAILED(hr)) { + return hr; + } + + hr = SetStringValue(clsid_key, nullptr, kOpenLessImeName); + RegCloseKey(clsid_key); + if (FAILED(hr)) { + return hr; + } + + HKEY inproc_key = nullptr; + status = RegCreateKeyExW(HKEY_CURRENT_USER, kInprocServer32Key, 0, nullptr, + REG_OPTION_NON_VOLATILE, KEY_WRITE, nullptr, + &inproc_key, nullptr); + hr = HResultFromWin32Error(status); + if (FAILED(hr)) { + return hr; + } + + hr = SetStringValue(inproc_key, nullptr, module_path); + if (SUCCEEDED(hr)) { + hr = SetStringValue(inproc_key, L"ThreadingModel", L"Apartment"); + } + RegCloseKey(inproc_key); + return hr; +} + +HRESULT DeleteComServerRegistration() { + const LSTATUS status = RegDeleteTreeW(HKEY_CURRENT_USER, kClsidKey); + if (status == ERROR_FILE_NOT_FOUND) { + return S_OK; + } + return HResultFromWin32Error(status); +} + +class ScopedComInit { + public: + ScopedComInit() : hr_(CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED)) {} + ScopedComInit(const ScopedComInit&) = delete; + ScopedComInit& operator=(const ScopedComInit&) = delete; + ~ScopedComInit() { + if (hr_ == S_OK || hr_ == S_FALSE) { + CoUninitialize(); + } + } + + HRESULT hr() const { + return hr_ == RPC_E_CHANGED_MODE ? S_OK : hr_; + } + + private: + HRESULT hr_; +}; + +HRESULT CreateProfiles(ITfInputProcessorProfiles** profiles) { + return CoCreateInstance(CLSID_TF_InputProcessorProfiles, nullptr, + CLSCTX_INPROC_SERVER, + IID_ITfInputProcessorProfiles, + reinterpret_cast(profiles)); +} + +HRESULT RegisterLanguageProfile() { + ScopedComInit com; + HRESULT hr = com.hr(); + if (FAILED(hr)) { + return hr; + } + + ITfInputProcessorProfiles* profiles = nullptr; + hr = CreateProfiles(&profiles); + if (FAILED(hr)) { + return hr; + } + + hr = profiles->Register(CLSID_OpenLessTextService); + if (SUCCEEDED(hr)) { + hr = profiles->AddLanguageProfile( + CLSID_OpenLessTextService, kOpenLessLangId, GUID_OpenLessProfile, + const_cast(kOpenLessImeName), + static_cast(ARRAYSIZE(kOpenLessImeName) - 1), nullptr, 0, 0); + } + if (SUCCEEDED(hr)) { + hr = profiles->EnableLanguageProfile(CLSID_OpenLessTextService, + kOpenLessLangId, GUID_OpenLessProfile, + TRUE); + } + + profiles->Release(); + return hr; +} + +HRESULT UnregisterLanguageProfile() { + ScopedComInit com; + HRESULT hr = com.hr(); + if (FAILED(hr)) { + return hr; + } + + ITfInputProcessorProfiles* profiles = nullptr; + hr = CreateProfiles(&profiles); + if (FAILED(hr)) { + return hr; + } + + hr = profiles->Unregister(CLSID_OpenLessTextService); + profiles->Release(); + return hr; +} + +} // namespace + +HRESULT RegisterOpenLessTextService(HINSTANCE module) { + if (module == nullptr) { + return E_INVALIDARG; + } + + HRESULT hr = RegisterComServer(module); + if (FAILED(hr)) { + return hr; + } + + hr = RegisterLanguageProfile(); + if (FAILED(hr)) { + DeleteComServerRegistration(); + } + return hr; +} + +HRESULT UnregisterOpenLessTextService() { + const HRESULT profile_hr = UnregisterLanguageProfile(); + const HRESULT registry_hr = DeleteComServerRegistration(); + return FAILED(profile_hr) ? profile_hr : registry_hr; +} diff --git a/openless-all/app/windows-ime/src/registry.h b/openless-all/app/windows-ime/src/registry.h new file mode 100644 index 00000000..74e4ab8d --- /dev/null +++ b/openless-all/app/windows-ime/src/registry.h @@ -0,0 +1,6 @@ +#pragma once + +#include + +HRESULT RegisterOpenLessTextService(HINSTANCE module); +HRESULT UnregisterOpenLessTextService(); diff --git a/openless-all/app/windows-ime/src/resource.rc b/openless-all/app/windows-ime/src/resource.rc new file mode 100644 index 00000000..c27e46d2 --- /dev/null +++ b/openless-all/app/windows-ime/src/resource.rc @@ -0,0 +1,33 @@ +#include + +1 VERSIONINFO +FILEVERSION 0,1,0,0 +PRODUCTVERSION 0,1,0,0 +FILEFLAGSMASK 0x3fL +#ifdef _DEBUG +FILEFLAGS 0x1L +#else +FILEFLAGS 0x0L +#endif +FILEOS 0x40004L +FILETYPE 0x2L +FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904b0" + BEGIN + VALUE "CompanyName", "OpenLess" + VALUE "FileDescription", "OpenLess Voice Input TSF IME" + VALUE "FileVersion", "0.1.0.0" + VALUE "InternalName", "OpenLessIme.dll" + VALUE "OriginalFilename", "OpenLessIme.dll" + VALUE "ProductName", "OpenLess Voice Input" + VALUE "ProductVersion", "0.1.0.0" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1200 + END +END diff --git a/openless-all/app/windows-ime/src/text_service.cpp b/openless-all/app/windows-ime/src/text_service.cpp new file mode 100644 index 00000000..7a0caa79 --- /dev/null +++ b/openless-all/app/windows-ime/src/text_service.cpp @@ -0,0 +1,95 @@ +#include "text_service.h" + +extern LONG g_object_count; + +OpenLessTextService::OpenLessTextService() { + InterlockedIncrement(&g_object_count); +} + +OpenLessTextService::~OpenLessTextService() { + Deactivate(); + InterlockedDecrement(&g_object_count); +} + +STDMETHODIMP OpenLessTextService::QueryInterface(REFIID iid, void** object) { + if (object == nullptr) { + return E_POINTER; + } + *object = nullptr; + + if (iid == IID_IUnknown || iid == IID_ITfTextInputProcessor || + iid == IID_ITfTextInputProcessorEx) { + *object = static_cast(this); + AddRef(); + return S_OK; + } + + return E_NOINTERFACE; +} + +STDMETHODIMP_(ULONG) OpenLessTextService::AddRef() { + return static_cast(InterlockedIncrement(&ref_count_)); +} + +STDMETHODIMP_(ULONG) OpenLessTextService::Release() { + const ULONG count = static_cast(InterlockedDecrement(&ref_count_)); + if (count == 0) { + delete this; + } + return count; +} + +STDMETHODIMP OpenLessTextService::Activate(ITfThreadMgr* thread_mgr, + TfClientId client_id) { + return ActivateEx(thread_mgr, client_id, 0); +} + +STDMETHODIMP OpenLessTextService::ActivateEx(ITfThreadMgr* thread_mgr, + TfClientId client_id, + DWORD flags) { + UNREFERENCED_PARAMETER(flags); + + if (thread_mgr == nullptr) { + return E_INVALIDARG; + } + + Deactivate(); + + thread_mgr_ = thread_mgr; + thread_mgr_->AddRef(); + client_id_ = client_id; + + const HRESULT hr = StartIpcServer(); + if (FAILED(hr)) { + Deactivate(); + return hr; + } + + return S_OK; +} + +STDMETHODIMP OpenLessTextService::Deactivate() { + StopIpcServer(); + + if (thread_mgr_ != nullptr) { + thread_mgr_->Release(); + thread_mgr_ = nullptr; + } + client_id_ = TF_CLIENTID_NULL; + + return S_OK; +} + +HRESULT OpenLessTextService::SubmitTextFromPipe( + const std::wstring& session_id, + const std::wstring& text) { + UNREFERENCED_PARAMETER(session_id); + UNREFERENCED_PARAMETER(text); + return E_NOTIMPL; +} + +HRESULT OpenLessTextService::StartIpcServer() { + return S_OK; +} + +void OpenLessTextService::StopIpcServer() {} diff --git a/openless-all/app/windows-ime/src/text_service.h b/openless-all/app/windows-ime/src/text_service.h new file mode 100644 index 00000000..ac1d8576 --- /dev/null +++ b/openless-all/app/windows-ime/src/text_service.h @@ -0,0 +1,34 @@ +#pragma once + +#include +#include +#include + +class OpenLessTextService final : public ITfTextInputProcessorEx { + public: + OpenLessTextService(); + OpenLessTextService(const OpenLessTextService&) = delete; + OpenLessTextService& operator=(const OpenLessTextService&) = delete; + ~OpenLessTextService() override; + + STDMETHODIMP QueryInterface(REFIID iid, void** object) override; + STDMETHODIMP_(ULONG) AddRef() override; + STDMETHODIMP_(ULONG) Release() override; + + STDMETHODIMP Activate(ITfThreadMgr* thread_mgr, TfClientId client_id) override; + STDMETHODIMP Deactivate() override; + STDMETHODIMP ActivateEx(ITfThreadMgr* thread_mgr, + TfClientId client_id, + DWORD flags) override; + + HRESULT SubmitTextFromPipe(const std::wstring& session_id, + const std::wstring& text); + + private: + HRESULT StartIpcServer(); + void StopIpcServer(); + + LONG ref_count_ = 1; + ITfThreadMgr* thread_mgr_ = nullptr; + TfClientId client_id_ = TF_CLIENTID_NULL; +};