feat: scaffold OpenLess TSF IME DLL

This commit is contained in:
millionart committed 2026-05-01 20:20:25 +08:00
1 parent 39d766d44d
commit e950d1ddc7
11 files changed
+626

No files matched your search

@@ -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
@@ -0,0 +1,91 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Debug|x64">
<Configuration>Debug</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|x64">
<Configuration>Release</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
</ItemGroup>
<PropertyGroup Label="Globals">
<VCProjectVersion>17.0</VCProjectVersion>
<Keyword>Win32Proj</Keyword>
<ProjectGuid>{F45287CB-1F31-4D7C-9B62-5748C2F8C9B0}</ProjectGuid>
<RootNamespace>OpenLessIme</RootNamespace>
<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<ImportGroup Label="ExtensionSettings" />
<ImportGroup Label="Shared" />
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<PropertyGroup Label="UserMacros" />
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<ClCompile>
<WarningLevel>Level4</WarningLevel>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>WIN32;_WINDOWS;_USRDLL;OPENLESSIME_EXPORTS;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<LanguageStandard>stdcpp17</LanguageStandard>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<AdditionalDependencies>msctf.lib;ole32.lib;uuid.lib;advapi32.lib;%(AdditionalDependencies)</AdditionalDependencies>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<ClCompile>
<WarningLevel>Level4</WarningLevel>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>WIN32;NDEBUG;_WINDOWS;_USRDLL;OPENLESSIME_EXPORTS;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<LanguageStandard>stdcpp17</LanguageStandard>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<EnableCOMDATFolding>true</EnableCOMDATFolding>
<OptimizeReferences>true</OptimizeReferences>
<AdditionalDependencies>msctf.lib;ole32.lib;uuid.lib;advapi32.lib;%(AdditionalDependencies)</AdditionalDependencies>
</Link>
</ItemDefinitionGroup>
<ItemGroup>
<ClCompile Include="src\class_factory.cpp" />
<ClCompile Include="src\dllmain.cpp" />
<ClCompile Include="src\registry.cpp" />
<ClCompile Include="src\text_service.cpp" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="src\class_factory.h" />
<ClInclude Include="src\guids.h" />
<ClInclude Include="src\registry.h" />
<ClInclude Include="src\text_service.h" />
</ItemGroup>
<ItemGroup>
<ResourceCompile Include="src\resource.rc" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<ImportGroup Label="ExtensionTargets" />
</Project>
@@ -0,0 +1,75 @@
#include "class_factory.h"
#include <new>
#include <windows.h>
#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<IClassFactory*>(this);
AddRef();
return S_OK;
}
return E_NOINTERFACE;
}
STDMETHODIMP_(ULONG) OpenLessClassFactory::AddRef() {
return static_cast<ULONG>(InterlockedIncrement(&ref_count_));
}
STDMETHODIMP_(ULONG) OpenLessClassFactory::Release() {
const ULONG count = static_cast<ULONG>(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;
}
@@ -0,0 +1,20 @@
#pragma once
#include <unknwn.h>
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;
};
@@ -0,0 +1,54 @@
#include <windows.h>
#include <new>
#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();
}
+21
View File
@@ -0,0 +1,21 @@
#pragma once
#include <msctf.h>
#include <windows.h>
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;
@@ -0,0 +1,176 @@
#include "registry.h"
#include <msctf.h>
#include <strsafe.h>
#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<DWORD>((wcslen(value) + 1) * sizeof(wchar_t));
return HResultFromWin32Error(
RegSetValueExW(key, name, 0, REG_SZ,
reinterpret_cast<const BYTE*>(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<void**>(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<wchar_t*>(kOpenLessImeName),
static_cast<ULONG>(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;
}
@@ -0,0 +1,6 @@
#pragma once
#include <windows.h>
HRESULT RegisterOpenLessTextService(HINSTANCE module);
HRESULT UnregisterOpenLessTextService();
@@ -0,0 +1,33 @@
#include <winver.h>
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
@@ -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<ITfTextInputProcessorEx*>(this);
AddRef();
return S_OK;
}
return E_NOINTERFACE;
}
STDMETHODIMP_(ULONG) OpenLessTextService::AddRef() {
return static_cast<ULONG>(InterlockedIncrement(&ref_count_));
}
STDMETHODIMP_(ULONG) OpenLessTextService::Release() {
const ULONG count = static_cast<ULONG>(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() {}
@@ -0,0 +1,34 @@
#pragma once
#include <msctf.h>
#include <string>
#include <windows.h>
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;
};