2507C++,介绍名字对象
原文
名字对象
简介
组件对象模型
(COM
)的基础由两个原则
组成,这里:
1,客户针对接口编程
,而不是针对具体类
编程.
2,位置透明
,客户无需知道实际对象
在哪(进程中,进程外,另一台机器
).
虽然原则上很简单
.本文,我想介绍COM
的一个叫怪物(名字对象,Moniker,名字)
的扩展方面.
名字对象
的想法是,根据串名
而不是某些自定义
机制来识别和定位特定对象
.窗口
提供了一些名字对象
的实现,其中大部分
与对象链接和嵌入
(OLE
)相关,最主要是办公
应用.
如,当在字
文档中嵌入Excel
图表链接时,可用项
名字对象,来使用名字对象
机制和所涉及的特定名字对象
所理解的特定格式
的串来指向该特定图表
.
这也表明可组合怪物
,事实确实如此.如,可通过转到特定工作表
,特定区间
和特定单元格
来定位某些Excel
文档中的单元格
,每个(表/区间/单元格
)都可通过一个名字(怪物)
指向,当链接
在一起时,可找到期望对象
.
从现有名字对象
(怪物
类)实现的最简单
示例开始.可用此名字对象
来替换创建操作
.下面是一例,该示例使用调用CoCreateInstance
的"标准"
机制创建COM
对象:
#include <shlobjidl.h>//...
CComPtr<IShellWindows> spShell;
auto hr = spShell.CoCreateInstance(__uuidof(ShellWindows));
我使用ATL
灵针(#include<atlcomcli.h>
或<atlbase.h>
).我使用的接口和类
只是一例,任何标准COM
类都可工作.
CoCreateInstance,方法
实际的CoCreateInstance
.为了更清楚,下面是不使用灵针提供的助手
的CoCreateInstance
调用:
CComPtr<IShellWindows> spShell;
auto hr = ::CoCreateInstance(__uuidof(ShellWindows), nullptr,CLSCTX_ALL, __uuidof(IShellWindows),reinterpret_cast<void**>(&spShell));
CoCreateInstance
自身是用来调用CoGetClassObject
来取类工厂,请求标准IClassFactory
接口,然后在其上面调用CreateInstance
的美化的包装器
:
这里
CComPtr<IClassFactory> spCF;
auto hr = ::CoGetClassObject(__uuidof(ShellWindows),CLSCTX_ALL, nullptr, __uuidof(IClassFactory),reinterpret_cast<void**>(&spCF));
if (SUCCEEDED(hr)) {CComPtr<IShellWindows> spShell;hr = spCF->CreateInstance(nullptr, __uuidof(IShellWindows),reinterpret_cast<void**>(&spShell));if (SUCCEEDED(hr)) {//使用`spShell`}
}
下面是类
名字对象的用武之地
:可用如下串直接取类工厂
:
CComPtr<IClassFactory> spCF;
BIND_OPTS opts{ sizeof(opts) };
auto hr = ::CoGetObject(L"clsid:9BA05972F6A8-11CFA442-00A0C90A8F39",&opts, __uuidof(IClassFactory),reinterpret_cast<void**>(&spCF));
使用CoGetObject
是C++
中基于名字对象
查找对象的最方便方法
.名字对象名
是提供给CoGetObject
的串.它以某种ProgID
开头,再加上冒号.
串的其余部分
将由场景解释.有了类工厂
,代码就可像前例
一样使用IClassFactory::CreateInstance
.
这里
它工作原理
与COM
一样,需要注册表
.如果打开RegEdit
或TotalRegistry
并浏览到HKYE_CLASSES_ROOT
,则ProgID
都在那里.
这里
其中之一是"clsid"
,是,有点奇怪,但怪物系统
的入口是ProgID
.每个ProgID
都应有指向名字对象
的类ID
的类标
子键.
所以这里,键
是HKCR\CLSID\CLSID
!
当然,其他名字不同(不是类标
).如果按右边的类标
到普通COMCLSID
注册的位置(HKCR\CLSID
).
InProcServer32
子键指向实现COM基础结构
的Combase.dll
.
此时,知道如何发现类名
了,但仍不清楚怪物是什么及它在哪里
.
如前,CoGetObject
是从名字对象
取对象的最简单
方法,因为它隐藏了名字对象
自身的细节.CoGetObject
是调用COM
名字对象名字空间
的实际入口的MkParseDisplayName
的快捷键
.
这里
这里
下面是通过名字取类怪物
的完整方法:
CComPtr<IMoniker> spClsMoniker;
CComPtr<IBindCtx> spBindCtx;
::CreateBindCtx(0, &spBindCtx);
ULONG eaten;
CComPtr<IClassFactory> spCF;
auto hr = ::MkParseDisplayName(spBindCtx,L"clsid:9BA05972F6A8-11CFA442-00A0C90A8F39",&eaten, &spClsMoniker);
if (SUCCEEDED(hr)) {spClsMoniker->BindToObject(spBindCtx, nullptr,__uuidof(IClassFactory), reinterpret_cast<void**>(&spCF));
MkParseDisplayName
取"显示名"串
,并试根据注册表中的信息
找到名字对象
.
绑定环境
是一个一般可包含一组,名字对象
可用来自定义
其解释显示名
的方式的任意属性
的助手对象
.名字对象
类不使用任何属性
,但即使其中没有感兴趣的数据
,仍需要提供对象.
如果成功,MkParseDisplayName
将返回名字对象
接口指针,实现所有名字对象
必须实现的IMoniker
接口.IMoniker
是一个(不包括IUnknown
)有20
个方法的有点可怕的接口
.
这里
幸好,并非所有方法都必须实现.很快就可自定义它.
IMoniker
中的主要方法
是解释显示名
,且如果可能,并返回客户试查找的真实
对象的BindToObject
.客户提供期望目标对象
实现的接口,如果是名字对象
类,则为IClassFactory
.
如果可直接使用
普通类工厂创建期望对象
,名字对象
类的意义何在.该名字对象
的一个优点是涉及到串时,它允许某种"延迟绑定
",并允许其他语言
(如脚本语言)间接创建COM
对象.
如,VBScript
提供调用CoGetObject
的GetObject
函数.
实现名字对象
仍缺少一些细节,如如何创建名字对象
自身?为此,实现自己的名字对象
.叫它进程名字对象
,其目的是查找允许同窗口进程对象
工作的COM进程对象
.
下面一例,客户根据其PID
查找进程对象
,然后显示其可执行路径
:
BIND_OPTS opts{ sizeof(opts) };
CComPtr<IWinProcess> spProcess;
auto hr = ::CoGetObject(L"process:3284",&opts, __uuidof(IWinProcess),reinterpret_cast<void**>(&spProcess));
if (SUCCEEDED(hr)) {CComBSTR path;if (S_OK == spProcess->get_ImagePath(&path)) {printf("Image path: %ws\n", path.m_str);}
}
IWinProcess
是进程对象
实现的接口,但无需知道其类标
(实际上,它没有,并且是由名字对象
私下创建)."prcess:3284"
显示名按名字对象名
标识"进程"
串,即HKCR
下必须有叫"进程"
的子键,才能使其可工作.
在"进程"
键下必须有名字
的类标
.
必须像所有COM
类一样,正常注册进程名字对象
的类标
.把冒号后面的文本
传递给合理
解释该名字对象
的名字对象
.
这里,它应该是现有进程的PID
.
来看看实现进程名字对象
期望的主要步骤
.技术上讲,我在VS
中创建了一个ATLDLL
项目(也可是EXE
),然后添加了一个"ATL简单对象
"类模板来取ATL
模板提供的样板代码.
只需要实现IMoniker
,不需要一些自定义
接口.这是类的布局:
class ATL_NO_VTABLE CProcessMoniker :public CComObjectRootEx<CComMultiThreadModel>,public CComCoClass<CProcessMoniker, &CLSID_ProcessMoniker>,public IMoniker {
public:DECLARE_REGISTRY_RESOURCEID(106)DECLARE_CLASSFACTORY_EX(CMonikerClassFactory)BEGIN_COM_MAP(CProcessMoniker)COM_INTERFACE_ENTRY(IMoniker)END_COM_MAP()DECLARE_PROTECT_FINAL_CONSTRUCT()HRESULT FinalConstruct() {return S_OK;}void FinalRelease() {}
public://通过`IMoniker`继承HRESULT __stdcall GetClassID(CLSID* pClassID) override;HRESULT __stdcall IsDirty(void) override;HRESULT __stdcall Load(IStream* pStm) override;HRESULT __stdcall Save(IStream* pStm, BOOL fClearDirty) override;HRESULT __stdcall GetSizeMax(ULARGE_INTEGER* pcbSize) override;HRESULT __stdcall BindToObject(IBindCtx* pbc, IMoniker* pmkToLeft, REFIID riidResult, void** ppvResult) override;//其他`IMoniker`方法...std::wstring m_DisplayName;
};
OBJECT_ENTRY_AUTO(__uuidof(ProcessMoniker), CProcessMoniker)
熟悉ATL
向导生成的典型代码
的用户,可能会注意到与标准模板
的一个重要区别:类工厂.
事实表明,当调用MkParseDisplayName
(或其CoGetObject
包装器)的客户调用时,不是由IClassFactory
创建的名字对象
,而是必须实现IParseDisplayName
接口.
因此使用DECLARE_CLASSFACTORY_EX(CMonikerClassFactory)
来指示ATL
使用必须实现的自定义
类工厂.
MkParseDisplayName
找注册表中的怪物DLL
加载怪物DLL
调用(Dll取类对象),请求(I解析显示名)
调用(I解析显示名::解析显示名)
给客户返回怪物
开始前,实现"主"
方法BindToObject
.必须假设m_DisplayName
成员已拥有创建名字的类工厂
提供的进程ID
.首先,按数字转换显示名:
这里
HRESULT __stdcall CProcessMoniker::BindToObject(IBindCtx* pbc, IMoniker* pmkToLeft, REFIID riidResult, void** ppvResult) {auto pid = std::stoul(m_DisplayName);
接着,试打开该进程的句柄
:
auto hProcess = ::OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION,FALSE, pid);
if (!hProcess)return HRESULT_FROM_WIN32(::GetLastError());
如果失败,只需返回失败的HRESULT
,即可完成.如果成功,可创建WinProcess
对象,传递句柄
并返回(如果支持)客户请求的接口
:
CComObject<CWinProcess>* pProcess;auto hr = pProcess->CreateInstance(&pProcess);pProcess->SetHandle(hProcess);pProcess->AddRef();hr = pProcess->QueryInterface(riidResult, ppvResult);pProcess->Release();return hr;
}
通过CComObject<>
内部创建的对象.还未注册WinProcess,COM
类,这只是一个选择问题
.我决定,WinProcess
对象只能通过ProcessMoniker
取得.
调用AddRef/Release
,可能令人费解.
但创建CComObject<>
对象时,对象的引用计数
为零.
然后,调用AddRef
的给它递增为1
.接着,如果QueryInterface
成功调用,则引用计数
将递增为2
.然后,释放
调用把它递减为1
,因为这是在给客户返回对象
时的正确
计数.
但是,如果调用QI
失败,则引用
计数将保持在1
,而释放
调用将析构对象
!比调用删更优雅.
SetHandle
是(在IWinProcess
接口外部的)CWinProcess
中的把句柄传递给对象
的一个函数.
WinProcess,COM
类很简单,因此我创建了一个最小的类
,如下:
class ATL_NO_VTABLE CWinProcess :public CComObjectRootEx<CComMultiThreadModel>,public IDispatchImpl<IWinProcess> {
public:DECLARE_NO_REGISTRY()BEGIN_COM_MAP(CWinProcess)COM_INTERFACE_ENTRY(IWinProcess)COM_INTERFACE_ENTRY(IDispatch)COM_INTERFACE_ENTRY_AGGREGATE(IID_IMarshal, m_pUnkMarshaler.p)END_COM_MAP()DECLARE_PROTECT_FINAL_CONSTRUCT()DECLARE_GET_CONTROLLING_UNKNOWN()HRESULT FinalConstruct() {return CoCreateFreeThreadedMarshaler(GetControllingUnknown(), &m_pUnkMarshaler.p);}void FinalRelease() {m_pUnkMarshaler.Release();if (m_hProcess)::CloseHandle(m_hProcess);}void SetHandle(HANDLE hProcess);
private:HANDLE m_hProcess{ nullptr };CComPtr<IUnknown> m_pUnkMarshaler;//通过`IWinProcess`继承HRESULT get_Id(DWORD* pId);HRESULT get_ImagePath(BSTR* path);HRESULT Terminate(DWORD exitCode);
};
两个属性
和一个方法
如下:
void CWinProcess::SetHandle(HANDLE hProcess) {m_hProcess = hProcess;
}
HRESULT CWinProcess::get_Id(DWORD* pId) {ATLASSERT(m_hProcess);return *pId = ::GetProcessId(m_hProcess), S_OK;
}
HRESULT CWinProcess::get_ImagePath(BSTR* pPath) {WCHAR path[MAX_PATH];DWORD size = _countof(path);if (::QueryFullProcessImageName(m_hProcess, 0, path, &size))return CComBSTR(path).CopyTo(pPath);return HRESULT_FROM_WIN32(::GetLastError());
}
HRESULT CWinProcess::Terminate(DWORD exitCode) {HANDLE hKill;if (::DuplicateHandle(::GetCurrentProcess(), m_hProcess,::GetCurrentProcess(), &hKill, PROCESS_TERMINATE, FALSE, 0)) {auto success = ::TerminateProcess(hKill, exitCode);auto error = ::GetLastError();::CloseHandle(hKill);return success? S_OK : HRESULT_FROM_WIN32(error);}return HRESULT_FROM_WIN32(::GetLastError());
}
上面使用的API
相当简单,也有完整的文档.
最后是怪物的类工厂
:
class ATL_NO_VTABLE CMonikerClassFactory :public ATL::CComObjectRootEx<ATL::CComMultiThreadModel>,public IParseDisplayName {
public:BEGIN_COM_MAP(CMonikerClassFactory)COM_INTERFACE_ENTRY(IParseDisplayName)END_COM_MAP()//通过`IParseDisplayName`继承HRESULT __stdcall ParseDisplayName(IBindCtx* pbc, LPOLESTR pszDisplayName, ULONG* pchEaten, IMoniker** ppmkOut) override;
};
只需一个实现方法
:
HRESULT __stdcall CMonikerClassFactory::ParseDisplayName(IBindCtx* pbc, LPOLESTR pszDisplayName,ULONG* pchEaten, IMoniker** ppmkOut) {auto colon = wcschr(pszDisplayName, L':');ATLASSERT(colon);if (colon == nullptr)return E_INVALIDARG;//简单,假设已使用所有显示名*pchEaten = (ULONG)wcslen(pszDisplayName);CComObject<CProcessMoniker>* pMon;auto hr = pMon->CreateInstance(&pMon);if (FAILED(hr))return hr;//提供进程`ID`pMon->m_DisplayName = colon + 1;pMon->AddRef();hr = pMon->QueryInterface(ppmkOut);pMon->Release();return hr;
}
首先,搜索冒号,因为显示名
类似"process:xxxx"
.在使用CComObject<>
创建的怪物
中保存"xxxx"
部分,类似前面的CWinProcess
.
pchEaten
值报告使用了多少个符,名字对象
工厂应该根据它所理解
的来解析,因为名字对象
组合可能也有效.
最后,必须注册怪物
.下面是ProcessMoniker.rgs
,添加的下半部分
,来将"进程"
的ProgId/名字对象名
连接到进程名字对象
的类标
:
HKCR
{NoRemove CLSID{ForceRemove {6ea3a80e-2936-43be-8725-2e95896da9a4} = s 'ProcessMoniker class'{InprocServer32 = s '%MODULE%'{val ThreadingModel = s 'Both'}TypeLib = s '{97a86fc5ffef-4e80-88a0fa3d1b438075}'Version = s '1.0'}}process = s 'Process Moniker Class'{CLSID = s '{6ea3a80e-2936-43be-8725-2e95896da9a4}'}
}
就是这样.下面是终止给定其ID
的进程的客户示例:
void Kill(DWORD pid) {std::wstring displayName(L"process:");displayName += std::to_wstring(pid);BIND_OPTS opts{ sizeof(opts) };CComPtr<IWinProcess> spProcess;auto hr = ::CoGetObject(displayName.c_str(), &opts,__uuidof(IWinProcess), reinterpret_cast<void**>(&spProcess));if (SUCCEEDED(hr)) {auto hr = spProcess->Terminate(1);if (SUCCEEDED(hr))printf("Process %u terminated.\n", pid);elseprintf("Error terminating process: hr=0x%X\n", hr);}
}
所有代码都可在该Github
仓库中找到:zodiacon/MonikerFun
:在此.
下面是VBScript
示例(有效,因为WinProcess
实现了IDispatch
):
set process = GetObject("process:25520")
MsgBox process.ImagePath
.NET
或强壳
怎么样?这是强壳
:
PS> $p = [System.Runtime.InteropServices.Marshal]::BindToMoniker("process:25520")
PS> $p | GetMemberTypeName: System.__ComObject#{3ab0471f-2635-429d-95e9f2baede2859e}
Name MemberType Definition
,, ,,,,, ,,,,,
Terminate Method void Terminate (uint)
Id Property uint Id () {get}
ImagePath Property string ImagePath () {get}PS> $p.ImagePath
C:\Windows\System32\notepad.exe
DisplayWindows
函数,仅显示使用IShellWindows
取的资管窗口名
:
void DisplayWindows(IShellWindows* pShell) {long count = 0;pShell->get_Count(&count);for (long i = 0; i < count; i++) {CComPtr<IDispatch> spDisp;pShell->Item(CComVariant(i), &spDisp);CComQIPtr<IWebBrowserApp> spWin(spDisp);if (spWin) {CComBSTR name;spWin->get_LocationName(&name);printf("Name: %ws\n", name.m_str);}}
}