[{"data":1,"prerenderedAt":7678},["ShallowReactive",2],{"page-\u002Fcpp\u002Fcom组件与windows编程入门":3},{"id":4,"title":5,"body":6,"description":7671,"extension":7672,"meta":7673,"navigation":1140,"path":7674,"seo":7675,"stem":7676,"__hash__":7677},"content\u002Fcpp\u002FCOM组件与Windows编程入门.md","COM 组件与 Windows 编程（加长版）",{"type":7,"value":8,"toc":7497},"minimark",[9,13,38,41,46,137,139,143,154,198,213,215,222,227,246,256,271,278,317,342,390,414,416,420,500,506,551,568,570,577,581,619,623,642,646,765,779,781,785,789,807,811,835,842,886,890,913,915,922,929,969,977,986,1015,1018,1098,1112,1114,1121,1162,1165,1208,1210,1218,1226,1247,1286,1293,1323,1372,1380,1411,1418,1476,1478,1482,1509,1513,1571,1575,1585,1653,1678,1682,1986,2015,2019,2147,2161,2168,2200,2202,2206,2215,2234,2238,2247,2255,2268,2272,2296,2298,2302,2333,2335,2339,2369,2371,2375,2411,2413,2417,2445,2447,2454,2458,2466,2503,2518,2522,2591,2604,2613,2632,2643,2667,2706,2720,2722,2726,2730,2855,2859,2884,2888,2918,2928,2930,2938,2951,2971,3039,3055,3057,3061,3105,3107,3111,3125,3168,3171,3200,3214,3216,3220,3224,3254,3281,3288,3321,3328,3337,3353,3355,3366,3401,3403,3407,3444,3446,3450,3478,3480,3488,3537,3540,3596,3598,3602,3608,3659,3662,3730,3752,3754,3764,3786,3912,3917,3955,3982,3984,3991,4046,4048,4056,4096,4098,4102,4130,4155,4169,4193,4212,4232,4234,4238,4323,4325,4329,4384,4386,4390,4404,4406,4410,4414,4483,4487,4493,4512,4520,4549,4551,4558,4566,4627,4637,4639,4646,4669,4834,4882,4884,4888,4891,4949,4979,5002,5004,5011,5050,5052,5060,5069,5108,5125,5127,5131,5156,5181,5183,5189,5255,5265,5267,5271,5293,5311,5331,5360,5362,5366,5434,5436,5440,5510,5512,5516,5518,5526,5592,5630,5632,5639,5653,5706,5723,5725,5735,5768,5771,5806,5825,5827,5834,5846,5866,5893,5895,5902,5906,5944,5948,5971,5975,6015,6017,6028,6067,6069,6073,6107,6109,6117,6148,6150,6158,6189,6191,6197,6235,6237,6241,6276,6285,6287,6291,6313,6315,6319,6333,6337,6401,6405,6425,6429,6480,6486,6522,6528,6537,6559,6566,6574,6607,6611,6620,6622,6626,6634,6642,6651,6658,6686,6690,6738,6749,6751,6755,6796,6798,6806,6836,6838,6842,6887,6889,6893,6900,6946,6948,6952,6954,6962,7187,7212,7214,7218,7248,7272,7296,7325,7327,7331,7359,7375,7407,7431,7446,7467,7491,7493],[10,11,5],"h1",{"id":12},"com-组件与-windows-编程加长版",[14,15,16,17,21,22,25,26,29,30,33,34,37],"p",{},"面向：",[18,19,20],"strong",{},"几乎没写过 Win32 \u002F C++ 原生","，但听说过 ",[18,23,24],{},"COM、ActiveX、DLL 注册"," 的同学。本文尽量 ",[18,27,28],{},"拓宽视野","：不只围绕某一个录屏工程，而是把 ",[18,31,32],{},"桌面右键菜单、Office 自动化、Shell、拖拽"," 等典型 COM 场景串起来；代码片段以 ",[18,35,36],{},"示意 + 可对照 MSDN 补全"," 为主。",[39,40],"hr",{},[42,43,45],"h2",{"id":44},"〇读完本篇你能做什么判断","〇、读完本篇你能做什么判断？",[47,48,49,74,93,116,127],"ul",{},[50,51,52,53,59,60,69,70,73],"li",{},"看到 ",[18,54,55],{},[56,57,58],"code",{},"HRESULT","，知道要用 ",[18,61,62,65,66],{},[56,63,64],{},"SUCCEEDED","\u002F",[56,67,68],{},"FAILED","，并能粗略读出 ",[18,71,72],{},"Facility","（失败来自 Win32、COM 还是 RPC）。",[50,75,76,77,82,83,86,87,92],{},"理解 ",[18,78,79],{},[56,80,81],{},"IUnknown"," 三根支柱：",[18,84,85],{},"QueryInterface \u002F AddRef \u002F Release","，以及为何 ",[18,88,89],{},[56,90,91],{},"ComPtr"," 能救命。",[50,94,95,96,105,106,109,110,115],{},"知道 ",[18,97,98,101,102],{},[56,99,100],{},"CoInitializeEx"," \u002F ",[56,103,104],{},"CoCreateInstance"," 在干什么，",[18,107,108],{},"STA 与 MTA"," 何时会踩 ",[18,111,112],{},[56,113,114],{},"RPC_E_CHANGED_MODE","。",[50,117,118,119,122,123,126],{},"明白 ",[18,120,121],{},"壳扩展（Shell Extension）"," 如何通过 ",[18,124,125],{},"注册表 CLSID + Inproc DLL"," 接到资源管理器右键菜单。",[50,128,95,129,132,133,136],{},[18,130,131],{},"IDispatch \u002F BSTR"," 在 ",[18,134,135],{},"脚本与 Office 自动化"," 里扮演什么角色。",[39,138],{},[42,140,142],{"id":141},"一com-要解决什么问题","一、COM 要解决什么问题？",[14,144,145,146,149,150,153],{},"早年同一台 Windows 上要共存：",[18,147,148],{},"C++ 写的内核插件、VB 写的表单、脚本宿主、Office","。若每种语言都为每一种别的语言手写绑定，组合爆炸。",[18,151,152],{},"组件对象模型（COM）"," 的核心约定可以概括成：",[155,156,157,167,184],"ol",{},[50,158,159,162,163,166],{},[18,160,161],{},"二进制接口契约","：只规定 ",[18,164,165],{},"vtable 布局 + 调用约定","，不关心实现语言。",[50,168,169,172,173,176,177,180,181,115],{},[18,170,171],{},"全局唯一名字","：接口用 ",[18,174,175],{},"IID","，组件类用 ",[18,178,179],{},"CLSID","，都是 ",[18,182,183],{},"GUID",[50,185,186,189,190,193,194,115],{},[18,187,188],{},"统一生命周期","：对象何时销毁由 ",[18,191,192],{},"引用计数"," 决定；所有接口继承 ",[18,195,196],{},[56,197,81],{},[14,199,200,201,204,205,208,209,212],{},"因此：",[18,202,203],{},"COM ≠「微软专属的 C++ 语法」","，而是一套 ",[18,206,207],{},"操作系统级的组件约定","。你今天接触的 ",[18,210,211],{},"WASAPI、DirectX、Media Foundation、Shell、Office"," 大量 API 都是 COM 接口。",[39,214],{},[42,216,218,219,221],{"id":217},"二接口vtableiunknown务必建立心智图像","二、接口、vtable、",[56,220,81],{},"（务必建立心智图像）",[223,224,226],"h3",{"id":225},"_21-虚函数表vtable","2.1 虚函数表（vtable）",[14,228,229,230,235,236,239,240,245],{},"一个 COM 接口在 C++ 里常写成 ",[18,231,232],{},[56,233,234],{},"struct IXxx : IUnknown { virtual HRESULT Foo() = 0; };","。编译器会为对象生成 ",[18,237,238],{},"vtable 指针","：对象内存layout开头往往是 ",[18,241,242],{},[56,243,244],{},"vtable*","，指向函数指针数组。",[247,248,253],"pre",{"className":249,"code":251,"language":252},[250],"language-text","对象实例内存（示意）           vtable 指向的函数槽位\n+------------------+          +------------------+\n| vtable* --------+|---------> | slot0: QueryInterface\n| 成员字段 ...      |          | slot1: AddRef\n+------------------+          | slot2: Release\n                              | slot3: Foo ...\n                              +------------------+\n","text",[56,254,251],{"__ignoreMap":255},"",[14,257,258,261,262,266,267,270],{},[18,259,260],{},"QueryInterface","：同一个物理对象，若实现了多个接口，通常有多张 vtable（多重继承），或共享 ",[18,263,264],{},[56,265,81],{}," 实现。",[18,268,269],{},"客户端只通过接口指针调用","，不必知道 C++ 类具体怎么继承。",[223,272,274,275,277],{"id":273},"_22-iunknown-三方法所有-com-接口的根","2.2 ",[56,276,81],{}," 三方法（所有 COM 接口的根）",[247,279,283],{"className":280,"code":281,"language":282,"meta":255,"style":255},"language-cpp shiki shiki-themes github-light github-dark","struct IUnknown {\n    HRESULT QueryInterface(REFIID riid, void** ppvObject);\n    ULONG AddRef();\n    ULONG Release();\n};\n","cpp",[56,284,285,293,299,305,311],{"__ignoreMap":255},[286,287,290],"span",{"class":288,"line":289},"line",1,[286,291,292],{},"struct IUnknown {\n",[286,294,296],{"class":288,"line":295},2,[286,297,298],{},"    HRESULT QueryInterface(REFIID riid, void** ppvObject);\n",[286,300,302],{"class":288,"line":301},3,[286,303,304],{},"    ULONG AddRef();\n",[286,306,308],{"class":288,"line":307},4,[286,309,310],{},"    ULONG Release();\n",[286,312,314],{"class":288,"line":313},5,[286,315,316],{},"};\n",[14,318,319,321,322,324,325,330,331,336,337,115],{},[18,320,260],{},"：输入 ",[18,323,175],{},"，若对象支持该接口，则 ",[18,326,327],{},[56,328,329],{},"*ppvObject = 指向合法接口指针","，并 ",[18,332,333],{},[56,334,335],{},"AddRef","（规范要求）；不支持返回 ",[18,338,339],{},[56,340,341],{},"E_NOINTERFACE",[247,343,345],{"className":280,"code":344,"language":282,"meta":255,"style":255},"void ExampleQuery(IAudioClient* pClient)\n{\n    IUnknown* pUnk = nullptr;\n    if (FAILED(pClient->QueryInterface(IID_IUnknown, reinterpret_cast\u003Cvoid**>(&pUnk))))\n        return;\n    \u002F\u002F 通常 QI 同对象的 IUnknown 会得到同一逻辑实体；调试引用计数时会用到\n    pUnk->Release();\n}\n",[56,346,347,352,357,362,367,372,378,384],{"__ignoreMap":255},[286,348,349],{"class":288,"line":289},[286,350,351],{},"void ExampleQuery(IAudioClient* pClient)\n",[286,353,354],{"class":288,"line":295},[286,355,356],{},"{\n",[286,358,359],{"class":288,"line":301},[286,360,361],{},"    IUnknown* pUnk = nullptr;\n",[286,363,364],{"class":288,"line":307},[286,365,366],{},"    if (FAILED(pClient->QueryInterface(IID_IUnknown, reinterpret_cast\u003Cvoid**>(&pUnk))))\n",[286,368,369],{"class":288,"line":313},[286,370,371],{},"        return;\n",[286,373,375],{"class":288,"line":374},6,[286,376,377],{},"    \u002F\u002F 通常 QI 同对象的 IUnknown 会得到同一逻辑实体；调试引用计数时会用到\n",[286,379,381],{"class":288,"line":380},7,[286,382,383],{},"    pUnk->Release();\n",[286,385,387],{"class":288,"line":386},8,[286,388,389],{},"}\n",[14,391,392,395,396,399,400,402,403,115,406,409,410,413],{},[18,393,394],{},"AddRef \u002F Release","：跨 DLL 边界传递指针时，文档常会写「接收方 AddRef」「调用完 Release」。",[18,397,398],{},"规则","：谁延长生命周期谁 ",[18,401,335],{},"；配对 ",[18,404,405],{},"Release",[18,407,408],{},"裸指针忘记 Release"," → 泄漏；",[18,411,412],{},"二次 Release"," → 崩溃。",[39,415],{},[42,417,419],{"id":418},"三guidclsidprogid注册表扫一眼地图","三、GUID、CLSID、ProgID、注册表（扫一眼地图）",[421,422,423,436],"table",{},[424,425,426],"thead",{},[427,428,429,433],"tr",{},[430,431,432],"th",{},"名称",[430,434,435],{},"含义",[437,438,439,454,468,478],"tbody",{},[427,440,441,446],{},[442,443,444],"td",{},[18,445,175],{},[442,447,448,449],{},"接口 ID，例如 ",[18,450,451],{},[56,452,453],{},"IID_IUnknown",[427,455,456,460],{},[442,457,458],{},[18,459,179],{},[442,461,462,463],{},"组件类 ID，",[18,464,465,467],{},[56,466,104],{}," 第一个参数",[427,469,470,475],{},[442,471,472],{},[18,473,474],{},"LIBID",[442,476,477],{},"类型库 ID",[427,479,480,485],{},[442,481,482],{},[18,483,484],{},"ProgID",[442,486,487,488,493,494,499],{},"人类可读字符串，如 ",[18,489,490],{},[56,491,492],{},"Excel.Application","，可用 ",[18,495,496],{},[56,497,498],{},"CLSIDFromProgID"," 换 CLSID",[14,501,502,505],{},[18,503,504],{},"注册表","（概念路径）：",[47,507,508,516,543],{},[50,509,510,515],{},[18,511,512],{},[56,513,514],{},"HKCR\\CLSID\\{xxxxxxxx-xxxx-...}","：某个组件类的配置。",[50,517,518,523,524,529,530,101,533,101,536,101,539,542],{},[18,519,520],{},[56,521,522],{},"\u003CCLSID>\\InprocServer32","：进程内 DLL 路径 + ",[18,525,526],{},[56,527,528],{},"ThreadingModel","（",[56,531,532],{},"Apartment",[56,534,535],{},"Free",[56,537,538],{},"Both",[56,540,541],{},"Neutral","）。",[50,544,545,550],{},[18,546,547],{},[56,548,549],{},"\u003CCLSID>\\LocalServer32","：本地 EXE 服务器路径。",[14,552,553,558,559,562,563,115],{},[18,554,555],{},[56,556,557],{},"CoCreateInstance(CLSID, ..., IID, ppv)"," 做的事：查注册表 → 加载 DLL 或启动 EXE → 通过 ",[18,560,561],{},"类对象（class object）"," 创建实例 → ",[18,564,565,567],{},[56,566,260],{}," 成你要的接口",[39,569],{},[42,571,573,574,576],{"id":572},"四hresult宁可当成带类型的错误码","四、",[56,575,58],{},"：宁可当成「带类型的错误码」",[223,578,580],{"id":579},"_41-判定方式再强调","4.1 判定方式（再强调）",[247,582,584],{"className":280,"code":583,"language":282,"meta":255,"style":255},"HRESULT hr = SomeMethod();\nif (FAILED(hr)) {\n    \u002F\u002F 失败路径：可用 FormatMessage、_com_error，或调试器看 hex\n}\nif (SUCCEEDED(hr)) {\n    \u002F\u002F 成功路径：包含 S_OK、S_FALSE（少数 API）\n}\n",[56,585,586,591,596,601,605,610,615],{"__ignoreMap":255},[286,587,588],{"class":288,"line":289},[286,589,590],{},"HRESULT hr = SomeMethod();\n",[286,592,593],{"class":288,"line":295},[286,594,595],{},"if (FAILED(hr)) {\n",[286,597,598],{"class":288,"line":301},[286,599,600],{},"    \u002F\u002F 失败路径：可用 FormatMessage、_com_error，或调试器看 hex\n",[286,602,603],{"class":288,"line":307},[286,604,389],{},[286,606,607],{"class":288,"line":313},[286,608,609],{},"if (SUCCEEDED(hr)) {\n",[286,611,612],{"class":288,"line":374},[286,613,614],{},"    \u002F\u002F 成功路径：包含 S_OK、S_FALSE（少数 API）\n",[286,616,617],{"class":288,"line":380},[286,618,389],{},[223,620,622],{"id":621},"_42-位域粗解入门够用","4.2 位域粗解（入门够用）",[14,624,625,626,629,630,634,635,637,638,641],{},"高位 ",[18,627,628],{},"bit31=1"," 通常表示失败（",[18,631,632],{},[56,633,68],{},"）；中间 ",[18,636,72],{}," 区分错误来源（COM、Win32、RPC 等）；低位 ",[18,639,640],{},"Code"," 是错误编号。",[223,643,645],{"id":644},"_43-常见-hresult混个脸熟","4.3 常见 HRESULT（混个脸熟）",[421,647,648,657],{},[424,649,650],{},[427,651,652,654],{},[430,653,58],{},[430,655,656],{},"含义（简述）",[437,658,659,671,683,694,706,718,730,742,753],{},[427,660,661,668],{},[442,662,663],{},[18,664,665],{},[56,666,667],{},"S_OK",[442,669,670],{},"成功",[427,672,673,680],{},[442,674,675],{},[18,676,677],{},[56,678,679],{},"S_FALSE",[442,681,682],{},"成功但布尔语义为「假」",[427,684,685,691],{},[442,686,687],{},[18,688,689],{},[56,690,341],{},[442,692,693],{},"QueryInterface 不支持该 IID",[427,695,696,703],{},[442,697,698],{},[18,699,700],{},[56,701,702],{},"E_POINTER",[442,704,705],{},"空指针参数",[427,707,708,715],{},[442,709,710],{},[18,711,712],{},[56,713,714],{},"E_OUTOFMEMORY",[442,716,717],{},"内存不足",[427,719,720,727],{},[442,721,722],{},[18,723,724],{},[56,725,726],{},"REGDB_E_CLASSNOTREG",[442,728,729],{},"CLSID 未注册",[427,731,732,739],{},[442,733,734],{},[18,735,736],{},[56,737,738],{},"CO_E_NOTINITIALIZED",[442,740,741],{},"线程未 CoInitialize",[427,743,744,750],{},[442,745,746],{},[18,747,748],{},[56,749,114],{},[442,751,752],{},"本线程 COM 已按另一种 apartment 初始化",[427,754,755,762],{},[442,756,757],{},[18,758,759],{},[56,760,761],{},"E_ACCESSDENIED",[442,763,764],{},"权限不足",[14,766,767,768,772,773,778],{},"把 ",[18,769,770],{},[56,771,58],{}," 当成 ",[18,774,775],{},[56,776,777],{},"int"," 随意算术运算，一般是错的。",[39,780],{},[42,782,784],{"id":783},"五线程与-apartmentsta-mta-neutral","五、线程与 Apartment（STA \u002F MTA \u002F Neutral）",[223,786,788],{"id":787},"_51-为什么要有-apartment","5.1 为什么要有 Apartment？",[14,790,791,792,795,796,798,799,802,803,806],{},"早期大量 COM 对象内部 ",[18,793,794],{},"不是线程安全的","，却又要被多线程桌面调用。",[18,797,532],{}," 大致是说：对象愿意运行在哪种线程语义里；跨 apartment 调用 COM 可能插入 ",[18,800,801],{},"proxy\u002Fstub"," 做 ",[18,804,805],{},"marshalling","（参数打包到另一线程）。",[223,808,810],{"id":809},"_52-两种最常念叨的模式","5.2 两种最常念叨的模式",[47,812,813,823],{},[50,814,815,818,819,822],{},[18,816,817],{},"STA（Single-Threaded Apartment）","：常与 ",[18,820,821],{},"UI 线程 + 消息泵"," 绑定；不少 Shell \u002F Office 相关对象偏 STA。",[50,824,825,828,829,834],{},[18,826,827],{},"MTA（Multi-Threaded Apartment）","：",[18,830,831],{},[56,832,833],{},"COINIT_MULTITHREADED","；并发更高，但对象自己要线程安全。",[223,836,838,839,841],{"id":837},"_53-coinitializeex-典型写法","5.3 ",[56,840,100],{}," 典型写法",[247,843,845],{"className":280,"code":844,"language":282,"meta":255,"style":255},"HRESULT hr = CoInitializeEx(nullptr, COINIT_MULTITHREADED);\nif (hr == RPC_E_CHANGED_MODE) {\n    \u002F\u002F 线程已被别的库初始化成 STA：要么接受现状，要么专门新建线程做 COM\n} else if (FAILED(hr)) {\n    return hr;\n}\n\u002F\u002F ...\nCoUninitialize(); \u002F\u002F 须与成功初始化的路径配对\n",[56,846,847,852,857,862,867,872,876,881],{"__ignoreMap":255},[286,848,849],{"class":288,"line":289},[286,850,851],{},"HRESULT hr = CoInitializeEx(nullptr, COINIT_MULTITHREADED);\n",[286,853,854],{"class":288,"line":295},[286,855,856],{},"if (hr == RPC_E_CHANGED_MODE) {\n",[286,858,859],{"class":288,"line":301},[286,860,861],{},"    \u002F\u002F 线程已被别的库初始化成 STA：要么接受现状，要么专门新建线程做 COM\n",[286,863,864],{"class":288,"line":307},[286,865,866],{},"} else if (FAILED(hr)) {\n",[286,868,869],{"class":288,"line":313},[286,870,871],{},"    return hr;\n",[286,873,874],{"class":288,"line":374},[286,875,389],{},[286,877,878],{"class":288,"line":380},[286,879,880],{},"\u002F\u002F ...\n",[286,882,883],{"class":288,"line":386},[286,884,885],{},"CoUninitialize(); \u002F\u002F 须与成功初始化的路径配对\n",[223,887,889],{"id":888},"_54-转换-封送入门一句","5.4 「转换 \u002F 封送」入门一句",[14,891,892,893,896,897,900,901,904,905,908,909,912],{},"跨 apartment、跨进程调用时，COM 可能把你的调用转到 ",[18,894,895],{},"代理对象","，参数通过 ",[18,898,899],{},"RPC \u002F LRPC"," 传到另一边的 ",[18,902,903],{},"stub","。初学者不必手写这些；只要记住：",[18,906,907],{},"接口指针不能盲目跨线程乱丢","，要看文档是否提供 ",[18,910,911],{},"Marshal API"," 或是否在 MTA 内安全。",[39,914],{},[42,916,918,919,921],{"id":917},"六创建对象的两条路cocreateinstance-与类工厂","六、创建对象的两条路：",[56,920,104],{}," 与类工厂",[223,923,925,926,928],{"id":924},"_61-cocreateinstance90-场景够用","6.1 ",[56,927,104],{},"（90% 场景够用）",[247,930,932],{"className":280,"code":931,"language":282,"meta":255,"style":255},"Microsoft::WRL::ComPtr\u003CIMessageFilter> filter; \u002F\u002F 举例任意 COM 接口\nHRESULT hr = CoCreateInstance(\n    CLSID_StdMessageFilter,   \u002F\u002F 哪个组件类\n    nullptr,                   \u002F\u002F pUnkOuter，聚合才用\n    CLSCTX_INPROC_SERVER,      \u002F\u002F 进程内 DLL；也可用 CLSCTX_LOCAL_SERVER 等\n    IID_IMessageFilter,\n    reinterpret_cast\u003Cvoid**>(filter.GetAddressOf()));\n",[56,933,934,939,944,949,954,959,964],{"__ignoreMap":255},[286,935,936],{"class":288,"line":289},[286,937,938],{},"Microsoft::WRL::ComPtr\u003CIMessageFilter> filter; \u002F\u002F 举例任意 COM 接口\n",[286,940,941],{"class":288,"line":295},[286,942,943],{},"HRESULT hr = CoCreateInstance(\n",[286,945,946],{"class":288,"line":301},[286,947,948],{},"    CLSID_StdMessageFilter,   \u002F\u002F 哪个组件类\n",[286,950,951],{"class":288,"line":307},[286,952,953],{},"    nullptr,                   \u002F\u002F pUnkOuter，聚合才用\n",[286,955,956],{"class":288,"line":313},[286,957,958],{},"    CLSCTX_INPROC_SERVER,      \u002F\u002F 进程内 DLL；也可用 CLSCTX_LOCAL_SERVER 等\n",[286,960,961],{"class":288,"line":374},[286,962,963],{},"    IID_IMessageFilter,\n",[286,965,966],{"class":288,"line":380},[286,967,968],{},"    reinterpret_cast\u003Cvoid**>(filter.GetAddressOf()));\n",[223,970,972,973,976],{"id":971},"_62-iclassfactory心里要有这张图","6.2 ",[56,974,975],{},"IClassFactory","（心里要有这张图）",[14,978,979,980,985],{},"当系统加载你的 DLL 时，不是凭空 ",[18,981,982],{},[56,983,984],{},"new","，而是：",[155,987,988,996],{},[50,989,990,995],{},[18,991,992],{},[56,993,994],{},"DllGetClassObject(CLSID, IID_IClassFactory, ...)"," 返回类工厂；",[50,997,998,1003,1004,1009,1010,1014],{},[18,999,1000],{},[56,1001,1002],{},"IClassFactory::CreateInstance"," 真正 ",[18,1005,1006,1008],{},[56,1007,984],{}," 你的对象"," 并 ",[18,1011,1012],{},[56,1013,260],{}," 成客户端要的接口。",[14,1016,1017],{},"示意：",[247,1019,1021],{"className":280,"code":1020,"language":282,"meta":255,"style":255},"\u002F\u002F 下面不是完整可运行代码，仅展示逻辑链条\nclass CFooFactory final : public IClassFactory {\npublic:\n    HRESULT STDMETHODCALLTYPE CreateInstance(IUnknown* pOuter, REFIID riid, void** ppv) override {\n        if (pOuter != nullptr && riid != IID_IUnknown)\n            return CLASS_E_NOAGGREGATION;\n        auto* obj = new(std::nothrow) CFoo();\n        if (!obj) return E_OUTOFMEMORY;\n        HRESULT hr = obj->QueryInterface(riid, ppv);\n        obj->Release(); \u002F\u002F 构造函数里通常为 1，QI 会 AddRef，这里 Release 工厂持有的初始计数—具体依实现而定\n        return hr;\n    }\n    \u002F\u002F LockServer \u002F Release \u002F QueryInterface … 省略\n};\n",[56,1022,1023,1028,1033,1038,1043,1048,1053,1058,1063,1069,1075,1081,1087,1093],{"__ignoreMap":255},[286,1024,1025],{"class":288,"line":289},[286,1026,1027],{},"\u002F\u002F 下面不是完整可运行代码，仅展示逻辑链条\n",[286,1029,1030],{"class":288,"line":295},[286,1031,1032],{},"class CFooFactory final : public IClassFactory {\n",[286,1034,1035],{"class":288,"line":301},[286,1036,1037],{},"public:\n",[286,1039,1040],{"class":288,"line":307},[286,1041,1042],{},"    HRESULT STDMETHODCALLTYPE CreateInstance(IUnknown* pOuter, REFIID riid, void** ppv) override {\n",[286,1044,1045],{"class":288,"line":313},[286,1046,1047],{},"        if (pOuter != nullptr && riid != IID_IUnknown)\n",[286,1049,1050],{"class":288,"line":374},[286,1051,1052],{},"            return CLASS_E_NOAGGREGATION;\n",[286,1054,1055],{"class":288,"line":380},[286,1056,1057],{},"        auto* obj = new(std::nothrow) CFoo();\n",[286,1059,1060],{"class":288,"line":386},[286,1061,1062],{},"        if (!obj) return E_OUTOFMEMORY;\n",[286,1064,1066],{"class":288,"line":1065},9,[286,1067,1068],{},"        HRESULT hr = obj->QueryInterface(riid, ppv);\n",[286,1070,1072],{"class":288,"line":1071},10,[286,1073,1074],{},"        obj->Release(); \u002F\u002F 构造函数里通常为 1，QI 会 AddRef，这里 Release 工厂持有的初始计数—具体依实现而定\n",[286,1076,1078],{"class":288,"line":1077},11,[286,1079,1080],{},"        return hr;\n",[286,1082,1084],{"class":288,"line":1083},12,[286,1085,1086],{},"    }\n",[286,1088,1090],{"class":288,"line":1089},13,[286,1091,1092],{},"    \u002F\u002F LockServer \u002F Release \u002F QueryInterface … 省略\n",[286,1094,1096],{"class":288,"line":1095},14,[286,1097,316],{},[14,1099,1100,1101,1109,1110,115],{},"进程内 COM DLL 通常还要导出 ",[18,1102,1103,101,1106],{},[56,1104,1105],{},"DllRegisterServer",[56,1107,1108],{},"DllUnregisterServer"," 写注册表，资源管理器才能找到你的 ",[18,1111,179],{},[39,1113],{},[42,1115,1117,1118,1120],{"id":1116},"七comptrwrl与智能指针纪律","七、",[56,1119,91],{},"（WRL）与智能指针纪律",[247,1122,1124],{"className":280,"code":1123,"language":282,"meta":255,"style":255},"#include \u003Cwrl\u002Fclient.h>\nusing Microsoft::WRL::ComPtr;\n\nComPtr\u003CIStream> spStream;\nHRESULT hr = SHCreateStreamOnFileEx(L\"C:\\\\temp.bin\", STGM_READWRITE | STGM_CREATE,\n    FILE_ATTRIBUTE_NORMAL, TRUE, nullptr, &spStream);\n\u002F\u002F spStream 析构时 Release\n",[56,1125,1126,1131,1136,1142,1147,1152,1157],{"__ignoreMap":255},[286,1127,1128],{"class":288,"line":289},[286,1129,1130],{},"#include \u003Cwrl\u002Fclient.h>\n",[286,1132,1133],{"class":288,"line":295},[286,1134,1135],{},"using Microsoft::WRL::ComPtr;\n",[286,1137,1138],{"class":288,"line":301},[286,1139,1141],{"emptyLinePlaceholder":1140},true,"\n",[286,1143,1144],{"class":288,"line":307},[286,1145,1146],{},"ComPtr\u003CIStream> spStream;\n",[286,1148,1149],{"class":288,"line":313},[286,1150,1151],{},"HRESULT hr = SHCreateStreamOnFileEx(L\"C:\\\\temp.bin\", STGM_READWRITE | STGM_CREATE,\n",[286,1153,1154],{"class":288,"line":374},[286,1155,1156],{},"    FILE_ATTRIBUTE_NORMAL, TRUE, nullptr, &spStream);\n",[286,1158,1159],{"class":288,"line":380},[286,1160,1161],{},"\u002F\u002F spStream 析构时 Release\n",[14,1163,1164],{},"常用：",[47,1166,1167,1179,1192,1200],{},[50,1168,1169,828,1174],{},[18,1170,1171],{},[56,1172,1173],{},"Get()",[18,1175,1176],{},[56,1177,1178],{},"T*",[50,1180,1181,828,1186,1191],{},[18,1182,1183],{},[56,1184,1185],{},"GetAddressOf()",[18,1187,1188],{},[56,1189,1190],{},"T**","，用于输出参数",[50,1193,1194,1199],{},[18,1195,1196],{},[56,1197,1198],{},"Attach(p)","：接管已有裸指针（不再自动 AddRef，除非你明确知道所有权）",[50,1201,1202,1207],{},[18,1203,1204],{},[56,1205,1206],{},"Detach()","：交出裸指针并清空智能指针",[39,1209],{},[42,1211,1213,1214,1217],{"id":1212},"八bstrvariant自动化idispatch","八、BSTR、",[56,1215,1216],{},"VARIANT","、自动化（IDispatch）",[223,1219,1221,1222,1225],{"id":1220},"_81-bstr带长度前缀的宽字符串","8.1 ",[56,1223,1224],{},"BSTR","：带长度前缀的宽字符串",[14,1227,1228,1229,529,1233,1241,1242,115],{},"COM 自动化里字符串常用 ",[18,1230,1231],{},[56,1232,1224],{},[18,1234,1235,101,1238],{},[56,1236,1237],{},"SysAllocString",[56,1239,1240],{},"SysFreeString","），不是普通的 ",[18,1243,1244],{},[56,1245,1246],{},"wchar_t*",[247,1248,1250],{"className":280,"code":1249,"language":282,"meta":255,"style":255},"#include \u003Ccomutil.h>\n#pragma comment(lib, \"comsuppw.lib\")\n\nBSTR bs = SysAllocString(L\"Hello\");\nif (!bs) { \u002F* OOM *\u002F }\n\u002F\u002F 传给接口...\nSysFreeString(bs);\n",[56,1251,1252,1257,1262,1266,1271,1276,1281],{"__ignoreMap":255},[286,1253,1254],{"class":288,"line":289},[286,1255,1256],{},"#include \u003Ccomutil.h>\n",[286,1258,1259],{"class":288,"line":295},[286,1260,1261],{},"#pragma comment(lib, \"comsuppw.lib\")\n",[286,1263,1264],{"class":288,"line":301},[286,1265,1141],{"emptyLinePlaceholder":1140},[286,1267,1268],{"class":288,"line":307},[286,1269,1270],{},"BSTR bs = SysAllocString(L\"Hello\");\n",[286,1272,1273],{"class":288,"line":313},[286,1274,1275],{},"if (!bs) { \u002F* OOM *\u002F }\n",[286,1277,1278],{"class":288,"line":374},[286,1279,1280],{},"\u002F\u002F 传给接口...\n",[286,1282,1283],{"class":288,"line":380},[286,1284,1285],{},"SysFreeString(bs);\n",[223,1287,1289,1290,1292],{"id":1288},"_82-variant万能联合体","8.2 ",[56,1291,1216],{},"：「万能联合体」",[14,1294,1295,1296,1301,1302,828,1306,1309,1310,1322],{},"脚本里 ",[18,1297,1298],{},[56,1299,1300],{},"Variant"," 对应 ",[18,1303,1304],{},[56,1305,1216],{},[56,1307,1308],{},"vt"," 字段区分 ",[18,1311,1312,1315,1316,1315,1319],{},[56,1313,1314],{},"VT_I4","、",[56,1317,1318],{},"VT_BSTR",[56,1320,1321],{},"VT_DISPATCH","…",[247,1324,1326],{"className":280,"code":1325,"language":282,"meta":255,"style":255},"VARIANT v{};\nv.vt = VT_I4;\nv.lVal = 42;\n\nVARIANT dest{};\nVariantInit(&dest);\nHRESULT hr = VariantCopy(&dest, &v);\nVariantClear(&dest);\nVariantClear(&v);\n",[56,1327,1328,1333,1338,1343,1347,1352,1357,1362,1367],{"__ignoreMap":255},[286,1329,1330],{"class":288,"line":289},[286,1331,1332],{},"VARIANT v{};\n",[286,1334,1335],{"class":288,"line":295},[286,1336,1337],{},"v.vt = VT_I4;\n",[286,1339,1340],{"class":288,"line":301},[286,1341,1342],{},"v.lVal = 42;\n",[286,1344,1345],{"class":288,"line":307},[286,1346,1141],{"emptyLinePlaceholder":1140},[286,1348,1349],{"class":288,"line":313},[286,1350,1351],{},"VARIANT dest{};\n",[286,1353,1354],{"class":288,"line":374},[286,1355,1356],{},"VariantInit(&dest);\n",[286,1358,1359],{"class":288,"line":380},[286,1360,1361],{},"HRESULT hr = VariantCopy(&dest, &v);\n",[286,1363,1364],{"class":288,"line":386},[286,1365,1366],{},"VariantClear(&dest);\n",[286,1368,1369],{"class":288,"line":1065},[286,1370,1371],{},"VariantClear(&v);\n",[223,1373,1375,1376,1379],{"id":1374},"_83-idispatch迟绑定脚本vba","8.3 ",[56,1377,1378],{},"IDispatch","：迟绑定（脚本、VBA）",[14,1381,1382,1383,1388,1389,1394,1395,1398,1399,1402,1403,115],{},"脚本调用 ",[18,1384,1385],{},[56,1386,1387],{},"obj.SaveAs(...)"," 时，往往走 ",[18,1390,1391],{},[56,1392,1393],{},"IDispatch::Invoke","，通过 ",[18,1396,1397],{},"DISPID"," 找方法。Office ",[18,1400,1401],{},"自动化","典型路径：",[18,1404,1405,1408,1409],{},[56,1406,1407],{},"CoCreateInstance(CLSID_Excel_Application,...)"," → ",[56,1410,1378],{},[14,1412,1413,1414,1417],{},"极简伪代码（真实项目请用 ",[18,1415,1416],{},"#import"," 或封装库）：",[247,1419,1421],{"className":280,"code":1420,"language":282,"meta":255,"style":255},"ComPtr\u003CIDispatch> app;\nCoCreateInstance(CLSID_Excel_Application, nullptr, CLSCTX_LOCAL_SERVER,\n    IID_IDispatch, reinterpret_cast\u003Cvoid**>(app.GetAddressOf()));\n\nDISPID dispid = 0;\nOLECHAR* name = const_cast\u003COLECHAR*>(L\"Quit\");\nHRESULT hr = app->GetIDsOfNames(IID_NULL, &name, 1, LOCALE_USER_DEFAULT, &dispid);\n\nDISPPARAMS dp{};\nVARIANT ret{};\nhr = app->Invoke(dispid, IID_NULL, LOCALE_USER_DEFAULT, DISPATCH_METHOD, &dp, &ret, nullptr, nullptr);\n",[56,1422,1423,1428,1433,1438,1442,1447,1452,1457,1461,1466,1471],{"__ignoreMap":255},[286,1424,1425],{"class":288,"line":289},[286,1426,1427],{},"ComPtr\u003CIDispatch> app;\n",[286,1429,1430],{"class":288,"line":295},[286,1431,1432],{},"CoCreateInstance(CLSID_Excel_Application, nullptr, CLSCTX_LOCAL_SERVER,\n",[286,1434,1435],{"class":288,"line":301},[286,1436,1437],{},"    IID_IDispatch, reinterpret_cast\u003Cvoid**>(app.GetAddressOf()));\n",[286,1439,1440],{"class":288,"line":307},[286,1441,1141],{"emptyLinePlaceholder":1140},[286,1443,1444],{"class":288,"line":313},[286,1445,1446],{},"DISPID dispid = 0;\n",[286,1448,1449],{"class":288,"line":374},[286,1450,1451],{},"OLECHAR* name = const_cast\u003COLECHAR*>(L\"Quit\");\n",[286,1453,1454],{"class":288,"line":380},[286,1455,1456],{},"HRESULT hr = app->GetIDsOfNames(IID_NULL, &name, 1, LOCALE_USER_DEFAULT, &dispid);\n",[286,1458,1459],{"class":288,"line":386},[286,1460,1141],{"emptyLinePlaceholder":1140},[286,1462,1463],{"class":288,"line":1065},[286,1464,1465],{},"DISPPARAMS dp{};\n",[286,1467,1468],{"class":288,"line":1071},[286,1469,1470],{},"VARIANT ret{};\n",[286,1472,1473],{"class":288,"line":1077},[286,1474,1475],{},"hr = app->Invoke(dispid, IID_NULL, LOCALE_USER_DEFAULT, DISPATCH_METHOD, &dp, &ret, nullptr, nullptr);\n",[39,1477],{},[42,1479,1481],{"id":1480},"九桌面右键菜单shell-扩展com-的经典秀场","九、桌面右键菜单：Shell 扩展（COM 的经典秀场）",[14,1483,1484,1485,1488,1489,1492,1493,1502,1503,1508],{},"资源管理器右键菜单上的「压缩」「杀毒扫描」「Git Bash Here」等，大量通过 ",[18,1486,1487],{},"Shell 扩展 DLL"," 实现：",[18,1490,1491],{},"进程内 COM 服务器","，实现 ",[18,1494,1495,1498,1499],{},[56,1496,1497],{},"IShellExtInit"," + ",[56,1500,1501],{},"IContextMenu","（或 ",[18,1504,1505],{},[56,1506,1507],{},"IExplorerCommand","，Win11 新叙事）。",[223,1510,1512],{"id":1511},"_91-用户能看到的效果链路","9.1 用户能看到的效果链路",[155,1514,1515,1525,1534,1539,1557],{},[50,1516,1517,1518,1524],{},"用户在 ",[18,1519,1520,1523],{},[56,1521,1522],{},"*.txt"," 文件","上右键。",[50,1526,1527,1528,1533],{},"Shell 查询注册表 ",[18,1529,1530],{},[56,1531,1532],{},"HKCR\\*\\shellex\\ContextMenuHandlers\\YourHandler","（或指定 ProgID 下）。",[50,1535,1536,1537,115],{},"默认值是你的 ",[18,1538,179],{},[50,1540,1541,1542,1546,1547,1552,1553,115],{},"Shell ",[18,1543,1544],{},[56,1545,104],{}," 加载 ",[18,1548,1549],{},[56,1550,1551],{},"YourHandler.dll","，拿到 ",[18,1554,1555],{},[56,1556,1501],{},[50,1558,1559,1560,1565,1566,115],{},"Shell 调 ",[18,1561,1562],{},[56,1563,1564],{},"QueryContextMenu"," 让你插入菜单项；用户点击后 ",[18,1567,1568],{},[56,1569,1570],{},"InvokeCommand",[223,1572,1574],{"id":1573},"_92-注册表示意手工改请先备份","9.2 注册表（示意，手工改请先备份）",[14,1576,1577,1578,1584],{},"下面这段 ",[18,1579,1580,1583],{},[56,1581,1582],{},".reg"," 思想","说明「把 CLSID 接到全局文件右键」的路径（具体 GUID 请换成你自己的）：",[247,1586,1590],{"className":1587,"code":1588,"language":1589,"meta":255,"style":255},"language-reg shiki shiki-themes github-light github-dark","Windows Registry Editor Version 5.00\n\n; 假设 CLSID = {11111111-1111-1111-1111-111111111111}\n\n[HKEY_CLASSES_ROOT\\CLSID\\{11111111-1111-1111-1111-111111111111}]\n@=\"My Context Menu Handler Sample\"\n\n[HKEY_CLASSES_ROOT\\CLSID\\{11111111-1111-1111-1111-111111111111}\\InprocServer32]\n@=\"C:\\\\Path\\\\To\\\\MyShellExt.dll\"\n\"ThreadingModel\"=\"Apartment\"\n\n[HKEY_CLASSES_ROOT\\*\\shellex\\ContextMenuHandlers\\MySample]\n@=\"{11111111-1111-1111-1111-111111111111}\"\n","reg",[56,1591,1592,1597,1601,1606,1610,1615,1620,1624,1629,1634,1639,1643,1648],{"__ignoreMap":255},[286,1593,1594],{"class":288,"line":289},[286,1595,1596],{},"Windows Registry Editor Version 5.00\n",[286,1598,1599],{"class":288,"line":295},[286,1600,1141],{"emptyLinePlaceholder":1140},[286,1602,1603],{"class":288,"line":301},[286,1604,1605],{},"; 假设 CLSID = {11111111-1111-1111-1111-111111111111}\n",[286,1607,1608],{"class":288,"line":307},[286,1609,1141],{"emptyLinePlaceholder":1140},[286,1611,1612],{"class":288,"line":313},[286,1613,1614],{},"[HKEY_CLASSES_ROOT\\CLSID\\{11111111-1111-1111-1111-111111111111}]\n",[286,1616,1617],{"class":288,"line":374},[286,1618,1619],{},"@=\"My Context Menu Handler Sample\"\n",[286,1621,1622],{"class":288,"line":380},[286,1623,1141],{"emptyLinePlaceholder":1140},[286,1625,1626],{"class":288,"line":386},[286,1627,1628],{},"[HKEY_CLASSES_ROOT\\CLSID\\{11111111-1111-1111-1111-111111111111}\\InprocServer32]\n",[286,1630,1631],{"class":288,"line":1065},[286,1632,1633],{},"@=\"C:\\\\Path\\\\To\\\\MyShellExt.dll\"\n",[286,1635,1636],{"class":288,"line":1071},[286,1637,1638],{},"\"ThreadingModel\"=\"Apartment\"\n",[286,1640,1641],{"class":288,"line":1077},[286,1642,1141],{"emptyLinePlaceholder":1140},[286,1644,1645],{"class":288,"line":1083},[286,1646,1647],{},"[HKEY_CLASSES_ROOT\\*\\shellex\\ContextMenuHandlers\\MySample]\n",[286,1649,1650],{"class":288,"line":1089},[286,1651,1652],{},"@=\"{11111111-1111-1111-1111-111111111111}\"\n",[14,1654,1655,1657,1658,1662,1663,1667,1668,1315,1674,1677],{},[18,1656,528],{},"：壳扩展历史上常用 ",[18,1659,1660],{},[56,1661,532],{},"（STA）。实现 ",[18,1664,1665],{},[56,1666,1497],{}," 时 Shell 会把 ",[18,1669,1670,1673],{},[56,1671,1672],{},"IDataObject","（选中文件）",[18,1675,1676],{},"PIDL"," 等给你。",[223,1679,1681],{"id":1680},"_93-接口骨架教育向精简版","9.3 接口骨架（教育向精简版）",[247,1683,1685],{"className":280,"code":1684,"language":282,"meta":255,"style":255},"#include \u003Cshlobj.h>\n#include \u003Cshobjidl.h>\n\nclass __declspec(uuid(\"11111111-1111-1111-1111-111111111111\"))\n    CMyContextMenu final : public IShellExtInit, public IContextMenu {\npublic:\n    \u002F\u002F IUnknown\n    HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void** ppv) override {\n        if (IsEqualIID(riid, IID_IUnknown))\n            *ppv = static_cast\u003CIShellExtInit*>(this);\n        else if (IsEqualIID(riid, IID_IShellExtInit))\n            *ppv = static_cast\u003CIShellExtInit*>(this);\n        else if (IsEqualIID(riid, IID_IContextMenu))\n            *ppv = static_cast\u003CIContextMenu*>(this);\n        else {\n            *ppv = nullptr;\n            return E_NOINTERFACE;\n        }\n        reinterpret_cast\u003CIUnknown*>(*ppv)->AddRef();\n        return S_OK;\n    }\n    ULONG STDMETHODCALLTYPE AddRef() override { return ++ref_; }\n    ULONG STDMETHODCALLTYPE Release() override {\n        ULONG c = --ref_;\n        if (!c) delete this;\n        return c;\n    }\n\n    \u002F\u002F IShellExtInit\n    HRESULT STDMETHODCALLTYPE Initialize(PCIDLIST_ABSOLUTE pidlFolder, IDataObject* pdtobj,\n        HKEY hkeyProgID) override {\n        \u002F\u002F 保存 pdtobj 里文件列表：可以用 CF_HDROP 解析路径（Shell API）\n        return S_OK;\n    }\n\n    \u002F\u002F IContextMenu\n    HRESULT STDMETHODCALLTYPE QueryContextMenu(HMENU hmenu, UINT indexMenu, UINT idCmdFirst,\n        UINT idCmdLast, UINT uFlags) override {\n        InsertMenuW(hmenu, indexMenu, MF_STRING | MF_BYPOSITION, idCmdFirst, L\"我的示例菜单\");\n        return MAKE_HRESULT(SEVERITY_SUCCESS, 0, 1); \u002F\u002F 占用 1 个命令 ID\n    }\n\n    HRESULT STDMETHODCALLTYPE InvokeCommand(LPCMINVOKECOMMANDINFO pici) override {\n        MessageBoxW(nullptr, L\"菜单被点击\", L\"COM Shell 扩展\", MB_OK);\n        return S_OK;\n    }\n\n    HRESULT STDMETHODCALLTYPE GetCommandString(UINT_PTR idCmd, UINT uFlags, UINT* pwReserved,\n        CHAR* pszName, UINT cchMax) override {\n        return E_NOTIMPL;\n    }\n\nprivate:\n    ULONG ref_ = 1;\n};\n",[56,1686,1687,1692,1697,1701,1706,1711,1715,1720,1725,1730,1735,1740,1744,1749,1754,1760,1766,1772,1778,1784,1790,1795,1801,1807,1813,1819,1825,1830,1835,1841,1847,1853,1859,1864,1869,1874,1880,1886,1892,1898,1904,1909,1914,1920,1926,1931,1936,1941,1947,1953,1959,1964,1969,1975,1981],{"__ignoreMap":255},[286,1688,1689],{"class":288,"line":289},[286,1690,1691],{},"#include \u003Cshlobj.h>\n",[286,1693,1694],{"class":288,"line":295},[286,1695,1696],{},"#include \u003Cshobjidl.h>\n",[286,1698,1699],{"class":288,"line":301},[286,1700,1141],{"emptyLinePlaceholder":1140},[286,1702,1703],{"class":288,"line":307},[286,1704,1705],{},"class __declspec(uuid(\"11111111-1111-1111-1111-111111111111\"))\n",[286,1707,1708],{"class":288,"line":313},[286,1709,1710],{},"    CMyContextMenu final : public IShellExtInit, public IContextMenu {\n",[286,1712,1713],{"class":288,"line":374},[286,1714,1037],{},[286,1716,1717],{"class":288,"line":380},[286,1718,1719],{},"    \u002F\u002F IUnknown\n",[286,1721,1722],{"class":288,"line":386},[286,1723,1724],{},"    HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void** ppv) override {\n",[286,1726,1727],{"class":288,"line":1065},[286,1728,1729],{},"        if (IsEqualIID(riid, IID_IUnknown))\n",[286,1731,1732],{"class":288,"line":1071},[286,1733,1734],{},"            *ppv = static_cast\u003CIShellExtInit*>(this);\n",[286,1736,1737],{"class":288,"line":1077},[286,1738,1739],{},"        else if (IsEqualIID(riid, IID_IShellExtInit))\n",[286,1741,1742],{"class":288,"line":1083},[286,1743,1734],{},[286,1745,1746],{"class":288,"line":1089},[286,1747,1748],{},"        else if (IsEqualIID(riid, IID_IContextMenu))\n",[286,1750,1751],{"class":288,"line":1095},[286,1752,1753],{},"            *ppv = static_cast\u003CIContextMenu*>(this);\n",[286,1755,1757],{"class":288,"line":1756},15,[286,1758,1759],{},"        else {\n",[286,1761,1763],{"class":288,"line":1762},16,[286,1764,1765],{},"            *ppv = nullptr;\n",[286,1767,1769],{"class":288,"line":1768},17,[286,1770,1771],{},"            return E_NOINTERFACE;\n",[286,1773,1775],{"class":288,"line":1774},18,[286,1776,1777],{},"        }\n",[286,1779,1781],{"class":288,"line":1780},19,[286,1782,1783],{},"        reinterpret_cast\u003CIUnknown*>(*ppv)->AddRef();\n",[286,1785,1787],{"class":288,"line":1786},20,[286,1788,1789],{},"        return S_OK;\n",[286,1791,1793],{"class":288,"line":1792},21,[286,1794,1086],{},[286,1796,1798],{"class":288,"line":1797},22,[286,1799,1800],{},"    ULONG STDMETHODCALLTYPE AddRef() override { return ++ref_; }\n",[286,1802,1804],{"class":288,"line":1803},23,[286,1805,1806],{},"    ULONG STDMETHODCALLTYPE Release() override {\n",[286,1808,1810],{"class":288,"line":1809},24,[286,1811,1812],{},"        ULONG c = --ref_;\n",[286,1814,1816],{"class":288,"line":1815},25,[286,1817,1818],{},"        if (!c) delete this;\n",[286,1820,1822],{"class":288,"line":1821},26,[286,1823,1824],{},"        return c;\n",[286,1826,1828],{"class":288,"line":1827},27,[286,1829,1086],{},[286,1831,1833],{"class":288,"line":1832},28,[286,1834,1141],{"emptyLinePlaceholder":1140},[286,1836,1838],{"class":288,"line":1837},29,[286,1839,1840],{},"    \u002F\u002F IShellExtInit\n",[286,1842,1844],{"class":288,"line":1843},30,[286,1845,1846],{},"    HRESULT STDMETHODCALLTYPE Initialize(PCIDLIST_ABSOLUTE pidlFolder, IDataObject* pdtobj,\n",[286,1848,1850],{"class":288,"line":1849},31,[286,1851,1852],{},"        HKEY hkeyProgID) override {\n",[286,1854,1856],{"class":288,"line":1855},32,[286,1857,1858],{},"        \u002F\u002F 保存 pdtobj 里文件列表：可以用 CF_HDROP 解析路径（Shell API）\n",[286,1860,1862],{"class":288,"line":1861},33,[286,1863,1789],{},[286,1865,1867],{"class":288,"line":1866},34,[286,1868,1086],{},[286,1870,1872],{"class":288,"line":1871},35,[286,1873,1141],{"emptyLinePlaceholder":1140},[286,1875,1877],{"class":288,"line":1876},36,[286,1878,1879],{},"    \u002F\u002F IContextMenu\n",[286,1881,1883],{"class":288,"line":1882},37,[286,1884,1885],{},"    HRESULT STDMETHODCALLTYPE QueryContextMenu(HMENU hmenu, UINT indexMenu, UINT idCmdFirst,\n",[286,1887,1889],{"class":288,"line":1888},38,[286,1890,1891],{},"        UINT idCmdLast, UINT uFlags) override {\n",[286,1893,1895],{"class":288,"line":1894},39,[286,1896,1897],{},"        InsertMenuW(hmenu, indexMenu, MF_STRING | MF_BYPOSITION, idCmdFirst, L\"我的示例菜单\");\n",[286,1899,1901],{"class":288,"line":1900},40,[286,1902,1903],{},"        return MAKE_HRESULT(SEVERITY_SUCCESS, 0, 1); \u002F\u002F 占用 1 个命令 ID\n",[286,1905,1907],{"class":288,"line":1906},41,[286,1908,1086],{},[286,1910,1912],{"class":288,"line":1911},42,[286,1913,1141],{"emptyLinePlaceholder":1140},[286,1915,1917],{"class":288,"line":1916},43,[286,1918,1919],{},"    HRESULT STDMETHODCALLTYPE InvokeCommand(LPCMINVOKECOMMANDINFO pici) override {\n",[286,1921,1923],{"class":288,"line":1922},44,[286,1924,1925],{},"        MessageBoxW(nullptr, L\"菜单被点击\", L\"COM Shell 扩展\", MB_OK);\n",[286,1927,1929],{"class":288,"line":1928},45,[286,1930,1789],{},[286,1932,1934],{"class":288,"line":1933},46,[286,1935,1086],{},[286,1937,1939],{"class":288,"line":1938},47,[286,1940,1141],{"emptyLinePlaceholder":1140},[286,1942,1944],{"class":288,"line":1943},48,[286,1945,1946],{},"    HRESULT STDMETHODCALLTYPE GetCommandString(UINT_PTR idCmd, UINT uFlags, UINT* pwReserved,\n",[286,1948,1950],{"class":288,"line":1949},49,[286,1951,1952],{},"        CHAR* pszName, UINT cchMax) override {\n",[286,1954,1956],{"class":288,"line":1955},50,[286,1957,1958],{},"        return E_NOTIMPL;\n",[286,1960,1962],{"class":288,"line":1961},51,[286,1963,1086],{},[286,1965,1967],{"class":288,"line":1966},52,[286,1968,1141],{"emptyLinePlaceholder":1140},[286,1970,1972],{"class":288,"line":1971},53,[286,1973,1974],{},"private:\n",[286,1976,1978],{"class":288,"line":1977},54,[286,1979,1980],{},"    ULONG ref_ = 1;\n",[286,1982,1984],{"class":288,"line":1983},55,[286,1985,316],{},[14,1987,1988,1991,1992,1996,1997,2002,2003,1315,2011,2014],{},[18,1989,1990],{},"DllGetClassObject"," 返回 ",[18,1993,1994],{},[56,1995,975],{},"，由 ",[18,1998,1999],{},[56,2000,2001],{},"CreateInstance"," 生成上面的对象——完整工程还需 ",[18,2004,2005,1315,2008],{},[56,2006,2007],{},"DllMain",[56,2009,2010],{},"DllCanUnloadNow",[18,2012,2013],{},"x64 与 Shell 位数一致","（64 位资源管理器只加载 64 位扩展）。",[223,2016,2018],{"id":2017},"_931-进程内-com-dll-必须导出的符号示意","9.3.1 进程内 COM DLL 必须导出的符号（示意）",[247,2020,2022],{"className":280,"code":2021,"language":282,"meta":255,"style":255},"\u002F\u002F stdafx \u002F 模块全局\nSTDAPI DllGetClassObject(REFCLSID rclsid, REFIID riid, LPVOID* ppv)\n{\n    if (!IsEqualCLSID(rclsid, CLSID_MyContextMenu))\n        return CLASS_E_CLASSNOTAVAILABLE;\n    \u002F\u002F 常见写法：实现 CClassFactory::QueryInterface(riid, ppv) 返回 IClassFactory\n    *ppv = nullptr;\n    return g_pClassFactory->QueryInterface(riid, ppv);\n}\n\nSTDAPI DllCanUnloadNow()\n{\n    \u002F\u002F 若模块内活动对象计数为 0，返回 S_OK，允许 OLE 卸载 DLL\n    return g_serverLocks == 0 && g_objCount == 0 ? S_OK : S_FALSE;\n}\n\nSTDAPI DllRegisterServer()\n{\n    \u002F\u002F RegCreateKeyEx 写入 HKCR\\CLSID\\{...}\\InprocServer32 …\n    return S_OK;\n}\n\nSTDAPI DllUnregisterServer()\n{\n    \u002F\u002F 删除注册表项\n    return S_OK;\n}\n",[56,2023,2024,2029,2034,2038,2043,2048,2053,2058,2063,2067,2071,2076,2080,2085,2090,2094,2098,2103,2107,2112,2117,2121,2125,2130,2134,2139,2143],{"__ignoreMap":255},[286,2025,2026],{"class":288,"line":289},[286,2027,2028],{},"\u002F\u002F stdafx \u002F 模块全局\n",[286,2030,2031],{"class":288,"line":295},[286,2032,2033],{},"STDAPI DllGetClassObject(REFCLSID rclsid, REFIID riid, LPVOID* ppv)\n",[286,2035,2036],{"class":288,"line":301},[286,2037,356],{},[286,2039,2040],{"class":288,"line":307},[286,2041,2042],{},"    if (!IsEqualCLSID(rclsid, CLSID_MyContextMenu))\n",[286,2044,2045],{"class":288,"line":313},[286,2046,2047],{},"        return CLASS_E_CLASSNOTAVAILABLE;\n",[286,2049,2050],{"class":288,"line":374},[286,2051,2052],{},"    \u002F\u002F 常见写法：实现 CClassFactory::QueryInterface(riid, ppv) 返回 IClassFactory\n",[286,2054,2055],{"class":288,"line":380},[286,2056,2057],{},"    *ppv = nullptr;\n",[286,2059,2060],{"class":288,"line":386},[286,2061,2062],{},"    return g_pClassFactory->QueryInterface(riid, ppv);\n",[286,2064,2065],{"class":288,"line":1065},[286,2066,389],{},[286,2068,2069],{"class":288,"line":1071},[286,2070,1141],{"emptyLinePlaceholder":1140},[286,2072,2073],{"class":288,"line":1077},[286,2074,2075],{},"STDAPI DllCanUnloadNow()\n",[286,2077,2078],{"class":288,"line":1083},[286,2079,356],{},[286,2081,2082],{"class":288,"line":1089},[286,2083,2084],{},"    \u002F\u002F 若模块内活动对象计数为 0，返回 S_OK，允许 OLE 卸载 DLL\n",[286,2086,2087],{"class":288,"line":1095},[286,2088,2089],{},"    return g_serverLocks == 0 && g_objCount == 0 ? S_OK : S_FALSE;\n",[286,2091,2092],{"class":288,"line":1756},[286,2093,389],{},[286,2095,2096],{"class":288,"line":1762},[286,2097,1141],{"emptyLinePlaceholder":1140},[286,2099,2100],{"class":288,"line":1768},[286,2101,2102],{},"STDAPI DllRegisterServer()\n",[286,2104,2105],{"class":288,"line":1774},[286,2106,356],{},[286,2108,2109],{"class":288,"line":1780},[286,2110,2111],{},"    \u002F\u002F RegCreateKeyEx 写入 HKCR\\CLSID\\{...}\\InprocServer32 …\n",[286,2113,2114],{"class":288,"line":1786},[286,2115,2116],{},"    return S_OK;\n",[286,2118,2119],{"class":288,"line":1792},[286,2120,389],{},[286,2122,2123],{"class":288,"line":1797},[286,2124,1141],{"emptyLinePlaceholder":1140},[286,2126,2127],{"class":288,"line":1803},[286,2128,2129],{},"STDAPI DllUnregisterServer()\n",[286,2131,2132],{"class":288,"line":1809},[286,2133,356],{},[286,2135,2136],{"class":288,"line":1815},[286,2137,2138],{},"    \u002F\u002F 删除注册表项\n",[286,2140,2141],{"class":288,"line":1821},[286,2142,2116],{},[286,2144,2145],{"class":288,"line":1827},[286,2146,389],{},[14,2148,2149,2150,2155,2156,115],{},"实际工程要用 ",[18,2151,2152],{},[56,2153,2154],{},"regsvr32 MyShellExt.dll"," 注册（管理员权限视路径而定），卸载 ",[18,2157,2158],{},[56,2159,2160],{},"regsvr32 \u002Fu",[223,2162,2164,2165,2167],{"id":2163},"_94-iexplorercommandwindows-10-1809","9.4 ",[56,2166,1507],{},"（Windows 10 1809+）",[14,2169,2170,2171,2175,2176,2181,2182,2185,2186,2191,2192,2196,2197,115],{},"微软在新示例里推广 ",[18,2172,2173],{},[56,2174,1507],{},"（配合 ",[18,2177,2178],{},[56,2179,2180],{},"IInitializeWithStream"," 等），模型仍是 ",[18,2183,2184],{},"COM","，但注册位置与 ",[18,2187,2188],{},[56,2189,2190],{},"Shell.ExtensionRegistration"," manifest 有关——思路与 ",[18,2193,2194],{},[56,2195,1501],{}," 类似：",[18,2198,2199],{},"实现接口 → 注册 CLSID → Shell 宿主 CoCreate",[39,2201],{},[42,2203,2205],{"id":2204},"十其它常见-com-版图拓宽视野","十、其它常见 COM 版图（拓宽视野）",[223,2207,2209,2210,1315,2212],{"id":2208},"_101-拖拽与剪贴板idataobjectidroptarget","10.1 拖拽与剪贴板：",[56,2211,1672],{},[56,2213,2214],{},"IDropTarget",[14,2216,2217,2218,2227,2228,2233],{},"OLE 拖拽核心是 ",[18,2219,2220,1498,2222,101,2225],{},[56,2221,1672],{},[56,2223,2224],{},"IDropSource",[56,2226,2214],{},"，全是 COM。写过 ",[18,2229,2230],{},[56,2231,2232],{},"DoDragDrop"," 的同学已经在用 COM。",[223,2235,2237],{"id":2236},"_102-activex-浏览器插件历史包袱","10.2 ActiveX \u002F 浏览器插件（历史包袱）",[14,2239,2240,2241,2246],{},"旧 IE ",[18,2242,2243],{},[56,2244,2245],{},"IBrowserPlugin"," 生态建立在 COM 上；现代浏览器路径已换道，但遗留系统维护仍会碰到。",[223,2248,2250,2251,2254],{"id":2249},"_103-wmiiwbemservices","10.3 WMI（",[56,2252,2253],{},"IWbemServices","）",[14,2256,2257,2258,2263,2264,2267],{},"脚本 ",[18,2259,2260],{},[56,2261,2262],{},"GetObject(\"winmgmts:\")"," 底层也是 COM；C++ 可用 ",[18,2265,2266],{},"WMI COM API"," 查询进程、磁盘。",[223,2269,2271],{"id":2270},"_104-directx-wasapi-media-foundation","10.4 DirectX \u002F WASAPI \u002F Media Foundation",[14,2273,2274,2275,1315,2280,1315,2285,2290,2291,115],{},"图形与音视频栈大量使用 COM：",[18,2276,2277],{},[56,2278,2279],{},"ID3D11Device",[18,2281,2282],{},[56,2283,2284],{},"IAudioClient",[18,2286,2287],{},[56,2288,2289],{},"IMFSinkWriter","……与本笔记前文一致：",[18,2292,2293,2295],{},[56,2294,91],{}," + HRESULT",[39,2297],{},[42,2299,2301],{"id":2300},"十一调试与工具知道名字即可","十一、调试与工具（知道名字即可）",[47,2303,2304,2312,2318],{},[50,2305,2306,2311],{},[18,2307,2308],{},[56,2309,2310],{},"OLE\u002FCOM Object Viewer (OleView.exe)","：浏览注册了的 CLSID、接口。",[50,2313,2314,2317],{},[18,2315,2316],{},"Process Monitor","：看 Shell 是否成功加载你的 DLL、路径是否错误。",[50,2319,2320,2323,2324,2328,2329,2332],{},[18,2321,2322],{},"日志","：对 ",[18,2325,2326],{},[56,2327,58],{}," 打印 ",[18,2330,2331],{},"十六进制","，对照 WinError.h \u002F ntstatus。",[39,2334],{},[42,2336,2338],{"id":2337},"十二与录屏工程如何对上号可选阅读","十二、与「录屏工程」如何对上号（可选阅读）",[14,2340,2341,2342,2345,2346,1315,2351,1315,2356,2361,2362,2365,2366,115],{},"若你同时在写一个 ",[18,2343,2344],{},"WGC + WASAPI + MF"," 工具：",[18,2347,2348,2349],{},"线程里 ",[56,2350,100],{},[18,2352,2353],{},[56,2354,2355],{},"CoCreateInstance(MMDeviceEnumerator)",[18,2357,2358],{},[56,2359,2360],{},"MFCreateSinkWriterFromURL","，全部都是本篇套路的子集；",[18,2363,2364],{},"没有出现 Shell 扩展","，不代表 COM 只能干那些事——",[18,2367,2368],{},"右键菜单只是 COM 桌面生态里最容易讲的一个故事",[39,2370],{},[42,2372,2374],{"id":2373},"十三推荐阅读顺序自学","十三、推荐阅读顺序（自学）",[155,2376,2377,2387,2390,2397,2405,2408],{},[50,2378,2379,2380,1408,2383,2386],{},"Microsoft Learn：",[18,2381,2382],{},"「Introduction to COM」",[18,2384,2385],{},"「Managing Memory Allocation」","（引用计数）。",[50,2388,2389],{},"《Inside COM》（旧书，概念仍清晰）——建立 mental model。",[50,2391,2392,2393,2396],{},"MSDN：",[18,2394,2395],{},"Shell Context Menu Handler"," 官方示例（结合本文第 9 节）。",[50,2398,2399,2400,115],{},"需要 Office 自动化再深入 ",[18,2401,2402,2404],{},[56,2403,1378],{}," \u002F 双重接口",[50,2406,2407],{},"Don Box《Essential COM》（绝版但扫描版仍可淘）——比官方文档「讲故事」。",[50,2409,2410],{},"《Professional Visual Studio 2022》类书里 COM 章节——偏实操注册表与 ATL。",[39,2412],{},[10,2414,2416],{"id":2415},"第二卷深度扩展可与上文穿插阅读","第二卷：深度扩展（可与上文穿插阅读）",[14,2418,2419,2420,1315,2423,1315,2426,1315,2429,1315,2432,1315,2435,1315,2438,1315,2441,2444],{},"下列章节独立编号，专门回答：",[18,2421,2422],{},"HRESULT 怎么拆",[18,2424,2425],{},"引用计数有哪些坑",[18,2427,2428],{},"聚合与连接点是什么",[18,2430,2431],{},"IDL\u002FMIDL 流水线",[18,2433,2434],{},"Shell 里如何从 IDataObject 拿出文件路径",[18,2436,2437],{},"WinRT 与经典 COM 边界",[18,2439,2440],{},"ATL 帮你省了哪些样板代码",[18,2442,2443],{},"FAQ","。篇幅不设上限，你可按需跳着读。",[39,2446],{},[42,2448,2450,2451,2453],{"id":2449},"十五hresult-手工解码与-facility-一览进阶","十五、",[56,2452,58],{}," 手工解码与 Facility 一览（进阶）",[223,2455,2457],{"id":2456},"_151-位域记住这张图就够应付面试","15.1 位域（记住这张图就够应付面试）",[14,2459,2460,2461,2465],{},"32 位 ",[18,2462,2463],{},[56,2464,58],{}," 常见划分（概念）：",[47,2467,2468,2481,2487,2495],{},[50,2469,2470,2473,2474,2477,2478,2480],{},[18,2471,2472],{},"Bit 31","：严重位；",[18,2475,2476],{},"1"," 通常表示失败（与 **",[56,2479,68],{}," 宏一致）。",[50,2482,2483,2486],{},[18,2484,2485],{},"Bits 30–16","：保留 \u002F 客户位（部分 HRESULT 用法）。",[50,2488,2489,828,2492,2494],{},[18,2490,2491],{},"Bits 15–0",[18,2493,640],{},"，具体错误号。",[50,2496,2497,828,2500,2502],{},[18,2498,2499],{},"中间若干位",[18,2501,72],{},"，区分错误「出自谁家」。",[14,2504,2505,2506,2512,2513,115],{},"实务上不要手算：",[18,2507,2508,2509],{},"打印十六进制 ",[56,2510,2511],{},"printf(\"hr=0x%08X\\n\", hr)","，再用文档或 ",[18,2514,2515],{},[56,2516,2517],{},"FormatMessage",[223,2519,2521],{"id":2520},"_152-常见-facility混脸熟即可","15.2 常见 Facility（混脸熟即可）",[421,2523,2524,2534],{},[424,2525,2526],{},[427,2527,2528,2531],{},[430,2529,2530],{},"Facility 值（节选）",[430,2532,2533],{},"大致含义",[437,2535,2536,2546,2556,2571,2581],{},[427,2537,2538,2543],{},[442,2539,2540],{},[18,2541,2542],{},"FACILITY_NULL",[442,2544,2545],{},"通用",[427,2547,2548,2553],{},[442,2549,2550],{},[18,2551,2552],{},"FACILITY_RPC",[442,2554,2555],{},"RPC 调用失败",[427,2557,2558,2563],{},[442,2559,2560],{},[18,2561,2562],{},"FACILITY_WIN32",[442,2564,2565,2566,2254],{},"Win32 错误包装（常见 ",[18,2567,2568],{},[56,2569,2570],{},"HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND)",[427,2572,2573,2578],{},[442,2574,2575],{},[18,2576,2577],{},"FACILITY_ITF",[442,2579,2580],{},"接口自定义错误区间",[427,2582,2583,2588],{},[442,2584,2585],{},[18,2586,2587],{},"FACILITY_WINDOWS",[442,2589,2590],{},"Shell \u002F WinRT 相关",[14,2592,767,2593,2599,2600,828],{},[18,2594,2595,2598],{},[56,2596,2597],{},"Win32"," 错误"," 包成 ",[18,2601,2602],{},[56,2603,58],{},[247,2605,2607],{"className":280,"code":2606,"language":282,"meta":255,"style":255},"HRESULT hr = HRESULT_FROM_WIN32(GetLastError());\n",[56,2608,2609],{"__ignoreMap":255},[286,2610,2611],{"class":288,"line":289},[286,2612,2606],{},[14,2614,2615,2616,1315,2621,2626,2627,542],{},"反向：",[18,2617,2618],{},[56,2619,2620],{},"HRESULT_CODE(hr)",[18,2622,2623],{},[56,2624,2625],{},"HRESULT_FACILITY(hr)","（宏定义在 ",[18,2628,2629],{},[56,2630,2631],{},"winerror.h",[223,2633,2635,2636,101,2639,2642],{"id":2634},"_153-ierrorinfo-_com_error拿到可读字符串","15.3 ",[56,2637,2638],{},"IErrorInfo",[56,2640,2641],{},"_com_error","（拿到「可读字符串」）",[14,2644,2645,2646,2650,2651,2656,2657,2662,2663,828],{},"不少 COM 服务器在失败时会附加 ",[18,2647,2648],{},[56,2649,2638],{},"（支持 ",[18,2652,2653],{},[56,2654,2655],{},"ISupportErrorInfo","）。在包含 ",[18,2658,2659],{},[56,2660,2661],{},"comdef.h"," 的场景可用 ",[18,2664,2665],{},[56,2666,2641],{},[247,2668,2670],{"className":280,"code":2669,"language":282,"meta":255,"style":255},"#include \u003Ccomdef.h>\ntry {\n    \u002F\u002F 调用 #import 生成的智能包装可能抛 _com_error\n} catch (_com_error& e) {\n    const wchar_t* desc = e.ErrorMessage(); \u002F\u002F 人类可读\n    HRESULT hr = e.Error();\n}\n",[56,2671,2672,2677,2682,2687,2692,2697,2702],{"__ignoreMap":255},[286,2673,2674],{"class":288,"line":289},[286,2675,2676],{},"#include \u003Ccomdef.h>\n",[286,2678,2679],{"class":288,"line":295},[286,2680,2681],{},"try {\n",[286,2683,2684],{"class":288,"line":301},[286,2685,2686],{},"    \u002F\u002F 调用 #import 生成的智能包装可能抛 _com_error\n",[286,2688,2689],{"class":288,"line":307},[286,2690,2691],{},"} catch (_com_error& e) {\n",[286,2693,2694],{"class":288,"line":313},[286,2695,2696],{},"    const wchar_t* desc = e.ErrorMessage(); \u002F\u002F 人类可读\n",[286,2698,2699],{"class":288,"line":374},[286,2700,2701],{},"    HRESULT hr = e.Error();\n",[286,2703,2704],{"class":288,"line":380},[286,2705,389],{},[14,2707,2708,2709,2713,2714,2719],{},"纯 C++\u002FWRL 手写时，失败路径往往只有 ",[18,2710,2711],{},[56,2712,58],{},"，要学会 ",[18,2715,2716],{},[56,2717,2718],{},"FormatMessageW(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_FROM_HMODULE, ...)"," 或自建映射表。",[39,2721],{},[42,2723,2725],{"id":2724},"十六引用计数的十诫与典型翻车现场","十六、引用计数的「十诫」与典型翻车现场",[223,2727,2729],{"id":2728},"_161-十诫口语版","16.1 十诫（口语版）",[155,2731,2732,2741,2753,2761,2777,2791,2797,2809,2827,2839],{},[50,2733,2734,2737,2738,115],{},[18,2735,2736],{},"每次接口指针所有权转移","，想清楚：",[18,2739,2740],{},"要不要 AddRef",[50,2742,2743,2748,2749,2752],{},[18,2744,2745,2747],{},[56,2746,260],{}," 成功","：返回的指针 ",[18,2750,2751],{},"已 AddRef","（对你来说是 +1）。",[50,2754,2755,2748,2759,115],{},[18,2756,2757,2747],{},[56,2758,104],{},[18,2760,2751],{},[50,2762,2763,2769,2770,2774,2775,115],{},[18,2764,2765,2766],{},"函数输出参数 ",[56,2767,2768],{},"[out]","：文档若写 ",[2771,2772,2773],"em",{},"caller must Release","，你必须 ",[18,2776,405],{},[50,2778,2779,2786,2787,2790],{},[18,2780,2781,2782,2785],{},"函数 ",[56,2783,2784],{},"[in]"," 接口指针","：通常 ",[18,2788,2789],{},"不接管所有权","（不调 Release），除非你额外 AddRef 保存。",[50,2792,2793,2796],{},[18,2794,2795],{},"不要把栈上临时对象的接口指针异步传到别的线程","——对象死了指针悬空。",[50,2798,2799,2804,2805,2808],{},[18,2800,2801],{},[56,2802,2803],{},"ComPtr::Attach","：接管 ",[18,2806,2807],{},"已有引用"," 的裸指针；若指针已是「偷来的」要数清楚。",[50,2810,2811,2814,2815,2819,2820,2823,2824,115],{},[18,2812,2813],{},"循环引用","：父持有子、子回调父，两边 ",[18,2816,2817],{},[56,2818,91],{}," 互相指 → ",[18,2821,2822],{},"泄漏","；典型解法：",[18,2825,2826],{},"弱引用 \u002F 显式断开 \u002F 不要双向 ComPtr",[50,2828,2829,2832,2833,2838],{},[18,2830,2831],{},"全局\u002F单例 COM 对象","：进程退出顺序要和 ",[18,2834,2835],{},[56,2836,2837],{},"CoUninitialize"," 对齐。",[50,2840,2841,2844,2845,2848,2849,2854],{},[18,2842,2843],{},"调试","：怀疑泄漏时用 ",[18,2846,2847],{},"Application Verifier"," 或在 ",[18,2850,2851,2853],{},[56,2852,405],{}," 里打日志","（仅调试版）。",[223,2856,2858],{"id":2857},"_162-翻车故事-a多一次-release","16.2 翻车故事 A：多一次 Release",[247,2860,2862],{"className":280,"code":2861,"language":282,"meta":255,"style":255},"IUnknown* p = nullptr;\nCoCreateInstance(..., (void**)&p); \u002F\u002F 计数 = 1\np->Release(); \u002F\u002F 0，对象销毁\np->Release(); \u002F\u002F 💥 双重释放\n",[56,2863,2864,2869,2874,2879],{"__ignoreMap":255},[286,2865,2866],{"class":288,"line":289},[286,2867,2868],{},"IUnknown* p = nullptr;\n",[286,2870,2871],{"class":288,"line":295},[286,2872,2873],{},"CoCreateInstance(..., (void**)&p); \u002F\u002F 计数 = 1\n",[286,2875,2876],{"class":288,"line":301},[286,2877,2878],{},"p->Release(); \u002F\u002F 0，对象销毁\n",[286,2880,2881],{"class":288,"line":307},[286,2882,2883],{},"p->Release(); \u002F\u002F 💥 双重释放\n",[223,2885,2887],{"id":2886},"_163-翻车故事-bqueryinterface-忘了-release","16.3 翻车故事 B：QueryInterface 忘了 Release",[247,2889,2891],{"className":280,"code":2890,"language":282,"meta":255,"style":255},"IAudioClient* ac = nullptr;\ndevice->Activate(..., (void**)&ac); \u002F\u002F 假设计数已 +1\nIUnknown* unk = nullptr;\nac->QueryInterface(IID_IUnknown, (void**)&unk); \u002F\u002F unk +1，此时同一对象计数更高\n\u002F\u002F 只用 unk，忘了 Release ac → 泄漏一种路径\n",[56,2892,2893,2898,2903,2908,2913],{"__ignoreMap":255},[286,2894,2895],{"class":288,"line":289},[286,2896,2897],{},"IAudioClient* ac = nullptr;\n",[286,2899,2900],{"class":288,"line":295},[286,2901,2902],{},"device->Activate(..., (void**)&ac); \u002F\u002F 假设计数已 +1\n",[286,2904,2905],{"class":288,"line":301},[286,2906,2907],{},"IUnknown* unk = nullptr;\n",[286,2909,2910],{"class":288,"line":307},[286,2911,2912],{},"ac->QueryInterface(IID_IUnknown, (void**)&unk); \u002F\u002F unk +1，此时同一对象计数更高\n",[286,2914,2915],{"class":288,"line":313},[286,2916,2917],{},"\u002F\u002F 只用 unk，忘了 Release ac → 泄漏一种路径\n",[14,2919,2920,2921,2924,2925,2927],{},"原则：",[18,2922,2923],{},"从一个入口拿到指针后，理清「这条所有权链」","，宁可 ",[18,2926,91],{}," 全自动。",[39,2929],{},[42,2931,2933,2934,2937],{"id":2932},"十七clsctx你到底想在哪创建对象","十七、",[56,2935,2936],{},"CLSCTX","：你到底想在哪创建对象？",[14,2939,2940,2944,2945,2950],{},[18,2941,2942],{},[56,2943,104],{}," 的 ",[18,2946,2947],{},[56,2948,2949],{},"dwClsContext"," 常见组合：",[247,2952,2954],{"className":280,"code":2953,"language":282,"meta":255,"style":255},"CoCreateInstance(CLSID_Foo, nullptr,\n    CLSCTX_INPROC_SERVER | CLSCTX_LOCAL_SERVER, \u002F\u002F 允许进程内 DLL 或本机 EXE\n    IID_IFoo, ...);\n",[56,2955,2956,2961,2966],{"__ignoreMap":255},[286,2957,2958],{"class":288,"line":289},[286,2959,2960],{},"CoCreateInstance(CLSID_Foo, nullptr,\n",[286,2962,2963],{"class":288,"line":295},[286,2964,2965],{},"    CLSCTX_INPROC_SERVER | CLSCTX_LOCAL_SERVER, \u002F\u002F 允许进程内 DLL 或本机 EXE\n",[286,2967,2968],{"class":288,"line":301},[286,2969,2970],{},"    IID_IFoo, ...);\n",[421,2972,2973,2982],{},[424,2974,2975],{},[427,2976,2977,2980],{},[430,2978,2979],{},"标志",[430,2981,435],{},[437,2983,2984,3000,3015,3027],{},[427,2985,2986,2993],{},[442,2987,2988],{},[18,2989,2990],{},[56,2991,2992],{},"CLSCTX_INPROC_SERVER",[442,2994,2995,2996,2999],{},"DLL ",[18,2997,2998],{},"进程内","（最快；右键菜单扩展多数如此）",[427,3001,3002,3009],{},[442,3003,3004],{},[18,3005,3006],{},[56,3007,3008],{},"CLSCTX_LOCAL_SERVER",[442,3010,3011,3014],{},[18,3012,3013],{},"本地 EXE"," COM 服务器（Excel 常见）",[427,3016,3017,3024],{},[442,3018,3019],{},[18,3020,3021],{},[56,3022,3023],{},"CLSCTX_REMOTE_SERVER",[442,3025,3026],{},"DCOM 远程（需额外安全配置）",[427,3028,3029,3036],{},[442,3030,3031],{},[18,3032,3033],{},[56,3034,3035],{},"CLSCTX_ALL",[442,3037,3038],{},"「你能用的都来试试」——初学者慎用",[14,3040,3041,3044,3045,3048,3049,3054],{},[18,3042,3043],{},"位数","：64 位 Shell 只能加载 ",[18,3046,3047],{},"64 位"," ",[18,3050,3051],{},[56,3052,3053],{},"InprocServer32","；混用是右键菜单「装了但不出现」的经典原因之一。",[39,3056],{},[42,3058,3060],{"id":3059},"十八聚合aggregation外层包装内层对象","十八、聚合（Aggregation）：外层包装内层对象",[14,3062,3063,3064,3069,3070,3075,3076,3081,3082,3086,3087,2944,3091,3096,3097,3100,3101,3104],{},"有时你要实现 ",[18,3065,3066],{},[56,3067,3068],{},"IX","，内部委托 ",[18,3071,3072],{},[56,3073,3074],{},"IY"," 给另一个 COM 对象；规范里 ",[18,3077,3078],{},[56,3079,3080],{},"pOuter"," 只有 ",[18,3083,3084],{},[56,3085,81],{}," 能用。",[18,3088,3089],{},[56,3090,104],{},[18,3092,3093],{},[56,3094,3095],{},"pUnkOuter"," 非空即进入聚合路径。",[18,3098,3099],{},"初学者","：除非你维护 Shell \u002F ActiveX 宿主，先 ",[18,3102,3103],{},"知道名词即可","，不要一上来手写聚合。",[39,3106],{},[42,3108,3110],{"id":3109},"十九连接点connection-pointscom-里的事件","十九、连接点（Connection Points）：COM 里的「事件」",[14,3112,2257,3113,3118,3119,3124],{},[18,3114,3115],{},[56,3116,3117],{},"Dim WithEvents","、浏览器 ",[18,3120,3121],{},[56,3122,3123],{},"attachEvent"," 的老祖宗之一是：",[47,3126,3127,3145,3159],{},[50,3128,3129,3130,1408,3135,1408,3140,115],{},"源对象实现 ",[18,3131,3132],{},[56,3133,3134],{},"IConnectionPointContainer",[18,3136,3137],{},[56,3138,3139],{},"EnumConnectionPoints",[18,3141,3142],{},[56,3143,3144],{},"IConnectionPoint",[50,3146,3147,3148,3152,3153,3158],{},"客户端实现 ",[18,3149,3150],{},[56,3151,1378],{},"（或自定义接收接口）→ ",[18,3154,3155],{},[56,3156,3157],{},"Advise"," 订阅。",[50,3160,3161,3162,3167],{},"触发事件 → ",[18,3163,3164],{},[56,3165,3166],{},"IConnectionPoint::Invoke"," 调客户端。",[14,3169,3170],{},"示意（极度精简）：",[247,3172,3174],{"className":280,"code":3173,"language":282,"meta":255,"style":255},"\u002F\u002F 客户端伪代码\nDWORD cookie = 0;\npConnectionPoint->Advise(pMySinkIUnknown, &cookie);\n\u002F\u002F ...\npConnectionPoint->Unadvise(cookie);\n",[56,3175,3176,3181,3186,3191,3195],{"__ignoreMap":255},[286,3177,3178],{"class":288,"line":289},[286,3179,3180],{},"\u002F\u002F 客户端伪代码\n",[286,3182,3183],{"class":288,"line":295},[286,3184,3185],{},"DWORD cookie = 0;\n",[286,3187,3188],{"class":288,"line":301},[286,3189,3190],{},"pConnectionPoint->Advise(pMySinkIUnknown, &cookie);\n",[286,3192,3193],{"class":288,"line":307},[286,3194,880],{},[286,3196,3197],{"class":288,"line":313},[286,3198,3199],{},"pConnectionPoint->Unadvise(cookie);\n",[14,3201,3202,3203,1315,3206,3209,3210,3213],{},"现实项目：",[18,3204,3205],{},"Office VBA 事件",[18,3207,3208],{},"IE WebBrowser 控件事件","、旧 ",[18,3211,3212],{},"MSXML"," 异步回调，底层常有连接点身影。",[39,3215],{},[42,3217,3219],{"id":3218},"二十idl-midl-类型库从文本接口到代理桩代码","二十、IDL \u002F MIDL \u002F 类型库：从文本接口到代理桩代码",[223,3221,3223],{"id":3222},"_201-idl-文件是什么","20.1 IDL 文件是什么？",[14,3225,3226,3229,3230,3233,3234,1315,3243,3247,3248,3253],{},[18,3227,3228],{},"接口定义语言（IDL）"," 描述：",[18,3231,3232],{},"UUID","、方法、参数方向 ",[18,3235,3236,65,3238,65,3240],{},[56,3237,2784],{},[56,3239,2768],{},[56,3241,3242],{},"[in,out]",[18,3244,3245],{},[56,3246,58],{}," 返回。 ",[18,3249,3250],{},[56,3251,3252],{},"midl.exe"," 编译生成：",[47,3255,3256,3264,3272],{},[50,3257,3258,3263],{},[18,3259,3260],{},[56,3261,3262],{},"xxx_h.h","：C\u002FC++ 声明；",[50,3265,3266,3271],{},[18,3267,3268],{},[56,3269,3270],{},"xxx_i.c","：IID 定义；",[50,3273,3274,3280],{},[18,3275,3276,3279],{},[56,3277,3278],{},"xxx_p.c"," \u002F dlldata.c","：代理\u002F桩（跨进程时需要）。",[223,3282,3284,3285],{"id":3283},"_202-类型库-tlb","20.2 类型库 ",[56,3286,3287],{},".tlb",[14,3289,3290,3295,3296,3301,3302,3305,3306,3311,3312,3317,3318,115],{},[18,3291,3292],{},[56,3293,3294],{},"MIDL \u002Ftlb"," 或 ",[18,3297,3298],{},[56,3299,3300],{},"LoadTypeLib"," 加载。",[18,3303,3304],{},"VBA \u002F .NET"," 早期常 ",[18,3307,3308],{},[56,3309,3310],{},"#import \"xxx.tlb\"","。手动浏览可用 ",[18,3313,3314],{},[56,3315,3316],{},"OleView.exe"," 打开 ",[18,3319,3320],{},"Type Library",[223,3322,3324,3325,3327],{"id":3323},"_203-import-生成-tlhtli-msvc","20.3 ",[56,3326,1416],{}," 生成 TLH\u002FTLI（ MSVC ）",[247,3329,3331],{"className":280,"code":3330,"language":282,"meta":255,"style":255},"#import \"C:\\\\path\\\\foo.dll\" named_guids raw_interfaces_only\n",[56,3332,3333],{"__ignoreMap":255},[286,3334,3335],{"class":288,"line":289},[286,3336,3330],{},[14,3338,3339,3340,3343,3344,3348,3349,3352],{},"会生成 ",[18,3341,3342],{},"智能指针包装","，也可能引入 ",[18,3345,3346],{},[56,3347,2641],{},"——大型团队往往 ",[18,3350,3351],{},"只 import 必要组件","，否则编译时间和命名污染可观。",[39,3354],{},[42,3356,3358,3359,101,3362,3365],{"id":3357},"二十一结构化存储istorage-istream复合文档","二十一、结构化存储：",[56,3360,3361],{},"IStorage",[56,3363,3364],{},"IStream","（复合文档）",[14,3367,3368,3369,3375,3376,3379,3380,3384,3385,3389,3390,1315,3393,3396,3397,3400],{},"老式 ",[18,3370,3371,3374],{},[56,3372,3373],{},".doc","（非 docx）","、部分安装包使用 ",[18,3377,3378],{},"结构化存储","：目录树 ",[18,3381,3382],{},[56,3383,3361],{},"，叶子二进制 ",[18,3386,3387],{},[56,3388,3364],{},"，全是 COM 接口。理解这一点有助于读 ",[18,3391,3392],{},"Office 自动化",[18,3394,3395],{},"拖放文件格式","。现代 ",[18,3398,3399],{},"OpenXML（docx\u002Fxlsx）"," 是 ZIP + XML，不靠这一套，但遗留系统仍在。",[39,3402],{},[42,3404,3406],{"id":3405},"二十二moniker名字即绑定入门只需印象","二十二、Moniker：名字即绑定（入门只需印象）",[14,3408,3409,3414,3415,1315,3418,1315,3421,115,3424,3429,3430,3435,3436,3439,3440,3443],{},[18,3410,3411],{},[56,3412,3413],{},"IMoniker"," 表示「可解析的名字」：",[18,3416,3417],{},"文件路径 moniker",[18,3419,3420],{},"反引用 moniker",[18,3422,3423],{},"复合 moniker",[18,3425,3426],{},[56,3427,3428],{},"BindToObject"," 一路解析到真正的 COM 对象。脚本 ",[18,3431,3432],{},[56,3433,3434],{},"GetObject(\"winmgmts:\\\\\\\\.\\\\root\\\\cimv2\")"," 本质也在玩 naming\u002Fbinding。初学不必深入 ",[18,3437,3438],{},"ROT（Running Object Table）","，知道 ",[18,3441,3442],{},"「名字→对象」这条链存在"," 即可。",[39,3445],{},[42,3447,3449],{"id":3448},"二十三dcom跨机器-com为什么你现在见得少","二十三、DCOM：跨机器 COM（为什么你现在见得少）",[14,3451,3452,3453,1315,3456,1315,3459,1315,3462,3465,3466,3469,3470,3473,3474,3477],{},"DCOM 依赖 ",[18,3454,3455],{},"RPC",[18,3457,3458],{},"防火墙端口",[18,3460,3461],{},"身份模拟级别（Impersonation）",[18,3463,3464],{},"Launch\u002FActivation 权限","。容器与微服务时代，很多团队改用 ",[18,3467,3468],{},"HTTP\u002FgRPC","，DCOM 多见于 ",[18,3471,3472],{},"遗留工业软件","。若遇见：",[18,3475,3476],{},"学会用 DCOMCNFG.EXE"," 看权限。",[39,3479],{},[42,3481,3483,3484,3487],{"id":3482},"二十四winrt-与经典-comiinspectable激活工厂","二十四、WinRT 与经典 COM：",[56,3485,3486],{},"IInspectable","、激活工厂",[14,3489,3490,3493,3494,3498,3499,3503,3504,529,3512,3517,3518,3521,3522,3525,3526,3531,3532,3536],{},[18,3491,3492],{},"Windows Runtime"," 接口继承 ",[18,3495,3496],{},[56,3497,3486],{},"（不是 ",[18,3500,3501],{},[56,3502,81],{}," 直接暴露给开发者那么简单）。",[18,3505,3506,101,3509],{},[56,3507,3508],{},"RoInitialize",[56,3510,3511],{},"RoActivateInstance",[18,3513,3514],{},[56,3515,3516],{},"Windows.Foundation.Uri"," 一类）背后仍有 ",[18,3519,3520],{},"COM RCW"," 思想的影子。",[18,3523,3524],{},"C++\u002FWinRT"," 生成的 ",[18,3527,3528],{},[56,3529,3530],{},"winrt::Windows::Foo"," 类型隐藏了 ",[18,3533,3534],{},[56,3535,260],{}," 细节。",[14,3538,3539],{},"对照表（口语）：",[421,3541,3542,3552],{},[424,3543,3544],{},[427,3545,3546,3549],{},[430,3547,3548],{},"经典 COM",[430,3550,3551],{},"WinRT 投影",[437,3553,3554,3569,3584],{},[427,3555,3556,3562],{},[442,3557,3558],{},[18,3559,3560],{},[56,3561,104],{},[442,3563,3564],{},[18,3565,3566,3568],{},[56,3567,3511],{}," \u002F get_activation_factory",[427,3570,3571,3577],{},[442,3572,3573],{},[18,3574,3575],{},[56,3576,81],{},[442,3578,3579],{},[18,3580,3581,3583],{},[56,3582,3486],{}," + 元数据（.winmd）",[427,3585,3586,3591],{},[442,3587,3588],{},[18,3589,3590],{},"注册表 CLSID",[442,3592,3593],{},[18,3594,3595],{},"打包清单 + 进程内激活",[39,3597],{},[42,3599,3601],{"id":3600},"二十五atl-画像为什么老牌-shell-扩展示例都爱用它","二十五、ATL 画像：为什么老牌 Shell 扩展示例都爱用它",[14,3603,3604,3607],{},[18,3605,3606],{},"Active Template Library（ATL）"," 提供：",[47,3609,3610,3623,3631,3647],{},[50,3611,3612,3617,3618,3622],{},[18,3613,3614],{},[56,3615,3616],{},"CComObjectRootEx","：线程模型与 ",[18,3619,3620],{},[56,3621,81],{}," 样板；",[50,3624,3625,3630],{},[18,3626,3627],{},[56,3628,3629],{},"CComCoClass","：类工厂胶水；",[50,3632,3633,3641,3642,3646],{},[18,3634,3635,101,3638],{},[56,3636,3637],{},"BEGIN_OBJECT_MAP",[56,3639,3640],{},"OBJECT_ENTRY_AUTO","：自动 ",[18,3643,3644],{},[56,3645,1990],{},"；",[50,3648,3649,3654,3655,115],{},[18,3650,3651],{},[56,3652,3653],{},"IDispatchImpl","：双接口 ",[18,3656,3657],{},[56,3658,1378],{},[14,3660,3661],{},"极简注册思路（示意，不是完整工程）：",[247,3663,3665],{"className":280,"code":3664,"language":282,"meta":255,"style":255},"class ATL_NO_VTABLE CMyMenu\n    : public CComObjectRootEx\u003CCComSingleThreadModel>,\n      public CComCoClass\u003CCMyMenu, &CLSID_MyMenu>,\n      public IShellExtInit,\n      public IContextMenu {\npublic:\n    DECLARE_REGISTRY_RESOURCEID(IDR_MYMENU)\n    BEGIN_COM_MAP(CMyMenu)\n        COM_INTERFACE_ENTRY(IShellExtInit)\n        COM_INTERFACE_ENTRY(IContextMenu)\n    END_COM_MAP()\n    \u002F\u002F ...\n};\n",[56,3666,3667,3672,3677,3682,3687,3692,3696,3701,3706,3711,3716,3721,3726],{"__ignoreMap":255},[286,3668,3669],{"class":288,"line":289},[286,3670,3671],{},"class ATL_NO_VTABLE CMyMenu\n",[286,3673,3674],{"class":288,"line":295},[286,3675,3676],{},"    : public CComObjectRootEx\u003CCComSingleThreadModel>,\n",[286,3678,3679],{"class":288,"line":301},[286,3680,3681],{},"      public CComCoClass\u003CCMyMenu, &CLSID_MyMenu>,\n",[286,3683,3684],{"class":288,"line":307},[286,3685,3686],{},"      public IShellExtInit,\n",[286,3688,3689],{"class":288,"line":313},[286,3690,3691],{},"      public IContextMenu {\n",[286,3693,3694],{"class":288,"line":374},[286,3695,1037],{},[286,3697,3698],{"class":288,"line":380},[286,3699,3700],{},"    DECLARE_REGISTRY_RESOURCEID(IDR_MYMENU)\n",[286,3702,3703],{"class":288,"line":386},[286,3704,3705],{},"    BEGIN_COM_MAP(CMyMenu)\n",[286,3707,3708],{"class":288,"line":1065},[286,3709,3710],{},"        COM_INTERFACE_ENTRY(IShellExtInit)\n",[286,3712,3713],{"class":288,"line":1071},[286,3714,3715],{},"        COM_INTERFACE_ENTRY(IContextMenu)\n",[286,3717,3718],{"class":288,"line":1077},[286,3719,3720],{},"    END_COM_MAP()\n",[286,3722,3723],{"class":288,"line":1083},[286,3724,3725],{},"    \u002F\u002F ...\n",[286,3727,3728],{"class":288,"line":1089},[286,3729,316],{},[14,3731,3732,3735,3736,3740,3741,3744,3745,3748,3749,3751],{},[18,3733,3734],{},"OBJECT_ENTRY_AUTO(CLSID_MyMenu, CMyMenu)"," 把「CLSID → C++ 类」塞进映射表，省去手写 ",[18,3737,3738],{},[56,3739,1990],{}," 分支。",[18,3742,3743],{},"初学","：可先 ",[18,3746,3747],{},"抄官方 Sample","，再对比「手写 ",[56,3750,260],{},"」版本，体会样板减少了什么。",[39,3753],{},[42,3755,3757,3758,3760,3761,2254],{"id":3756},"二十六shell-进阶从-idataobject-解析选中文件cf_hdrop","二十六、Shell 进阶：从 ",[56,3759,1672],{}," 解析选中文件（",[56,3762,3763],{},"CF_HDROP",[14,3765,3766,3767,115,3770,3775,3776,3781,3782,115],{},"右键菜单扩展真正有用时，多半要拿到 ",[18,3768,3769],{},"用户选中了哪些路径",[18,3771,3772],{},[56,3773,3774],{},"IShellExtInit::Initialize"," 给你 ",[18,3777,3778],{},[56,3779,3780],{},"IDataObject*","；常见格式 ",[18,3783,3784],{},[56,3785,3763],{},[247,3787,3789],{"className":280,"code":3788,"language":282,"meta":255,"style":255},"#include \u003Cshellapi.h>\n\nHRESULT ExtractFirstPath(IDataObject* pdtobj, std::wstring& outPath)\n{\n    FORMATETC fe = { CF_HDROP, nullptr, DVASPECT_CONTENT, -1, TYMED_HGLOBAL };\n    STGMEDIUM stg{};\n    HRESULT hr = pdtobj->GetData(&fe, &stg);\n    if (FAILED(hr))\n        return hr;\n\n    HDROP hDrop = static_cast\u003CHDROP>(GlobalLock(stg.hGlobal));\n    if (!hDrop) {\n        ReleaseStgMedium(&stg);\n        return E_FAIL;\n    }\n\n    wchar_t buf[MAX_PATH]{};\n    UINT n = DragQueryFileW(hDrop, 0xFFFFFFFF, nullptr, 0); \u002F\u002F 文件个数\n    if (n > 0) {\n        DragQueryFileW(hDrop, 0, buf, MAX_PATH);\n        outPath = buf;\n    }\n    GlobalUnlock(stg.hGlobal);\n    ReleaseStgMedium(&stg);\n    return S_OK;\n}\n",[56,3790,3791,3796,3800,3805,3809,3814,3819,3824,3829,3833,3837,3842,3847,3852,3857,3861,3865,3870,3875,3880,3885,3890,3894,3899,3904,3908],{"__ignoreMap":255},[286,3792,3793],{"class":288,"line":289},[286,3794,3795],{},"#include \u003Cshellapi.h>\n",[286,3797,3798],{"class":288,"line":295},[286,3799,1141],{"emptyLinePlaceholder":1140},[286,3801,3802],{"class":288,"line":301},[286,3803,3804],{},"HRESULT ExtractFirstPath(IDataObject* pdtobj, std::wstring& outPath)\n",[286,3806,3807],{"class":288,"line":307},[286,3808,356],{},[286,3810,3811],{"class":288,"line":313},[286,3812,3813],{},"    FORMATETC fe = { CF_HDROP, nullptr, DVASPECT_CONTENT, -1, TYMED_HGLOBAL };\n",[286,3815,3816],{"class":288,"line":374},[286,3817,3818],{},"    STGMEDIUM stg{};\n",[286,3820,3821],{"class":288,"line":380},[286,3822,3823],{},"    HRESULT hr = pdtobj->GetData(&fe, &stg);\n",[286,3825,3826],{"class":288,"line":386},[286,3827,3828],{},"    if (FAILED(hr))\n",[286,3830,3831],{"class":288,"line":1065},[286,3832,1080],{},[286,3834,3835],{"class":288,"line":1071},[286,3836,1141],{"emptyLinePlaceholder":1140},[286,3838,3839],{"class":288,"line":1077},[286,3840,3841],{},"    HDROP hDrop = static_cast\u003CHDROP>(GlobalLock(stg.hGlobal));\n",[286,3843,3844],{"class":288,"line":1083},[286,3845,3846],{},"    if (!hDrop) {\n",[286,3848,3849],{"class":288,"line":1089},[286,3850,3851],{},"        ReleaseStgMedium(&stg);\n",[286,3853,3854],{"class":288,"line":1095},[286,3855,3856],{},"        return E_FAIL;\n",[286,3858,3859],{"class":288,"line":1756},[286,3860,1086],{},[286,3862,3863],{"class":288,"line":1762},[286,3864,1141],{"emptyLinePlaceholder":1140},[286,3866,3867],{"class":288,"line":1768},[286,3868,3869],{},"    wchar_t buf[MAX_PATH]{};\n",[286,3871,3872],{"class":288,"line":1774},[286,3873,3874],{},"    UINT n = DragQueryFileW(hDrop, 0xFFFFFFFF, nullptr, 0); \u002F\u002F 文件个数\n",[286,3876,3877],{"class":288,"line":1780},[286,3878,3879],{},"    if (n > 0) {\n",[286,3881,3882],{"class":288,"line":1786},[286,3883,3884],{},"        DragQueryFileW(hDrop, 0, buf, MAX_PATH);\n",[286,3886,3887],{"class":288,"line":1792},[286,3888,3889],{},"        outPath = buf;\n",[286,3891,3892],{"class":288,"line":1797},[286,3893,1086],{},[286,3895,3896],{"class":288,"line":1803},[286,3897,3898],{},"    GlobalUnlock(stg.hGlobal);\n",[286,3900,3901],{"class":288,"line":1809},[286,3902,3903],{},"    ReleaseStgMedium(&stg);\n",[286,3905,3906],{"class":288,"line":1815},[286,3907,2116],{},[286,3909,3910],{"class":288,"line":1821},[286,3911,389],{},[14,3913,3914,828],{},[18,3915,3916],{},"解释",[47,3918,3919,3927,3942],{},[50,3920,3921,3926],{},[18,3922,3923],{},[56,3924,3925],{},"FORMATETC","：描述「我要哪一种剪贴板格式」。",[50,3928,3929,3934,3935,3941],{},[18,3930,3931],{},[56,3932,3933],{},"STGMEDIUM","：实际介质可能是 ",[18,3936,3937,3938],{},"全局内存 ",[56,3939,3940],{},"HGLOBAL","、流、存储等。",[50,3943,3944,3949,3950,115],{},[18,3945,3946],{},[56,3947,3948],{},"DragQueryFile","：经典 Shell API，读 ",[18,3951,3952],{},[56,3953,3954],{},"HDROP",[14,3956,3957,3958,3964,3965,3970,3971,101,3976,3981],{},"若要多选文件：",[18,3959,3960,3961],{},"循环 ",[56,3962,3963],{},"DragQueryFileW(hDrop, index, ...)","。若 ",[18,3966,3967,3969],{},[56,3968,3763],{}," 不存在","（某些虚拟对象），可能要 ",[18,3972,3973],{},[56,3974,3975],{},"IShellItemArray",[18,3977,3978],{},[56,3979,3980],{},"CFSTR_SHELLIDLIST","——那是下一档难度。",[39,3983],{},[42,3985,3987,3988,3990],{"id":3986},"二十七拖拽再放dodragdrop-双方在干什么","二十七、拖拽再放：",[56,3989,2232],{}," 双方在干什么？",[14,3992,3993,3996,3997,1498,4001,4005,4006,4013,4014,4017,3996,4020,828,4024,115,4035,4037,4041,4042,4045],{},[18,3994,3995],{},"源侧","：实现 ",[18,3998,3999],{},[56,4000,2224],{},[18,4002,4003],{},[56,4004,1672],{},"（提供 ",[18,4007,4008,65,4011],{},[56,4009,4010],{},"CF_TEXT",[56,4012,3763],{}," 等）。",[4015,4016],"br",{},[18,4018,4019],{},"目标侧",[18,4021,4022],{},[56,4023,2214],{},[18,4025,4026,65,4029,65,4032],{},[56,4027,4028],{},"DragEnter",[56,4030,4031],{},"DragOver",[56,4033,4034],{},"Drop",[4015,4036],{},[18,4038,4039],{},[56,4040,2232],{}," 内部跑消息循环协调——全是 COM。写过 ",[18,4043,4044],{},"桌面拖拽上传"," 的同学，其实已经做过半个 COM 教程。",[39,4047],{},[42,4049,4051,4052,4055],{"id":4050},"二十八safearrayvbvba-数组如何越过边界","二十八、",[56,4053,4054],{},"SAFEARRAY","：VB\u002FVBA 数组如何越过边界",[14,4057,4058,4059,115,4063,4077,4078,4080,4081,4084,4085,4090,4091,4095],{},"自动化接口有时参数是 ",[18,4060,4061],{},[56,4062,4054],{},[18,4064,4065,101,4068,101,4071,101,4074],{},[56,4066,4067],{},"SafeArrayCreate",[56,4069,4070],{},"SafeArrayAccessData",[56,4072,4073],{},"SafeArrayUnaccessData",[56,4075,4076],{},"SafeArrayDestroy","。跨语言边界传数组几乎必碰。",[18,4079,3099],{},"：优先用 ",[18,4082,4083],{},"封装库","；手写时注意 ",[18,4086,4087],{},[56,4088,4089],{},"VT_ARRAY | VT_UI1"," 等与 ",[18,4092,4093],{},[56,4094,1216],{}," 的组合。",[39,4097],{},[42,4099,4101],{"id":4100},"二十九长篇-faq摘录","二十九、长篇 FAQ（摘录）",[14,4103,4104,4110,4112,4113,4116,4117,4120,4121,4129],{},[18,4105,4106,4107,4109],{},"问：我是不是每个线程都要 ",[56,4108,100],{},"？",[4015,4111],{},"\n答：",[18,4114,4115],{},"STA\u002FMTA 以线程为单位","。工作线程要用 COM，就在该线程初始化（或专门搞 ",[18,4118,4119],{},"COM 线程"," 其它线程 ",[18,4122,4123,65,4126],{},[56,4124,4125],{},"PostMessage",[56,4127,4128],{},"QueueUserAPC"," 转发）。",[14,4131,4132,4142,4112,4144,4149,4150,115],{},[18,4133,4134,4135,4138,4139,4141],{},"问：",[56,4136,4137],{},"CoInitialize(nullptr)"," 和 ",[56,4140,100],{}," 区别？",[4015,4143],{},[18,4145,4146],{},[56,4147,4148],{},"CoInitialize"," 等价旧版默认 STA；新项目请 ",[18,4151,4152,4153],{},"显式 ",[56,4154,100],{},[14,4156,4157,4163,4112,4165,4168],{},[18,4158,4159,4160,4162],{},"问：我能在线程池里随便 ",[56,4161,104],{}," 吗？",[4015,4164],{},[18,4166,4167],{},"可以，但要与该线程的 COM 初始化一致","；对象是否线程安全看文档。",[14,4170,4171,4174,4176,4177,1315,4180,1315,4182,1315,4185,4188,4189,4192],{},[18,4172,4173],{},"问：右键菜单扩展注册了为什么不显示？",[4015,4175],{},"\n答：检查 ",[18,4178,4179],{},"位数（x64\u002Fx86）",[18,4181,528],{},[18,4183,4184],{},"regsvr32 是否成功",[18,4186,4187],{},"HKCR 权限","、是否被 ",[18,4190,4191],{},"Shell 缓存","（有时需重启 explorer）。",[14,4194,4195,4198,4112,4200,4203,4204,4207,4208,4211],{},[18,4196,4197],{},"问：COM 和 .NET 互操作？",[4015,4199],{},[18,4201,4202],{},"RCW","（.NET 调 COM）、",[18,4205,4206],{},"CCW","（COM 调 .NET）。",[18,4209,4210],{},"Marshal.ReleaseComObject"," 坑极多——官方文档专门警告。",[14,4213,4214,4217,4112,4219,4222,4223,4227,4228,4231],{},[18,4215,4216],{},"问：WinRT 还要学 COM 吗？",[4015,4218],{},[18,4220,4221],{},"底层原理相通","；日常 WinRT 开发可少碰 ",[18,4224,4225],{},[56,4226,81],{},"，但 ",[18,4229,4230],{},"排查激活失败 \u002F 权限 \u002F ABI"," 时经典 COM 知识仍值钱。",[39,4233],{},[42,4235,4237],{"id":4236},"三十术语表扩展","三十、术语表（扩展）",[421,4239,4240,4250],{},[424,4241,4242],{},[427,4243,4244,4247],{},[430,4245,4246],{},"术语",[430,4248,4249],{},"一句话",[437,4251,4252,4262,4272,4282,4292,4302],{},[427,4253,4254,4259],{},[442,4255,4256],{},[18,4257,4258],{},"Proxy\u002FStub",[442,4260,4261],{},"跨 apartment \u002F 进程时的替身接口",[427,4263,4264,4269],{},[442,4265,4266],{},[18,4267,4268],{},"Marshalling",[442,4270,4271],{},"把参数打包交给另一边",[427,4273,4274,4279],{},[442,4275,4276],{},[18,4277,4278],{},"ROT",[442,4280,4281],{},"Running Object Table：运行中对象登记簿",[427,4283,4284,4289],{},[442,4285,4286],{},[18,4287,4288],{},"APTTYPE",[442,4290,4291],{},"Apartment 类型枚举（调试诊断）",[427,4293,4294,4299],{},[442,4295,4296],{},[18,4297,4298],{},"STA thread",[442,4300,4301],{},"消息泵线程模型下的 COM 线程",[427,4303,4304,4309],{},[442,4305,4306],{},[18,4307,4308],{},"Dual interface",[442,4310,4311,4312,4316,4317],{},"继承 ",[18,4313,4314],{},[56,4315,1378],{}," 又可直接 ",[18,4318,4319,4322],{},[56,4320,4321],{},"vtable"," 调用的接口",[39,4324],{},[42,4326,4328],{"id":4327},"三十一可以自己动手的小实验不靠作业打分","三十一、可以自己动手的小实验（不靠作业打分）",[155,4330,4331,4340,4349,4368],{},[50,4332,4333,4334,4339],{},"写最小 ",[18,4335,4336],{},[56,4337,4338],{},"CoCreateInstance(CLSID_FileOpenDialog, IID_IFileOpenDialog)"," 弹出文件框（Shell COM）。",[50,4341,4342,4345,4346,4348],{},[18,4343,4344],{},"OleView"," 打开任意已安装 ",[18,4347,3320],{},"，截图接口树。",[50,4350,4351,4352,4355,4356,4361,4362,4367],{},"虚拟机里注册一个 ",[18,4353,4354],{},"假 CLSID"," 指向 ",[18,4357,4358],{},[56,4359,4360],{},"notepad.exe"," 作为 ",[18,4363,4364],{},[56,4365,4366],{},"LocalServer32","（极端实验，勿在生产机乱搞）。",[50,4369,4370,4371,4376,4377,4380,4381,115],{},"读 ",[18,4372,4373],{},[56,4374,4375],{},"sdkdi"," 示例：",[18,4378,4379],{},"GitHub microsoft\u002FWindows-classic-samples"," 里的 ",[18,4382,4383],{},"ShellExtensions",[39,4385],{},[10,4387,4389],{"id":4388},"第三辑仍可以继续往下钻线程注册表工厂模板排障剧本","第三辑：仍可以继续往下钻（线程、注册表、工厂模板、排障剧本）",[14,4391,4392,4393,4396,4397,3295,4400,4403],{},"这一辑的目标是：",[18,4394,4395],{},"把前面打散的知识点收成几张「可随时翻阅」的工作表","——尤其是你要真做一个 ",[18,4398,4399],{},"Inproc Shell 扩展",[18,4401,4402],{},"Office 自动化服务"," 时，脑子里得有路线图。",[39,4405],{},[42,4407,4409],{"id":4408},"三十三线程模型把它当成并发契约而不是玄学","三十三、线程模型：把它当成「并发契约」而不是玄学",[223,4411,4413],{"id":4412},"_331-三个常用-apartment","33.1 三个常用 Apartment",[421,4415,4416,4429],{},[424,4417,4418],{},[427,4419,4420,4422,4426],{},[430,4421,532],{},[430,4423,4424],{},[56,4425,100],{},[430,4427,4428],{},"典型宿主",[437,4430,4431,4451,4467],{},[427,4432,4433,4438,4445],{},[442,4434,4435],{},[18,4436,4437],{},"STA",[442,4439,4440],{},[18,4441,4442],{},[56,4443,4444],{},"COINIT_APARTMENTTHREADED",[442,4446,4447,4448],{},"UI 线程、旧 Shell 扩展、",[56,4449,4450],{},"ThreadingModel=Apartment",[427,4452,4453,4458,4464],{},[442,4454,4455],{},[18,4456,4457],{},"MTA",[442,4459,4460],{},[18,4461,4462],{},[56,4463,833],{},[442,4465,4466],{},"控制台后台线程、你自己保证线程安全的对象",[427,4468,4469,4473,4480],{},[442,4470,4471],{},[18,4472,541],{},[442,4474,4475,4476],{},"少用直接初始化；对象标注 ",[18,4477,4478],{},[56,4479,541],{},[442,4481,4482],{},"介于两者之间，避免频繁切换",[223,4484,4486],{"id":4485},"_332-指针能不能传给别的线程决策树简化","33.2 「指针能不能传给别的线程？」决策树（简化）",[247,4488,4491],{"className":4489,"code":4490,"language":252},[250],"手里有接口指针 p\n├─ 同线程继续用？──通常 OK（仍要看对象文档）\n├─ 要传给 worker 线程？\n│   ├─ 对象是 apartment-neutral \u002F free-threaded？──文档说可以就直接传\n│   └─ 不确定？──CoMarshalInterThreadInterfaceInStream \u002F CoGetInterfaceAndReleaseStream\n│       （初学阶段：**干脆在同一线程 CoCreate**，别到处甩指针）\n└─ 跨进程？──必须 marshal \u002F 换 CLSCTX remoting\n",[56,4492,4490],{"__ignoreMap":255},[14,4494,4495,828,4498,4501,4502,4505,4506,4505,4509,115],{},[18,4496,4497],{},"初学者口诀",[18,4499,4500],{},"不确定就别跨线程裸传 COM 接口指针","；要么 ",[18,4503,4504],{},"文档写明","，要么 ",[18,4507,4508],{},"在同一线程创建并使用",[18,4510,4511],{},"学 marshal API",[223,4513,4515,4516,4519],{"id":4514},"_333-comarshalinterthreadinterfaceinstream只记用途","33.3 ",[56,4517,4518],{},"CoMarshalInterThreadInterfaceInStream","（只记用途）",[14,4521,4522,4523,4530,4531,4534,4535,4540,4541,4544,4545,4548],{},"当你 ",[18,4524,4525,4526,4529],{},"必须把 ",[56,4527,4528],{},"IAudioClient*"," 从线程 A 交到线程 B","，且两边 apartment 不同，规范上要 ",[18,4532,4533],{},"marshal","。音频采集线程我们自己 ",[18,4536,4537,4539],{},[56,4538,100],{}," + GetService","，就是为了 ",[18,4542,4543],{},"避免把 apartment 对象到处迁移","。这也是许多开源项目「",[18,4546,4547],{},"COM 线程专门一根","」的原因。",[39,4550],{},[42,4552,4554,4555,4557],{"id":4553},"三十四threadingmodel-注册表值到底写给谁看","三十四、",[56,4556,528],{}," 注册表值到底写给谁看？",[14,4559,4560,4561,828],{},"位于 ",[18,4562,4563],{},[56,4564,4565],{},"HKCR\\CLSID\\{...}\\InprocServer32\\ThreadingModel",[421,4567,4568,4578],{},[424,4569,4570],{},[427,4571,4572,4575],{},[430,4573,4574],{},"值",[430,4576,4577],{},"含义（口语）",[437,4579,4580,4591,4605,4616],{},[427,4581,4582,4588],{},[442,4583,4584],{},[18,4585,4586],{},[56,4587,532],{},[442,4589,4590],{},"偏 STA；实例可能被 STA 线程创建",[427,4592,4593,4599],{},[442,4594,4595],{},[18,4596,4597],{},[56,4598,535],{},[442,4600,4601,4602,2254],{},"偏 MTA（老式称呼 ",[18,4603,4604],{},"free-threaded",[427,4606,4607,4613],{},[442,4608,4609],{},[18,4610,4611],{},[56,4612,538],{},[442,4614,4615],{},"STA\u002FMTA 都可创建；对象自己要线程安全",[427,4617,4618,4624],{},[442,4619,4620],{},[18,4621,4622],{},[56,4623,541],{},[442,4625,4626],{},".NET 时代常用",[14,4628,4629,4632,4633,4636],{},[18,4630,4631],{},"Shell 扩展默认 Apartment","：因为你的 DLL 跟资源管理器 STA 线程在同一个进程房间活动时更安全。",[18,4634,4635],{},"不要乱改成 Free","，除非你熟知后果并在对象内部加锁。",[39,4638],{},[42,4640,4642,4643,4645],{"id":4641},"三十五手写-iclassfactory最小可讲故事版本","三十五、手写 ",[56,4644,975],{},"：最小可讲故事版本",[14,4647,1577,4648,4651,4652,1991,4654,4659,4660,3048,4664,115],{},[18,4649,4650],{},"不是 ATL","，纯粹手写心理模型：",[18,4653,1990],{},[18,4655,4656],{},[56,4657,4658],{},"CFactory","，",[18,4661,4662],{},[56,4663,2001],{},[18,4665,4666,4668],{},[56,4667,984],{}," 出真正的菜单对象",[247,4670,4672],{"className":280,"code":4671,"language":282,"meta":255,"style":255},"class CFactory : public IClassFactory {\n    LONG refCount_{ 1 };\n\npublic:\n    HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void** ppv) override {\n        if (!ppv) return E_POINTER;\n        if (IsEqualIID(riid, IID_IUnknown) || IsEqualIID(riid, IID_IClassFactory)) {\n            *ppv = static_cast\u003CIClassFactory*>(this);\n            AddRef();\n            return S_OK;\n        }\n        *ppv = nullptr;\n        return E_NOINTERFACE;\n    }\n    ULONG STDMETHODCALLTYPE AddRef() override { return InterlockedIncrement(&refCount_); }\n    ULONG STDMETHODCALLTYPE Release() override {\n        ULONG c = InterlockedDecrement(&refCount_);\n        if (!c) delete this;\n        return c;\n    }\n\n    HRESULT STDMETHODCALLTYPE CreateInstance(IUnknown* pOuter, REFIID riid, void** ppv) override {\n        if (pOuter)\n            return CLASS_E_NOAGGREGATION;\n        auto* obj = new(std::nothrow) CMyContextMenu();\n        if (!obj) return E_OUTOFMEMORY;\n        HRESULT hr = obj->QueryInterface(riid, ppv);\n        obj->Release(); \u002F\u002F 交给 QI 后的引用计数管理对象生命周期\n        return hr;\n    }\n\n    HRESULT STDMETHODCALLTYPE LockServer(BOOL fLock) override {\n        \u002F\u002F g_serverLocks++ \u002F -- ; DllCanUnloadNow 用\n        return S_OK;\n    }\n};\n",[56,4673,4674,4679,4684,4688,4692,4696,4701,4706,4711,4716,4721,4725,4730,4735,4739,4744,4748,4753,4757,4761,4765,4769,4773,4778,4782,4787,4791,4795,4800,4804,4808,4812,4817,4822,4826,4830],{"__ignoreMap":255},[286,4675,4676],{"class":288,"line":289},[286,4677,4678],{},"class CFactory : public IClassFactory {\n",[286,4680,4681],{"class":288,"line":295},[286,4682,4683],{},"    LONG refCount_{ 1 };\n",[286,4685,4686],{"class":288,"line":301},[286,4687,1141],{"emptyLinePlaceholder":1140},[286,4689,4690],{"class":288,"line":307},[286,4691,1037],{},[286,4693,4694],{"class":288,"line":313},[286,4695,1724],{},[286,4697,4698],{"class":288,"line":374},[286,4699,4700],{},"        if (!ppv) return E_POINTER;\n",[286,4702,4703],{"class":288,"line":380},[286,4704,4705],{},"        if (IsEqualIID(riid, IID_IUnknown) || IsEqualIID(riid, IID_IClassFactory)) {\n",[286,4707,4708],{"class":288,"line":386},[286,4709,4710],{},"            *ppv = static_cast\u003CIClassFactory*>(this);\n",[286,4712,4713],{"class":288,"line":1065},[286,4714,4715],{},"            AddRef();\n",[286,4717,4718],{"class":288,"line":1071},[286,4719,4720],{},"            return S_OK;\n",[286,4722,4723],{"class":288,"line":1077},[286,4724,1777],{},[286,4726,4727],{"class":288,"line":1083},[286,4728,4729],{},"        *ppv = nullptr;\n",[286,4731,4732],{"class":288,"line":1089},[286,4733,4734],{},"        return E_NOINTERFACE;\n",[286,4736,4737],{"class":288,"line":1095},[286,4738,1086],{},[286,4740,4741],{"class":288,"line":1756},[286,4742,4743],{},"    ULONG STDMETHODCALLTYPE AddRef() override { return InterlockedIncrement(&refCount_); }\n",[286,4745,4746],{"class":288,"line":1762},[286,4747,1806],{},[286,4749,4750],{"class":288,"line":1768},[286,4751,4752],{},"        ULONG c = InterlockedDecrement(&refCount_);\n",[286,4754,4755],{"class":288,"line":1774},[286,4756,1818],{},[286,4758,4759],{"class":288,"line":1780},[286,4760,1824],{},[286,4762,4763],{"class":288,"line":1786},[286,4764,1086],{},[286,4766,4767],{"class":288,"line":1792},[286,4768,1141],{"emptyLinePlaceholder":1140},[286,4770,4771],{"class":288,"line":1797},[286,4772,1042],{},[286,4774,4775],{"class":288,"line":1803},[286,4776,4777],{},"        if (pOuter)\n",[286,4779,4780],{"class":288,"line":1809},[286,4781,1052],{},[286,4783,4784],{"class":288,"line":1815},[286,4785,4786],{},"        auto* obj = new(std::nothrow) CMyContextMenu();\n",[286,4788,4789],{"class":288,"line":1821},[286,4790,1062],{},[286,4792,4793],{"class":288,"line":1827},[286,4794,1068],{},[286,4796,4797],{"class":288,"line":1832},[286,4798,4799],{},"        obj->Release(); \u002F\u002F 交给 QI 后的引用计数管理对象生命周期\n",[286,4801,4802],{"class":288,"line":1837},[286,4803,1080],{},[286,4805,4806],{"class":288,"line":1843},[286,4807,1086],{},[286,4809,4810],{"class":288,"line":1849},[286,4811,1141],{"emptyLinePlaceholder":1140},[286,4813,4814],{"class":288,"line":1855},[286,4815,4816],{},"    HRESULT STDMETHODCALLTYPE LockServer(BOOL fLock) override {\n",[286,4818,4819],{"class":288,"line":1861},[286,4820,4821],{},"        \u002F\u002F g_serverLocks++ \u002F -- ; DllCanUnloadNow 用\n",[286,4823,4824],{"class":288,"line":1866},[286,4825,1789],{},[286,4827,4828],{"class":288,"line":1871},[286,4829,1086],{},[286,4831,4832],{"class":288,"line":1876},[286,4833,316],{},[14,4835,4836,4846,4848,4849,4860,4861,1301,4865,4870,4871,4877,4878,4881],{},[18,4837,4838,4839,4841,4842,4845],{},"为什么 ",[56,4840,2001],{}," 里 ",[56,4843,4844],{},"obj->Release()"," 还能活？",[4015,4847],{},"\n因为 ",[18,4850,4851,4853,4854,4856,4857],{},[56,4852,260],{}," 已成功 ",[56,4855,335],{}," 了一份给 ",[56,4858,4859],{},"*ppv","；这里的 ",[18,4862,4863],{},[56,4864,405],{},[18,4866,4867,4869],{},[56,4868,984],{}," 出来时的初始引用","——具体计数约定要与你的 ",[18,4872,4873,4876],{},[56,4874,4875],{},"CMyContextMenu"," 构造函数 \u002F CComObject 风格","一致。",[18,4879,4880],{},"ATL 帮你把这些算盘打好","，手写就要特别小心。",[39,4883],{},[42,4885,4887],{"id":4886},"三十六shell-扩展注册你要改的键不止一条清单","三十六、Shell 扩展注册：你要改的键不止一条（清单）",[14,4889,4890],{},"做一个「对所有文件右键」的菜单，至少要心里有数：",[155,4892,4893,4901,4915,4925,4939],{},[50,4894,4895,4900],{},[18,4896,4897],{},[56,4898,4899],{},"HKCR\\CLSID\\{GUID}","：友好名称。",[50,4902,4903,828,4908,4659,4911,115],{},[18,4904,4905],{},[56,4906,4907],{},"HKCR\\CLSID\\{GUID}\\InprocServer32",[56,4909,4910],{},"默认 = 完整路径.dll",[18,4912,4913],{},[56,4914,528],{},[50,4916,4917,828,4922,115],{},[18,4918,4919],{},[56,4920,4921],{},"HKCR\\*\\shellex\\ContextMenuHandlers\\YourName",[56,4923,4924],{},"默认 = {GUID}",[50,4926,4927,4928,1315,4933,4938],{},"（可选）",[18,4929,4930],{},[56,4931,4932],{},"NoRecentDocs",[18,4934,4935],{},[56,4936,4937],{},"Applet"," 等高级键——查官方文档。",[50,4940,4941,828,4944,4948],{},[18,4942,4943],{},"卸载",[18,4945,4946],{},[56,4947,1108],{}," 必须对称删除。",[14,4950,4951,4952,4957,4958,4963,4964,4968,4969,4974,4975,4978],{},"只想对 ",[18,4953,4954],{},[56,4955,4956],{},".txt","：把 ",[18,4959,4960],{},[56,4961,4962],{},"*"," 换成 ",[18,4965,4966],{},[56,4967,4956],{}," 下的 ",[18,4970,4971],{},[56,4972,4973],{},"ShellEx"," 路径（具体布局随文档版本略有出入，以 ",[18,4976,4977],{},"当前 MSDN「Shell Context Menu Handler」"," 为准）。",[14,4980,4981,828,4983,4986,4987,1408,4990,4993,4994,4997,4998,5001],{},[18,4982,3043],{},[18,4984,4985],{},"纯 64 位 Windows"," 上 ",[18,4988,4989],{},"64 位 explorer",[18,4991,4992],{},"必须注册到 64 位视图","；若错误用了 ",[18,4995,4996],{},"WoW64 重定向","，会出现「",[18,4999,5000],{},"regsvr32 成功但不加载","」灵异事件。",[39,5003],{},[42,5005,5007,5008,5010],{"id":5006},"三十七iexplorercommand新-shell-扩展范式概念","三十七、",[56,5009,1507],{},"：新 Shell 扩展范式（概念）",[14,5012,5013,5014,5018,5019,3295,5022,5025,5026,5030,5031,1315,5034,5037,5038,5041,5042,5045,5046,5049],{},"Windows 10 1809 以后官方推动 ",[18,5015,5016],{},[56,5017,1507],{},"：**实现接口 → 注册 AppID \u002F 关联 ",[56,5020,5021],{},".exe",[18,5023,5024],{},"打包清单","，而不是老式纯 ",[18,5027,5028],{},[56,5029,3053],{},"。优点是 ",[18,5032,5033],{},"Package identity",[18,5035,5036],{},"稀疏包","、与 ",[18,5039,5040],{},"资源管理器现代化命令栏"," 对齐。你若看见示例工程一大坨 ",[18,5043,5044],{},"XML manifest","，不要慌：",[18,5047,5048],{},"COM 仍是内核","，注册方式是外层包装进化。",[39,5051],{},[42,5053,5055,5056,5059],{"id":5054},"三十八office-自动化再往深处走一步workbooksopen-的参数数组","三十八、Office 自动化再往深处走一步：",[56,5057,5058],{},"Workbooks.Open"," 的参数数组",[14,5061,5062,5063,5068],{},"真实脚本几乎总要 ",[18,5064,5065,5067],{},[56,5066,1216],{}," 数组 + 命名参数","。伪代码思路：",[247,5070,5072],{"className":280,"code":5071,"language":282,"meta":255,"style":255},"VARIANT args[4]{};\nargs[0].vt = VT_BSTR; args[0].bstrVal = SysAllocString(L\"C:\\\\book.xlsx\");\n\u002F\u002F … Row 读之类 ...\nDISPPARAMS dp{};\ndp.rgvarg = args;\ndp.cArgs = 4;\n\u002F\u002F Invoke DISPID 对应 Open… \n",[56,5073,5074,5079,5084,5089,5093,5098,5103],{"__ignoreMap":255},[286,5075,5076],{"class":288,"line":289},[286,5077,5078],{},"VARIANT args[4]{};\n",[286,5080,5081],{"class":288,"line":295},[286,5082,5083],{},"args[0].vt = VT_BSTR; args[0].bstrVal = SysAllocString(L\"C:\\\\book.xlsx\");\n",[286,5085,5086],{"class":288,"line":301},[286,5087,5088],{},"\u002F\u002F … Row 读之类 ...\n",[286,5090,5091],{"class":288,"line":307},[286,5092,1465],{},[286,5094,5095],{"class":288,"line":313},[286,5096,5097],{},"dp.rgvarg = args;\n",[286,5099,5100],{"class":288,"line":374},[286,5101,5102],{},"dp.cArgs = 4;\n",[286,5104,5105],{"class":288,"line":380},[286,5106,5107],{},"\u002F\u002F Invoke DISPID 对应 Open…\n",[14,5109,5110,828,5113,5124],{},[18,5111,5112],{},"教训",[18,5114,5115,5116,101,5118,5120,5121],{},"谁分配 ",[56,5117,1224],{},[56,5119,4054],{},"，谁 ",[56,5122,5123],{},"VariantClear","；循环引用与大数组泄漏自动化客户端内存，是工业软件常见 bug。",[39,5126],{},[42,5128,5130],{"id":5129},"三十九wmicom-仍在后台等你","三十九、WMI：COM 仍在后台等你",[247,5132,5134],{"className":280,"code":5133,"language":282,"meta":255,"style":255},"IWbemLocator* pLoc = nullptr;\nCoCreateInstance(CLSID_WbemLocator, nullptr, CLSCTX_INPROC_SERVER,\n    IID_IWbemLocator, reinterpret_cast\u003Cvoid**>(&pLoc));\n\u002F\u002F IWbemServices *pSvc … ConnectServer(\"ROOT\\\\CIMV2\") … ExecQuery(\"SELECT … FROM Win32_Process\") …\n",[56,5135,5136,5141,5146,5151],{"__ignoreMap":255},[286,5137,5138],{"class":288,"line":289},[286,5139,5140],{},"IWbemLocator* pLoc = nullptr;\n",[286,5142,5143],{"class":288,"line":295},[286,5144,5145],{},"CoCreateInstance(CLSID_WbemLocator, nullptr, CLSCTX_INPROC_SERVER,\n",[286,5147,5148],{"class":288,"line":301},[286,5149,5150],{},"    IID_IWbemLocator, reinterpret_cast\u003Cvoid**>(&pLoc));\n",[286,5152,5153],{"class":288,"line":307},[286,5154,5155],{},"\u002F\u002F IWbemServices *pSvc … ConnectServer(\"ROOT\\\\CIMV2\") … ExecQuery(\"SELECT … FROM Win32_Process\") …\n",[14,5157,5158,828,5161,5165,5166,3646,5170,5173,5174,5180],{},[18,5159,5160],{},"要点",[18,5162,5163],{},[56,5164,2253],{}," 也要 ",[18,5167,5168],{},[56,5169,405],{},[18,5171,5172],{},"查询返回枚举器","也是 COM。",[18,5175,5176,5177],{},"PowerShell ",[56,5178,5179],{},"Get-WmiObject"," 替你包了这些。",[39,5182],{},[42,5184,5186,5187],{"id":5185},"四十结构化存储十几行代码摸一下-istorage","四十、结构化存储：十几行代码摸一下 ",[56,5188,3361],{},[247,5190,5192],{"className":280,"code":5191,"language":282,"meta":255,"style":255},"#include \u003Cobjbase.h>\n#pragma comment(lib, \"ole32.lib\")\n\nHRESULT DemoStorage()\n{\n    IStorage* pStg = nullptr;\n    HRESULT hr = StgOpenStorage(L\"C:\\\\temp\\\\legacy.doc\", nullptr,\n        STGM_READWRITE | STGM_SHARE_EXCLUSIVE, nullptr, 0, &pStg);\n    if (FAILED(hr)) return hr;\n    \u002F\u002F EnumElements \u002F OpenStorage \u002F OpenStream …\n    pStg->Release();\n    return S_OK;\n}\n",[56,5193,5194,5199,5204,5208,5213,5217,5222,5227,5232,5237,5242,5247,5251],{"__ignoreMap":255},[286,5195,5196],{"class":288,"line":289},[286,5197,5198],{},"#include \u003Cobjbase.h>\n",[286,5200,5201],{"class":288,"line":295},[286,5202,5203],{},"#pragma comment(lib, \"ole32.lib\")\n",[286,5205,5206],{"class":288,"line":301},[286,5207,1141],{"emptyLinePlaceholder":1140},[286,5209,5210],{"class":288,"line":307},[286,5211,5212],{},"HRESULT DemoStorage()\n",[286,5214,5215],{"class":288,"line":313},[286,5216,356],{},[286,5218,5219],{"class":288,"line":374},[286,5220,5221],{},"    IStorage* pStg = nullptr;\n",[286,5223,5224],{"class":288,"line":380},[286,5225,5226],{},"    HRESULT hr = StgOpenStorage(L\"C:\\\\temp\\\\legacy.doc\", nullptr,\n",[286,5228,5229],{"class":288,"line":386},[286,5230,5231],{},"        STGM_READWRITE | STGM_SHARE_EXCLUSIVE, nullptr, 0, &pStg);\n",[286,5233,5234],{"class":288,"line":1065},[286,5235,5236],{},"    if (FAILED(hr)) return hr;\n",[286,5238,5239],{"class":288,"line":1071},[286,5240,5241],{},"    \u002F\u002F EnumElements \u002F OpenStorage \u002F OpenStream …\n",[286,5243,5244],{"class":288,"line":1077},[286,5245,5246],{},"    pStg->Release();\n",[286,5248,5249],{"class":288,"line":1083},[286,5250,2116],{},[286,5252,5253],{"class":288,"line":1089},[286,5254,389],{},[14,5256,5257,5258,5261,5262,115],{},"失败不要紧——重要的是你知道：",[18,5259,5260],{},"复合文档","曾经 ",[18,5263,5264],{},"COM 一统天下",[39,5266],{},[42,5268,5270],{"id":5269},"四十一扩展-faq第二辑","四十一、扩展 FAQ（第二辑）",[14,5272,5273,5278,5280,5281,5286,5287,5292],{},[18,5274,4134,5275,5277],{},[56,5276,1105],{}," 必须用管理员吗？",[4015,5279],{},"\n答：写到 ",[18,5282,5283],{},[56,5284,5285],{},"HKLM"," 通常要；写到 ",[18,5288,5289],{},[56,5290,5291],{},"HKCU","（当前用户）有时不必——取决于你的安装策略。",[14,5294,5295,5298,4112,5300,5303,5304,5307,5308,115],{},[18,5296,5297],{},"问：我能用 C# 写 Shell 扩展吗？",[4015,5299],{},[18,5301,5302],{},"可以但不推荐 Inproc","：CLR 会把 ",[18,5305,5306],{},"整个 CLR 运行时"," 载入 explorer，进程体积与版本地狱。",[18,5309,5310],{},"微软倾向：非托管 Inproc 或 Out-of-proc surrogate",[14,5312,5313,5319,4112,5321,5324,5325,5330],{},[18,5314,4134,5315,5318],{},[56,5316,5317],{},"CoInitializeSecurity"," 何时调用？",[4015,5320],{},[18,5322,5323],{},"DCOM \u002F 某些 WMI"," 场景要在 ",[18,5326,5327,5329],{},[56,5328,100],{}," 后尽早","；顺序错了 HRESULT 很玄学。",[14,5332,5333,5345,4112,5347,4227,5350,5353,5354,115],{},[18,5334,5335,5336,5338,5339,3048,5342,4162],{},"问：Smart Pointer ",[56,5337,91],{}," 能用在 ",[18,5340,5341],{},"线程间",[56,5343,5344],{},"std::move",[4015,5346],{},[18,5348,5349],{},"移动语义转移所有权",[18,5351,5352],{},"底层指针仍可能不能跨 apartment","——别指望 ",[18,5355,5356,5359],{},[56,5357,5358],{},"move"," 解决 marshal 问题",[39,5361],{},[42,5363,5365],{"id":5364},"四十二当你卡住时的排障剧本shell-扩展专用","四十二、当你卡住时的「排障剧本」（Shell 扩展专用）",[155,5367,5368,5388,5397,5411,5419,5428],{},[50,5369,5370,5373,5374,1498,5379,5384,5385,115],{},[18,5371,5372],{},"ProcMon","：过滤 ",[18,5375,5376],{},[56,5377,5378],{},"explorer.exe",[18,5380,5381],{},[56,5382,5383],{},"PATH contains YourDll.dll","，看是否 ",[18,5386,5387],{},"LoadImage",[50,5389,5390,5393,5394,4109],{},[18,5391,5392],{},"Dependency Walker \u002F Dependencies.exe","：缺 ",[18,5395,5396],{},"VC++ Runtime \u002F API Set",[50,5398,5399,5404,5405,5410],{},[18,5400,5401],{},[56,5402,5403],{},"regsvr32 \u002Fs Your.dll"," 静默失败：换 ",[18,5406,5407],{},[56,5408,5409],{},"regsvr32 Your.dll"," 看弹窗。",[50,5412,5413,5418],{},[18,5414,5415],{},[56,5416,5417],{},"sxstrace","（若有并行程序集问题）。",[50,5420,5421,5427],{},[18,5422,5423,5424,5426],{},"注释掉 ",[56,5425,1564],{}," 里一半菜单","，二分法定位崩溃。",[50,5429,5430,5433],{},[18,5431,5432],{},"虚拟机快照","：注册表乱了就回到快照。",[39,5435],{},[42,5437,5439],{"id":5438},"四十三术语表再扩一圈","四十三、术语表（再扩一圈）",[421,5441,5442,5450],{},[424,5443,5444],{},[427,5445,5446,5448],{},[430,5447,4246],{},[430,5449,4249],{},[437,5451,5452,5465,5480,5490,5500],{},[427,5453,5454,5459],{},[442,5455,5456],{},[18,5457,5458],{},"Surrogate",[442,5460,5461,5464],{},[18,5462,5463],{},"dllhost.exe"," 代替 DLL 跑 LocalServer 的技术",[427,5466,5467,5471],{},[442,5468,5469],{},[18,5470,4278],{},[442,5472,5473,5474,5479],{},"运行对象表： ",[18,5475,5476],{},[56,5477,5478],{},"GetObject(\"Excel.Application\")"," 之类",[427,5481,5482,5487],{},[442,5483,5484],{},[18,5485,5486],{},"APTTYPEQUALIFIER",[442,5488,5489],{},"细分线程公寓限定符（调试）",[427,5491,5492,5497],{},[442,5493,5494],{},[18,5495,5496],{},"GIT",[442,5498,5499],{},"Global Interface Table：跨线程 marshaled cookie",[427,5501,5502,5507],{},[442,5503,5504],{},[18,5505,5506],{},"NA",[442,5508,5509],{},"「Neutral Apartment」缩写（文档常见）",[39,5511],{},[10,5513,5515],{"id":5514},"第四辑内存封送git安全与-clr-interop再加厚一层","第四辑：内存、封送、GIT、安全与 CLR Interop（再加厚一层）",[39,5517],{},[42,5519,5521,5522,5525],{"id":5520},"四十五imalloccom-世界里的谁来-malloc","四十五、",[56,5523,5524],{},"IMalloc","：COM 世界里的「谁来 malloc？」",[14,5527,5528,5529,1991,5534,5539,5540,115,5547,1315,5550,5553,5554,828,5557,5563,5564,5569,5570,5574,5575,5578,5579,5584,5585,5591],{},"不少老式接口用 ",[18,5530,5531],{},[56,5532,5533],{},"CoGetMalloc(MEMCTX_TASK)",[18,5535,5536],{},[56,5537,5538],{},"IMalloc*","，再用 ",[18,5541,5542,65,5545],{},[56,5543,5544],{},"Alloc",[56,5546,535],{},[18,5548,5549],{},"Shell PIDL 解析",[18,5551,5552],{},"STRRET"," 转换时常碰到。",[18,5555,5556],{},"原则",[18,5558,5559,5560,5562],{},"谁家的 ",[56,5561,5524],{}," 分配，就用谁家 Free","；不要被 ",[18,5565,5566],{},[56,5567,5568],{},"CoTaskMemAlloc","（任务内存分配器）与 ",[18,5571,5572],{},[56,5573,1237],{}," 混在一起。",[18,5576,5577],{},"现代代码","多用 ",[18,5580,5581],{},[56,5582,5583],{},"CoTaskMemFree"," 释放 ",[18,5586,5587,5590],{},[56,5588,5589],{},"SH*** API** 返回给你的 **","PIDL`","——文档为准。",[247,5593,5595],{"className":280,"code":5594,"language":282,"meta":255,"style":255},"LPMALLOC pMalloc = nullptr;\nif (SUCCEEDED(CoGetMalloc(1, &pMalloc))) {\n    void* mem = pMalloc->Alloc(1024);\n    \u002F\u002F ...\n    pMalloc->Free(mem);\n    pMalloc->Release();\n}\n",[56,5596,5597,5602,5607,5612,5616,5621,5626],{"__ignoreMap":255},[286,5598,5599],{"class":288,"line":289},[286,5600,5601],{},"LPMALLOC pMalloc = nullptr;\n",[286,5603,5604],{"class":288,"line":295},[286,5605,5606],{},"if (SUCCEEDED(CoGetMalloc(1, &pMalloc))) {\n",[286,5608,5609],{"class":288,"line":301},[286,5610,5611],{},"    void* mem = pMalloc->Alloc(1024);\n",[286,5613,5614],{"class":288,"line":307},[286,5615,3725],{},[286,5617,5618],{"class":288,"line":313},[286,5619,5620],{},"    pMalloc->Free(mem);\n",[286,5622,5623],{"class":288,"line":374},[286,5624,5625],{},"    pMalloc->Release();\n",[286,5627,5628],{"class":288,"line":380},[286,5629,389],{},[39,5631],{},[42,5633,5635,5636],{"id":5634},"四十六全局接口表-gitiglobalinterfacetable","四十六、全局接口表 GIT：",[56,5637,5638],{},"IGlobalInterfaceTable",[14,5640,4522,5641,5644,5645,5648,5649,5652],{},[18,5642,5643],{},"已经把接口指针 marshal 好","，想在 ",[18,5646,5647],{},"任意线程"," 用 ",[18,5650,5651],{},"cookie"," 取回：",[247,5654,5656],{"className":280,"code":5655,"language":282,"meta":255,"style":255},"IGlobalInterfaceTable* pGIT = nullptr;\nCoCreateInstance(CLSID_StdGlobalInterfaceTable, nullptr, CLSCTX_INPROC_SERVER,\n    IID_IGlobalInterfaceTable, reinterpret_cast\u003Cvoid**>(&pGIT));\n\nDWORD cookie = 0;\npGIT->RegisterInterfaceInGlobal(pUnknownFromMarshal, IID_IFoo, &cookie);\n\u002F\u002F 另一线程：\nIFoo* pFoo = nullptr;\npGIT->GetInterfaceFromGlobal(cookie, IID_IFoo, reinterpret_cast\u003Cvoid**>(&pFoo));\n\u002F\u002F … 用完 RevokeInterfaceFromGlobal(cookie)\n",[56,5657,5658,5663,5668,5673,5677,5681,5686,5691,5696,5701],{"__ignoreMap":255},[286,5659,5660],{"class":288,"line":289},[286,5661,5662],{},"IGlobalInterfaceTable* pGIT = nullptr;\n",[286,5664,5665],{"class":288,"line":295},[286,5666,5667],{},"CoCreateInstance(CLSID_StdGlobalInterfaceTable, nullptr, CLSCTX_INPROC_SERVER,\n",[286,5669,5670],{"class":288,"line":301},[286,5671,5672],{},"    IID_IGlobalInterfaceTable, reinterpret_cast\u003Cvoid**>(&pGIT));\n",[286,5674,5675],{"class":288,"line":307},[286,5676,1141],{"emptyLinePlaceholder":1140},[286,5678,5679],{"class":288,"line":313},[286,5680,3185],{},[286,5682,5683],{"class":288,"line":374},[286,5684,5685],{},"pGIT->RegisterInterfaceInGlobal(pUnknownFromMarshal, IID_IFoo, &cookie);\n",[286,5687,5688],{"class":288,"line":380},[286,5689,5690],{},"\u002F\u002F 另一线程：\n",[286,5692,5693],{"class":288,"line":386},[286,5694,5695],{},"IFoo* pFoo = nullptr;\n",[286,5697,5698],{"class":288,"line":1065},[286,5699,5700],{},"pGIT->GetInterfaceFromGlobal(cookie, IID_IFoo, reinterpret_cast\u003Cvoid**>(&pFoo));\n",[286,5702,5703],{"class":288,"line":1071},[286,5704,5705],{},"\u002F\u002F … 用完 RevokeInterfaceFromGlobal(cookie)\n",[14,5707,5708,5711,5712,5715,5716,5718,5719,5722],{},[18,5709,5710],{},"典型用途","：长生命周期 worker 线程与 UI 线程交换 ",[18,5713,5714],{},"已经 marshal 过的"," 指针。",[18,5717,3099],{},"：优先 ",[18,5720,5721],{},"专用 COM 线程模型","，别一上来 GIT。",[39,5724],{},[42,5726,5728,5729,101,5731,5734],{"id":5727},"四十七comarshalinterthreadinterfaceinstream-cogetinterfaceandreleasestream配对记忆","四十七、",[56,5730,4518],{},[56,5732,5733],{},"CoGetInterfaceAndReleaseStream","（配对记忆）",[14,5736,5737,4957,5740,5745,5746,529,5751,542,5755,5757,828,5760,5764,5765,115],{},[18,5738,5739],{},"线程 A",[18,5741,5742],{},[56,5743,5744],{},"IUnknown*"," 封进 ",[18,5747,5748],{},[56,5749,5750],{},"IStream*",[18,5752,5753],{},[56,5754,4518],{},[4015,5756],{},[18,5758,5759],{},"线程 B",[18,5761,5762],{},[56,5763,5733],{}," 解出 ",[18,5766,5767],{},"可直接用的代理指针",[14,5769,5770],{},"伪代码：",[247,5772,5774],{"className":280,"code":5773,"language":282,"meta":255,"style":255},"IStream* pStm = nullptr;\nCoMarshalInterThreadInterfaceInStream(GetMTAIdentifier(), pSourceUnk, &pStm);\n\u002F\u002F Post to thread B with pStm\n\u002F\u002F Thread B:\nIUnknown* pProxy = nullptr;\nCoGetInterfaceAndReleaseStream(pStm, IID_IFoo, reinterpret_cast\u003Cvoid**>(&pFoo));\n",[56,5775,5776,5781,5786,5791,5796,5801],{"__ignoreMap":255},[286,5777,5778],{"class":288,"line":289},[286,5779,5780],{},"IStream* pStm = nullptr;\n",[286,5782,5783],{"class":288,"line":295},[286,5784,5785],{},"CoMarshalInterThreadInterfaceInStream(GetMTAIdentifier(), pSourceUnk, &pStm);\n",[286,5787,5788],{"class":288,"line":301},[286,5789,5790],{},"\u002F\u002F Post to thread B with pStm\n",[286,5792,5793],{"class":288,"line":307},[286,5794,5795],{},"\u002F\u002F Thread B:\n",[286,5797,5798],{"class":288,"line":313},[286,5799,5800],{},"IUnknown* pProxy = nullptr;\n",[286,5802,5803],{"class":288,"line":374},[286,5804,5805],{},"CoGetInterfaceAndReleaseStream(pStm, IID_IFoo, reinterpret_cast\u003Cvoid**>(&pFoo));\n",[14,5807,5808,828,5811,1315,5814,1315,5817,5820,5821,5824],{},[18,5809,5810],{},"细节",[18,5812,5813],{},"MTA identifier",[18,5815,5816],{},"STA 消息泵",[18,5818,5819],{},"流所有权"," 极易错——",[18,5822,5823],{},"官方示例照抄","，不要凭感觉手写。",[39,5826],{},[42,5828,5830,5831,5833],{"id":5829},"四十八dcom-安全coinitializesecurity-调用顺序","四十八、DCOM 安全：",[56,5832,5317],{}," 调用顺序",[14,5835,5836,5837,5840,5841,828],{},"若进程要用 ",[18,5838,5839],{},"DCOM","（远程 WMI、远程 OPC、分布式组件），常在 ",[18,5842,5843,5845],{},[56,5844,100],{}," 之后第一时间",[247,5847,5849],{"className":280,"code":5848,"language":282,"meta":255,"style":255},"CoInitializeSecurity(nullptr, -1, nullptr, nullptr,\n    RPC_C_AUTHN_LEVEL_PKT_PRIVACY, RPC_C_IMP_LEVEL_IDENTIFY,\n    nullptr, EOAC_NONE, nullptr);\n",[56,5850,5851,5856,5861],{"__ignoreMap":255},[286,5852,5853],{"class":288,"line":289},[286,5854,5855],{},"CoInitializeSecurity(nullptr, -1, nullptr, nullptr,\n",[286,5857,5858],{"class":288,"line":295},[286,5859,5860],{},"    RPC_C_AUTHN_LEVEL_PKT_PRIVACY, RPC_C_IMP_LEVEL_IDENTIFY,\n",[286,5862,5863],{"class":288,"line":301},[286,5864,5865],{},"    nullptr, EOAC_NONE, nullptr);\n",[14,5867,5868,5871,5872,5875,5876,5879,5880,5885,5886,5888,5889,5892],{},[18,5869,5870],{},"失败 HRESULT"," 常见：",[18,5873,5874],{},"80010119","（重复初始化安全）、",[18,5877,5878],{},"权限不够","。顺序错了会导致后续 ",[18,5881,5882],{},[56,5883,5884],{},"CoCreateInstanceEx"," 莫名其妙失败。",[18,5887,5112],{},"：读官方 ",[18,5890,5891],{},"「Initializing COM Security」"," 一页，按顺序抄。",[39,5894],{},[42,5896,5898,5899,5901],{"id":5897},"四十九net-clr-互操作rcwccwmarshalreleasecomobject-为何臭名昭著","四十九、.NET \u002F CLR 互操作：RCW、CCW、",[56,5900,4210],{}," 为何臭名昭著",[223,5903,5905],{"id":5904},"_491-rcwruntime-callable-wrapper","49.1 RCW（Runtime Callable Wrapper）",[14,5907,5908,5909,5914,5915,5917,5918,115,5922,5925,5926,5930,5931,5934,5935,5940,5941,115],{},"托管代码里的 ",[18,5910,5911],{},[56,5912,5913],{},"new Excel.Application()"," 背后是 ",[18,5916,4202],{}," 包了一层 ",[18,5919,5920],{},[56,5921,1378],{},[18,5923,5924],{},"Finalize"," 时才 ",[18,5927,5928],{},[56,5929,405],{},"——可能导致 ",[18,5932,5933],{},"COM 对象活得比你想的长","。于是有人 ",[18,5936,5937],{},[56,5938,5939],{},"Marshal.ReleaseComObject(excel)"," 强行 Release——若仍有别的托管引用指向同一 RCW，会 ",[18,5942,5943],{},"崩",[223,5945,5947],{"id":5946},"_492-ccwcom-callable-wrapper","49.2 CCW（COM Callable Wrapper）",[14,5949,5950,5951,5958,5959,115,5963,5966,5967,5970],{},".NET ",[18,5952,5953,5954,5957],{},"公开 ",[56,5955,5956],{},"[ComVisible]"," 类","给原生 ",[18,5960,5961],{},[56,5962,104],{},[18,5964,5965],{},"GC"," 与 ",[18,5968,5969],{},"COM 引用计数"," 两套生命周期叠在一起，调试地狱。",[223,5972,5974],{"id":5973},"_493-实务建议","49.3 实务建议",[47,5976,5977,5991,6009],{},[50,5978,5979,5986,5987,115],{},[18,5980,5981,5982,5985],{},"尽早显式 ",[56,5983,5984],{},"Quit()"," Office","，不要只靠 ",[18,5988,5989],{},[56,5990,5965],{},[50,5992,5993,5998,5999,6002,6003,115],{},[18,5994,5995],{},[56,5996,5997],{},"FinalReleaseComObject"," 之类 API ",[18,6000,6001],{},"慎用","；读最新文档是否有 ",[18,6004,6005,6008],{},[56,6006,6007],{},"NET Core COM interop"," 改进",[50,6010,6011,6014],{},[18,6012,6013],{},"优先「薄 RCW」","：业务逻辑仍在 native，托管只做 UI——边界清晰。",[39,6016],{},[42,6018,6020,6021,101,6024,6027],{"id":6019},"五十ipersistfile-ipersiststream对象请把自己保存起来","五十、",[56,6022,6023],{},"IPersistFile",[56,6025,6026],{},"IPersistStream","：「对象，请把自己保存起来」",[14,6029,6030,6031,828,6034,101,6039,115,6044,1315,6047,3209,6050,6053,6054,828,6057,6060,6061,115],{},"某些 COM 对象支持 ",[18,6032,6033],{},"持久化",[18,6035,6036],{},[56,6037,6038],{},"Save",[18,6040,6041],{},[56,6042,6043],{},"Load",[18,6045,6046],{},"快捷方式（Shell Link）",[18,6048,6049],{},"ActiveX 控件状态",[18,6051,6052],{},"Office"," 文档嵌入对象都涉及。",[18,6055,6056],{},"心智模型",[18,6058,6059],{},"COM 对象序列化成流","，而非随便 ",[18,6062,6063,6066],{},[56,6064,6065],{},"fwrite"," C++ 对象内存",[39,6068],{},[42,6070,6072],{"id":6071},"五十一ole-链接与嵌入ole不只右键菜单","五十一、OLE 链接与嵌入（OLE）：不只右键菜单",[14,6074,6075,6078,6079,6082,6083,6086,6087,1315,6092,6097,6098,6101,6102,115],{},[18,6076,6077],{},"OLE"," 当年等于 ",[18,6080,6081],{},"复合文档 + 就地激活 + 链接嵌入","。今天 ",[18,6084,6085],{},"Word 里插入 Excel 表","仍有遗产。",[18,6088,6089],{},[56,6090,6091],{},"IOleObject",[18,6093,6094],{},[56,6095,6096],{},"IOleClientSite","——全是 COM。你若维护 ",[18,6099,6100],{},"旧工业软件","，仍会见到 ",[18,6103,6104],{},[56,6105,6106],{},"OleCreate",[39,6108],{},[42,6110,6112,6113,6116],{"id":6111},"五十二idataobjectsetdata自定义剪贴板格式","五十二、",[56,6114,6115],{},"IDataObject::SetData","：自定义剪贴板格式",[14,6118,6119,6120,6125,6126,6130,6131,115,6135,6141,6142,828,6144,6147],{},"拖拽 \u002F 壳扩展里有时注册 ",[18,6121,6122],{},[56,6123,6124],{},"RegisterClipboardFormat(L\"MyAppData\")","，再在 ",[18,6127,6128],{},[56,6129,6115],{}," 塞进 ",[18,6132,6133],{},[56,6134,3925],{},[18,6136,6137,6138],{},"接收端 ",[56,6139,6140],{},"GetData"," 对称。",[18,6143,2843],{},[18,6145,6146],{},"InsideClipboard"," 小工具看格式列表。",[39,6149],{},[42,6151,6153,6154,6157],{"id":6152},"五十三cowaitformultiplehandlescom-与-win32-等待共存","五十三、",[56,6155,6156],{},"CoWaitForMultipleHandles","：COM 与 Win32 等待共存",[14,6159,6160,6161,6164,6165,6170,6171,6175,6176,6181,6182,115,6185,6188],{},"STA 线程既要 ",[18,6162,6163],{},"消息泵","又要 ",[18,6166,6167],{},[56,6168,6169],{},"WaitForSingleObject"," 时，可用 ",[18,6172,6173],{},[56,6174,6156],{},"（带 ",[18,6177,6178],{},[56,6179,6180],{},"COWAIT_DISPATCH_WINDOWS_MESSAGES","），避免 ",[18,6183,6184],{},"死锁",[18,6186,6187],{},"Media Foundation \u002F DirectShow"," 某些示例线程模型会扯到这个。",[39,6190],{},[42,6192,6194,6195],{"id":6193},"五十四vbscript-jscript-迟绑定脚本引擎里的-idispatch","五十四、VBScript \u002F JScript 迟绑定：脚本引擎里的 ",[56,6196,1378],{},[14,6198,6199,6200,1408,6205,1408,6209,1408,6213,115,6217,1408,6222,115,6227,6230,6231,6234],{},"老式 ASP：",[18,6201,6202],{},[56,6203,6204],{},"Server.CreateObject",[18,6206,6207],{},[56,6208,484],{},[18,6210,6211],{},[56,6212,104],{},[18,6214,6215],{},[56,6216,1378],{},[18,6218,6219],{},[56,6220,6221],{},"GetIDsOfNames(\"Open\")",[18,6223,6224],{},[56,6225,6226],{},"Invoke",[18,6228,6229],{},"为何慢","：每一步名字解析；",[18,6232,6233],{},"为何灵活","：脚本不改编译。",[39,6236],{},[42,6238,6240],{"id":6239},"五十五性能与文化为什么有人恨-com","五十五、性能与文化：为什么有人恨 COM",[155,6242,6243,6252,6261,6267],{},[50,6244,6245,6248,6249,115],{},[18,6246,6247],{},"HRESULT 噪音","：失败路径遍布；未检查 → ",[18,6250,6251],{},"随机状态污染",[50,6253,6254,828,6257,6260],{},[18,6255,6256],{},"引用计数心智负担",[18,6258,6259],{},"泄漏 \u002F UAF"," 永不过时。",[50,6262,6263,6266],{},[18,6264,6265],{},"注册表 + DLL Hell","：版本冲突折磨几代程序员。",[50,6268,6269,6272,6273,115],{},[18,6270,6271],{},"Side-by-Side \u002F WinSxS \u002F Manifest","：补救 DLL Hell，又引入 ",[18,6274,6275],{},"manifest 地狱",[14,6277,6278,6281,6282,115],{},[18,6279,6280],{},"但仍要说","：没有 COM，就没有现代 Windows 多媒体与 Shell 生态——",[18,6283,6284],{},"恨与爱并存",[39,6286],{},[42,6288,6290],{"id":6289},"五十六可选书单-视频续","五十六、可选书单 \u002F 视频（续）",[155,6292,6293,6296,6307],{"start":380},[50,6294,6295],{},"Chris Sells《ATL Internals》（硬核）。",[50,6297,6298,6299,6302,6303,6306],{},"YouTube 搜索 ",[18,6300,6301],{},"「Raymond Chen COM」"," 系列短文（博客 ",[18,6304,6305],{},"The Old New Thing"," 纸质书也可）。",[50,6308,2379,6309,6312],{},[18,6310,6311],{},"「COM threading apartments」"," 官方长文（务必读完 STA 消息泵段落）。",[39,6314],{},[42,6316,6318],{"id":6317},"五十七从零做一个最小-inproc-com-服务器只做-checklist长","五十七、从零做一个最小 Inproc COM 服务器：只做 checklist（长）",[14,6320,6321,6322,6325,6326,6329,6330,115],{},"下面是一份 ",[18,6323,6324],{},"「照着一条条勾」"," 的工程清单，适用于 ",[18,6327,6328],{},"教学用 DLL","，不等价于 ",[18,6331,6332],{},"微软官方 Sample 的全部健壮性",[223,6334,6336],{"id":6335},"_571-工程准备","57.1 工程准备",[155,6338,6339,6352,6358,6384],{},[50,6340,6341,6342,6345,6346,6349,6350,542],{},"新建 ",[18,6343,6344],{},"DLL 工程","，平台 ",[18,6347,6348],{},"x64","（若目标是 ",[18,6351,4989],{},[50,6353,6354,6357],{},[18,6355,6356],{},"C\u002FC++ → 预编译头","：可关（减小样板）。",[50,6359,6360,828,6366,6377,6378,6383],{},[18,6361,6362,6363],{},"链接器 → 模块定义文件 ",[56,6364,6365],{},".def",[18,6367,6368,6369,1315,6371,1315,6373,1315,6375],{},"导出 ",[56,6370,1990],{},[56,6372,2010],{},[56,6374,1105],{},[56,6376,1108],{},"（也可用 ",[18,6379,6380],{},[56,6381,6382],{},"__declspec(dllexport)","，团队要有统一规范）。",[50,6385,6386,828,6389,6400],{},[18,6387,6388],{},"附加依赖",[18,6390,6391,3048,6394,3048,6397],{},[56,6392,6393],{},"ole32.lib",[56,6395,6396],{},"oleaut32.lib",[56,6398,6399],{},"shell32.lib","（按接口而定）。",[223,6402,6404],{"id":6403},"_572-定义-clsid-iid","57.2 定义 CLSID \u002F IID",[247,6406,6408],{"className":280,"code":6407,"language":282,"meta":255,"style":255},"\u002F\u002F 用 guidgen.exe 或 VS「工具 → 创建 GUID」生成唯一值\nstatic const CLSID CLSID_MyServer =\n{ 0xaaaaaaaa, 0xbbbb, 0xcccc, { 0xdd,0xee,0xff,0x01,0x02,0x03,0x04,0x05 } };\n",[56,6409,6410,6415,6420],{"__ignoreMap":255},[286,6411,6412],{"class":288,"line":289},[286,6413,6414],{},"\u002F\u002F 用 guidgen.exe 或 VS「工具 → 创建 GUID」生成唯一值\n",[286,6416,6417],{"class":288,"line":295},[286,6418,6419],{},"static const CLSID CLSID_MyServer =\n",[286,6421,6422],{"class":288,"line":301},[286,6423,6424],{},"{ 0xaaaaaaaa, 0xbbbb, 0xcccc, { 0xdd,0xee,0xff,0x01,0x02,0x03,0x04,0x05 } };\n",[223,6426,6428],{"id":6427},"_573-实现-com-对象与类工厂","57.3 实现 COM 对象与类工厂",[155,6430,6431,6447,6464],{},[50,6432,6433,828,6438,6446],{},[18,6434,6435],{},[56,6436,6437],{},"CMyObject",[18,6439,6440,101,6442,101,6444],{},[56,6441,260],{},[56,6443,335],{},[56,6445,405],{}," + 业务接口。",[50,6448,6449,828,6453,828,6457,115],{},[18,6450,6451],{},[56,6452,4658],{},[18,6454,6455],{},[56,6456,975],{},[18,6458,6459,101,6461],{},[56,6460,2001],{},[56,6462,6463],{},"LockServer",[50,6465,6466,828,6469,1315,6474,6479],{},[18,6467,6468],{},"全局计数",[18,6470,6471],{},[56,6472,6473],{},"g_serverLocks",[18,6475,6476],{},[56,6477,6478],{},"g_objCount","（调试用）。",[223,6481,6483,6484],{"id":6482},"_574-dllgetclassobject","57.4 ",[56,6485,1990],{},[247,6487,6489],{"className":280,"code":6488,"language":282,"meta":255,"style":255},"STDAPI DllGetClassObject(REFCLSID rclsid, REFIID riid, LPVOID* ppv)\n{\n    if (!IsEqualCLSID(rclsid, CLSID_MyServer))\n        return CLASS_E_CLASSNOTAVAILABLE;\n    static CFactory factory;\n    return factory.QueryInterface(riid, ppv);\n}\n",[56,6490,6491,6495,6499,6504,6508,6513,6518],{"__ignoreMap":255},[286,6492,6493],{"class":288,"line":289},[286,6494,2033],{},[286,6496,6497],{"class":288,"line":295},[286,6498,356],{},[286,6500,6501],{"class":288,"line":301},[286,6502,6503],{},"    if (!IsEqualCLSID(rclsid, CLSID_MyServer))\n",[286,6505,6506],{"class":288,"line":307},[286,6507,2047],{},[286,6509,6510],{"class":288,"line":313},[286,6511,6512],{},"    static CFactory factory;\n",[286,6514,6515],{"class":288,"line":374},[286,6516,6517],{},"    return factory.QueryInterface(riid, ppv);\n",[286,6519,6520],{"class":288,"line":380},[286,6521,389],{},[223,6523,6525,6526],{"id":6524},"_575-dllregisterserver","57.5 ",[56,6527,1105],{},[14,6529,6530,6531,6536],{},"用 ",[18,6532,6533],{},[56,6534,6535],{},"RegCreateKeyEx"," 写：",[47,6538,6539,6546,6553],{},[50,6540,6541],{},[18,6542,6543],{},[56,6544,6545],{},"HKCR\\CLSID\\{...}",[50,6547,6548],{},[18,6549,6550,6552],{},[56,6551,3053],{}," = 模块完整路径",[50,6554,6555],{},[18,6556,6557],{},[56,6558,528],{},[14,6560,6561,6562,6565],{},"壳扩展还要写 ",[18,6563,6564],{},"Shell 挂接键","（见前文第三十六节）。",[223,6567,6569,6570,6573],{"id":6568},"_576-regsvr32-与验证","57.6 ",[56,6571,6572],{},"regsvr32"," 与验证",[155,6575,6576,6583,6591],{},[50,6577,6578],{},[18,6579,6580],{},[56,6581,6582],{},"regsvr32 My.dll",[50,6584,6585,1408,6587,6590],{},[18,6586,4344],{},[18,6588,6589],{},"All Classes"," → 搜你的 CLSID。",[50,6592,6593,6595,6596,6600,6601,6606],{},[18,6594,5372],{}," 过滤 ",[18,6597,6598],{},[56,6599,6572],{}," 看 ",[18,6602,6603],{},[56,6604,6605],{},"CreateFile"," 是否指向正确 DLL。",[223,6608,6610],{"id":6609},"_577-卸载","57.7 卸载",[14,6612,6613,1498,6617,115],{},[18,6614,6615],{},[56,6616,2160],{},[18,6618,6619],{},"确认注册表消失",[39,6621],{},[42,6623,6625],{"id":6624},"五十八hresult字典页facility-与-win32-映射实用向","五十八、HRESULT「字典页」：Facility 与 Win32 映射（实用向）",[223,6627,6629,6630,6633],{"id":6628},"_581-hresult_from_win32-速查","58.1 ",[56,6631,6632],{},"HRESULT_FROM_WIN32"," 速查",[14,6635,6636,6637,828],{},"当你拿到 ",[18,6638,6639],{},[56,6640,6641],{},"GetLastError() == ERROR_ACCESS_DENIED (5)",[247,6643,6645],{"className":280,"code":6644,"language":282,"meta":255,"style":255},"HRESULT hr = HRESULT_FROM_WIN32(ERROR_ACCESS_DENIED); \u002F\u002F 0x80070005\n",[56,6646,6647],{"__ignoreMap":255},[286,6648,6649],{"class":288,"line":289},[286,6650,6644],{},[223,6652,6654,6655,6657],{"id":6653},"_582-facility_itf-自定义区间","58.2 ",[56,6656,2577],{}," 自定义区间",[14,6659,6660,6661,6666,6667,6672,6673,6676,6677,6682,6683,115],{},"接口作者可在 ",[18,6662,6663],{},[56,6664,6665],{},"0x0200–0xFFFF"," 区间定义 ",[18,6668,6669],{},[56,6670,6671],{},"IFACEMETHODIMP"," 返回值——",[18,6674,6675],{},"不要与系统 HRESULT 冲突","。看见 ",[18,6678,6679],{},[56,6680,6681],{},"0x8004xxxx"," 多看一眼是否 ",[18,6684,6685],{},"自定义接口错误",[223,6687,6689],{"id":6688},"_583-打印友好字符串示意","58.3 打印友好字符串（示意）",[247,6691,6693],{"className":280,"code":6692,"language":282,"meta":255,"style":255},"void PrintHR(HRESULT hr)\n{\n    wchar_t buf[512]{};\n    FormatMessageW(\n        FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,\n        nullptr, hr, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),\n        buf, (DWORD)(std::size(buf)), nullptr);\n    std::fwprintf(stderr, L\"hr=0x%08X %s\\n\", hr, buf);\n}\n",[56,6694,6695,6700,6704,6709,6714,6719,6724,6729,6734],{"__ignoreMap":255},[286,6696,6697],{"class":288,"line":289},[286,6698,6699],{},"void PrintHR(HRESULT hr)\n",[286,6701,6702],{"class":288,"line":295},[286,6703,356],{},[286,6705,6706],{"class":288,"line":301},[286,6707,6708],{},"    wchar_t buf[512]{};\n",[286,6710,6711],{"class":288,"line":307},[286,6712,6713],{},"    FormatMessageW(\n",[286,6715,6716],{"class":288,"line":313},[286,6717,6718],{},"        FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,\n",[286,6720,6721],{"class":288,"line":374},[286,6722,6723],{},"        nullptr, hr, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),\n",[286,6725,6726],{"class":288,"line":380},[286,6727,6728],{},"        buf, (DWORD)(std::size(buf)), nullptr);\n",[286,6730,6731],{"class":288,"line":386},[286,6732,6733],{},"    std::fwprintf(stderr, L\"hr=0x%08X %s\\n\", hr, buf);\n",[286,6735,6736],{"class":288,"line":1065},[286,6737,389],{},[14,6739,6740,6741,6745,6746,6748],{},"失败时 ",[18,6742,6743],{},[56,6744,2517],{}," 不一定总有文本——仍以 ",[18,6747,2331],{}," 为准。",[39,6750],{},[42,6752,6754],{"id":6753},"五十九directshow仍是-com只是图特别大","五十九、DirectShow：仍是 COM，只是图特别大",[14,6756,6757,828,6760,1315,6765,1315,6770,6775,6776,6779,6780,6783,6784,6787,6788,6791,6792,6795],{},[18,6758,6759],{},"Filter Graph",[18,6761,6762],{},[56,6763,6764],{},"IGraphBuilder",[18,6766,6767],{},[56,6768,6769],{},"IBaseFilter",[18,6771,6772],{},[56,6773,6774],{},"IMediaControl","……全套 COM。",[18,6777,6778],{},"GraphEdit"," 工具可视化。",[18,6781,6782],{},"现状","：微软主推 ",[18,6785,6786],{},"Media Foundation","，但监控摄像头、旧采集卡驱动仍 ",[18,6789,6790],{},"DirectShow","。心法与本笔记 ",[18,6793,6794],{},"IUnknown + HRESULT"," 完全一致，只是接口数量爆炸。",[39,6797],{},[42,6799,6801,6802,6805],{"id":6800},"六十active-scriptingiactivescript-与宿主","六十、Active Scripting：",[56,6803,6804],{},"IActiveScript"," 与宿主",[14,6807,6808,6809,6812,6813,6817,6818,6823,6824,6827,6828,6835],{},"IE 时代的 ",[18,6810,6811],{},"JScript\u002FVBScript 引擎","暴露 ",[18,6814,6815],{},[56,6816,6804],{},"，宿主 ",[18,6819,6820],{},[56,6821,6822],{},"IActiveScriptSite","——仍是 COM。现代 ",[18,6825,6826],{},"Chakra \u002F V8"," 替代了浏览器脚本栈，但 ",[18,6829,6830,6831,6834],{},"Windows Script Host (",[56,6832,6833],{},"wscript.exe",")"," 仍在。",[39,6837],{},[42,6839,6841],{"id":6840},"六十一扩展阅读windows-runtime-投影cppwinrt里的-com-幽灵","六十一、扩展阅读：Windows Runtime 投影（CppWinRT）里的 COM 幽灵",[14,6843,6844,6845,6850,6851,1315,6855,6860,6861,6864,6865,6869,6870,6874,6875,529,6878,6883,6884,115],{},"打开 ",[18,6846,6847],{},[56,6848,6849],{},"winrt\u002Fbase.h","，你会看见 ",[18,6852,6853],{},[56,6854,81],{},[18,6856,6857],{},[56,6858,6859],{},"WINRT_IMPL_IUnknown"," 之类痕迹。",[18,6862,6863],{},"不要混淆","：日常 ",[18,6866,6867],{},[56,6868,3530],{}," 写法不必手写 ",[18,6871,6872],{},[56,6873,260],{},"，但一旦 ",[18,6876,6877],{},"激活失败",[18,6879,6880,6882],{},[56,6881,3511],{}," HRESULT","），排查链条仍会回落到 ",[18,6885,6886],{},"组件注册 \u002F manifest \u002F apartment",[39,6888],{},[42,6890,6892],{"id":6891},"六十二私货小结什么时候该停止抄笔记去写代码","六十二、私货小结：什么时候该停止抄笔记去写代码？",[14,6894,6895,6896,6899],{},"当你能 ",[18,6897,6898],{},"独立解释"," 如下五个问题，就可以把本篇合上：",[155,6901,6902,6913,6924,6934,6939],{},[50,6903,6904,6912],{},[18,6905,6906,6908,6909,6911],{},[56,6907,104],{}," 失败返回 ",[56,6910,726],{},"，你最先检查哪三处？","（注册表 CLSID、位数、DLL 路径）",[50,6914,6915,6920,6921,6923],{},[18,6916,6917,6919],{},[56,6918,114],{}," 出现时，你有几种应对策略？","（忽略继续 \u002F 换线程 \u002F ",[56,6922,100],{}," 参数对齐）",[50,6925,6926,6933],{},[18,6927,6928,6930,6931,4109],{},[56,6929,260],{}," 成功后指针是否需要 ",[56,6932,405],{},"（看所有权转移约定）",[50,6935,6936],{},[18,6937,6938],{},"Inproc Shell 扩展为什么要匹配 explorer 位数？",[50,6940,6941],{},[18,6942,6943,6945],{},[56,6944,4202],{}," 为什么可能导致 Excel 进程残留？",[39,6947],{},[10,6949,6951],{"id":6950},"第五辑头文件与库伪面试题再续-faq冲体量也用干货撑着","第五辑：头文件与库、伪面试题、再续 FAQ（冲体量也用干货撑着）",[39,6953],{},[42,6955,6957,6958,6961],{"id":6956},"六十三常用头文件-对应-lib桌面原生-com-速查","六十三、常用头文件 → 对应 ",[56,6959,6960],{},".lib","（桌面原生 COM 速查）",[421,6963,6964,6977],{},[424,6965,6966],{},[427,6967,6968,6971,6974],{},[430,6969,6970],{},"头文件",[430,6972,6973],{},"典型接口 \u002F API",[430,6975,6976],{},"常见静态链接库",[437,6978,6979,6997,7025,7045,7069,7095,7114,7137,7165],{},[427,6980,6981,6988,6994],{},[442,6982,6983],{},[18,6984,6985],{},[56,6986,6987],{},"unknwn.h",[442,6989,6990],{},[18,6991,6992],{},[56,6993,81],{},[442,6995,6996],{},"（通常不需要单独 lib）",[427,6998,6999,7009,7019],{},[442,7000,7001],{},[18,7002,7003,101,7006],{},[56,7004,7005],{},"objidl.h",[56,7007,7008],{},"objidlbase.h",[442,7010,7011],{},[18,7012,7013,1315,7015,1315,7017],{},[56,7014,5524],{},[56,7016,3364],{},[56,7018,3361],{},[442,7020,7021],{},[18,7022,7023],{},[56,7024,6393],{},[427,7026,7027,7034,7037],{},[442,7028,7029],{},[18,7030,7031],{},[56,7032,7033],{},"ole2.h",[442,7035,7036],{},"OLE 复合文档全家桶",[442,7038,7039],{},[18,7040,7041,3048,7043],{},[56,7042,6393],{},[56,7044,6396],{},[427,7046,7047,7054,7063],{},[442,7048,7049],{},[18,7050,7051],{},[56,7052,7053],{},"combaseapi.h",[442,7055,7056,7057,7062],{},"现代 ",[18,7058,7059],{},[56,7060,7061],{},"Ro*","、基础 COM",[442,7064,7065],{},[18,7066,7067],{},[56,7068,6393],{},[427,7070,7071,7081,7089],{},[442,7072,7073],{},[18,7074,7075,3048,7078],{},[56,7076,7077],{},"shlobj.h",[56,7079,7080],{},"shobjidl.h",[442,7082,7083,7084],{},"Shell、",[18,7085,7086],{},[56,7087,7088],{},"IShellItem",[442,7090,7091],{},[18,7092,7093],{},[56,7094,6399],{},[427,7096,7097,7104,7107],{},[442,7098,7099],{},[18,7100,7101],{},[56,7102,7103],{},"mmdeviceapi.h",[442,7105,7106],{},"MMDevice",[442,7108,7109,7113],{},[18,7110,7111],{},[56,7112,6393],{},"（间接）",[427,7115,7116,7123,7126],{},[442,7117,7118],{},[18,7119,7120],{},[56,7121,7122],{},"audioclient.h",[442,7124,7125],{},"WASAPI",[442,7127,7128,1498,7132],{},[18,7129,7130],{},[56,7131,6393],{},[18,7133,7134],{},[56,7135,7136],{},"Mmdevapi",[427,7138,7139,7146,7152],{},[442,7140,7141],{},[18,7142,7143],{},[56,7144,7145],{},"mfreadwrite.h",[442,7147,7148],{},[18,7149,7150],{},[56,7151,2289],{},[442,7153,7154],{},[18,7155,7156,3048,7159,3048,7162],{},[56,7157,7158],{},"mfplat",[56,7160,7161],{},"mfuuid",[56,7163,7164],{},"mfreadwrite",[427,7166,7167,7174,7180],{},[442,7168,7169],{},[18,7170,7171],{},[56,7172,7173],{},"d3d11.h",[442,7175,7176],{},[18,7177,7178],{},[56,7179,2279],{},[442,7181,7182],{},[18,7183,7184],{},[56,7185,7186],{},"d3d11.lib",[14,7188,7189,828,7191,7198,7199,3295,7203,7206,7207,542],{},[18,7190,5556],{},[18,7192,7193,7194,7197],{},"链接错误「unresolved external ",[56,7195,7196],{},"__imp_...","」"," → 先少一个 ",[18,7200,7201],{},[56,7202,6960],{},[18,7204,7205],{},"调用约定"," 不对（",[18,7208,7209],{},[56,7210,7211],{},"__stdcall",[39,7213],{},[42,7215,7217],{"id":7216},"六十四伪面试题口试向自己对着镜子练","六十四、伪面试题（口试向，自己对着镜子练）",[14,7219,7220,828,7223,7232,7234,828,7237,3646,7242],{},[18,7221,7222],{},"Q1",[18,7224,7225,7227,7228,7231],{},[56,7226,260],{}," 和 C++ ",[56,7229,7230],{},"dynamic_cast"," 有什么本质不同？",[4015,7233],{},[18,7235,7236],{},"A1",[18,7238,7239,7241],{},[56,7240,7230],{}," 是编译期\u002FRTTI 概念，跨模块 DLL 未必安全",[18,7243,7244,7247],{},[56,7245,7246],{},"QI"," 是 COM 约定，跨语言、跨版本接口扩展都靠它。",[14,7249,7250,828,7253,7258,7260,828,7263,7266,7267,7271],{},[18,7251,7252],{},"Q2",[18,7254,7255,7256,4109],{},"为什么 Inproc 服务器要导 ",[56,7257,2010],{},[4015,7259],{},[18,7261,7262],{},"A2",[18,7264,7265],{},"OLE 要判断能否卸载 DLL 释放内存","；返回 ",[18,7268,7269],{},[56,7270,667],{}," 才安全卸载。",[14,7273,7274,828,7277,7280,7282,828,7285,3646,7288,7291,7292,7295],{},[18,7275,7276],{},"Q3",[18,7278,7279],{},"STA 线程没有消息泵会怎样？",[4015,7281],{},[18,7283,7284],{},"A3",[18,7286,7287],{},"跨 apartment 封送可能死锁",[18,7289,7290],{},"UI 线程","必须 ",[18,7293,7294],{},"GetMessage 循环","（或现代消息调度）。",[14,7297,7298,828,7301,7308,7310,828,7313,5966,7316,7319,7320,3443],{},[18,7299,7300],{},"Q4",[18,7302,7303,4138,7305,4141],{},[56,7304,2992],{},[56,7306,7307],{},"INPROC_HANDLER",[4015,7309],{},[18,7311,7312],{},"A4",[18,7314,7315],{},"Handler",[18,7317,7318],{},"In-place activation"," 有关（OLE 文档服务器）；多数桌面扩展接触 ",[18,7321,7322],{},[56,7323,7324],{},"INPROC_SERVER",[39,7326],{},[42,7328,7330],{"id":7329},"六十五faq第三辑絮叨版","六十五、FAQ（第三辑·絮叨版）",[14,7332,7333,7339,4112,7341,7349,7350,7354,7355,115],{},[18,7334,7335,7336,7338],{},"问：我用 CMake，",[56,7337,6365],{}," 导出怎么写？",[4015,7340],{},[18,7342,7343,1498,7346],{},[56,7344,7345],{},"target_sources PRIVATE exports.def",[56,7347,7348],{},"LINK_FLAGS \u002FDEF:","，或用 ",[18,7351,7352],{},[56,7353,6382],{}," 导出 ",[18,7356,7357],{},[56,7358,1990],{},[14,7360,7361,7366,4112,7368,7371,7372,115],{},[18,7362,4134,7363,7365],{},[56,7364,6572],{}," 提示「加载失败找不到模块」？",[4015,7367],{},[18,7369,7370],{},"依赖 DLL 缺失","（VC runtime、UCRT）、或 ",[18,7373,7374],{},"路径含奇怪字符",[14,7376,7377,7382,4112,7384,7387,7388,7392,7393,7398,7399,7402,7403,7406],{},[18,7378,4134,7379,7381],{},[56,7380,1105],{}," 返回成功但注册表没键？",[4015,7383],{},[18,7385,7386],{},"重定向","：32 位 ",[18,7389,7390],{},[56,7391,6572],{}," 写 ",[18,7394,7395],{},[56,7396,7397],{},"WOW6432Node","；你用 ",[18,7400,7401],{},"64 位 OleView"," 却去看 ",[18,7404,7405],{},"32 位视图","，会对不上。",[14,7408,7409,7415,4112,7417,7420,7421,7425,7426,7430],{},[18,7410,4134,7411,7414],{},[56,7412,7413],{},"ZeroMemory"," 接口指针可以吗？",[4015,7416],{},[18,7418,7419],{},"绝对不行","，除非你确认它不是活的 COM 指针；忘了 ",[18,7422,7423],{},[56,7424,405],{}," 就用 ",[18,7427,7428],{},[56,7429,7413],{}," ——那是自欺欺人。",[14,7432,7433,7440,4112,7442,7445],{},[18,7434,7435,7436,4138,7438,4162],{},"问：我能混用 ",[56,7437,4148],{},[56,7439,100],{},[4015,7441],{},[18,7443,7444],{},"同一线程不要混","；遗留代码另说。",[14,7447,7448,7456,4112,7458,7461,7462,7466],{},[18,7449,4134,7450,7452,7453,4162],{},[56,7451,5744],{}," 能放进 ",[56,7454,7455],{},"std::vector",[4015,7457],{},[18,7459,7460],{},"可以，但要明确所有权","：用 ",[18,7463,7464],{},[56,7465,91],{}," 包一层更安全。",[14,7468,7469,7472,4112,7474,1315,7480,7487,7488,115],{},[18,7470,7471],{},"问：Rust \u002F Go 调 COM？",[4015,7473],{},[18,7475,7476,7477],{},"Rust ",[56,7478,7479],{},"windows-rs",[18,7481,7482,7483,7486],{},"Go ",[56,7484,7485],{},"syscall"," + COM ABI"," 都有人做；本质仍是 ",[18,7489,7490],{},"vtable + HRESULT",[39,7492],{},[7494,7495,7496],"style",{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":255,"searchDepth":295,"depth":295,"links":7498},[7499,7500,7501,7507,7508,7514,7521,7528,7530,7539,7547,7555,7556,7557,7558,7565,7570,7572,7573,7574,7581,7583,7584,7585,7587,7588,7590,7592,7594,7595,7596,7597,7603,7605,7607,7608,7610,7612,7613,7615,7616,7617,7618,7620,7622,7624,7626,7632,7634,7635,7637,7639,7641,7642,7643,7655,7662,7663,7665,7666,7667,7669,7670],{"id":44,"depth":295,"text":45},{"id":141,"depth":295,"text":142},{"id":217,"depth":295,"text":7502,"children":7503},"二、接口、vtable、IUnknown（务必建立心智图像）",[7504,7505],{"id":225,"depth":301,"text":226},{"id":273,"depth":301,"text":7506},"2.2 IUnknown 三方法（所有 COM 接口的根）",{"id":418,"depth":295,"text":419},{"id":572,"depth":295,"text":7509,"children":7510},"四、HRESULT：宁可当成「带类型的错误码」",[7511,7512,7513],{"id":579,"depth":301,"text":580},{"id":621,"depth":301,"text":622},{"id":644,"depth":301,"text":645},{"id":783,"depth":295,"text":784,"children":7515},[7516,7517,7518,7520],{"id":787,"depth":301,"text":788},{"id":809,"depth":301,"text":810},{"id":837,"depth":301,"text":7519},"5.3 CoInitializeEx 典型写法",{"id":888,"depth":301,"text":889},{"id":917,"depth":295,"text":7522,"children":7523},"六、创建对象的两条路：CoCreateInstance 与类工厂",[7524,7526],{"id":924,"depth":301,"text":7525},"6.1 CoCreateInstance（90% 场景够用）",{"id":971,"depth":301,"text":7527},"6.2 IClassFactory（心里要有这张图）",{"id":1116,"depth":295,"text":7529},"七、ComPtr（WRL）与智能指针纪律",{"id":1212,"depth":295,"text":7531,"children":7532},"八、BSTR、VARIANT、自动化（IDispatch）",[7533,7535,7537],{"id":1220,"depth":301,"text":7534},"8.1 BSTR：带长度前缀的宽字符串",{"id":1288,"depth":301,"text":7536},"8.2 VARIANT：「万能联合体」",{"id":1374,"depth":301,"text":7538},"8.3 IDispatch：迟绑定（脚本、VBA）",{"id":1480,"depth":295,"text":1481,"children":7540},[7541,7542,7543,7544,7545],{"id":1511,"depth":301,"text":1512},{"id":1573,"depth":301,"text":1574},{"id":1680,"depth":301,"text":1681},{"id":2017,"depth":301,"text":2018},{"id":2163,"depth":301,"text":7546},"9.4 IExplorerCommand（Windows 10 1809+）",{"id":2204,"depth":295,"text":2205,"children":7548},[7549,7551,7552,7554],{"id":2208,"depth":301,"text":7550},"10.1 拖拽与剪贴板：IDataObject、IDropTarget",{"id":2236,"depth":301,"text":2237},{"id":2249,"depth":301,"text":7553},"10.3 WMI（IWbemServices）",{"id":2270,"depth":301,"text":2271},{"id":2300,"depth":295,"text":2301},{"id":2337,"depth":295,"text":2338},{"id":2373,"depth":295,"text":2374},{"id":2449,"depth":295,"text":7559,"children":7560},"十五、HRESULT 手工解码与 Facility 一览（进阶）",[7561,7562,7563],{"id":2456,"depth":301,"text":2457},{"id":2520,"depth":301,"text":2521},{"id":2634,"depth":301,"text":7564},"15.3 IErrorInfo \u002F _com_error（拿到「可读字符串」）",{"id":2724,"depth":295,"text":2725,"children":7566},[7567,7568,7569],{"id":2728,"depth":301,"text":2729},{"id":2857,"depth":301,"text":2858},{"id":2886,"depth":301,"text":2887},{"id":2932,"depth":295,"text":7571},"十七、CLSCTX：你到底想在哪创建对象？",{"id":3059,"depth":295,"text":3060},{"id":3109,"depth":295,"text":3110},{"id":3218,"depth":295,"text":3219,"children":7575},[7576,7577,7579],{"id":3222,"depth":301,"text":3223},{"id":3283,"depth":301,"text":7578},"20.2 类型库 .tlb",{"id":3323,"depth":301,"text":7580},"20.3 #import 生成 TLH\u002FTLI（ MSVC ）",{"id":3357,"depth":295,"text":7582},"二十一、结构化存储：IStorage \u002F IStream（复合文档）",{"id":3405,"depth":295,"text":3406},{"id":3448,"depth":295,"text":3449},{"id":3482,"depth":295,"text":7586},"二十四、WinRT 与经典 COM：IInspectable、激活工厂",{"id":3600,"depth":295,"text":3601},{"id":3756,"depth":295,"text":7589},"二十六、Shell 进阶：从 IDataObject 解析选中文件（CF_HDROP）",{"id":3986,"depth":295,"text":7591},"二十七、拖拽再放：DoDragDrop 双方在干什么？",{"id":4050,"depth":295,"text":7593},"二十八、SAFEARRAY：VB\u002FVBA 数组如何越过边界",{"id":4100,"depth":295,"text":4101},{"id":4236,"depth":295,"text":4237},{"id":4327,"depth":295,"text":4328},{"id":4408,"depth":295,"text":4409,"children":7598},[7599,7600,7601],{"id":4412,"depth":301,"text":4413},{"id":4485,"depth":301,"text":4486},{"id":4514,"depth":301,"text":7602},"33.3 CoMarshalInterThreadInterfaceInStream（只记用途）",{"id":4553,"depth":295,"text":7604},"三十四、ThreadingModel 注册表值到底写给谁看？",{"id":4641,"depth":295,"text":7606},"三十五、手写 IClassFactory：最小可讲故事版本",{"id":4886,"depth":295,"text":4887},{"id":5006,"depth":295,"text":7609},"三十七、IExplorerCommand：新 Shell 扩展范式（概念）",{"id":5054,"depth":295,"text":7611},"三十八、Office 自动化再往深处走一步：Workbooks.Open 的参数数组",{"id":5129,"depth":295,"text":5130},{"id":5185,"depth":295,"text":7614},"四十、结构化存储：十几行代码摸一下 IStorage",{"id":5269,"depth":295,"text":5270},{"id":5364,"depth":295,"text":5365},{"id":5438,"depth":295,"text":5439},{"id":5520,"depth":295,"text":7619},"四十五、IMalloc：COM 世界里的「谁来 malloc？」",{"id":5634,"depth":295,"text":7621},"四十六、全局接口表 GIT：IGlobalInterfaceTable",{"id":5727,"depth":295,"text":7623},"四十七、CoMarshalInterThreadInterfaceInStream \u002F CoGetInterfaceAndReleaseStream（配对记忆）",{"id":5829,"depth":295,"text":7625},"四十八、DCOM 安全：CoInitializeSecurity 调用顺序",{"id":5897,"depth":295,"text":7627,"children":7628},"四十九、.NET \u002F CLR 互操作：RCW、CCW、Marshal.ReleaseComObject 为何臭名昭著",[7629,7630,7631],{"id":5904,"depth":301,"text":5905},{"id":5946,"depth":301,"text":5947},{"id":5973,"depth":301,"text":5974},{"id":6019,"depth":295,"text":7633},"五十、IPersistFile \u002F IPersistStream：「对象，请把自己保存起来」",{"id":6071,"depth":295,"text":6072},{"id":6111,"depth":295,"text":7636},"五十二、IDataObject::SetData：自定义剪贴板格式",{"id":6152,"depth":295,"text":7638},"五十三、CoWaitForMultipleHandles：COM 与 Win32 等待共存",{"id":6193,"depth":295,"text":7640},"五十四、VBScript \u002F JScript 迟绑定：脚本引擎里的 IDispatch",{"id":6239,"depth":295,"text":6240},{"id":6289,"depth":295,"text":6290},{"id":6317,"depth":295,"text":6318,"children":7644},[7645,7646,7647,7648,7650,7652,7654],{"id":6335,"depth":301,"text":6336},{"id":6403,"depth":301,"text":6404},{"id":6427,"depth":301,"text":6428},{"id":6482,"depth":301,"text":7649},"57.4 DllGetClassObject",{"id":6524,"depth":301,"text":7651},"57.5 DllRegisterServer",{"id":6568,"depth":301,"text":7653},"57.6 regsvr32 与验证",{"id":6609,"depth":301,"text":6610},{"id":6624,"depth":295,"text":6625,"children":7656},[7657,7659,7661],{"id":6628,"depth":301,"text":7658},"58.1 HRESULT_FROM_WIN32 速查",{"id":6653,"depth":301,"text":7660},"58.2 FACILITY_ITF 自定义区间",{"id":6688,"depth":301,"text":6689},{"id":6753,"depth":295,"text":6754},{"id":6800,"depth":295,"text":7664},"六十、Active Scripting：IActiveScript 与宿主",{"id":6840,"depth":295,"text":6841},{"id":6891,"depth":295,"text":6892},{"id":6956,"depth":295,"text":7668},"六十三、常用头文件 → 对应 .lib（桌面原生 COM 速查）",{"id":7216,"depth":295,"text":7217},{"id":7329,"depth":295,"text":7330},"面向：几乎没写过 Win32 \u002F C++ 原生，但听说过 COM、ActiveX、DLL 注册 的同学。本文尽量 拓宽视野：不只围绕某一个录屏工程，而是把 桌面右键菜单、Office 自动化、Shell、拖拽 等典型 COM 场景串起来；代码片段以 示意 + 可对照 MSDN 补全 为主。","md",{},"\u002Fcpp\u002Fcom组件与windows编程入门",{"title":5,"description":7671},"cpp\u002FCOM组件与Windows编程入门","tkmyGm-2x8s3ED1Oz7sWVihJIb1_fOReN22l4qcMZm4",1791042461419]